diff --git a/.github/workflows/build_docs.yaml b/.github/workflows/build_docs.yaml index 0213126..b3958bb 100644 --- a/.github/workflows/build_docs.yaml +++ b/.github/workflows/build_docs.yaml @@ -17,14 +17,14 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - python-version: ["3.11"] + python-version: ["3.12"] os: [ubuntu-latest] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7.0.0 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6.3.0 with: python-version: ${{ matrix.python-version }} diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index 74d7e37..135a9ae 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -12,11 +12,11 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7.0.0 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6.3.0 with: - python-version: '3.11' + python-version: '3.12' cache: 'pip' - name: Installing dependencies diff --git a/.github/workflows/publish_release.yml b/.github/workflows/publish_release.yml index 138de02..c99095e 100644 --- a/.github/workflows/publish_release.yml +++ b/.github/workflows/publish_release.yml @@ -13,11 +13,11 @@ jobs: environment: production runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7.0.0 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6.3.0 with: - python-version: "3.11" + python-version: "3.12" - name: build run: | diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 3b84395..db06124 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -14,24 +14,24 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - python-version: ["3.11", "3.12"] + python-version: ["3.12"] os: [ubuntu-latest, windows-latest, macos-15] permissions: contents: read id-token: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7.0.0 - name: Azure Login - uses: Azure/login@v2 + uses: Azure/login@v3.0.0 with: client-id: f96c150d-cacf-4257-9cc9-54b2c68ec4ce tenant-id: 3aa4a235-b6e2-48d5-9195-7fcf05b459b0 subscription-id: 87897772-fb27-495f-ae40-486a2df57baa - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6.3.0 with: python-version: ${{ matrix.python-version }} diff --git a/pyproject.toml b/pyproject.toml index ab48eae..0620af3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,7 +52,7 @@ exclude = [".env", ".git", ".github", ".venv", "venv"] line-length = 79 [tool.ruff.lint] -ignore = ["E501", "N802"] +ignore = ["E501", "N802", "TRY002", "BLE001"] extend-select = [ "C4", # Flake8-comprehensions diff --git a/src/sumo/wrapper/__init__.py b/src/sumo/wrapper/__init__.py index c7714de..08eba2a 100644 --- a/src/sumo/wrapper/__init__.py +++ b/src/sumo/wrapper/__init__.py @@ -8,4 +8,4 @@ except ImportError: __version__ = "0.0.0" -__all__ = ["SumoClient", "RetryStrategy"] +__all__ = ["RetryStrategy", "SumoClient"] diff --git a/src/sumo/wrapper/_auth_provider.py b/src/sumo/wrapper/_auth_provider.py index cc0bc36..3ba140a 100644 --- a/src/sumo/wrapper/_auth_provider.py +++ b/src/sumo/wrapper/_auth_provider.py @@ -5,9 +5,8 @@ import stat import sys import time -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from pathlib import Path -from typing import Dict from urllib.parse import parse_qs import jwt @@ -55,8 +54,6 @@ def __init__(self, resource_id): self._login_timeout_minutes = 5 os.system("") # Ensure color init on all platforms (win10) - return - @tn.retry( retry=tn.retry_if_exception(_maybe_nfs_exception), stop=tn.stop_after_attempt(6), @@ -77,7 +74,7 @@ def get_token(self): # ELSE return result["access_token"] - def get_authorization(self) -> Dict: + def get_authorization(self) -> dict: token = self.get_token() if token is None: return {} @@ -92,7 +89,6 @@ def store_shared_access_key_for_case(self, case_uuid, token): ) as f: f.write(token) protect_token_cache(self._resource_id, ".sharedkey", case_uuid) - return def has_case_token(self, case_uuid): return os.path.exists( @@ -102,15 +98,11 @@ def has_case_token(self, case_uuid): def delete_token(self): return False - pass - class AuthProviderNone(AuthProvider): def get_token(self): raise Exception("No valid authorization provider found.") - pass - class AuthProviderSilent(AuthProvider): def __init__(self, client_id, authority, resource_id): @@ -130,7 +122,6 @@ def __init__(self, access_token): payload = jwt.decode(access_token, options={"verify_signature": False}) self._expires = payload["exp"] self._resource_id = payload["aud"] - return def get_token(self): if time.time() >= self._expires: @@ -138,8 +129,6 @@ def get_token(self): # ELSE return self._access_token - pass - class AuthProviderRefreshToken(AuthProvider): def __init__(self, refresh_token, client_id, authority, resource_id): @@ -149,9 +138,6 @@ def __init__(self, refresh_token, client_id, authority, resource_id): ) self._scope = scope_for_resource(resource_id) self._app.acquire_token_by_refresh_token(refresh_token, [self._scope]) - return - - pass @tn.retry( @@ -186,14 +172,10 @@ def get_token_cache(resource_id, suffix): token = FilePersistence(token_path).load() with open(token_path, "w") as f: f.truncate() - pass encrypted_persistence.save(token) - pass - pass persistence = build_encrypted_persistence(token_path) cache = PersistedTokenCache(persistence) - pass return cache @@ -218,10 +200,7 @@ def protect_token_cache(resource_id, suffix, case_uuid=None): foldermode = stat.filemode(os.stat(folder).st_mode) if foldermode != "drwx------": os.chmod(os.path.dirname(token_path), 0o700) - pass - pass return - pass class AuthProviderInteractive(AuthProvider): @@ -237,8 +216,6 @@ def __init__(self, client_id, authority, resource_id): if self.get_token() is None: self.login() - pass - return @tn.retry( retry=tn.retry_if_exception(_maybe_nfs_exception), @@ -262,7 +239,7 @@ def login(self): + "that is before " + str( ( - datetime.now() + datetime.now().astimezone() + timedelta(minutes=self._login_timeout_minutes) ).strftime("%H:%M:%S") ) @@ -291,8 +268,6 @@ def login(self): ) return - pass - class AuthProviderDeviceCode(AuthProvider): def __init__(self, client_id, authority, resource_id): @@ -305,8 +280,6 @@ def __init__(self, client_id, authority, resource_id): self._scope = scope_for_resource(resource_id) if self.get_token() is None: self.login() - pass - return @tn.retry( retry=tn.retry_if_exception(_maybe_nfs_exception), @@ -324,9 +297,10 @@ def login(self): flow = self._app.initiate_device_flow(scopes) if "error" in flow: print( - "\n\n \033[31m" - + "Failed to initiate device-code login. Err: %s\033[0m" - % json.dumps(flow, indent=4) + ( + "\n\n \033[31m" + + "Failed to initiate device-code login. Err: {}\033[0m" + ).format(json.dumps(flow, indent=4)) ) return flow["expires_at"] = ( @@ -363,15 +337,12 @@ def login(self): return - pass - class AuthProviderManaged(AuthProvider): def __init__(self, resource_id): super().__init__(resource_id) self._app = ManagedIdentityCredential() self._scope = scope_for_resource(resource_id) - return @tn.retry( retry=tn.retry_if_exception(_maybe_nfs_exception), @@ -386,8 +357,6 @@ def __init__(self, resource_id): def get_token(self): return self._app.get_token(self._scope).token - pass - class AuthProviderSumoToken(AuthProvider): @tn.retry( @@ -407,8 +376,6 @@ def __init__(self, resource_id, case_uuid=None): with open(self.token_path, "r") as f: self._token = f.readline().strip() - return - def get_token(self): return self._token @@ -460,7 +427,6 @@ def get_auth_provider( token = auth_silent.get_token() if token is not None: return auth_silent - pass # ELSE if all( os.getenv(x) @@ -484,7 +450,6 @@ def get_auth_provider( "\n\n\033[1mDetected chromium lockfile for different node; using firefox to authenticate.\033[0m" ) os.environ["BROWSER"] = "firefox" - pass return AuthProviderInteractive(client_id, authority, resource_id) # ELSE @@ -510,15 +475,10 @@ def cleanup_shared_keys(): token = file.read() pq = parse_qs(token) se = pq["se"][0] - end = datetime.strptime(se, "%Y-%m-%dT%H:%M:%S.%fZ") - now = datetime.now(timezone.utc) + end = datetime.fromisoformat(se) + now = datetime.now(UTC) if now.timestamp() > end.timestamp(): os.unlink(ff) - pass - pass - pass - except Exception: + except Exception: # noqa: S110 pass - pass - pass return diff --git a/src/sumo/wrapper/_blob_client.py b/src/sumo/wrapper/_blob_client.py index 3aa38fb..7f29be9 100644 --- a/src/sumo/wrapper/_blob_client.py +++ b/src/sumo/wrapper/_blob_client.py @@ -12,7 +12,6 @@ def __init__(self, client, async_client, timeout, retry_strategy): self._async_client = async_client self._timeout = timeout self._retry_strategy = retry_strategy - return @raise_for_status def upload_blob(self, blob: bytes, url: str): diff --git a/src/sumo/wrapper/_logging.py b/src/sumo/wrapper/_logging.py index 4f32e19..97fe5c8 100644 --- a/src/sumo/wrapper/_logging.py +++ b/src/sumo/wrapper/_logging.py @@ -1,17 +1,16 @@ import logging -from datetime import datetime, timezone +from datetime import UTC, datetime class LogHandlerSumo(logging.Handler): def __init__(self, sumo_client): logging.Handler.__init__(self) self._sumoClient = sumo_client - return def emit(self, record): try: dt = ( - datetime.now(timezone.utc) + datetime.now(UTC) .replace(microsecond=0, tzinfo=None) .isoformat() + "Z" @@ -32,10 +31,6 @@ def emit(self, record): json["details"] = record.__dict__.get("details") self._sumoClient.post("/message-log/new", json=json) - except Exception: + except Exception: # noqa: S110 # Never fail on logging pass - - return - - pass diff --git a/src/sumo/wrapper/_retry_strategy.py b/src/sumo/wrapper/_retry_strategy.py index 1928dfa..74fd954 100644 --- a/src/sumo/wrapper/_retry_strategy.py +++ b/src/sumo/wrapper/_retry_strategy.py @@ -12,7 +12,6 @@ def _log_retry_info(retry_state): f"Attempts: {retry_state.attempt_number}; " f"Elapsed: {retry_state.seconds_since_start}" ) - return # Define the conditions for retrying based on exception types @@ -49,7 +48,6 @@ def __init__( self._multiplier = multiplier self._exp_base = exp_base self._before_sleep = before_sleep - return def make_retryer(self) -> tn.Retrying: return tn.Retrying( diff --git a/src/sumo/wrapper/sumo_client.py b/src/sumo/wrapper/sumo_client.py index b2cf89f..55c3c44 100644 --- a/src/sumo/wrapper/sumo_client.py +++ b/src/sumo/wrapper/sumo_client.py @@ -4,7 +4,6 @@ import os import re import time -from typing import Dict, Optional, Tuple import httpx import jwt @@ -38,16 +37,16 @@ class SumoClient: def __init__( self, env: str = "prod", - token: Optional[str] = None, + token: str | None = None, interactive: bool = True, devicecode: bool = False, verbosity: str = "CRITICAL", - retry_strategy=RetryStrategy(), + retry_strategy=None, timeout=DEFAULT_TIMEOUT, case_uuid=None, http_client=None, async_http_client=None, - client_id: Optional[str] = None, + client_id: str | None = None, ): """Initialize a new Sumo object @@ -71,10 +70,17 @@ def __init__( AZURE_CLIENT_ID from environment variables or the config. Defaults to None. """ + if retry_strategy is None: + retry_strategy = RetryStrategy() logger.setLevel(verbosity) global well_known if well_known is None: - well_known = httpx.get(WELL_KNOWN).json() + + def _get(): + return httpx.get(WELL_KNOWN, timeout=timeout) + + retryer = retry_strategy.make_retryer() + well_known = retryer(_get).json() if env not in well_known["envs"]: raise ValueError(f"Invalid environment: {env}") @@ -128,8 +134,6 @@ def __init__( "treating it as a refresh token" ) refresh_token = token - pass - pass cleanup_shared_keys() self.auth = get_auth_provider( @@ -144,7 +148,6 @@ def __init__( ) self.base_url = base_url - return def __enter__(self): return self @@ -165,19 +168,16 @@ async def __aexit__(self, *_): def __del__(self): if self._client is not None and not self._borrowed_client: self._client.close() - pass if self._async_client is not None and not self._borrowed_async_client: async def closeit(client): await client.aclose() - return try: loop = asyncio.get_running_loop() loop.create_task(closeit(self._async_client)) except RuntimeError: pass - pass def authenticate(self): if self.auth is None: @@ -222,8 +222,8 @@ def _handle_invalid_shared_key(self): def get( self, path: str, - params: Optional[Dict] = None, - retry_strategy: Optional[RetryStrategy] = None, + params: dict | None = None, + retry_strategy: RetryStrategy | None = None, ) -> httpx.Response: """Performs a GET-request to the Sumo API. @@ -261,12 +261,12 @@ def get( follow_redirects = False if ( re.match( - r"^/objects\('[0-9a-fA-F-]{8}-[0-9a-fA-F-]{4}-[0-9a-fA-F-]{4}-[0-9a-fA-F-]{4}-[0-9a-fA-F-]{12}'\)/blob$", # noqa: E501 + r"^/objects\('[0-9a-fA-F-]{8}-[0-9a-fA-F-]{4}-[0-9a-fA-F-]{4}-[0-9a-fA-F-]{4}-[0-9a-fA-F-]{12}'\)/blob$", path, ) is not None or re.match( - r"^/tasks\('[0-9a-fA-F-]{8}-[0-9a-fA-F-]{4}-[0-9a-fA-F-]{4}-[0-9a-fA-F-]{4}-[0-9a-fA-F-]{12}'\)/result$", # noqa: E501 + r"^/tasks\('[0-9a-fA-F-]{8}-[0-9a-fA-F-]{4}-[0-9a-fA-F-]{4}-[0-9a-fA-F-]{4}-[0-9a-fA-F-]{12}'\)/result$", path, ) is not None @@ -291,10 +291,10 @@ def _get(): def post( self, path: str, - blob: Optional[bytes] = None, - json: Optional[dict] = None, - params: Optional[dict] = None, - retry_strategy: Optional[RetryStrategy] = None, + blob: bytes | None = None, + json: dict | None = None, + params: dict | None = None, + retry_strategy: RetryStrategy | None = None, ) -> httpx.Response: """Performs a POST-request to the Sumo API. @@ -369,9 +369,9 @@ def _post(): def put( self, path: str, - blob: Optional[bytes] = None, - json: Optional[dict] = None, - retry_strategy: Optional[RetryStrategy] = None, + blob: bytes | None = None, + json: dict | None = None, + retry_strategy: RetryStrategy | None = None, ) -> httpx.Response: """Performs a PUT-request to the Sumo API. @@ -421,8 +421,8 @@ def _put(): def delete( self, path: str, - params: Optional[dict] = None, - retry_strategy: Optional[RetryStrategy] = None, + params: dict | None = None, + retry_strategy: RetryStrategy | None = None, ) -> httpx.Response: """Performs a DELETE-request to the Sumo API. @@ -462,7 +462,7 @@ def _delete(): return retryer(_delete) - def _get_retry_details(self, response_in) -> Tuple[str, int]: + def _get_retry_details(self, response_in) -> tuple[str, int]: assert response_in.status_code == 202, ( "Incorrect status code; expcted 202" ) @@ -480,7 +480,7 @@ def poll( self, response_in: httpx.Response, timeout=None, - retry_strategy: Optional[RetryStrategy] = None, + retry_strategy: RetryStrategy | None = None, ) -> httpx.Response: """Poll a specific endpoint until a result is obtained. @@ -502,7 +502,6 @@ def poll( "No response within specified timeout." ) location, retry_after = self._get_retry_details(response) - pass def getLogger(self, name): """Gets a logger object that sends log objects into the message_log @@ -521,7 +520,6 @@ def getLogger(self, name): if len(logger.handlers) == 0: handler = LogHandlerSumo(self) logger.addHandler(handler) - pass return logger def create_shared_access_key_for_case(self, case_uuid): @@ -561,8 +559,8 @@ def client_for_case(self, case_uuid, interactive=False): async def get_async( self, path: str, - params: Optional[dict] = None, - retry_strategy: Optional[RetryStrategy] = None, + params: dict | None = None, + retry_strategy: RetryStrategy | None = None, ) -> httpx.Response: """Performs an async GET-request to the Sumo API. @@ -600,12 +598,12 @@ async def get_async( follow_redirects = False if ( re.match( - r"^/objects\('[0-9a-fA-F-]{8}-[0-9a-fA-F-]{4}-[0-9a-fA-F-]{4}-[0-9a-fA-F-]{4}-[0-9a-fA-F-]{12}'\)/blob$", # noqa: E501 + r"^/objects\('[0-9a-fA-F-]{8}-[0-9a-fA-F-]{4}-[0-9a-fA-F-]{4}-[0-9a-fA-F-]{4}-[0-9a-fA-F-]{12}'\)/blob$", path, ) is not None or re.match( - r"^/tasks\('[0-9a-fA-F-]{8}-[0-9a-fA-F-]{4}-[0-9a-fA-F-]{4}-[0-9a-fA-F-]{4}-[0-9a-fA-F-]{12}'\)/result$", # noqa: E501 + r"^/tasks\('[0-9a-fA-F-]{8}-[0-9a-fA-F-]{4}-[0-9a-fA-F-]{4}-[0-9a-fA-F-]{4}-[0-9a-fA-F-]{12}'\)/result$", path, ) is not None @@ -631,10 +629,10 @@ async def _get(): async def post_async( self, path: str, - blob: Optional[bytes] = None, - json: Optional[dict] = None, - params: Optional[dict] = None, - retry_strategy: Optional[RetryStrategy] = None, + blob: bytes | None = None, + json: dict | None = None, + params: dict | None = None, + retry_strategy: RetryStrategy | None = None, ) -> httpx.Response: """Performs an async POST-request to the Sumo API. @@ -710,9 +708,9 @@ async def _post(): async def put_async( self, path: str, - blob: Optional[bytes] = None, - json: Optional[dict] = None, - retry_strategy: Optional[RetryStrategy] = None, + blob: bytes | None = None, + json: dict | None = None, + retry_strategy: RetryStrategy | None = None, ) -> httpx.Response: """Performs an async PUT-request to the Sumo API. @@ -762,8 +760,8 @@ async def _put(): async def delete_async( self, path: str, - params: Optional[dict] = None, - retry_strategy: Optional[RetryStrategy] = None, + params: dict | None = None, + retry_strategy: RetryStrategy | None = None, ) -> httpx.Response: """Performs an async DELETE-request to the Sumo API. @@ -807,7 +805,7 @@ async def poll_async( self, response_in: httpx.Response, timeout=None, - retry_strategy: Optional[RetryStrategy] = None, + retry_strategy: RetryStrategy | None = None, ) -> httpx.Response: """Poll a specific endpoint until a result is obtained. @@ -831,4 +829,3 @@ async def poll_async( "No response within specified timeout." ) location, retry_after = self._get_retry_details(response) - pass diff --git a/tests/test_sumo_thin_client.py b/tests/test_sumo_thin_client.py index 071812f..33216b2 100644 --- a/tests/test_sumo_thin_client.py +++ b/tests/test_sumo_thin_client.py @@ -7,10 +7,11 @@ import pytest import yaml +from httpx import HTTPStatusError sys.path.append(os.path.abspath(os.path.join("src"))) -from sumo.wrapper import SumoClient # noqa: E402 +from sumo.wrapper import SumoClient def _upload_parent_object(conn, json): @@ -99,7 +100,7 @@ def test_upload_search_delete_ensemble_child(token): ) except Exception as ex: print(ex.response.text) - raise ex + raise assert 200 <= response_surface.status_code <= 202 assert isinstance(response_surface.json(), dict) @@ -169,10 +170,8 @@ def test_fail_on_wrong_metadata(token): Upload a parent object with erroneous metadata, confirm failure """ conn = SumoClient(env="dev", token=token) - with pytest.raises(Exception): - assert _upload_parent_object( - conn=conn, json={"some field": "some value"} - ) + with pytest.raises(HTTPStatusError): + _upload_parent_object(conn=conn, json={"some field": "some value"}) def test_upload_duplicate_ensemble(token): @@ -220,8 +219,8 @@ def test_upload_duplicate_ensemble(token): sleep(61) # Search for ensemble - with pytest.raises(Exception): - assert _download_object(conn, object_id=case_id2) + with pytest.raises(HTTPStatusError): + _download_object(conn, object_id=case_id2) def test_poll(token):