From 2f2d27ba9fa66a8400ecb42f4bb5ee2a161a6bd2 Mon Sep 17 00:00:00 2001 From: MrCreosote Date: Sun, 12 Oct 2025 11:29:42 -0700 Subject: [PATCH] Refactor modules to make imports easier Now it's always ``` from kbase.auth import ``` ...and puts everything else in an _auth package to protect from collisions if we want to put other modules in the kbase namespace --- scripts/process_unasync.py | 6 +-- src/kbase/{auth => _auth}/_async/client.py | 43 ++-------------------- src/kbase/{auth => _auth}/_sync/client.py | 43 ++-------------------- src/kbase/{auth => _auth}/exceptions.py | 0 src/kbase/_auth/models.py | 43 ++++++++++++++++++++++ src/kbase/auth.py | 15 ++++++++ src/kbase/auth/client.py | 6 --- test/test_client.py | 35 +++++++++--------- 8 files changed, 87 insertions(+), 104 deletions(-) rename src/kbase/{auth => _auth}/_async/client.py (85%) rename src/kbase/{auth => _auth}/_sync/client.py (85%) rename src/kbase/{auth => _auth}/exceptions.py (100%) create mode 100644 src/kbase/_auth/models.py create mode 100644 src/kbase/auth.py delete mode 100644 src/kbase/auth/client.py diff --git a/scripts/process_unasync.py b/scripts/process_unasync.py index 2bff851..950c72b 100644 --- a/scripts/process_unasync.py +++ b/scripts/process_unasync.py @@ -16,14 +16,14 @@ def main(): rules = [ unasync.Rule( - fromdir="/src/kbase/auth/_async/", - todir="/src/kbase/auth/_sync/", + fromdir="/src/kbase/_auth/_async/", + todir="/src/kbase/_auth/_sync/", additional_replacements=additional_replacements, ), ] filepaths = [ - str(Path(__file__).parent.parent / "src" / "kbase" / "auth" / "_async" / "client.py") + str(Path(__file__).parent.parent / "src" / "kbase" / "_auth" / "_async" / "client.py") ] unasync.unasync_files(filepaths, rules) diff --git a/src/kbase/auth/_async/client.py b/src/kbase/_auth/_async/client.py similarity index 85% rename from src/kbase/auth/_async/client.py rename to src/kbase/_auth/_async/client.py index 4905d34..429abc1 100644 --- a/src/kbase/auth/_async/client.py +++ b/src/kbase/_auth/_async/client.py @@ -8,55 +8,20 @@ # the sync client. from cacheout.lru import LRUCache -from dataclasses import dataclass, fields import httpx import logging import time from typing import Self, Callable -from uuid import UUID -from kbase.auth.exceptions import InvalidTokenError, InvalidUserError +from kbase._auth.exceptions import InvalidTokenError, InvalidUserError +from kbase._auth.models import Token, User, VALID_TOKEN_FIELDS, VALID_USER_FIELDS # TODO PUBLISH make a pypi kbase org and publish there # TODO RELIABILITY could add retries for these methods, tenacity looks useful # should be safe since they're all read only -# TODO NOW CODE make a kbase/auth.py module, move other code into _auth, and import everything -# TODO NOW CODE move Token and User into a common class # We might want to expand exceptions to include the request ID for debugging purposes -@dataclass -class Token: - """ A KBase authentication token. """ - id: UUID - """ The token's unique ID. """ - user: str - """ The username of the user associated with the token. """ - created: int - """ The time the token was created in epoch milliseconds. """ - expires: int - """ The time the token expires in epoch milliseconds. """ - cachefor: int - """ The time the token should be cached for in milliseconds. """ - # TODO MFA add mfa info when the auth service supports it - -_VALID_TOKEN_FIELDS = {f.name for f in fields(Token)} - - -@dataclass -class User: - """ Information about a KBase user. """ - user: str - """ The username of the user associated with the token. """ - customroles: list[str] - """ The Auth2 custom roles the user possesses. """ - # Not seeing any other fields that are generally useful right now - # Don't really want to expose idents unless there's a very good reason - - -_VALID_USER_FIELDS = {f.name for f in fields(User)} - - def _require_string(putative: str, name: str) -> str: if not isinstance(putative, str) or not putative.strip(): raise ValueError(f"{name} is required and cannot be a whitespace only string") @@ -169,7 +134,7 @@ async def get_token(self, token: str, on_cache_miss: Callable[[], None]=None) -> if on_cache_miss: on_cache_miss() res = await self._get(self._token_url, headers={"Authorization": token}) - tk = Token(**{k: v for k, v in res.items() if k in _VALID_TOKEN_FIELDS}) + tk = Token(**{k: v for k, v in res.items() if k in VALID_TOKEN_FIELDS}) # TODO TEST later may want to add tests that change the cachefor value. self._token_cache.set(token, tk, ttl=tk.cachefor / 1000) return tk @@ -193,7 +158,7 @@ async def get_user(self, token: str, on_cache_miss: Callable[[], None]=None) -> on_cache_miss() tk = await self.get_token(token) res = await self._get(self._me_url, headers={"Authorization": token}) - u = User(**{k: v for k, v in res.items() if k in _VALID_USER_FIELDS}) + u = User(**{k: v for k, v in res.items() if k in VALID_USER_FIELDS}) # TODO TEST later may want to add tests that change the cachefor value. self._user_cache.set(token, u, ttl=tk.cachefor / 1000) return u diff --git a/src/kbase/auth/_sync/client.py b/src/kbase/_auth/_sync/client.py similarity index 85% rename from src/kbase/auth/_sync/client.py rename to src/kbase/_auth/_sync/client.py index f1af398..a52a538 100644 --- a/src/kbase/auth/_sync/client.py +++ b/src/kbase/_auth/_sync/client.py @@ -8,55 +8,20 @@ # the sync client. from cacheout.lru import LRUCache -from dataclasses import dataclass, fields import httpx import logging import time from typing import Self, Callable -from uuid import UUID -from kbase.auth.exceptions import InvalidTokenError, InvalidUserError +from kbase._auth.exceptions import InvalidTokenError, InvalidUserError +from kbase._auth.models import Token, User, VALID_TOKEN_FIELDS, VALID_USER_FIELDS # TODO PUBLISH make a pypi kbase org and publish there # TODO RELIABILITY could add retries for these methods, tenacity looks useful # should be safe since they're all read only -# TODO NOW CODE make a kbase/auth.py module, move other code into _auth, and import everything -# TODO NOW CODE move Token and User into a common class # We might want to expand exceptions to include the request ID for debugging purposes -@dataclass -class Token: - """ A KBase authentication token. """ - id: UUID - """ The token's unique ID. """ - user: str - """ The username of the user associated with the token. """ - created: int - """ The time the token was created in epoch milliseconds. """ - expires: int - """ The time the token expires in epoch milliseconds. """ - cachefor: int - """ The time the token should be cached for in milliseconds. """ - # TODO MFA add mfa info when the auth service supports it - -_VALID_TOKEN_FIELDS = {f.name for f in fields(Token)} - - -@dataclass -class User: - """ Information about a KBase user. """ - user: str - """ The username of the user associated with the token. """ - customroles: list[str] - """ The Auth2 custom roles the user possesses. """ - # Not seeing any other fields that are generally useful right now - # Don't really want to expose idents unless there's a very good reason - - -_VALID_USER_FIELDS = {f.name for f in fields(User)} - - def _require_string(putative: str, name: str) -> str: if not isinstance(putative, str) or not putative.strip(): raise ValueError(f"{name} is required and cannot be a whitespace only string") @@ -169,7 +134,7 @@ def get_token(self, token: str, on_cache_miss: Callable[[], None]=None) -> Token if on_cache_miss: on_cache_miss() res = self._get(self._token_url, headers={"Authorization": token}) - tk = Token(**{k: v for k, v in res.items() if k in _VALID_TOKEN_FIELDS}) + tk = Token(**{k: v for k, v in res.items() if k in VALID_TOKEN_FIELDS}) # TODO TEST later may want to add tests that change the cachefor value. self._token_cache.set(token, tk, ttl=tk.cachefor / 1000) return tk @@ -193,7 +158,7 @@ def get_user(self, token: str, on_cache_miss: Callable[[], None]=None) -> User: on_cache_miss() tk = self.get_token(token) res = self._get(self._me_url, headers={"Authorization": token}) - u = User(**{k: v for k, v in res.items() if k in _VALID_USER_FIELDS}) + u = User(**{k: v for k, v in res.items() if k in VALID_USER_FIELDS}) # TODO TEST later may want to add tests that change the cachefor value. self._user_cache.set(token, u, ttl=tk.cachefor / 1000) return u diff --git a/src/kbase/auth/exceptions.py b/src/kbase/_auth/exceptions.py similarity index 100% rename from src/kbase/auth/exceptions.py rename to src/kbase/_auth/exceptions.py diff --git a/src/kbase/_auth/models.py b/src/kbase/_auth/models.py new file mode 100644 index 0000000..7184518 --- /dev/null +++ b/src/kbase/_auth/models.py @@ -0,0 +1,43 @@ +""" +Data classes for the clients. +""" + +from dataclasses import dataclass, fields +from uuid import UUID + +@dataclass +class Token: + """ A KBase authentication token. """ + id: UUID + """ The token's unique ID. """ + user: str + """ The username of the user associated with the token. """ + created: int + """ The time the token was created in epoch milliseconds. """ + expires: int + """ The time the token expires in epoch milliseconds. """ + cachefor: int + """ The time the token should be cached for in milliseconds. """ + # TODO MFA add mfa info when the auth service supports it + +VALID_TOKEN_FIELDS: set[str] = {f.name for f in fields(Token)} +""" +The field names for the Token dataclass. +""" + + +@dataclass +class User: + """ Information about a KBase user. """ + user: str + """ The username of the user associated with the token. """ + customroles: list[str] + """ The Auth2 custom roles the user possesses. """ + # Not seeing any other fields that are generally useful right now + # Don't really want to expose idents unless there's a very good reason + + +VALID_USER_FIELDS: set[str] = {f.name for f in fields(User)} +""" +The field names for the user dataclass. +""" diff --git a/src/kbase/auth.py b/src/kbase/auth.py new file mode 100644 index 0000000..24ac442 --- /dev/null +++ b/src/kbase/auth.py @@ -0,0 +1,15 @@ +""" +The aync and sync versions of the KBase Auth Client. +""" + +from kbase._auth._async.client import AsyncKBaseAuthClient # @UnusedImport +from kbase._auth._sync.client import KBaseAuthClient # @UnusedImport +from kbase._auth.exceptions import ( + AuthenticationError, # @UnusedImport + InvalidTokenError, # @UnusedImport + InvalidUserError, # @UnusedImport +) +from kbase._auth.models import ( + Token, # @UnusedImport + User, # @UnusedImport +) diff --git a/src/kbase/auth/client.py b/src/kbase/auth/client.py deleted file mode 100644 index 9b8b353..0000000 --- a/src/kbase/auth/client.py +++ /dev/null @@ -1,6 +0,0 @@ -""" -The aync and sync versions of the KBase Auth Client. -""" - -from kbase.auth._async.client import AsyncKBaseAuthClient # @UnusedImport -from kbase.auth._sync.client import KBaseAuthClient # @UnusedImport diff --git a/test/test_client.py b/test/test_client.py index b43ce72..bad72ae 100644 --- a/test/test_client.py +++ b/test/test_client.py @@ -5,8 +5,14 @@ from conftest import AUTH_URL, AUTH_VERSION -from kbase.auth.client import KBaseAuthClient, AsyncKBaseAuthClient -from kbase.auth.exceptions import InvalidTokenError, InvalidUserError +from kbase.auth import ( + AsyncKBaseAuthClient, + InvalidTokenError, + InvalidUserError, + KBaseAuthClient, + Token, + User, +) async def _create_fail(url: str, expected: Exception, cachesize=1, timer=time.time): @@ -90,15 +96,17 @@ async def test_get_token_basic(auth_users): async with await AsyncKBaseAuthClient.create(AUTH_URL) as cli: t2 = await cli.get_token(auth_users["user_random1"]) + assert t1 == Token( + id=t1.id, user="user", cachefor=300000, created=t1.created, expires=t1.expires + ) assert is_valid_uuid(t1.id) - assert t1.user == "user" - assert t1.cachefor == 300000 assert time_close_to_now(t1.created, 10) assert t1.expires - t1.created == 3600000 + assert t2 == Token( + id=t2.id, user="user_random1", cachefor=300000, created=t2.created, expires=t2.expires + ) assert is_valid_uuid(t2.id) - assert t2.user == "user_random1" - assert t2.cachefor == 300000 assert time_close_to_now(t2.created, 10) assert t2.expires - t2.created == 3600000 @@ -222,17 +230,10 @@ async def test_get_user_basic(auth_users): u3 = await cli.get_user(auth_users["user_random1"]) u4 = await cli.get_user(auth_users["user_random2"]) - assert u1.user == "user" - assert u1.customroles == [] - - assert u2.user == "user_all" - assert u2.customroles == ["random1", "random2"] - - assert u3.user == "user_random1" - assert u3.customroles == ["random1"] - - assert u4.user == "user_random2" - assert u4.customroles == ["random2"] + assert u1 == User(user="user", customroles=[]) + assert u2 == User(user="user_all", customroles=["random1", "random2"]) + assert u3 == User(user="user_random1", customroles=["random1"]) + assert u4 == User(user="user_random2", customroles=["random2"]) @pytest.mark.asyncio