Async Python client for the Styrmin GraphQL API. The SDK exposes one resource
per backend area (auth, clusters, environments, deployments, drivers, tasks,
backups, environment-backups, environment-clones, schedules,
backup-storage-locations, users, groups) on a single
StyrminClient. The users and groups namespaces are admin-only — non-admin
sessions receive FORBIDDEN.
- Transport: httpx (
AsyncClient) - Models: Pydantic v2 (regenerated from the GraphQL schema)
- GraphQL operations: static
.graphqlfiles, loaded by name viaload_query() - Tooling: uv, ruff, ty, pytest
| Path | Role |
|---|---|
src/styrmin_sdk/client.py |
StyrminClient — public entry point. Constructs the transport and instantiates one resource per area, exposed as attributes (client.clusters, client.deployments, …). |
src/styrmin_sdk/_transport.py |
Internal HTTP + GraphQL transport over httpx.AsyncClient. Owns the connection lifetime; wraps HTTP errors into StyrminTransportError. The client timeout defaults to 30 seconds; execute(..., request_timeout=) overrides it for one call. |
src/styrmin_sdk/_queries.py |
load_query(name) helper — reads graphql/<name>.graphql from disk and returns the query string. |
src/styrmin_sdk/_input_checks.py |
Checks for config dicts sent through a generated input model: validate_input_strict and to_graphql_enum_names. See Updating a cluster config. |
src/styrmin_sdk/exceptions.py |
StyrminError hierarchy: StyrminTransportError (HTTP failures), StyrminGraphQLError (raw GraphQL errors), and StyrminAPIError with the subclasses (ResourceNotFoundError, ResourceAlreadyExistsError, OperationFailedError, DeploymentRestoringError, StyrminAuthenticationError, StyrminAuthorizationError) that _check_graphql_errors raises based on extensions.code. Each exception gives the code as typename and all the extensions of the first error as extensions. |
src/styrmin_sdk/resources/ |
One *Resource class per area; each method maps to one GraphQL operation. |
src/styrmin_sdk/resources/_base.py |
BaseResource — holds the transport reference and provides _check_graphql_errors() and _parse() helpers. |
src/styrmin_sdk/graphql/*.graphql |
One operation per file; file name is snake_case of the GraphQL operation name. |
src/styrmin_sdk/graphql/_generated/ |
Pydantic models regenerated by uv run invoke generate. Never hand-edit. |
tests/ |
Unit and contract tests for the SDK. |
The repo-root AGENTS.md documents repo-wide GraphQL conventions (one
operation per file, snake_case naming, named operations only). Those rules
apply here verbatim and are not restated.
Run from the repository root unless noted otherwise.
uv sync # Install dependencies
uv run pytest python_sdk/tests # SDK tests
uv run invoke validate-graphql # Validate every .graphql file
uv run styrmin export graphql-schema schema.graphql # Re-export the schema
uv run invoke generate # Regenerate Pydantic types
uv run invoke format lint # Required before commit
⚠️ Regenerating types is not enough. Adding a.graphqlfile and runninguv run invoke generateupdates the Pydantic models under_generated/, but the SDK's Python surface (client.py,resources/<area>.py) does not auto-wire new operations. You must update the Python code by hand, or the operation will not be reachable through the SDK. This is the single most common SDK mistake.
Step by step:
-
Author the
.graphqlfile. Place it undersrc/styrmin_sdk/graphql/, one operation per file, with a named operation (e.g.,query GetCluster { … }). The file name is the snake_case of the operation name (GetCluster→get_cluster.graphql). See the repo-rootAGENTS.md"GraphQL Queries" section for the full repo-wide rules. -
Regenerate the schema and types from the repo root:
uv run styrmin export graphql-schema schema.graphql uv run invoke generate -
Validate the operation:
uv run invoke validate-graphql
-
Wire the Python surface.
- Existing area (clusters, deployments, environments, …): add an
async def <method>(...)to the matching*Resourceclass insrc/styrmin_sdk/resources/<area>.py. Inside the method:- Build the
variablesdict. result = await self._transport.execute(load_query("<file_name>"), variables)self._check_graphql_errors(result)- Return either
self._parse(Outer, result["data"], "<field>", Inner)for single-object responses orOuter.model_validate(result["data"]).<field> or []for list-shaped responses (see existing methods for both shapes).
- Build the
- New area (no existing resource module): create a new
src/styrmin_sdk/resources/<area>.pywith a*Resource(BaseResource)subclass, then instantiate it insideStyrminClient.__init__so it becomes reachable asclient.<area>.
- Existing area (clusters, deployments, environments, …): add an
-
Add tests under
python_sdk/tests/. Cover the success path and at least one error code mapping (e.g.,NOT_FOUND→ResourceNotFoundError). -
Format and lint before commit:
uv run invoke format lint
From src/styrmin_sdk/resources/clusters.py:
async def get(
self,
cluster_id: str | UUID | None = None,
cluster_name: str | None = None,
) -> GetClusterCluster:
variables: dict[str, Any] = {}
if cluster_id is not None:
variables["clusterId"] = str(cluster_id)
if cluster_name is not None:
variables["clusterName"] = cluster_name
result = await self._transport.execute(load_query("get_cluster"), variables)
self._check_graphql_errors(result)
return self._parse(GetCluster, result["data"], "cluster", GetClusterCluster)load_query("get_cluster") reads graphql/get_cluster.graphql. The
response is sent through _transport.execute, structured GraphQL errors
are mapped to typed exceptions by _check_graphql_errors, and the
generated GetCluster outer model is validated and the inner cluster
field is returned as the typed GetClusterCluster.
DeploymentResource.get_by_name(name, *, environment_id) resolves a
deployment by its natural identity (name, environment). Internal and
soft-deleted rows are filtered out; a miss returns None. The return
type, GetDeploymentByNameDeployment, is re-exported from the package
root so callers can import it without reaching into _generated/.
from styrmin_sdk import GetDeploymentByNameDeployment, StyrminClient
async with StyrminClient(server_address="...") as client:
deployment: GetDeploymentByNameDeployment | None = (
await client.deployments.get_by_name("my-app", environment_id=env_id)
)See specs/archive/009-deployments-apply/contracts/sdk-apply.md
for the full contract, and ADR-048
for why the upsert lives in the SDK.
ClusterResource.update(config, cluster_id=None, cluster_name=None, *, dry_run=False)
replaces a cluster's whole config through the updateCluster mutation. It takes a
plain mapping, not the generated UpdateClusterInput, and returns the
updateCluster result, which styrmin_sdk.models exports as ClusterUpdateResult.
UpdateClusterInput is not exported.
The generated input models ignore keys they do not know. Under full replacement, a
misspelled key would be dropped, and the stored value it was meant to keep would be
removed. So before any request, update passes the config through
cluster_update_input (resources/clusters.py), which:
- refuses, with a
ValueErrorthat names each path, any key at any depth that is neither a field name nor an alias ofUpdateClusterInput(_input_checks.validate_input_strict); - accepts snake_case or camelCase keys;
- converts stored enum values, such as
httporNoScheduleasget()returns them, to their GraphQL names (_input_checks.to_graphql_enum_names).
styrminctl clusters update calls cluster_update_input before it connects to the
server. The server answers updateCluster only after it has started one redeploy per
changed deployment, so update waits up to 600 seconds
(UPDATE_CLUSTER_TIMEOUT_SECONDS) instead of the 30-second default. See
ADR-066 §4 and §5.
- Run
uv run invoke validate-graphqlafter touching any.graphqlfile. - Regenerate types after any
.graphqlchange:uv run invoke generate. - Update both the
.graphqlfile and the Python wiring (resource method, andStyrminClient.__init__if a new resource module is added) in the same change — neither is automatic. - Run
uv run invoke format lintbefore commit. - Write tests for new resource methods (success path + at least one error code).
- Changes to
_transport.py— affects every SDK operation. - Changes that break the public client API surface (
StyrminClientor any*Resourcepublic method signature). - New third-party dependencies in
python_sdk/pyproject.toml. - Reorganizing
resources/(splitting or merging modules, renaming resources visible onStyrminClient).
- Hand-edit anything under
src/styrmin_sdk/graphql/_generated/— regenerate instead. - Inline GraphQL strings in Python; every operation lives in its own
.graphqlfile and is loaded viaload_query(). - Add unnamed operations (
query { … }). - Skip
validate-graphqlafter.graphqlchanges.