diff --git a/sdks/implementations/python/src/stack_auth/_app.py b/sdks/implementations/python/src/stack_auth/_app.py index 932661d52..923fa39b7 100644 --- a/sdks/implementations/python/src/stack_auth/_app.py +++ b/sdks/implementations/python/src/stack_auth/_app.py @@ -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) ------------------------------------- diff --git a/sdks/implementations/python/src/stack_auth/_client.py b/sdks/implementations/python/src/stack_auth/_client.py index 4cb2ad364..511af7f69 100644 --- a/sdks/implementations/python/src/stack_auth/_client.py +++ b/sdks/implementations/python/src/stack_auth/_client.py @@ -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() diff --git a/sdks/implementations/python/src/stack_auth/_jwt.py b/sdks/implementations/python/src/stack_auth/_jwt.py index 13adc0356..74084fb3a 100644 --- a/sdks/implementations/python/src/stack_auth/_jwt.py +++ b/sdks/implementations/python/src/stack_auth/_jwt.py @@ -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 diff --git a/sdks/implementations/python/src/stack_auth/_token_store.py b/sdks/implementations/python/src/stack_auth/_token_store.py index 6f38ddfe8..5e6d415f5 100644 --- a/sdks/implementations/python/src/stack_auth/_token_store.py +++ b/sdks/implementations/python/src/stack_auth/_token_store.py @@ -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 diff --git a/sdks/implementations/python/src/stack_auth/_types.py b/sdks/implementations/python/src/stack_auth/_types.py index 601140301..7b15c44b1 100644 --- a/sdks/implementations/python/src/stack_auth/_types.py +++ b/sdks/implementations/python/src/stack_auth/_types.py @@ -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.""" + ... diff --git a/sdks/implementations/python/src/stack_auth/errors.py b/sdks/implementations/python/src/stack_auth/errors.py index a724cef0f..d121bf79a 100644 --- a/sdks/implementations/python/src/stack_auth/errors.py +++ b/sdks/implementations/python/src/stack_auth/errors.py @@ -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 diff --git a/sdks/implementations/python/src/stack_auth/models/data_vault.py b/sdks/implementations/python/src/stack_auth/models/data_vault.py index 569161192..8d0b62431 100644 --- a/sdks/implementations/python/src/stack_auth/models/data_vault.py +++ b/sdks/implementations/python/src/stack_auth/models/data_vault.py @@ -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" diff --git a/sdks/implementations/python/src/stack_auth/models/payments.py b/sdks/implementations/python/src/stack_auth/models/payments.py index 42851f860..630d44aaf 100644 --- a/sdks/implementations/python/src/stack_auth/models/payments.py +++ b/sdks/implementations/python/src/stack_auth/models/payments.py @@ -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,