Metadata-Version: 2.4
Name: smplkit-sdk
Version: 3.2.11
Summary: Official Python SDK for the smplkit platform
Project-URL: Homepage, https://www.smplkit.com
Project-URL: Documentation, https://docs.smplkit.com
Project-URL: Repository, https://github.com/smplkit/python-sdk
License-Expression: MIT
License-File: LICENSE
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Requires-Python: >=3.10
Requires-Dist: attrs>=21.3.0
Requires-Dist: httpx>=0.25.0
Requires-Dist: json-logic-qubit>=0.9.1
Requires-Dist: pydantic>=2.0.0
Requires-Dist: python-dateutil>=2.9.0
Requires-Dist: websockets>=13.0
Provides-Extra: dev
Requires-Dist: loguru>=0.7.0; extra == 'dev'
Requires-Dist: openapi-python-client>=0.21.0; extra == 'dev'
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
Requires-Dist: pytest>=7.0; extra == 'dev'
Requires-Dist: ruff>=0.4.0; extra == 'dev'
Provides-Extra: loguru
Requires-Dist: loguru>=0.7.0; extra == 'loguru'
Description-Content-Type: text/markdown

# smplkit Python SDK

[![PyPI Version](https://img.shields.io/pypi/v/smplkit-sdk)](https://pypi.org/project/smplkit-sdk/) [![Build](https://github.com/smplkit/python-sdk/actions/workflows/ci-cd.yml/badge.svg)](https://github.com/smplkit/python-sdk/actions) [![Coverage](https://codecov.io/gh/smplkit/python-sdk/branch/main/graph/badge.svg)](https://codecov.io/gh/smplkit/python-sdk) [![License](https://img.shields.io/pypi/l/smplkit-sdk)](LICENSE) [![Docs](https://img.shields.io/badge/docs-docs.smplkit.com-blue)](https://docs.smplkit.com)

The official Python SDK for [smplkit](https://www.smplkit.com) — simple application infrastructure that just works.

## Installation

```bash
pip install smplkit-sdk
```

## Requirements

- Python 3.10+

## Quick Start

The SDK ships two top-level clients, each with a clearly-scoped purpose:

| Client | Use case | Construction side effects |
|--------|----------|---------------------------|
| `SmplClient` / `AsyncSmplClient` | **Runtime instrumentation** — flag evaluation, config reads, log emission | Auto-registers a service context, starts a metrics thread, opens a websocket |
| `SmplManagementClient` / `AsyncSmplManagementClient` | **Management / CRUD** — setup scripts, CI/CD, admin tooling | None — pure HTTP setup |

### Runtime: `SmplClient`

```python
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):

```python
with client.set_context([Context("user", "u-impersonated", plan="enterprise")]):
    if checkout_v2.get():
        ...
# original context restored here
```

For 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.

### Management: `SmplManagementClient`

```python
from smplkit import SmplManagementClient

with SmplManagementClient(api_key="sk_api_...") as mgmt:
    # 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()
```

The management client takes only `api_key` (plus optional `profile`, `base_domain`, `scheme`, `debug`) — `environment` and `service` have no meaning for CRUD work and are deliberately rejected.

For async usage, swap `SmplClient` → `AsyncSmplClient` and `SmplManagementClient` → `AsyncSmplManagementClient`; method bodies become `await`-able:

```python
from smplkit import AsyncSmplClient, AsyncSmplManagementClient

async with AsyncSmplClient(api_key="sk_api_...", environment="prod", service="svc") as client:
    db = await client.config.get("database")

async with AsyncSmplManagementClient(api_key="sk_api_...") as mgmt:
    cfg = await mgmt.config.get("my_service")
    configs = await mgmt.config.list()
```

### Which client should I use?

- **Inside a request handler / running service** → `SmplClient`. You want lazy-fetched runtime state, the context registration loop, metrics, and the live-update websocket.
- **In a setup script / CI job / admin CLI / seeder** → `SmplManagementClient`. No runtime side effects, no auto-registered service rows leaking into target accounts, no websocket dangling open.

The two clients can be used together in the same process — e.g. a runtime app that occasionally needs to reach into the management API for an admin endpoint. To save you from juggling two clients, every `SmplClient` exposes a built-in management client at `client.manage` (sharing HTTP transports under the hood); reach for `SmplManagementClient` directly only for setup scripts, CI jobs, and admin tooling that have no runtime side effects to begin with.

### Management namespaces

`SmplManagementClient` (and `client.manage` on the runtime client) exposes eight 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 |

## Logging Adapters

`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`](https://github.com/Delgan/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()`:

```python
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:

```toml
[project.entry-points."smplkit.logging.adapters"]
structlog = "my_package.adapter:StructlogAdapter"
```

## Configuration

All settings are resolved from three sources, in order of precedence:

1. **Constructor arguments** — highest priority, always wins.
2. **Environment variables** — e.g. `SMPLKIT_API_KEY`, `SMPLKIT_ENVIRONMENT`.
3. **Configuration file** (`~/.smplkit`) — INI-format with profile support.
4. **Defaults** — built-in SDK defaults.

### Configuration File

The `~/.smplkit` file supports a `[common]` section (applied to all profiles) and named profiles:

```ini
[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
```

### Constructor Examples

```python
# 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](https://docs.smplkit.com/getting-started/configuration).

## Error Handling

All SDK errors extend `smplkit.Error`:

```python
from smplkit import Error, NotFoundError

try:
    config = mgmt.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           |

## Debug Logging

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.

```bash
SMPLKIT_DEBUG=1 python my_app.py
```

Accepted values: `1`, `true`, `yes` (case-insensitive). Any other value (or unset) disables debug output.

## Documentation

- [Getting Started](https://docs.smplkit.com/getting-started)
- [Python SDK Guide](https://docs.smplkit.com/sdks/python)
- [API Reference](https://docs.smplkit.com/api)

## License

MIT
