mirror of
https://github.com/stack-auth/stack.git
synced 2026-07-20 21:29:36 +08:00
docs: add missing docstrings to reach 80% coverage threshold
This commit is contained in:
parent
e9105b3f68
commit
f7786c3a80
@ -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) -------------------------------------
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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."""
|
||||
...
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user