uniqos 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. uniqos-0.4.0/.gitignore +21 -0
  2. uniqos-0.4.0/CHANGELOG.md +113 -0
  3. uniqos-0.4.0/Makefile +59 -0
  4. uniqos-0.4.0/PKG-INFO +160 -0
  5. uniqos-0.4.0/README.md +138 -0
  6. uniqos-0.4.0/docs/dev.md +92 -0
  7. uniqos-0.4.0/examples/README.md +26 -0
  8. uniqos-0.4.0/examples/dogfood.py +39 -0
  9. uniqos-0.4.0/examples/error_handling.py +48 -0
  10. uniqos-0.4.0/examples/hello_world.py +35 -0
  11. uniqos-0.4.0/examples/stateful.py +44 -0
  12. uniqos-0.4.0/examples/streaming.py +37 -0
  13. uniqos-0.4.0/pyproject.toml +89 -0
  14. uniqos-0.4.0/src/uniqos/__init__.py +81 -0
  15. uniqos-0.4.0/src/uniqos/_core/__init__.py +7 -0
  16. uniqos-0.4.0/src/uniqos/_core/config.py +130 -0
  17. uniqos-0.4.0/src/uniqos/_core/logging.py +109 -0
  18. uniqos-0.4.0/src/uniqos/_core/retry.py +50 -0
  19. uniqos-0.4.0/src/uniqos/_core/streaming.py +157 -0
  20. uniqos-0.4.0/src/uniqos/_core/transport.py +315 -0
  21. uniqos-0.4.0/src/uniqos/async_client.py +126 -0
  22. uniqos-0.4.0/src/uniqos/client.py +133 -0
  23. uniqos-0.4.0/src/uniqos/errors.py +282 -0
  24. uniqos-0.4.0/src/uniqos/generated/__init__.py +7 -0
  25. uniqos-0.4.0/src/uniqos/generated/stream_events.py +115 -0
  26. uniqos-0.4.0/src/uniqos/py.typed +0 -0
  27. uniqos-0.4.0/src/uniqos/resources/__init__.py +5 -0
  28. uniqos-0.4.0/src/uniqos/resources/_base.py +52 -0
  29. uniqos-0.4.0/src/uniqos/resources/_mixin.py +81 -0
  30. uniqos-0.4.0/src/uniqos/resources/api_keys.py +25 -0
  31. uniqos-0.4.0/src/uniqos/resources/billing.py +107 -0
  32. uniqos-0.4.0/src/uniqos/resources/catalog.py +16 -0
  33. uniqos-0.4.0/src/uniqos/resources/end_users.py +68 -0
  34. uniqos-0.4.0/src/uniqos/resources/engine.py +80 -0
  35. uniqos-0.4.0/src/uniqos/resources/interactions.py +35 -0
  36. uniqos-0.4.0/src/uniqos/resources/me.py +16 -0
  37. uniqos-0.4.0/src/uniqos/resources/organizations.py +38 -0
  38. uniqos-0.4.0/src/uniqos/resources/personalities.py +56 -0
  39. uniqos-0.4.0/src/uniqos/resources/relationships.py +64 -0
  40. uniqos-0.4.0/src/uniqos/resources/vocabularies.py +36 -0
  41. uniqos-0.4.0/tests/static/test_typing.py +30 -0
  42. uniqos-0.4.0/tests/test_client.py +31 -0
  43. uniqos-0.4.0/tests/test_config.py +77 -0
  44. uniqos-0.4.0/tests/test_errors.py +129 -0
  45. uniqos-0.4.0/tests/test_examples.py +22 -0
  46. uniqos-0.4.0/tests/test_parity.py +38 -0
  47. uniqos-0.4.0/tests/test_resources.py +160 -0
  48. uniqos-0.4.0/tests/test_respond.py +61 -0
  49. uniqos-0.4.0/tests/test_retry.py +144 -0
  50. uniqos-0.4.0/tests/test_smoke.py +10 -0
  51. uniqos-0.4.0/tests/test_streaming.py +156 -0
  52. uniqos-0.4.0/tests/test_transport.py +144 -0
  53. uniqos-0.4.0/uv.lock +1005 -0
@@ -0,0 +1,21 @@
1
+ node_modules/
2
+ dist/
3
+ .env
4
+ .env.local
5
+ *.log
6
+ .DS_Store
7
+ coverage/
8
+ *.tsbuildinfo
9
+ .turbo/
10
+ *.tgz
11
+
12
+ # Python
13
+ __pycache__/
14
+ *.py[cod]
15
+ .venv/
16
+ .mypy_cache/
17
+ .ruff_cache/
18
+ .pytest_cache/
19
+ *.egg-info/
20
+ build/
21
+ *.whl
@@ -0,0 +1,113 @@
1
+ # Changelog
2
+
3
+ All notable changes to the `uniqos` Python SDK are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to
5
+ [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.4.0] - 2026-06-13
10
+
11
+ Coordinated refresh release. Both SDKs re-vendor the post-S1+S2 canonical OpenAPI contract
12
+ (root `openapi/openapi.json`) and move onto a single `0.4.0` version line. Python jumps
13
+ `0.2.0 → 0.4.0` (skipping `0.3.0`) to align with `@uniqos/sdk`; the two packages now share a
14
+ version. This cuts the previously-Unreleased `504 turn_timeout` handling (below).
15
+
16
+ ### Added
17
+
18
+ - **`TurnTimeoutError`** — typed error for `504 turn_timeout` (a `/v1/respond` turn that
19
+ exceeded the server's engine budget and was aborted; ADR-0009). The aborted turn is
20
+ **not billed**. Mapped both code-first (`turn_timeout`) and by status (504).
21
+
22
+ ### Notes
23
+
24
+ - **OpenAPI snapshot refreshed; generated models unchanged.** The vendored snapshot was
25
+ re-vendored to the current contract, but `make generate` is a no-op for Python: code
26
+ generation is scoped to the 6 named streaming schemas (ADR PY-0004), which are byte-identical
27
+ in the new contract. The contract's additive changes — `big_five_facets` on personality
28
+ responses, the formal `504` on `/v1/respond`, the new `/v1/auth/refresh` endpoint — live in
29
+ the path surface, which Python passes through as snake_case dicts rather than generating. The
30
+ personality dict simply carries the new `big_five_facets` key; `/v1/auth/refresh` stays outside
31
+ the integration surface (platform/auth, `docs/api-sdk-delta.md` §4). Behavioral parity with
32
+ `@uniqos/sdk@0.4.0` is maintained.
33
+
34
+ ### Changed
35
+
36
+ - **504 is no longer auto-retried.** A budget timeout means the turn was already too
37
+ slow; a blind retry risks another long turn and — combined with client-vs-server
38
+ timing — a silent double bill. Retrying a 504 is now the integrator's explicit choice.
39
+ 429 `rate_limit_exceeded` and 500/502/503 retry as before.
40
+ - **Default request timeout raised 30s → 90s** (`DEFAULT_TIMEOUT`), comfortably above the
41
+ server's ~75s turn ceiling (`ENGINE_TURN_BUDGET_MS` 60s + 15s socket margin, exported
42
+ as `SERVER_TURN_CEILING_SECONDS`). This guarantees the SDK receives the server's clean
43
+ `504` instead of abandoning a turn the server may still finish **and bill**.
44
+ Integrators who set `timeout` below the server budget take on that risk explicitly.
45
+
46
+ ## [0.2.0] - 2026-06-04
47
+
48
+ ### Added
49
+
50
+ - **`relationships.delete` now accepts `cascade` + `reason` (SPEC-14 §11.4, ADR-0006).** Mirrors
51
+ `end_users.delete`: keyword-only `cascade` (`Literal["all", "memory_only"]`) and `reason`
52
+ (`str`). `cascade` selects the deletion granularity (default `all` server-side); `reason`
53
+ records an optional audit note. Both travel in the DELETE body; omitting them keeps the prior
54
+ behavior (a bodyless full deletion). Unlike `end_users.delete`, there is **no** `model_only` —
55
+ the psychological model lives at the user level, not the relationship level (use
56
+ `end_users.delete(cascade="model_only")` for that). Closes the last TS↔Python `delete` surface
57
+ asymmetry (see `docs/api-sdk-delta.md`).
58
+
59
+ ### Notes
60
+
61
+ - Purely additive: the new arguments are keyword-only with `None` defaults, so existing
62
+ `relationships.delete(relationship_id)` callers are unaffected (no breaking change — unlike the
63
+ TypeScript `@uniqos/sdk@0.3.0` bump, where `config` shifted from the 2nd to the 3rd positional
64
+ argument).
65
+ - A type-only regression guard (`tests/static/test_typing.py`, checked by `mypy --strict`, not run
66
+ by `pytest`) pins `cascade` to the two contract literals and asserts `model_only` is rejected.
67
+
68
+ ## [0.1.0] - 2026-06-02
69
+
70
+ First published version. Functional parity with the TypeScript SDK (`@uniqos/sdk@0.1.0`),
71
+ implemented idiomatically for Python (snake_case, type hints, `py.typed`).
72
+
73
+ ### Added
74
+
75
+ - **Sync and async clients** (`UniqOS` / `AsyncUniqOS`) over httpx, with an identical public
76
+ surface — only the I/O is awaited (ADR PY-0001).
77
+ - **Configuration with fail-fast validation** (SPEC-17 §6): API-key format + environment
78
+ detection (`uniq_live_` / `uniq_test_`), `base_url` normalization, `timeout`, `max_retries`,
79
+ and log levels. An invalid key raises `ConfigurationError` before any HTTP call.
80
+ - **Idiomatic namespaces** (SPEC-17 §7): `engine` (+ the `respond` shortcut), `personalities`,
81
+ `catalog`, `vocabularies`, `end_users`, `relationships` (+ `memory`), `interactions`, `me`,
82
+ `organizations`, `api_keys`, `billing`.
83
+ - **Typed error hierarchy** with a code-first mapping (SPEC-17 §8): `rate_limit_exceeded` vs
84
+ `quota_exhausted` are distinguished by `code` on the same HTTP 429;
85
+ `RateLimitError.retry_after_seconds` is read from the `Retry-After` header.
86
+ - **Automatic retries** with exponential backoff + jitter, honoring `Retry-After`
87
+ (SPEC-17 §10.2): retries network errors / `429 rate_limit_exceeded` / 500–504; never
88
+ `quota_exhausted` / 401 / 403 / other 4xx.
89
+ - **Automatic idempotency keys** (UUID v4) on creation POSTs when none is provided, reused
90
+ across retries; an explicit key is honored (SPEC-17 §10.1, §10.4).
91
+ - **SSE streaming**: `respond(stream=True)` yields the five generated `RespondStreamEvent`
92
+ variants as typed dataclasses — a sync iterator or an async iterator (SPEC-17 §9). A
93
+ mid-stream drop raises `NetworkError` with the interruption point.
94
+ - **Configurable logging** (SPEC-17 §11) that never logs the full API key (only the prefix) nor
95
+ message content beyond 50 characters.
96
+ - **Five runnable examples** (`examples/`) and a quickstart README.
97
+
98
+ ### Implementation notes
99
+
100
+ - Data model: **dataclasses + `TypedDict`**, with **httpx** as the only runtime dependency
101
+ (ADR PY-0002). Responses are passed through as snake_case dicts; the streaming events are
102
+ frozen dataclasses generated from the OpenAPI contract.
103
+ - Generation: `make generate` (datamodel-code-generator) reads the shared root
104
+ `openapi/openapi.json` snapshot, scoped to the contract's 6 named schemas (ADR PY-0004).
105
+
106
+ ### Compatibility notes
107
+
108
+ - Streaming consumers that branch exhaustively on `event.type` must handle all five variants
109
+ (`metadata`, `text`, `guardrail_modulation`, `completion`, `error`).
110
+ - `end_users.delete` accepts `cascade` (`'all' | 'model_only' | 'memory_only'`) and `reason`,
111
+ ahead of the TypeScript SDK (tracked in `../../docs/api-sdk-delta.md`).
112
+
113
+ [0.1.0]: https://github.com/filuco2001/uniqos-sdks/releases/tag/python-v0.1.0
uniqos-0.4.0/Makefile ADDED
@@ -0,0 +1,59 @@
1
+ # Developer commands for the uniqos Python SDK.
2
+ # Mirrors the pnpm scripts of the TS SDK; all commands run through uv (ADR PY-0003).
3
+ #
4
+ # make install # create the venv and install the package + dev tools
5
+ # make generate # regenerate models from the shared OpenAPI snapshot (ADR PY-0004)
6
+ # make check # format-check + lint + typecheck + test (what CI runs)
7
+
8
+ .PHONY: install generate lint format format-check typecheck test build check clean
9
+
10
+ # Shared OpenAPI snapshot at the monorepo root — the SAME file the TS SDK reads.
11
+ OPENAPI := ../../openapi/openapi.json
12
+ GENERATED := src/uniqos/generated
13
+
14
+ install:
15
+ uv sync
16
+
17
+ # Regenerate the streaming-event dataclasses from the shared OpenAPI snapshot.
18
+ # Scoped to `schemas` (the contract's only 6 named schemas, all streaming — ADR PY-0004).
19
+ # --disable-timestamp keeps the output byte-stable so regeneration is a no-op when the
20
+ # snapshot is unchanged (CI verifies with `git diff --exit-code`).
21
+ generate:
22
+ uv run datamodel-codegen \
23
+ --input $(OPENAPI) \
24
+ --input-file-type openapi \
25
+ --output $(GENERATED)/stream_events.py \
26
+ --output-model-type dataclasses.dataclass \
27
+ --openapi-scopes schemas \
28
+ --enum-field-as-literal all \
29
+ --frozen-dataclasses \
30
+ --keyword-only \
31
+ --target-python-version 3.10 \
32
+ --use-standard-collections \
33
+ --use-union-operator \
34
+ --use-schema-description \
35
+ --disable-timestamp
36
+
37
+ lint:
38
+ uv run ruff check src tests examples
39
+
40
+ format:
41
+ uv run ruff format src tests examples
42
+
43
+ format-check:
44
+ uv run ruff format --check src tests examples
45
+
46
+ typecheck:
47
+ uv run mypy
48
+
49
+ test:
50
+ uv run pytest
51
+
52
+ build:
53
+ uv build
54
+
55
+ check: format-check lint typecheck test
56
+
57
+ clean:
58
+ rm -rf dist build .mypy_cache .ruff_cache .pytest_cache
59
+ find . -type d -name __pycache__ -prune -exec rm -rf {} +
uniqos-0.4.0/PKG-INFO ADDED
@@ -0,0 +1,160 @@
1
+ Metadata-Version: 2.4
2
+ Name: uniqos
3
+ Version: 0.4.0
4
+ Summary: Official uniqOS SDK — Personality & Emotional Intelligence Engine + Relational Memory for AI agents.
5
+ Project-URL: Homepage, https://docs.uniqos.ai
6
+ Project-URL: Documentation, https://docs.uniqos.ai
7
+ Project-URL: Repository, https://github.com/filuco2001/uniqos-sdks
8
+ Author: uniqOS
9
+ License-Expression: MIT
10
+ Keywords: agents,ai,emotional-intelligence,personality,relational-memory,sdk,uniqos
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Typing :: Typed
19
+ Requires-Python: >=3.10
20
+ Requires-Dist: httpx<1,>=0.27
21
+ Description-Content-Type: text/markdown
22
+
23
+ # uniqos
24
+
25
+ Official Python SDK for [uniqOS](https://uniqos.ai) — a Personality & Emotional Intelligence
26
+ Engine + Relational Memory layer for AI agents.
27
+
28
+ - Sync **and** async clients (`UniqOS` / `AsyncUniqOS`)
29
+ - Typed errors, automatic retries with backoff, automatic idempotency keys
30
+ - SSE streaming as a plain iterator
31
+ - Fully type-hinted (`py.typed`); one runtime dependency (`httpx`)
32
+ - Python 3.10+
33
+
34
+ ## Install
35
+
36
+ ```bash
37
+ pip install uniqos
38
+ ```
39
+
40
+ ## Quickstart
41
+
42
+ ```python
43
+ from uniqos import UniqOS
44
+
45
+ client = UniqOS(api_key="uniq_test_...")
46
+ response = client.respond(personality_id="pers_...", message="hola")
47
+ ```
48
+
49
+ `response` is the parsed JSON (snake_case, mirroring the API): `response["response"]` is the
50
+ agent's reply.
51
+
52
+ Get an API key from the [uniqOS dashboard](https://uniqos.ai). Keys start with `uniq_live_`
53
+ (production) or `uniq_test_` (sandbox); the SDK detects the environment from the prefix and
54
+ rejects a malformed key immediately.
55
+
56
+ ## Async
57
+
58
+ ```python
59
+ from uniqos import AsyncUniqOS
60
+
61
+ async with AsyncUniqOS(api_key="uniq_test_...") as client:
62
+ response = await client.respond(personality_id="pers_...", message="hola")
63
+ ```
64
+
65
+ Both clients expose the identical surface (`client.personalities`, `client.end_users`,
66
+ `client.billing`, …); the async one just awaits.
67
+
68
+ ## Streaming
69
+
70
+ ```python
71
+ stream = client.respond(personality_id="pers_...", message="tell me a story", stream=True)
72
+ for event in stream:
73
+ if event.type == "text":
74
+ print(event.delta, end="", flush=True)
75
+ elif event.type == "completion":
76
+ print(f"\n[{event.latency_ms}ms]")
77
+ ```
78
+
79
+ The five event types (`metadata`, `text`, `guardrail_modulation`, `completion`, `error`) are
80
+ typed dataclasses with attribute access. Async uses `async for`.
81
+
82
+ ## Errors
83
+
84
+ ```python
85
+ from uniqos import RateLimitError, QuotaExhaustedError
86
+
87
+ try:
88
+ response = client.respond(personality_id="pers_...", message="hi")
89
+ except RateLimitError as err:
90
+ print(f"Rate limited. Retry after {err.retry_after_seconds}s")
91
+ except QuotaExhaustedError as err:
92
+ print(f"Quota exhausted: {err.details}")
93
+ ```
94
+
95
+ Every error carries `code`, `message`, `request_id`, `http_status`, and `details`. Catch
96
+ `UniqOSError` for anything the SDK can raise. The SDK retries transient failures
97
+ (`429 rate_limit_exceeded`, `500/502/503`, network) automatically with exponential backoff; it
98
+ never retries `quota_exhausted`, `401`, `403`, or `504 turn_timeout` (see Configuration below).
99
+
100
+ ## Configuration
101
+
102
+ ```python
103
+ client = UniqOS(
104
+ api_key="uniq_test_...",
105
+ base_url="https://api.uniqos.ai", # host only; do NOT include /v1
106
+ timeout=90, # seconds; default 90s; 0 disables
107
+ max_retries=3, # 0 disables retries (504 is never auto-retried)
108
+ log_level="warning", # debug | info | warning | error | silent
109
+ logger=my_logger, # optional: any object with debug/info/warning/error
110
+ )
111
+ ```
112
+
113
+ > **Default timeout is 90s**, deliberately above the server's ~75s turn ceiling, so a slow
114
+ > turn surfaces as the server's clean `504 turn_timeout` (a `TurnTimeoutError`) instead of a
115
+ > client-side abort. A `504 turn_timeout` is **not retried automatically**: the turn already
116
+ > ran to the budget, so a blind retry risks a second long turn and — combined with
117
+ > client-vs-server timing — a double bill (ADR-0009). Retrying it is your explicit choice.
118
+
119
+ The SDK never logs the full API key (only the prefix) nor message content beyond 50 chars.
120
+
121
+ ## Examples
122
+
123
+ Runnable scripts live in [`examples/`](examples): `hello_world.py`, `stateful.py`,
124
+ `streaming.py`, `error_handling.py`, and `dogfood.py`.
125
+
126
+ **With a real (or local) API** — set the environment and run:
127
+
128
+ ```bash
129
+ export UNIQOS_API_KEY="uniq_test_..."
130
+ export UNIQOS_PERSONALITY_ID="pers_..."
131
+ # optional, for a local backend:
132
+ export UNIQOS_BASE_URL="http://localhost:3000"
133
+
134
+ python examples/hello_world.py
135
+ ```
136
+
137
+ **Without a network** — the test suite drives every code path against a mocked HTTP layer
138
+ ([`respx`](https://lundberg.github.io/respx/)), so no live API is needed to exercise the SDK:
139
+
140
+ ```python
141
+ import httpx, respx
142
+ from uniqos import UniqOS
143
+
144
+ @respx.mock
145
+ def test_it():
146
+ respx.post("https://api.uniqos.ai/v1/respond").mock(
147
+ return_value=httpx.Response(200, json={"response": "hi"})
148
+ )
149
+ with UniqOS(api_key="uniq_test_abcd1234") as client:
150
+ assert client.respond(personality_id="p_1", message="hi")["response"] == "hi"
151
+ ```
152
+
153
+ ## Documentation
154
+
155
+ Full reference and guides live at [docs.uniqos.ai](https://docs.uniqos.ai). This README and the
156
+ in-editor docstrings cover the essentials; the deep documentation is not duplicated here.
157
+
158
+ ## License
159
+
160
+ MIT
uniqos-0.4.0/README.md ADDED
@@ -0,0 +1,138 @@
1
+ # uniqos
2
+
3
+ Official Python SDK for [uniqOS](https://uniqos.ai) — a Personality & Emotional Intelligence
4
+ Engine + Relational Memory layer for AI agents.
5
+
6
+ - Sync **and** async clients (`UniqOS` / `AsyncUniqOS`)
7
+ - Typed errors, automatic retries with backoff, automatic idempotency keys
8
+ - SSE streaming as a plain iterator
9
+ - Fully type-hinted (`py.typed`); one runtime dependency (`httpx`)
10
+ - Python 3.10+
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ pip install uniqos
16
+ ```
17
+
18
+ ## Quickstart
19
+
20
+ ```python
21
+ from uniqos import UniqOS
22
+
23
+ client = UniqOS(api_key="uniq_test_...")
24
+ response = client.respond(personality_id="pers_...", message="hola")
25
+ ```
26
+
27
+ `response` is the parsed JSON (snake_case, mirroring the API): `response["response"]` is the
28
+ agent's reply.
29
+
30
+ Get an API key from the [uniqOS dashboard](https://uniqos.ai). Keys start with `uniq_live_`
31
+ (production) or `uniq_test_` (sandbox); the SDK detects the environment from the prefix and
32
+ rejects a malformed key immediately.
33
+
34
+ ## Async
35
+
36
+ ```python
37
+ from uniqos import AsyncUniqOS
38
+
39
+ async with AsyncUniqOS(api_key="uniq_test_...") as client:
40
+ response = await client.respond(personality_id="pers_...", message="hola")
41
+ ```
42
+
43
+ Both clients expose the identical surface (`client.personalities`, `client.end_users`,
44
+ `client.billing`, …); the async one just awaits.
45
+
46
+ ## Streaming
47
+
48
+ ```python
49
+ stream = client.respond(personality_id="pers_...", message="tell me a story", stream=True)
50
+ for event in stream:
51
+ if event.type == "text":
52
+ print(event.delta, end="", flush=True)
53
+ elif event.type == "completion":
54
+ print(f"\n[{event.latency_ms}ms]")
55
+ ```
56
+
57
+ The five event types (`metadata`, `text`, `guardrail_modulation`, `completion`, `error`) are
58
+ typed dataclasses with attribute access. Async uses `async for`.
59
+
60
+ ## Errors
61
+
62
+ ```python
63
+ from uniqos import RateLimitError, QuotaExhaustedError
64
+
65
+ try:
66
+ response = client.respond(personality_id="pers_...", message="hi")
67
+ except RateLimitError as err:
68
+ print(f"Rate limited. Retry after {err.retry_after_seconds}s")
69
+ except QuotaExhaustedError as err:
70
+ print(f"Quota exhausted: {err.details}")
71
+ ```
72
+
73
+ Every error carries `code`, `message`, `request_id`, `http_status`, and `details`. Catch
74
+ `UniqOSError` for anything the SDK can raise. The SDK retries transient failures
75
+ (`429 rate_limit_exceeded`, `500/502/503`, network) automatically with exponential backoff; it
76
+ never retries `quota_exhausted`, `401`, `403`, or `504 turn_timeout` (see Configuration below).
77
+
78
+ ## Configuration
79
+
80
+ ```python
81
+ client = UniqOS(
82
+ api_key="uniq_test_...",
83
+ base_url="https://api.uniqos.ai", # host only; do NOT include /v1
84
+ timeout=90, # seconds; default 90s; 0 disables
85
+ max_retries=3, # 0 disables retries (504 is never auto-retried)
86
+ log_level="warning", # debug | info | warning | error | silent
87
+ logger=my_logger, # optional: any object with debug/info/warning/error
88
+ )
89
+ ```
90
+
91
+ > **Default timeout is 90s**, deliberately above the server's ~75s turn ceiling, so a slow
92
+ > turn surfaces as the server's clean `504 turn_timeout` (a `TurnTimeoutError`) instead of a
93
+ > client-side abort. A `504 turn_timeout` is **not retried automatically**: the turn already
94
+ > ran to the budget, so a blind retry risks a second long turn and — combined with
95
+ > client-vs-server timing — a double bill (ADR-0009). Retrying it is your explicit choice.
96
+
97
+ The SDK never logs the full API key (only the prefix) nor message content beyond 50 chars.
98
+
99
+ ## Examples
100
+
101
+ Runnable scripts live in [`examples/`](examples): `hello_world.py`, `stateful.py`,
102
+ `streaming.py`, `error_handling.py`, and `dogfood.py`.
103
+
104
+ **With a real (or local) API** — set the environment and run:
105
+
106
+ ```bash
107
+ export UNIQOS_API_KEY="uniq_test_..."
108
+ export UNIQOS_PERSONALITY_ID="pers_..."
109
+ # optional, for a local backend:
110
+ export UNIQOS_BASE_URL="http://localhost:3000"
111
+
112
+ python examples/hello_world.py
113
+ ```
114
+
115
+ **Without a network** — the test suite drives every code path against a mocked HTTP layer
116
+ ([`respx`](https://lundberg.github.io/respx/)), so no live API is needed to exercise the SDK:
117
+
118
+ ```python
119
+ import httpx, respx
120
+ from uniqos import UniqOS
121
+
122
+ @respx.mock
123
+ def test_it():
124
+ respx.post("https://api.uniqos.ai/v1/respond").mock(
125
+ return_value=httpx.Response(200, json={"response": "hi"})
126
+ )
127
+ with UniqOS(api_key="uniq_test_abcd1234") as client:
128
+ assert client.respond(personality_id="p_1", message="hi")["response"] == "hi"
129
+ ```
130
+
131
+ ## Documentation
132
+
133
+ Full reference and guides live at [docs.uniqos.ai](https://docs.uniqos.ai). This README and the
134
+ in-editor docstrings cover the essentials; the deep documentation is not duplicated here.
135
+
136
+ ## License
137
+
138
+ MIT
@@ -0,0 +1,92 @@
1
+ # Developing the `uniqos` Python SDK
2
+
3
+ Conventions for this package. The decisions behind them live in the ADRs at
4
+ `../../docs/adr/py-0001..0004`. The TypeScript SDK (`../typescript`) is the reference
5
+ pattern; this package mirrors its structure and behavior with idiomatic Python.
6
+
7
+ ## Setup
8
+
9
+ ```bash
10
+ cd packages/python
11
+ make install # uv sync — creates .venv and installs the package + dev tools
12
+ ```
13
+
14
+ Requires [uv](https://docs.astral.sh/uv/). Everything else (ruff, mypy, pytest,
15
+ datamodel-code-generator) is a dev dependency installed by `uv sync`.
16
+
17
+ ## Commands
18
+
19
+ | Command | What it does |
20
+ | ---------------- | ----------------------------------------------------- |
21
+ | `make generate` | Regenerate models from the shared OpenAPI snapshot |
22
+ | `make format` | `ruff format` the code |
23
+ | `make lint` | `ruff check` |
24
+ | `make typecheck` | `mypy --strict` over `src/uniqos` |
25
+ | `make test` | `pytest` |
26
+ | `make build` | `uv build` → wheel + sdist in `dist/` |
27
+ | `make check` | format-check + lint + typecheck + test (what CI runs) |
28
+
29
+ ## Layout
30
+
31
+ ```
32
+ src/uniqos/
33
+ __init__.py # public surface + __version__ (single source of the version)
34
+ py.typed # PEP 561 marker — the package ships types
35
+ client.py # UniqOS (sync) — Bloque 2
36
+ async_client.py # AsyncUniqOS (async) — Bloque 2
37
+ errors.py # typed error hierarchy — Bloque 3
38
+ resources/ # idiomatic namespaces — Bloque 4
39
+ generated/ # AUTO-GENERATED, never edit — Bloque 1
40
+ _core/ # pure shared core (transport, config, retry, logging, streaming)
41
+ tests/ # pytest; mock httpx with respx (no live network)
42
+ examples/ # runnable examples — Bloque 6
43
+ ```
44
+
45
+ ## Core conventions
46
+
47
+ - **snake_case passthrough.** Request/response fields mirror the API verbatim. No
48
+ casing translation — in Python this is also the idiomatic form.
49
+ - **dataclasses + TypedDict, no runtime modeling (ADR PY-0002).** The only runtime
50
+ dependency is `httpx`. Streaming events are generated frozen dataclasses (the
51
+ contract's only named schemas); request payloads and key returned entities are
52
+ hand-written `TypedDict`s; the long tail is `dict[str, Any]` passthrough. We do
53
+ **not** validate responses at runtime — like the TS SDK, we trust the contract and
54
+ type structurally. The only fail-fast validation is client configuration
55
+ (api_key format, invalid option combinations).
56
+ - **Shared pure core, two thin clients (ADR PY-0001).** `_core/` holds I/O-agnostic
57
+ logic (config, errors, retry policy, SSE parsing) used by both `UniqOS` (over
58
+ `httpx.Client`) and `AsyncUniqOS` (over `httpx.AsyncClient`). Add a method to one
59
+ client → add it to the other; a surface-parity test guards against drift.
60
+ - **Never edit `generated/`.** It is overwritten by `make generate` from the vendored
61
+ snapshot and excluded from hand-maintenance. CI regenerates and fails on any diff.
62
+ - **Secrets & message content never logged.** Only the api_key prefix
63
+ (`uniq_test_kx7H...`); message content truncated to 50 chars at debug (SPEC-17 §11.3).
64
+
65
+ ## Code generation
66
+
67
+ `make generate` runs `datamodel-code-generator` over the shared snapshot and writes
68
+ `src/uniqos/generated/stream_events.py` — the **frozen dataclasses** for the five SSE
69
+ events plus their nested objects, and the `RespondStreamEvent` union (ADR PY-0004).
70
+
71
+ It is scoped to `--openapi-scopes schemas`: the contract's _only_ 6 named schemas are
72
+ the streaming ones, so nothing else is generated. Everything else in the SDK is
73
+ hand-written. `--disable-timestamp` keeps the output byte-stable, so **regenerating an
74
+ unchanged snapshot is a no-op** — CI runs `make generate` then `git diff --exit-code`
75
+ and fails if the committed file drifts. The formatter (black/isort, pinned via
76
+ `uv.lock`) makes the output reproducible across machines and CI.
77
+
78
+ ruff skips `generated/` (it is machine-emitted); mypy `--strict` still type-checks it.
79
+ Never edit the file by hand — change the contract and regenerate.
80
+
81
+ ## OpenAPI snapshot
82
+
83
+ Both SDKs read the **same** vendored snapshot at the monorepo root,
84
+ [`../../openapi/openapi.json`](../../openapi/openapi.json). To refresh the contract,
85
+ update the API, export its OpenAPI, refresh the snapshot, review the diff, regenerate,
86
+ and commit — see `../../openapi/README.md`.
87
+
88
+ ## Style
89
+
90
+ ruff (`line-length = 100`, target `py310`) for both format and lint; mypy `--strict`.
91
+ Match the surrounding code: function over lambda for exported helpers, `async`/`await`,
92
+ named exports, Google-style docstrings on public methods (SPEC-17 §14.4).
@@ -0,0 +1,26 @@
1
+ # Examples
2
+
3
+ Runnable scripts for the `uniqos` SDK:
4
+
5
+ | File | What it shows |
6
+ | ------------------- | ---------------------------------------------------------- |
7
+ | `hello_world.py` | A single stateless `respond` call |
8
+ | `stateful.py` | Persistent `user_id`, multi-turn relational memory |
9
+ | `streaming.py` | Token-by-token SSE (`for event in stream`) |
10
+ | `error_handling.py` | Catching `RateLimitError` / `QuotaExhaustedError` / others |
11
+ | `dogfood.py` | Smoke test against a local (or remote) API |
12
+
13
+ ## Running
14
+
15
+ Set the environment and run any script directly:
16
+
17
+ ```bash
18
+ export UNIQOS_API_KEY="uniq_test_..."
19
+ export UNIQOS_PERSONALITY_ID="pers_..."
20
+ export UNIQOS_BASE_URL="http://localhost:3000" # optional, for a local backend
21
+
22
+ python examples/hello_world.py
23
+ ```
24
+
25
+ To exercise the SDK **without a network**, see the mocked-`httpx` (`respx`) pattern in the
26
+ [package README](../README.md#examples) and the test suite under [`../tests/`](../tests).
@@ -0,0 +1,39 @@
1
+ """Dogfood smoke test — exercise the SDK against a local (or remote) API.
2
+
3
+ Mirror of the TypeScript SDK's dogfood smoke. Reads a couple of endpoints and runs one
4
+ stateless turn, printing what came back.
5
+
6
+ Run against a local API:
7
+ export UNIQOS_API_KEY="uniq_test_..."
8
+ export UNIQOS_BASE_URL="http://localhost:3000"
9
+ export UNIQOS_PERSONALITY_ID="pers_..."
10
+ python examples/dogfood.py
11
+ """
12
+
13
+ import os
14
+
15
+ from uniqos import UniqOS
16
+
17
+ DOGFOOD_USER = "dogfood_user_chess_001"
18
+
19
+
20
+ def main() -> None:
21
+ client = UniqOS(
22
+ api_key=os.environ["UNIQOS_API_KEY"],
23
+ base_url=os.environ.get("UNIQOS_BASE_URL", "http://localhost:3000"),
24
+ )
25
+ with client:
26
+ print(f"environment: {client.environment} base_url: {client.base_url}")
27
+
28
+ page = client.interactions.list(user_id=DOGFOOD_USER)
29
+ print(f"interactions for {DOGFOOD_USER}: {len(page.get('data', []))}")
30
+
31
+ result = client.respond(
32
+ personality_id=os.environ["UNIQOS_PERSONALITY_ID"],
33
+ message="Hello from the Python dogfood smoke test.",
34
+ )
35
+ print("respond:", result.get("response", result))
36
+
37
+
38
+ if __name__ == "__main__":
39
+ main()