Configuration, Transport and Errors

smarter.core.conf

The configuration of the Smarter SDK.

A client needs an api key and the base url of a Smarter platform. Each is taken from the first of these that sets it:

  1. the arguments of smarter.Smarter,

  2. environment variables: SMARTER_API_KEY, SMARTER_BASE_URL, SMARTER_ENVIRONMENT, SMARTER_ROOT_DOMAIN, SMARTER_TIMEOUT, SMARTER_MAX_RETRIES and SMARTER_LOG (SmarterSettings),

  3. the smarter cli’s configuration file, ~/.smarter/config.yaml (SMARTER_CONFIG_FILE), so that a developer who has run smarter configure needs no other setup,

  4. the defaults: the prod environment of platform.smarter.sh.

Without an explicit base url, the url is derived from the environment as the cli derives it: local is http://localhost:9357, alpha, beta and next are https://<environment>.<root domain>, and prod is https://<root domain>.

This follows the Smarter platform’s settings design (ADR-007): one validated Pydantic settings model is the only place that reads the environment.

smarter.core.conf.SmarterEnvironment

The environments of the smarter cli’s configuration file.

smarter.core.conf.LOCAL_URL

The url of the Smarter platform’s local development server.

smarter.core.conf.API_PATH

The path of the Smarter api, which every request url is relative to.

smarter.core.conf.DEFAULT_TIMEOUT

Seconds.

Prompts that call several tools can take tens of seconds.

class smarter.core.conf.SmarterSettings

Bases: pydantic_settings.main.BaseSettings

The SDK’s settings, from SMARTER_* environment variables.

api_key: Optional

A Smarter api key: 64 characters.

base_url: Optional

e.g. https://platform.example.com or http://localhost:9357.

environment: Optional
root_domain: Optional

The platform’s root domain, e.g. platform.smarter.sh.

timeout: float
max_retries: int
config_file: Path
log: Optional

Log the SDK’s requests to stderr.

class smarter.core.conf.CliConfig

Bases: pydantic.main.BaseModel

The parts of the smarter cli’s ~/.smarter/config.yaml that the SDK reads.

environment: Optional
root_domain: Optional
api_key: Optional

The top-level api_key, used by any environment.

api_keys: dict

The api key of each environment.

classmethod load(path: pathlib.Path) → 'CliConfig'

Read the cli’s configuration file.

A missing or unreadable file is an empty configuration: the file is optional.

Parameters:

path – The file’s path.

Returns:

The configuration.

class smarter.core.conf.ClientConfig

Bases: pydantic.main.BaseModel

A client’s resolved configuration.

api_key: SecretStr
base_url: str

The api’s base url, ending in /api/v1/.

environment: Literal
timeout: float
max_retries: int
smarter.core.conf.environment_url(environment: Literal['local', 'alpha', 'beta', 'next', 'prod'], root_domain: str) → str

The platform url of an environment, as the smarter cli derives it.

Parameters:
  • environment – The environment.

  • root_domain – The platform’s root domain, e.g. platform.smarter.sh.

Returns:

e.g. https://alpha.platform.smarter.sh.

smarter.core.conf.api_base_url(url: str) → str

The api’s base url for a platform url.

https://example.com and https://example.com/api/v1 both become https://example.com/api/v1/.

Parameters:

url – A platform url, with or without the api’s path.

Returns:

The url, with the api’s path and a trailing slash.

Raises:

SmarterConfigurationError – If the url is not an http or https url.

smarter.core.conf.resolve_client_config(*, api_key: str | None = None, base_url: str | None = None, environment: Literal['local', 'alpha', 'beta', 'next', 'prod'] | None = None, timeout: float | None = None, max_retries: int | None = None, settings: smarter.core.conf.SmarterSettings | None = None) → smarter.core.conf.ClientConfig

Resolve a client’s configuration from its arguments, the environment, and the cli’s configuration file.

Parameters:
  • api_key – The api key, or None to look it up.

  • base_url – The platform’s url, or None to derive it from the environment.

  • environment – The environment, or None to look it up.

  • timeout – The request timeout in seconds, or None for the setting.

  • max_retries – How many times to retry a failed request, or None for the setting.

  • settings – The settings to use instead of reading the environment variables, for tests.

Returns:

The configuration.

Raises:

SmarterConfigurationError – If no api key is found, or the base url is invalid.

smarter.core.exceptions

The exceptions of the Smarter Python SDK.

Every exception that the SDK raises is a SmarterError, so a caller can catch them all with one except clause. This follows the Smarter platform’s own rule (ADR-012) that exceptions subclass one hierarchy.

The api errors mirror the http status that the Smarter api returned:

SmarterError
├── SmarterConfigurationError        the client has no api key or base url
├── SmarterValueError                an argument is invalid, before any request is sent
├── ResourceNotFoundError            a resource was not found, by the api (NotFoundError) or by find()
└── APIError
    ├── APIConnectionError           the request never got a response
    │   └── APITimeoutError
    ├── APIResponseValidationError   the response does not match the SDK's model of it
    └── APIStatusError               the api returned a 4xx or 5xx status
        ├── BadRequestError          400
        ├── AuthenticationError      401
        ├── PermissionDeniedError    403
        ├── NotFoundError            404, also a ResourceNotFoundError
        ├── ConflictError            409
        ├── UnprocessableEntityError 422
        ├── RateLimitError           429
        └── InternalServerError      5xx
class smarter.core.exceptions.SmarterError

Bases: builtins.Exception

The base class of every exception that the Smarter SDK raises.

class smarter.core.exceptions.SmarterConfigurationError

Bases: smarter.core.exceptions.SmarterError

The client is missing configuration that it needs, e.g. an api key or a base url.

class smarter.core.exceptions.SmarterValueError

Bases: smarter.core.exceptions.SmarterError, builtins.ValueError

An argument is invalid.

Raised before a request is sent.

class smarter.core.exceptions.ResourceNotFoundError

Bases: smarter.core.exceptions.SmarterError, builtins.LookupError

A resource was not found: the api answered 404 (NotFoundError), or find() found no resource of that name.

class smarter.core.exceptions.APIError(message: str, request: httpx.Request, *, body: object = None) → None

Bases: smarter.core.exceptions.SmarterError

A request to the Smarter api failed.

Parameters:
  • message – A human-readable description of the error.

  • request – The http request that failed.

  • body – The response’s parsed JSON body, or its text, or None when there is no response.

class smarter.core.exceptions.APIConnectionError(request: httpx.Request, message: str = 'Connection error.') → None

Bases: smarter.core.exceptions.APIError

The request did not get a response, e.g. the server is unreachable.

class smarter.core.exceptions.APITimeoutError(request: httpx.Request) → None

Bases: smarter.core.exceptions.APIConnectionError

The request timed out.

class smarter.core.exceptions.APIResponseValidationError(response: httpx.Response, body: object, message: str) → None

Bases: smarter.core.exceptions.APIError

The api’s response does not match the SDK’s model of it.

This usually means that the SDK and the Smarter platform are of incompatible versions.

Parameters:
  • response – The http response.

  • body – The response’s parsed JSON body.

  • message – A description of the mismatch.

class smarter.core.exceptions.APIStatusError(message: str, *, response: httpx.Response, body: object, error: ForwardRef('SAMJournalApiErrorBody') | None = None) → None

Bases: smarter.core.exceptions.APIError

The api returned a 4xx or 5xx status.

Parameters:
  • message – The error’s description, from the response when it has one.

  • response – The http response.

  • body – The response’s parsed JSON body, or its text.

  • error – The Smarter Journal error body, when the response is a journaled error.

property error_class

The name of the server-side exception class, e.g. SAMBrokerErrorNotFound, when the platform sent it.

class smarter.core.exceptions.BadRequestError

Bases: smarter.core.exceptions.APIStatusError

400: the request is invalid, e.g. a malformed manifest.

class smarter.core.exceptions.AuthenticationError

Bases: smarter.core.exceptions.APIStatusError

401: the api key is missing, invalid, or inactive.

class smarter.core.exceptions.PermissionDeniedError

Bases: smarter.core.exceptions.APIStatusError

403: the api key’s user may not perform the operation.

class smarter.core.exceptions.NotFoundError

Bases: smarter.core.exceptions.APIStatusError, smarter.core.exceptions.ResourceNotFoundError

404: the resource does not exist, or the api key’s user may not read it.

class smarter.core.exceptions.ConflictError

Bases: smarter.core.exceptions.APIStatusError

409: the request conflicts with the resource’s current state.

class smarter.core.exceptions.UnprocessableEntityError

Bases: smarter.core.exceptions.APIStatusError

422: the request is well-formed but cannot be processed.

class smarter.core.exceptions.RateLimitError

Bases: smarter.core.exceptions.APIStatusError

429: too many requests.

class smarter.core.exceptions.InternalServerError

Bases: smarter.core.exceptions.APIStatusError

5xx: the platform failed to process the request.

smarter.core.http

The SDK’s http transport: authentication, retries, error mapping, and response parsing.

SmarterHttpClient sends every request of a smarter.Smarter client. It adds the api key and the SDK’s headers, retries requests that failed for transient reasons, raises a smarter.core.exceptions error for a 4xx or 5xx response, and validates the response’s JSON against a Pydantic model.

smarter.core.http.QueryParamValue

A query parameter’s value.

None leaves the parameter out.

smarter.core.http.CLIENT_NAME

The SDK’s X-Smarter-Client header, which identifies it to the platform’s logs and journal.

smarter.core.http.parse_body(response: httpx.Response) → object

A response’s body: its JSON, or its text when it is not JSON.

Parameters:

response – The response, which must have been read.

Returns:

The parsed JSON, the text, or None for an empty body.

smarter.core.http.journal_error(body: object) → smarter.models.journal.SAMJournalApiErrorBody | None

The Smarter Journal error of a response body, if it is a journaled error response.

Parameters:

body – A parsed response body.

Returns:

The error body, or None.

smarter.core.http.error_message(response: httpx.Response, body: object, error: smarter.models.journal.SAMJournalApiErrorBody | None) → str

A human-readable message for an error response.

The platform’s views return errors in several shapes: the journal’s error object, an error string, Django REST framework’s detail, plain text, or a Django error page.

Parameters:
  • response – The response.

  • body – Its parsed body.

  • error – Its journal error, if any.

Returns:

The message.

smarter.core.http.make_status_error(response: httpx.Response) → smarter.core.exceptions.APIStatusError

The exception for a 4xx or 5xx response, by its status.

Parameters:

response – The response, which must have been read.

Returns:

e.g. a NotFoundError for a 404.

smarter.core.http.is_retryable(response: httpx.Response) → bool

True if a failed response may succeed when the request is sent again.

A 503 is retried only when it is not a journaled response. The Smarter platform itself answers 503 for a resource that is not ready (e.g. SAMBrokerErrorNotReady), which a retry won’t change, whereas a 503 from a load balancer or proxy means the platform is briefly unavailable.

Parameters:

response – The response, which must have been read.

Returns:

True to retry.

smarter.core.http.retry_delay(attempt: int, response: httpx.Response | None = None) → float

Seconds to wait before a retry: the response’s Retry-After, or exponential backoff with jitter.

Parameters:
  • attempt – The number of the attempt that failed, from 0.

  • response – The failed response, if there was one.

Returns:

The delay.

smarter.core.http.query_params(params: collections.abc.Mapping[str, str | int | float | bool | NoneType] | None) → dict[str, str]

Query parameters as strings: booleans as true or false, and None values left out.

Parameters:

params – The parameters.

Returns:

The parameters that httpx sends.

class smarter.core.http.SmarterHttpClient(config: smarter.core.conf.ClientConfig, *, default_headers: collections.abc.Mapping[str, str] | None = None, http_client: httpx.Client | None = None) → None

Sends the requests of a Smarter client.

Parameters:
  • config – The client’s resolved configuration.

  • default_headers – Headers to add to every request.

  • http_client – An httpx.Client to use, e.g. with a proxy or a test transport. Its own base url and timeout are ignored.

property base_url

The api’s base url, e.g. https://platform.smarter.sh/api/v1/.

property max_retries

How many times a failed request is retried.

url(path: str) → str

The absolute url of an api path.

Parameters:

path – A path relative to the base url, e.g. cli/whoami/. An absolute url is returned as is.

Returns:

The url.

headers(extra: collections.abc.Mapping[str, str] | None = None) → dict[str, str]

The headers of a request.

Parameters:

extra – Headers for this request, which override the defaults.

Returns:

The headers.

build_request(method: str, path: str, *, params: collections.abc.Mapping[str, str | int | float | bool | NoneType] | None = None, json: object = None, content: str | bytes | NoneType = None, files: collections.abc.Mapping[str, Any] | None = None, data: collections.abc.Mapping[str, Any] | None = None, headers: collections.abc.Mapping[str, str] | None = None, timeout: float | None = None) → httpx.Request

Build a request.

See request() for the parameters.

Returns:

The request.

send(request: httpx.Request, *, stream: bool = False) → httpx.Response

Send a request, with retries, and raise for an error status.

Parameters:
  • request – The request.

  • stream – True to return before the body is read, for Server-Sent Events. The caller must close the response.

Returns:

The successful response.

Raises:
request(method: str, path: str, *, cast_to: Any, params: collections.abc.Mapping[str, str | int | float | bool | NoneType] | None = None, json: object = None, content: str | bytes | NoneType = None, files: collections.abc.Mapping[str, Any] | None = None, data: collections.abc.Mapping[str, Any] | None = None, headers: collections.abc.Mapping[str, str] | None = None, timeout: float | None = None) → Any

Send a request, and validate its JSON response.

Parameters:
  • method – The http method, e.g. GET.

  • path – The path, relative to the api’s base url, e.g. guardrails/.

  • cast_to – The type of the response’s JSON, e.g. Guardrail or list[Guardrail]. None ignores the body.

  • params – Query parameters.

  • json – A JSON body.

  • content – A raw body, e.g. a YAML manifest.

  • files – Files, for a multipart/form-data body.

  • data – Form fields, for a multipart/form-data body.

  • headers – Headers for this request.

  • timeout – The timeout in seconds, instead of the client’s.

Returns:

The validated response, an instance of cast_to.

Raises:

APIResponseValidationError – If the response does not match cast_to.

stream_events(method: str, path: str, *, json: object = None, headers: collections.abc.Mapping[str, str] | None = None, timeout: float | None = None) → collections.abc.Iterator[collections.abc.Iterator[smarter.core.streaming.ServerSentEvent]]

Send a request that asks for Server-Sent Events, and iterate its events.

with http.stream_events("POST", "llm-clients/5/prompt/", json=body) as events:
    for event in events:
        ...
Parameters:
  • method – The http method.

  • path – The path, relative to the api’s base url.

  • json – A JSON body.

  • headers – Headers for this request.

  • timeout – The timeout in seconds, instead of the client’s. It applies to each read.

Returns:

A context manager whose value iterates the events. The response is closed on exit.

close() → None

Close the underlying httpx.Client, unless it was passed in by the caller.

smarter.core.streaming

A parser of Server-Sent Events.

The Smarter prompt api streams a prompt’s progress as Server-Sent Events when the request’s Accept header includes text/event-stream: progress events while it runs, then one result event with the same JSON as a non-streaming response.

class smarter.core.streaming.ServerSentEvent(event: str | None, data: str, id: str | None = None, retry: int | None = None) → None

One Server-Sent Event.

Parameters:
  • event – The event’s type, e.g. progress, or None for an unnamed event.

  • data – The event’s data: its data: lines, joined by newlines.

  • id – The event’s id, if any.

  • retry – The reconnection time that the server asked for, in milliseconds, if any.

json() → JsonValue

The event’s data, parsed as JSON.

Returns:

The parsed data.

Raises:

json.JSONDecodeError – If the data is not JSON.

smarter.core.streaming.iter_sse(lines: collections.abc.Iterable[str]) → collections.abc.Iterator[smarter.core.streaming.ServerSentEvent]

Parse a stream’s lines into events, as the HTML Living Standard specifies.

A blank line ends an event. Comments (lines that start with :), and blocks that set only retry, yield nothing.

Parameters:

lines – The stream’s lines, without their line endings.

Returns:

The events, in order.

smarter.core.log

Logging for the Smarter SDK.

The SDK logs to the smarter logger hierarchy (smarter.client, smarter.resources...) and, as a library should, adds no handlers unless asked to. Set SMARTER_LOG=info to log each request’s method, url, status and duration to stderr, or SMARTER_LOG=debug to log their bodies too. Or configure the smarter logger as you would any other.

The Smarter platform controls its logging per module with waffle switches (ADR-011). The SDK’s equivalent is the logger hierarchy: e.g. logging.getLogger("smarter.client").setLevel(...).

Api keys are never logged: see mask_secret().

smarter.core.log.mask_secret(secret: str | None) → str

A secret, masked for logging: its last four characters.

Parameters:

secret – The secret, e.g. an api key.

Returns:

e.g. ****5150, or None.

smarter.core.log.setup_logging(level: str | None) → None

Log the SDK’s messages to stderr, at the given level, once per process.

Parameters:

level – debug or info, or None to leave logging as it is.

smarter.core.typing

Type aliases of the Smarter SDK.

These mirror smarter.lib.typing in the Smarter platform. This module imports only from the standard library and pydantic, so that any module of the SDK can import it without an import cycle.

smarter.core.typing.SmarterDictType

A mapping whose values really can be of any type.

Prefer a Pydantic model when the shape is known.

smarter.core.typing.SmarterJsonType

A value that is arbitrary JSON: a string, number, boolean, null, or a list or object of them.

smarter.core.typing.SmarterJsonObjectType

A JSON object whose shape is not fixed, e.g. a manifest’s spec, whose fields depend on its kind.

smarter.core.typing.SmarterIdType

A resource’s id: its integer primary key, or its hashed id, e.g. rMTAwMDAwNQx.