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.API_PATH = '/api/v1/'

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

class smarter.core.conf.CliConfig(*, environment: Literal['local', 'alpha', 'beta', 'next', 'prod'] | None=None, root_domain: str | None = None, api_key: SecretStr | None = None, api_keys: dict[str, ~pydantic.types.SecretStr]=<factory>)[source]

Bases: BaseModel

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

classmethod load(path: Path) → CliConfig[source]

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.

model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class smarter.core.conf.ClientConfig(*, api_key: SecretStr, base_url: str, environment: Literal['local', 'alpha', 'beta', 'next', 'prod'], timeout: float, max_retries: int)[source]

Bases: BaseModel

A client’s resolved configuration.

model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

smarter.core.conf.DEFAULT_TIMEOUT = 60.0

Seconds.

Prompts that call several tools can take tens of seconds.

smarter.core.conf.LOCAL_URL = 'http://localhost:9357'

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

smarter.core.conf.SmarterEnvironment

The environments of the smarter cli’s configuration file.

alias of Literal[‘local’, ‘alpha’, ‘beta’, ‘next’, ‘prod’]

class smarter.core.conf.SmarterSettings(_case_sensitive: bool | None = None, _nested_model_default_partial_update: bool | None = None, _env_prefix: str | None = None, _env_prefix_target: EnvPrefixTarget | None = None, _env_file: DotenvType | None = PosixPath('.'), _env_file_encoding: str | None = None, _env_file_depth: int | None = None, _env_ignore_empty: bool | None = None, _env_nested_delimiter: str | None = None, _env_nested_max_split: int | None = None, _env_parse_none_str: str | None = None, _env_parse_enums: bool | None = None, _cli_prog_name: str | None = None, _cli_parse_args: bool | list[str] | tuple[str, ...] | None = None, _cli_settings_source: CliSettingsSource[Any] | None = None, _cli_parse_none_str: str | None = None, _cli_hide_none_type: bool | None = None, _cli_avoid_json: bool | None = None, _cli_enforce_required: bool | None = None, _cli_use_class_docs_for_groups: bool | None = None, _cli_show_env_vars: bool | None = None, _cli_exit_on_error: bool | None = None, _cli_prefix: str | None = None, _cli_flag_prefix_char: str | None = None, _cli_implicit_flags: bool | Literal['dual', 'toggle'] | None = None, _cli_ignore_unknown_args: bool | None = None, _cli_kebab_case: bool | Literal['all', 'no_enums'] | None = None, _cli_shortcuts: Mapping[str, str | list[str]] | None = None, _secrets_dir: PathType | None = None, _build_sources: tuple[tuple[PydanticBaseSettingsSource, ...], dict[str, Any]] | None = None, *, api_key: SecretStr | None = None, base_url: str | None = None, environment: Literal['local', 'alpha', 'beta', 'next', 'prod'] | None = None, root_domain: str | None = None, timeout: Annotated[float, Gt(gt=0)] = 60.0, max_retries: Annotated[int, Ge(ge=0)] = 2, config_file: Path = PosixPath('/Users/mcdaniel/.smarter/config.yaml'), log: Literal['debug', 'info'] | None = None)[source]

Bases: BaseSettings

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

model_config = {'arbitrary_types_allowed': True, 'case_sensitive': False, 'cli_avoid_json': False, 'cli_enforce_required': False, 'cli_exit_on_error': True, 'cli_flag_prefix_char': '-', 'cli_hide_none_type': False, 'cli_ignore_unknown_args': False, 'cli_implicit_flags': False, 'cli_kebab_case': False, 'cli_parse_args': None, 'cli_parse_none_str': None, 'cli_prefix': '', 'cli_prog_name': None, 'cli_shortcuts': None, 'cli_show_env_vars': False, 'cli_use_class_docs_for_groups': False, 'enable_decoding': True, 'env_file': None, 'env_file_depth': 0, 'env_file_encoding': None, 'env_ignore_empty': False, 'env_nested_delimiter': None, 'env_nested_max_split': None, 'env_parse_enums': None, 'env_parse_none_str': None, 'env_prefix': 'SMARTER_', 'env_prefix_target': 'variable', 'extra': 'ignore', 'json_file': None, 'json_file_encoding': None, 'nested_model_default_partial_update': False, 'protected_namespaces': ('model_validate', 'model_dump', 'settings_cached', 'settings_clear_cache', 'settings_customise_sources'), 'secrets_case_sensitive': None, 'secrets_dir': None, 'secrets_dir_max_size': None, 'secrets_dir_missing': None, 'secrets_nested_delimiter': None, 'secrets_nested_subdir': None, 'secrets_prefix': None, 'toml_file': None, 'validate_default': True, 'yaml_config_section': None, 'yaml_file': None, 'yaml_file_encoding': None}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

smarter.core.conf.api_base_url(url: str) → str[source]

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.environment_url(environment: Literal['local', 'alpha', 'beta', 'next', 'prod'], root_domain: str) → str[source]

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.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: SmarterSettings | None = None) → ClientConfig[source]

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
exception smarter.core.exceptions.APIConnectionError(request: Request, message: str = 'Connection error.')[source]

Bases: APIError

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

exception smarter.core.exceptions.APIError(message: str, request: Request, *, body: object = None)[source]

Bases: 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.

exception smarter.core.exceptions.APIResponseValidationError(response: Response, body: object, message: str)[source]

Bases: 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.

exception smarter.core.exceptions.APIStatusError(message: str, *, response: Response, body: object, error: SAMJournalApiErrorBody | None = None)[source]

Bases: 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: str | None

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

exception smarter.core.exceptions.APITimeoutError(request: Request)[source]

Bases: APIConnectionError

The request timed out.

exception smarter.core.exceptions.AuthenticationError(message: str, *, response: Response, body: object, error: SAMJournalApiErrorBody | None = None)[source]

Bases: APIStatusError

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

exception smarter.core.exceptions.BadRequestError(message: str, *, response: Response, body: object, error: SAMJournalApiErrorBody | None = None)[source]

Bases: APIStatusError

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

exception smarter.core.exceptions.ConflictError(message: str, *, response: Response, body: object, error: SAMJournalApiErrorBody | None = None)[source]

Bases: APIStatusError

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

exception smarter.core.exceptions.InternalServerError(message: str, *, response: Response, body: object, error: SAMJournalApiErrorBody | None = None)[source]

Bases: APIStatusError

5xx: the platform failed to process the request.

exception smarter.core.exceptions.NotFoundError(message: str, *, response: Response, body: object, error: SAMJournalApiErrorBody | None = None)[source]

Bases: APIStatusError, ResourceNotFoundError

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

exception smarter.core.exceptions.PermissionDeniedError(message: str, *, response: Response, body: object, error: SAMJournalApiErrorBody | None = None)[source]

Bases: APIStatusError

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

exception smarter.core.exceptions.RateLimitError(message: str, *, response: Response, body: object, error: SAMJournalApiErrorBody | None = None)[source]

Bases: APIStatusError

429: too many requests.

exception smarter.core.exceptions.ResourceNotFoundError[source]

Bases: SmarterError, LookupError

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

exception smarter.core.exceptions.SmarterConfigurationError[source]

Bases: SmarterError

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

exception smarter.core.exceptions.SmarterError[source]

Bases: Exception

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

exception smarter.core.exceptions.SmarterValueError[source]

Bases: SmarterError, ValueError

An argument is invalid.

Raised before a request is sent.

exception smarter.core.exceptions.UnprocessableEntityError(message: str, *, response: Response, body: object, error: SAMJournalApiErrorBody | None = None)[source]

Bases: APIStatusError

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

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.CLIENT_NAME = 'smarter-python'

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

smarter.core.http.QueryParamValue

A query parameter’s value.

None leaves the parameter out.

alias of str | int | float | bool | None

class smarter.core.http.SmarterHttpClient(config: ClientConfig, *, default_headers: Mapping[str, str] | None = None, http_client: Client | None = None)[source]

Bases: object

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: str

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

property max_retries: int

How many times a failed request is retried.

url(path: str) → str[source]

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: Mapping[str, str] | None = None) → dict[str, str][source]

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: Mapping[str, str | int | float | bool | None] | None = None, json: object = None, content: str | bytes | None = None, files: Mapping[str, Any] | None = None, data: Mapping[str, Any] | None = None, headers: Mapping[str, str] | None = None, timeout: float | None = None) → Request[source]

Build a request.

See request() for the parameters.

Returns:

The request.

send(request: Request, *, stream: bool = False) → Response[source]

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: Mapping[str, str | int | float | bool | None] | None = None, json: object = None, content: str | bytes | None = None, files: Mapping[str, Any] | None = None, data: Mapping[str, Any] | None = None, headers: Mapping[str, str] | None = None, timeout: float | None = None) → Any[source]

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: Mapping[str, str] | None = None, timeout: float | None = None) → Iterator[Iterator[ServerSentEvent]][source]

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[source]

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

smarter.core.http.error_message(response: Response, body: object, error: SAMJournalApiErrorBody | None) → str[source]

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.is_retryable(response: Response) → bool[source]

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.journal_error(body: object) → SAMJournalApiErrorBody | None[source]

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.make_status_error(response: Response) → APIStatusError[source]

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.parse_body(response: Response) → object[source]

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.query_params(params: Mapping[str, str | int | float | bool | None] | None) → dict[str, str][source]

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

Parameters:

params – The parameters.

Returns:

The parameters that httpx sends.

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

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.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)[source]

Bases: object

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[source]

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: Iterable[str]) → Iterator[ServerSentEvent][source]

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[source]

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[source]

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.

alias of dict[str, Any]

smarter.core.typing.SmarterIdType

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

alias of int | str

smarter.core.typing.SmarterJsonObjectType

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

alias of dict[str, JsonValue]

type smarter.core.typing.SmarterJsonType = _AllowAnyJson]

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