docs: add missing docstrings to reach 80% coverage threshold

This commit is contained in:
Ejiro Asiuwhu 2026-03-26 10:28:07 +01:00
parent e9105b3f68
commit f7786c3a80
8 changed files with 150 additions and 1 deletions

View File

@ -125,6 +125,15 @@ class StackServerApp:
base_url: str = DEFAULT_BASE_URL,
token_store: TokenStoreInit | None = None,
) -> None:
"""Initialize the synchronous Stack Auth application client.
Args:
project_id: The Stack Auth project identifier.
secret_server_key: Server-side secret key for authentication.
publishable_client_key: Optional publishable client key.
base_url: Stack Auth API base URL.
token_store: Optional token storage initializer for user sessions.
"""
self._project_id = project_id
self._client = SyncAPIClient(
project_id=project_id,
@ -150,9 +159,11 @@ class StackServerApp:
self._client.close()
def __enter__(self) -> StackServerApp:
"""Enter the context manager."""
return self
def __exit__(self, *_: Any) -> None:
"""Exit the context manager and close the client."""
self.close()
# -- partial user (local JWT decode) -------------------------------------
@ -1446,6 +1457,15 @@ class AsyncStackServerApp:
base_url: str = DEFAULT_BASE_URL,
token_store: TokenStoreInit | None = None,
) -> None:
"""Initialize the asynchronous Stack Auth application client.
Args:
project_id: The Stack Auth project identifier.
secret_server_key: Server-side secret key for authentication.
publishable_client_key: Optional publishable client key.
base_url: Stack Auth API base URL.
token_store: Optional token storage initializer for user sessions.
"""
self._project_id = project_id
self._client = AsyncAPIClient(
project_id=project_id,
@ -1471,9 +1491,11 @@ class AsyncStackServerApp:
await self._client.aclose()
async def __aenter__(self) -> AsyncStackServerApp:
"""Enter the async context manager."""
return self
async def __aexit__(self, *_: Any) -> None:
"""Exit the async context manager and close the client."""
await self.aclose()
# -- partial user (local JWT decode) -------------------------------------

View File

@ -40,6 +40,14 @@ class BaseAPIClient(Generic[HttpxClientT]):
secret_server_key: str,
publishable_client_key: str | None = None,
) -> None:
"""Initialize the API client.
Args:
base_url: Stack Auth API base URL.
project_id: The Stack Auth project identifier.
secret_server_key: Server-side secret key for authentication.
publishable_client_key: Optional publishable client key.
"""
self._base_url = base_url.rstrip("/")
self._project_id = project_id
self._secret_server_key = secret_server_key
@ -51,6 +59,7 @@ class BaseAPIClient(Generic[HttpxClientT]):
# ------------------------------------------------------------------
def _build_headers(self) -> dict[str, str]:
"""Build HTTP headers required for every Stack Auth API request."""
headers = {
"x-stack-project-id": self._project_id,
"x-stack-access-type": "server",
@ -64,6 +73,7 @@ class BaseAPIClient(Generic[HttpxClientT]):
return headers
def _build_url(self, path: str) -> str:
"""Construct the full API URL for the given endpoint path."""
return f"{self._base_url}/api/{API_VERSION}{path}"
# ------------------------------------------------------------------
@ -114,10 +124,12 @@ class BaseAPIClient(Generic[HttpxClientT]):
# ------------------------------------------------------------------
def _should_retry(self, method: str, attempt: int) -> bool:
"""Return True if the request method is idempotent and retries remain."""
return method.upper() in self.IDEMPOTENT_METHODS and attempt < self.MAX_RETRIES
@staticmethod
def _get_retry_delay(attempt: int, response: httpx.Response | None = None) -> float:
"""Calculate retry delay using Retry-After header or exponential backoff."""
if response is not None and response.status_code == 429:
retry_after = response.headers.get("Retry-After")
if retry_after is not None:
@ -142,6 +154,7 @@ class SyncAPIClient(BaseAPIClient[httpx.Client]):
"""Synchronous HTTP client using :class:`httpx.Client`."""
def _get_client(self) -> httpx.Client:
"""Return the underlying httpx.Client, creating it lazily if needed."""
if self._client is None:
self._client = httpx.Client(timeout=httpx.Timeout(30.0))
return self._client
@ -154,6 +167,20 @@ class SyncAPIClient(BaseAPIClient[httpx.Client]):
body: dict[str, Any] | None = None,
params: dict[str, Any] | None = None,
) -> dict[str, Any] | None:
"""Send a synchronous HTTP request with retry and error handling.
Args:
method: HTTP method (GET, POST, PUT, PATCH, DELETE).
path: API endpoint path (appended to the base URL).
body: Optional JSON body for the request.
params: Optional query parameters.
Returns:
Parsed JSON response dict, or None for empty responses.
Raises:
StackAuthError: On known API errors or non-2xx responses.
"""
url = self._build_url(path)
headers = self._build_headers()
@ -213,14 +240,17 @@ class SyncAPIClient(BaseAPIClient[httpx.Client]):
# ------------------------------------------------------------------
def close(self) -> None:
"""Close the underlying HTTP client and release resources."""
if self._client is not None:
self._client.close()
self._client = None
def __enter__(self) -> SyncAPIClient:
"""Enter the context manager."""
return self
def __exit__(self, *_: Any) -> None:
"""Exit the context manager and close the client."""
self.close()
@ -228,6 +258,7 @@ class AsyncAPIClient(BaseAPIClient[httpx.AsyncClient]):
"""Asynchronous HTTP client using :class:`httpx.AsyncClient`."""
def _get_client(self) -> httpx.AsyncClient:
"""Return the underlying httpx.AsyncClient, creating it lazily if needed."""
if self._client is None:
self._client = httpx.AsyncClient(timeout=httpx.Timeout(30.0))
return self._client
@ -240,6 +271,20 @@ class AsyncAPIClient(BaseAPIClient[httpx.AsyncClient]):
body: dict[str, Any] | None = None,
params: dict[str, Any] | None = None,
) -> dict[str, Any] | None:
"""Send an asynchronous HTTP request with retry and error handling.
Args:
method: HTTP method (GET, POST, PUT, PATCH, DELETE).
path: API endpoint path (appended to the base URL).
body: Optional JSON body for the request.
params: Optional query parameters.
Returns:
Parsed JSON response dict, or None for empty responses.
Raises:
StackAuthError: On known API errors or non-2xx responses.
"""
url = self._build_url(path)
headers = self._build_headers()
@ -296,12 +341,15 @@ class AsyncAPIClient(BaseAPIClient[httpx.AsyncClient]):
# ------------------------------------------------------------------
async def aclose(self) -> None:
"""Close the underlying async HTTP client and release resources."""
if self._client is not None:
await self._client.aclose()
self._client = None
async def __aenter__(self) -> AsyncAPIClient:
"""Enter the async context manager."""
return self
async def __aexit__(self, *_: Any) -> None:
"""Exit the async context manager and close the client."""
await self.aclose()

View File

@ -32,6 +32,12 @@ class AsyncJWKSFetcher:
"""
def __init__(self, jwks_url: str, http_client: httpx.AsyncClient) -> None:
"""Initialize the async JWKS fetcher.
Args:
jwks_url: The URL of the JWKS endpoint.
http_client: An httpx.AsyncClient for making HTTP requests.
"""
self._jwks_url = jwks_url
self._http_client = http_client
self._cache: dict[str, Any] | None = None
@ -84,6 +90,12 @@ class SyncJWKSFetcher:
"""
def __init__(self, jwks_url: str, http_client: httpx.Client) -> None:
"""Initialize the sync JWKS fetcher.
Args:
jwks_url: The URL of the JWKS endpoint.
http_client: An httpx.Client for making HTTP requests.
"""
self._jwks_url = jwks_url
self._http_client = http_client
self._cache: dict[str, Any] | None = None

View File

@ -38,6 +38,7 @@ class TokenStore(ABC):
"""
def __init__(self) -> None:
"""Initialize the token store with sync and async CAS locks."""
self._sync_lock = threading.Lock()
self._async_lock = asyncio.Lock()
@ -70,14 +71,17 @@ class MemoryTokenStore(TokenStore):
"""In-memory token store. Shared per project_id via the module registry."""
def __init__(self) -> None:
"""Initialize with empty access and refresh tokens."""
super().__init__()
self._access_token: str | None = None
self._refresh_token: str | None = None
def get_stored_access_token(self) -> str | None:
"""Return the stored access token, or None if not set."""
return self._access_token
def get_stored_refresh_token(self) -> str | None:
"""Return the stored refresh token, or None if not set."""
return self._refresh_token
def compare_and_set(
@ -86,6 +90,7 @@ class MemoryTokenStore(TokenStore):
new_refresh_token: str | None,
new_access_token: str | None,
) -> None:
"""Atomically update tokens if the current refresh token matches."""
if self._refresh_token == compare_refresh_token:
self._refresh_token = new_refresh_token
self._access_token = new_access_token
@ -102,14 +107,22 @@ class ExplicitTokenStore(TokenStore):
"""
def __init__(self, access_token: str | None = None, refresh_token: str | None = None) -> None:
"""Initialize with explicit token values.
Args:
access_token: Initial access token, or None.
refresh_token: Initial refresh token, or None.
"""
super().__init__()
self._access_token: str | None = access_token
self._refresh_token: str | None = refresh_token
def get_stored_access_token(self) -> str | None:
"""Return the stored access token, or None if not set."""
return self._access_token
def get_stored_refresh_token(self) -> str | None:
"""Return the stored refresh token, or None if not set."""
return self._refresh_token
def compare_and_set(
@ -118,6 +131,7 @@ class ExplicitTokenStore(TokenStore):
new_refresh_token: str | None,
new_access_token: str | None,
) -> None:
"""Atomically update tokens if the current refresh token matches."""
if self._refresh_token == compare_refresh_token:
self._refresh_token = new_refresh_token
self._access_token = new_access_token
@ -134,6 +148,11 @@ class RequestTokenStore(TokenStore):
"""
def __init__(self, request: RequestLike) -> None:
"""Initialize by extracting tokens from the request's x-stack-auth header.
Args:
request: A request-like object whose headers may contain x-stack-auth.
"""
super().__init__()
self._access_token: str | None = None
self._refresh_token: str | None = None
@ -147,9 +166,11 @@ class RequestTokenStore(TokenStore):
pass
def get_stored_access_token(self) -> str | None:
"""Return the stored access token, or None if not set."""
return self._access_token
def get_stored_refresh_token(self) -> str | None:
"""Return the stored refresh token, or None if not set."""
return self._refresh_token
def compare_and_set(
@ -158,6 +179,7 @@ class RequestTokenStore(TokenStore):
new_refresh_token: str | None,
new_access_token: str | None,
) -> None:
"""Atomically update tokens if the current refresh token matches."""
if self._refresh_token == compare_refresh_token:
self._refresh_token = new_refresh_token
self._access_token = new_access_token

View File

@ -5,5 +5,9 @@ from typing import Mapping, Protocol, runtime_checkable
@runtime_checkable
class RequestLike(Protocol):
"""Protocol for objects that expose HTTP headers (e.g. Django/Flask/Starlette requests)."""
@property
def headers(self) -> Mapping[str, str]: ...
def headers(self) -> Mapping[str, str]:
"""Return the request headers as a string mapping."""
...

View File

@ -15,6 +15,13 @@ class StackAuthError(Exception):
"""
def __init__(self, code: str, message: str, details: dict[str, Any] | None = None) -> None:
"""Initialize a StackAuthError.
Args:
code: The error code string from the Stack Auth API.
message: Human-readable error description.
details: Optional dictionary with additional error context.
"""
self.code = code
self.message = message
self.details = details

View File

@ -22,6 +22,12 @@ class DataVaultStore:
"""
def __init__(self, store_id: str, *, _client: Any) -> None:
"""Initialize a synchronous data vault store.
Args:
store_id: The data vault store identifier.
_client: The internal HTTP client used for API requests.
"""
self.id = store_id
self._client = _client
self._base_path = f"/data-vault/stores/{store_id}/items"
@ -69,6 +75,12 @@ class AsyncDataVaultStore:
"""
def __init__(self, store_id: str, *, _client: Any) -> None:
"""Initialize an asynchronous data vault store.
Args:
store_id: The data vault store identifier.
_client: The internal async HTTP client used for API requests.
"""
self.id = store_id
self._client = _client
self._base_path = f"/data-vault/stores/{store_id}/items"

View File

@ -47,6 +47,16 @@ class ServerItem:
_customer_id_field: str,
_customer_id_value: str,
) -> None:
"""Initialize a server-side item from an Item model.
Args:
item: The underlying Item data model.
_client: The internal HTTP client used for API requests.
_customer_path: API path prefix for the customer.
_item_id: The item identifier.
_customer_id_field: Body field name for the customer ID.
_customer_id_value: The customer ID value.
"""
self.display_name = item.display_name
self.quantity = item.quantity
self.non_negative_quantity = item.non_negative_quantity
@ -57,6 +67,7 @@ class ServerItem:
self._customer_id_value = _customer_id_value
def _quantity_body(self, quantity: int) -> dict[str, Any]:
"""Build the request body for a quantity change operation."""
return {
self._customer_id_field: self._customer_id_value,
"item_id": self._item_id,
@ -112,6 +123,16 @@ class AsyncServerItem:
_customer_id_field: str,
_customer_id_value: str,
) -> None:
"""Initialize an async server-side item from an Item model.
Args:
item: The underlying Item data model.
_client: The internal async HTTP client used for API requests.
_customer_path: API path prefix for the customer.
_item_id: The item identifier.
_customer_id_field: Body field name for the customer ID.
_customer_id_value: The customer ID value.
"""
self.display_name = item.display_name
self.quantity = item.quantity
self.non_negative_quantity = item.non_negative_quantity
@ -122,6 +143,7 @@ class AsyncServerItem:
self._customer_id_value = _customer_id_value
def _quantity_body(self, quantity: int) -> dict[str, Any]:
"""Build the request body for a quantity change operation."""
return {
self._customer_id_field: self._customer_id_value,
"item_id": self._item_id,