The official Python SDK for smplkit — simple application infrastructure that just works.
pip install smplkit-sdk- Python 3.10+
One client is the whole SDK: SmplClient (and AsyncSmplClient). Construction is
side-effect-free — no threads, no network until you actually use a feature — and
only an api_key is required. Every product hangs off it:
| Namespace | Purpose |
|---|---|
client.flags / client.config / client.logging |
Runtime instrumentation — flag evaluation, config reads, log-level control (environment-scoped) |
client.audit |
Audit — record/read events, discovery, SIEM forwarders |
client.jobs |
Jobs — scheduled or on-demand HTTP jobs |
client.manage |
Management / CRUD — flag/config/logger definitions, environments, contexts, account settings |
environment and service are optional — supply them when instrumenting with
flags/config/logging (the server can also derive the environment from the API key);
audit- or jobs-only callers need nothing but the key. For single-product use,
standalone SmplAuditClient / SmplJobsClient are also available.
from smplkit import Context, SmplClient
with SmplClient(api_key="sk_api_...", environment="production", service="my-svc") as client:
# Resolve config values for the current environment
db = client.config.get("database") # {"host": "...", "port": 5432}
# Set the current request's context once (typically from middleware) —
# contextvars provides per-request / per-thread isolation automatically.
client.set_context([
Context("user", request.user.id, plan=request.user.plan),
Context("account", request.account.id, region=request.account.region),
])
# Evaluate a flag — picks up the context set above.
checkout_v2 = client.flags.boolean_flag("checkout-v2", default=False)
if checkout_v2.get():
...
# Opt in to runtime logging level control
client.logging.install()
# Need to reach the management API from a runtime context?
# Every SmplClient owns an internal management client at `client.manage`.
cfg = client.manage.config.get("database")set_context() returns a scope object that doubles as a with block, so you can override context for a single block (e.g. impersonation):
with client.set_context([Context("user", "u-impersonated", plan="enterprise")]):
if checkout_v2.get():
...
# original context restored hereFor deterministic startup — pre-fetch all flags + configs and wait for the live-updates websocket before serving traffic — call client.wait_until_ready() once at boot.
CRUD lives on the client.manage namespace. Just an API key — no environment or
service, and (because construction is side-effect-free) no threads, no websocket,
no service rows registered in the target account until you actually instrument with
flags/config/logging:
from smplkit import SmplClient
with SmplClient(api_key="sk_api_...") as client:
mgmt = client.manage
# Configs
cfg = mgmt.config.new("my_service", name="My Service")
cfg.save()
configs = mgmt.config.list()
# Flags
flag = mgmt.flags.new_boolean_flag("checkout-v2", default=False)
flag.save()
flags = mgmt.flags.list()
# Loggers + log groups
logger = mgmt.loggers.new("sql", name="SQL Logger")
logger.save()
grp = mgmt.log_groups.new("databases", name="Databases")
grp.save()
# App-service-owned resources
for env in mgmt.environments.list():
print(env.id)
mgmt.contexts.register([...])
settings = mgmt.account_settings.get()A setup script, CI job, or admin CLI constructs SmplClient(api_key=...) and uses
client.manage.* — it pays nothing for the runtime machinery it never touches.
Audit and Jobs are single clients (no runtime/management split). Reach them on the client, or construct them standalone:
from smplkit import SmplAuditClient, SmplClient
with SmplClient(api_key="sk_api_...", environment="production") as client:
client.audit.events.record("invoice.created", "invoice", "inv-1", flush=True)
forwarders = client.audit.forwarders.list()
# jobs are enabled (and overridden) per environment:
job = client.jobs.new_recurring_job("nightly", name="Nightly", schedule="0 2 * * *", configuration=...)
job.environment("production").enabled = True
job.save()
# audit-only? construct just the audit client (no environment needed):
with SmplAuditClient(api_key="sk_api_...") as audit:
audit.events.record("invoice.created", "invoice", "inv-1", flush=True)Swap SmplClient → AsyncSmplClient (likewise SmplAuditClient → AsyncSmplAuditClient,
SmplJobsClient → AsyncSmplJobsClient); method bodies become await-able:
from smplkit import AsyncSmplClient
async with AsyncSmplClient(api_key="sk_api_...", environment="prod", service="svc") as client:
db = await client.config.get("database")
cfg = await client.manage.config.get("my_service")
page = await client.audit.events.list(resource_type="invoice")client.manage exposes flat namespaces (one per resource family):
| Namespace | Resource |
|---|---|
manage.contexts |
Context instances (register / list / get / delete) |
manage.context_types |
Targeting-rule entity schemas |
manage.environments |
Environments (built-ins + AD_HOC) |
manage.account_settings |
Per-account settings |
manage.config |
Smpl Config CRUD |
manage.flags |
Smpl Flags CRUD |
manage.loggers |
Smpl Logging logger CRUD |
manage.log_groups |
Smpl Logging log-group CRUD |
Endpoints outside this curated set — for example, Environment Access
Groups and their memberships — are reachable via the generated client
at smplkit._generated.app.api.{groups,group_memberships}. The
generated module accepts the same authenticated client object you
already construct for the curated namespaces.
client.logging.install() auto-loads adapters for every supported framework it finds installed. Two adapters ship with the SDK:
| Adapter | Covers |
|---|---|
stdlib-logging |
Python logging.getLogger(...) — discovered and managed automatically |
loguru |
The loguru library — requires pip install smplkit-sdk[loguru] |
Both are registered as smplkit.logging.adapters entry points in pyproject.toml, so they are discovered via importlib.metadata at install() time with no extra wiring.
Adding a custom adapter — implement LoggingAdapter and register it before install():
from smplkit.logging.adapters.base import LoggingAdapter
class StructlogAdapter(LoggingAdapter):
@property
def name(self) -> str:
return "structlog"
def discover(self): ...
def apply_level(self, name, level): ...
def install_hook(self, on_new_logger): ...
def uninstall_hook(self): ...
client.logging.register_adapter(StructlogAdapter())
client.logging.install()Calling register_adapter() disables auto-loading — only the adapters you register are used.
Packaging an adapter for auto-discovery — declare the entry point in your package's pyproject.toml so it is picked up without any caller code change:
[project.entry-points."smplkit.logging.adapters"]
structlog = "my_package.adapter:StructlogAdapter"All settings are resolved from three sources, in order of precedence:
- Constructor arguments — highest priority, always wins.
- Environment variables — e.g.
SMPLKIT_API_KEY,SMPLKIT_ENVIRONMENT. - Configuration file (
~/.smplkit) — INI-format with profile support. - Defaults — built-in SDK defaults.
The ~/.smplkit file supports a [common] section (applied to all profiles) and named profiles:
[common]
environment = production
service = my-app
[default]
api_key = sk_api_abc123
[local]
base_domain = localhost
scheme = http
api_key = sk_api_local_xyz
environment = development
debug = true# Use a named profile
client = SmplClient(profile="local")
# Or configure explicitly
client = SmplClient(
api_key="sk_api_...",
environment="production",
service="my-service",
)For the complete configuration reference, see the Configuration Guide.
All SDK errors extend smplkit.Error:
from smplkit import Error, NotFoundError
try:
config = client.manage.config.get("nonexistent")
except NotFoundError:
print("Config not found")
except Error as e:
print(f"SDK error: {e}")The error classes shadow built-ins (ConnectionError, TimeoutError, ValidationError), so import them from smplkit rather than relying on from smplkit import *, or alias on import (e.g. from smplkit import NotFoundError as SmplNotFound) if that collides with your own names.
| Exception | Cause |
|---|---|
NotFoundError |
Resource not found |
ConflictError |
Conflict (e.g., has children) |
ValidationError |
Validation error |
TimeoutError |
Request timed out |
ConnectionError |
Network connectivity issue |
Error |
Any other SDK error |
Set SMPLKIT_DEBUG=1 to enable verbose diagnostic output to stderr. This is useful for troubleshooting real-time level changes, WebSocket connectivity, and SDK initialization. Debug output bypasses the managed logging framework and writes directly to stderr.
SMPLKIT_DEBUG=1 python my_app.pyAccepted values: 1, true, yes (case-insensitive). Any other value (or unset) disables debug output.
MIT