matilda-client 0.3.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.
- matilda_client-0.3.0/MANIFEST.in +2 -0
- matilda_client-0.3.0/PKG-INFO +217 -0
- matilda_client-0.3.0/README.md +194 -0
- matilda_client-0.3.0/pyproject.toml +53 -0
- matilda_client-0.3.0/setup.cfg +4 -0
- matilda_client-0.3.0/src/matilda_client/__init__.py +196 -0
- matilda_client-0.3.0/src/matilda_client/_client.py +379 -0
- matilda_client-0.3.0/src/matilda_client/_config.py +78 -0
- matilda_client-0.3.0/src/matilda_client/_errors.py +117 -0
- matilda_client-0.3.0/src/matilda_client/_events.py +173 -0
- matilda_client-0.3.0/src/matilda_client/_schema.py +102 -0
- matilda_client-0.3.0/src/matilda_client/_sse.py +320 -0
- matilda_client-0.3.0/src/matilda_client/_version.py +28 -0
- matilda_client-0.3.0/src/matilda_client/api_keys.py +44 -0
- matilda_client-0.3.0/src/matilda_client/auth.py +616 -0
- matilda_client-0.3.0/src/matilda_client/auth_local.py +598 -0
- matilda_client-0.3.0/src/matilda_client/chat.py +376 -0
- matilda_client-0.3.0/src/matilda_client/cli.py +116 -0
- matilda_client-0.3.0/src/matilda_client/conversations.py +45 -0
- matilda_client-0.3.0/src/matilda_client/feedback.py +107 -0
- matilda_client-0.3.0/src/matilda_client/files.py +312 -0
- matilda_client-0.3.0/src/matilda_client/py.typed +0 -0
- matilda_client-0.3.0/src/matilda_client.egg-info/PKG-INFO +217 -0
- matilda_client-0.3.0/src/matilda_client.egg-info/SOURCES.txt +26 -0
- matilda_client-0.3.0/src/matilda_client.egg-info/dependency_links.txt +1 -0
- matilda_client-0.3.0/src/matilda_client.egg-info/entry_points.txt +2 -0
- matilda_client-0.3.0/src/matilda_client.egg-info/requires.txt +6 -0
- matilda_client-0.3.0/src/matilda_client.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: matilda-client
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Public Python client SDK for the Matilda API: OAuth login (PKCE + device flow), streaming chat, structured output, chunked file uploads, conversations, feedback, and API keys.
|
|
5
|
+
Author: Maincode
|
|
6
|
+
Project-URL: Repository, https://github.com/MaincodeHQ/matilda-core/tree/main/packages/python/matilda-client
|
|
7
|
+
Keywords: matilda,sdk,ai,llm,streaming
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
15
|
+
Classifier: Typing :: Typed
|
|
16
|
+
Requires-Python: >=3.12
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
Requires-Dist: httpx>=0.27
|
|
19
|
+
Requires-Dist: h11>=0.16.0
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
22
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
23
|
+
|
|
24
|
+
# matilda-client
|
|
25
|
+
|
|
26
|
+
Public Python client SDK for the Matilda API — the Python port of
|
|
27
|
+
`@maincode-ai/matilda-client-sdk`. OAuth login (PKCE + device flow) with
|
|
28
|
+
managed token refresh, streaming chat with typed events,
|
|
29
|
+
grammar-constrained structured output, robust chunked parallel file uploads,
|
|
30
|
+
conversations, feedback, and API-key management. Async-first, minimal
|
|
31
|
+
dependencies: `httpx` + stdlib only (per ADR-025 SDK-boundary rule).
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install matilda-client
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Authentication
|
|
40
|
+
|
|
41
|
+
Two managed login flows (core-auth is an OAuth facade over FusionAuth;
|
|
42
|
+
endpoints are discovered via RFC 8414 metadata). On success the client is
|
|
43
|
+
auto-wired with a TokenManager — every request carries a managed access
|
|
44
|
+
token, refreshed automatically (single-flight) before expiry and once on a
|
|
45
|
+
401:
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
async with MatildaClient(base_url="https://matilda.maincode.com/api") as client:
|
|
49
|
+
# Headless (servers, CI, SSH): prints a URL + user code, polls until approved
|
|
50
|
+
tokens = await client.auth.login_with_device_flow(client_id="matilda-code")
|
|
51
|
+
|
|
52
|
+
# Desktop: opens the browser, receives the callback on 127.0.0.1 (RFC 8252)
|
|
53
|
+
tokens = await client.auth.login_with_browser(
|
|
54
|
+
client_id="matilda-code", open_browser=lambda url: webbrowser.open(url)
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Persist the session across runs (0600 JSON file + cross-process refresh lock):
|
|
58
|
+
from matilda_client import create_file_token_store
|
|
59
|
+
store = create_file_token_store("~/.matilda/tokens.json")
|
|
60
|
+
await client.auth.login_with_device_flow(
|
|
61
|
+
client_id="matilda-code", token_store=store.store, token_lock=store.lock
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
await client.auth.get_tokens() # current TokenSet or None
|
|
65
|
+
await client.auth.logout() # clears the session + detaches the provider
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Or skip the flows and bring your own token:
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
MatildaClient(token="eyJ...") # static JWT
|
|
72
|
+
MatildaClient(get_token=my_async_token_provider) # provider, 401→refresh→retry
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The lower-level protocol is exported too (`fetch_auth_server_metadata`,
|
|
76
|
+
`create_pkce_pair`, `begin_login`/`complete_login` for web BFF redirects,
|
|
77
|
+
`request_device_code`/`poll_device_token`, `TokenManager`,
|
|
78
|
+
`StorageAdapter`) — mirror of the TS SDK's `auth-core`/`auth-node` split.
|
|
79
|
+
|
|
80
|
+
### `matilda-key` CLI
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
matilda-key create-api-key --name "ci-runner" [--scopes api:code,api:chat] [--expires-at 2026-12-31T23:59:59Z]
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Runs a device-flow login, mints an API key, and prints the **secret to
|
|
87
|
+
stdout** (pipeable) with all metadata on stderr.
|
|
88
|
+
|
|
89
|
+
## Quick start
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
import asyncio
|
|
93
|
+
from matilda_client import MatildaClient
|
|
94
|
+
|
|
95
|
+
async def main():
|
|
96
|
+
# Token comes from the matilda-code OAuth/PKCE flow (a FusionAuth Bearer JWT).
|
|
97
|
+
async with MatildaClient(token="eyJ...", base_url="https://matilda.maincode.com/api") as client:
|
|
98
|
+
response = await client.chat.create(input="G'day")
|
|
99
|
+
print(response.output_text)
|
|
100
|
+
|
|
101
|
+
asyncio.run(main())
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Chat
|
|
105
|
+
|
|
106
|
+
`stream` yields the full typed event stream; `create` accumulates one turn;
|
|
107
|
+
`stream_text` / `create_text` are the text-only conveniences.
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
# Full typed events
|
|
111
|
+
async for event in client.chat.stream(input="Summarise this", file_ids=["f_..."]):
|
|
112
|
+
if event.type == "response.output_text.delta":
|
|
113
|
+
print(event.delta, end="")
|
|
114
|
+
|
|
115
|
+
# Just the text, raising on stream errors
|
|
116
|
+
text = await client.chat.create_text(input="Hello")
|
|
117
|
+
|
|
118
|
+
# Multi-turn: thread the conversation id you want to continue. Omitting it
|
|
119
|
+
# starts a NEW conversation, and the response does not return the auto-created
|
|
120
|
+
# id — so hold the id yourself (from conversations.list or your own store).
|
|
121
|
+
await client.chat.create(input="Hi", conversation_id="conv_...")
|
|
122
|
+
await client.chat.create(input="And a follow-up", conversation_id="conv_...")
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Events mirror the TS SDK union: `ResponseCreated`, `OutputTextDelta`,
|
|
126
|
+
`OutputTextReplace`, `StatusEvent` / `QueuedEvent`, `ToolCall*`,
|
|
127
|
+
`GenerationStatus`, `UsageEvent`, `CursorEvent`, `Truncated`, `Completed`,
|
|
128
|
+
`ResponseError`.
|
|
129
|
+
|
|
130
|
+
### Structured output
|
|
131
|
+
|
|
132
|
+
Grammar-constrained generation from a JSON-schema dict or a pydantic model
|
|
133
|
+
class (duck-typed — pydantic is *not* a dependency; local `$ref`/`$defs` are
|
|
134
|
+
inlined before sending since the server's grammar compiler does not resolve
|
|
135
|
+
pointers):
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
schema = {
|
|
139
|
+
"type": "object",
|
|
140
|
+
"properties": {"name": {"type": "string"}, "age": {"type": "integer"}},
|
|
141
|
+
"required": ["name"],
|
|
142
|
+
}
|
|
143
|
+
result = await client.chat.create_object(schema, input="Extract a person from 'Ada, 36'")
|
|
144
|
+
print(result.object) # {'name': 'Ada', 'age': 36} — MatildaObjectParseError on failure
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Resume a detached stream
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
active = await client.chat.active_stream(conversation_id)
|
|
151
|
+
if active.get("streamId"):
|
|
152
|
+
async for event in client.chat.resume(active["streamId"], last_event_id=cursor):
|
|
153
|
+
...
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Files
|
|
157
|
+
|
|
158
|
+
Chunked parallel uploads (OpenAI-shaped, S3-multipart-backed), auto-selecting
|
|
159
|
+
single-shot vs chunked per file.
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
result = await client.files.upload("dataset.jsonl", on_progress=lambda pct: print(f"{pct}%"))
|
|
163
|
+
results = await client.files.upload_many(["a.jsonl", "b.jsonl"], file_concurrency=3)
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
- **Small files** (< `chunked_threshold`, default 16 MiB): single-shot `POST /files/upload`.
|
|
167
|
+
- **Large files**: create session → upload parts in parallel (default 4
|
|
168
|
+
concurrent) with per-part exponential-backoff retry (default 3) → complete
|
|
169
|
+
(assembles S3 object + runs scan/extract). A dropped chunk just retries.
|
|
170
|
+
|
|
171
|
+
## Conversations, feedback, API keys
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
conversations = await client.conversations.list(limit=20)
|
|
175
|
+
await client.conversations.update(conv_id, title="Quarterly report")
|
|
176
|
+
await client.conversations.set_message_feedback(conv_id, msg_id, "positive")
|
|
177
|
+
|
|
178
|
+
await client.feedback.report(message_id=msg_id, conversation_id=conv_id, reason="inaccurate")
|
|
179
|
+
await client.feedback.report_bug(title="SDK crash on resume", description="...")
|
|
180
|
+
|
|
181
|
+
key = await client.api_keys.create(name="ci") # secret returned exactly once
|
|
182
|
+
await client.api_keys.revoke(key_id)
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
`api_keys` hits core-auth at `{origin}/api/auth/api-keys` — the origin is
|
|
186
|
+
derived from `base_url`, so a `base_url` of `https://host/api` works as-is.
|
|
187
|
+
|
|
188
|
+
## Errors
|
|
189
|
+
|
|
190
|
+
`MatildaError` is the base class. Non-2xx responses raise `MatildaAPIError`
|
|
191
|
+
(`QuotaExceededError` on 429 with a structured quota body). Stream errors are
|
|
192
|
+
`ResponseError` events on `stream`/`create`, and raise as `MatildaStreamError`
|
|
193
|
+
on the text/object paths; `SafetyReplaceError` fires there when the server
|
|
194
|
+
replaces in-flight output. Uploads raise `UploadError` after retries are
|
|
195
|
+
exhausted.
|
|
196
|
+
|
|
197
|
+
If `config.get_token` (an async token provider) is configured, a 401 triggers
|
|
198
|
+
one automatic retry with a force-refreshed token.
|
|
199
|
+
|
|
200
|
+
## Configuration
|
|
201
|
+
|
|
202
|
+
| Parameter | Default | Notes |
|
|
203
|
+
|---|---|---|
|
|
204
|
+
| `chunk_size` | 8 MiB | Part size (server is authoritative) |
|
|
205
|
+
| `chunked_threshold` | 16 MiB | Files ≥ this use chunked |
|
|
206
|
+
| `part_concurrency` | 4 | Parallel parts per file |
|
|
207
|
+
| `max_part_retries` | 3 | Per-part retry count |
|
|
208
|
+
| `timeout` | 120 s | httpx request timeout |
|
|
209
|
+
| `api_version` | `2026-06-23` | Sent as `X-Matilda-API-Version` |
|
|
210
|
+
|
|
211
|
+
## Development
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
python -m venv .venv && .venv/bin/pip install -e ".[dev]"
|
|
215
|
+
.venv/bin/python -m pytest tests # includes a contract-drift guard against @matilda/contracts
|
|
216
|
+
.venv/bin/ruff check src tests
|
|
217
|
+
```
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# matilda-client
|
|
2
|
+
|
|
3
|
+
Public Python client SDK for the Matilda API — the Python port of
|
|
4
|
+
`@maincode-ai/matilda-client-sdk`. OAuth login (PKCE + device flow) with
|
|
5
|
+
managed token refresh, streaming chat with typed events,
|
|
6
|
+
grammar-constrained structured output, robust chunked parallel file uploads,
|
|
7
|
+
conversations, feedback, and API-key management. Async-first, minimal
|
|
8
|
+
dependencies: `httpx` + stdlib only (per ADR-025 SDK-boundary rule).
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pip install matilda-client
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Authentication
|
|
17
|
+
|
|
18
|
+
Two managed login flows (core-auth is an OAuth facade over FusionAuth;
|
|
19
|
+
endpoints are discovered via RFC 8414 metadata). On success the client is
|
|
20
|
+
auto-wired with a TokenManager — every request carries a managed access
|
|
21
|
+
token, refreshed automatically (single-flight) before expiry and once on a
|
|
22
|
+
401:
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
async with MatildaClient(base_url="https://matilda.maincode.com/api") as client:
|
|
26
|
+
# Headless (servers, CI, SSH): prints a URL + user code, polls until approved
|
|
27
|
+
tokens = await client.auth.login_with_device_flow(client_id="matilda-code")
|
|
28
|
+
|
|
29
|
+
# Desktop: opens the browser, receives the callback on 127.0.0.1 (RFC 8252)
|
|
30
|
+
tokens = await client.auth.login_with_browser(
|
|
31
|
+
client_id="matilda-code", open_browser=lambda url: webbrowser.open(url)
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# Persist the session across runs (0600 JSON file + cross-process refresh lock):
|
|
35
|
+
from matilda_client import create_file_token_store
|
|
36
|
+
store = create_file_token_store("~/.matilda/tokens.json")
|
|
37
|
+
await client.auth.login_with_device_flow(
|
|
38
|
+
client_id="matilda-code", token_store=store.store, token_lock=store.lock
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
await client.auth.get_tokens() # current TokenSet or None
|
|
42
|
+
await client.auth.logout() # clears the session + detaches the provider
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Or skip the flows and bring your own token:
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
MatildaClient(token="eyJ...") # static JWT
|
|
49
|
+
MatildaClient(get_token=my_async_token_provider) # provider, 401→refresh→retry
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The lower-level protocol is exported too (`fetch_auth_server_metadata`,
|
|
53
|
+
`create_pkce_pair`, `begin_login`/`complete_login` for web BFF redirects,
|
|
54
|
+
`request_device_code`/`poll_device_token`, `TokenManager`,
|
|
55
|
+
`StorageAdapter`) — mirror of the TS SDK's `auth-core`/`auth-node` split.
|
|
56
|
+
|
|
57
|
+
### `matilda-key` CLI
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
matilda-key create-api-key --name "ci-runner" [--scopes api:code,api:chat] [--expires-at 2026-12-31T23:59:59Z]
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Runs a device-flow login, mints an API key, and prints the **secret to
|
|
64
|
+
stdout** (pipeable) with all metadata on stderr.
|
|
65
|
+
|
|
66
|
+
## Quick start
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
import asyncio
|
|
70
|
+
from matilda_client import MatildaClient
|
|
71
|
+
|
|
72
|
+
async def main():
|
|
73
|
+
# Token comes from the matilda-code OAuth/PKCE flow (a FusionAuth Bearer JWT).
|
|
74
|
+
async with MatildaClient(token="eyJ...", base_url="https://matilda.maincode.com/api") as client:
|
|
75
|
+
response = await client.chat.create(input="G'day")
|
|
76
|
+
print(response.output_text)
|
|
77
|
+
|
|
78
|
+
asyncio.run(main())
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Chat
|
|
82
|
+
|
|
83
|
+
`stream` yields the full typed event stream; `create` accumulates one turn;
|
|
84
|
+
`stream_text` / `create_text` are the text-only conveniences.
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
# Full typed events
|
|
88
|
+
async for event in client.chat.stream(input="Summarise this", file_ids=["f_..."]):
|
|
89
|
+
if event.type == "response.output_text.delta":
|
|
90
|
+
print(event.delta, end="")
|
|
91
|
+
|
|
92
|
+
# Just the text, raising on stream errors
|
|
93
|
+
text = await client.chat.create_text(input="Hello")
|
|
94
|
+
|
|
95
|
+
# Multi-turn: thread the conversation id you want to continue. Omitting it
|
|
96
|
+
# starts a NEW conversation, and the response does not return the auto-created
|
|
97
|
+
# id — so hold the id yourself (from conversations.list or your own store).
|
|
98
|
+
await client.chat.create(input="Hi", conversation_id="conv_...")
|
|
99
|
+
await client.chat.create(input="And a follow-up", conversation_id="conv_...")
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Events mirror the TS SDK union: `ResponseCreated`, `OutputTextDelta`,
|
|
103
|
+
`OutputTextReplace`, `StatusEvent` / `QueuedEvent`, `ToolCall*`,
|
|
104
|
+
`GenerationStatus`, `UsageEvent`, `CursorEvent`, `Truncated`, `Completed`,
|
|
105
|
+
`ResponseError`.
|
|
106
|
+
|
|
107
|
+
### Structured output
|
|
108
|
+
|
|
109
|
+
Grammar-constrained generation from a JSON-schema dict or a pydantic model
|
|
110
|
+
class (duck-typed — pydantic is *not* a dependency; local `$ref`/`$defs` are
|
|
111
|
+
inlined before sending since the server's grammar compiler does not resolve
|
|
112
|
+
pointers):
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
schema = {
|
|
116
|
+
"type": "object",
|
|
117
|
+
"properties": {"name": {"type": "string"}, "age": {"type": "integer"}},
|
|
118
|
+
"required": ["name"],
|
|
119
|
+
}
|
|
120
|
+
result = await client.chat.create_object(schema, input="Extract a person from 'Ada, 36'")
|
|
121
|
+
print(result.object) # {'name': 'Ada', 'age': 36} — MatildaObjectParseError on failure
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Resume a detached stream
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
active = await client.chat.active_stream(conversation_id)
|
|
128
|
+
if active.get("streamId"):
|
|
129
|
+
async for event in client.chat.resume(active["streamId"], last_event_id=cursor):
|
|
130
|
+
...
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Files
|
|
134
|
+
|
|
135
|
+
Chunked parallel uploads (OpenAI-shaped, S3-multipart-backed), auto-selecting
|
|
136
|
+
single-shot vs chunked per file.
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
result = await client.files.upload("dataset.jsonl", on_progress=lambda pct: print(f"{pct}%"))
|
|
140
|
+
results = await client.files.upload_many(["a.jsonl", "b.jsonl"], file_concurrency=3)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
- **Small files** (< `chunked_threshold`, default 16 MiB): single-shot `POST /files/upload`.
|
|
144
|
+
- **Large files**: create session → upload parts in parallel (default 4
|
|
145
|
+
concurrent) with per-part exponential-backoff retry (default 3) → complete
|
|
146
|
+
(assembles S3 object + runs scan/extract). A dropped chunk just retries.
|
|
147
|
+
|
|
148
|
+
## Conversations, feedback, API keys
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
conversations = await client.conversations.list(limit=20)
|
|
152
|
+
await client.conversations.update(conv_id, title="Quarterly report")
|
|
153
|
+
await client.conversations.set_message_feedback(conv_id, msg_id, "positive")
|
|
154
|
+
|
|
155
|
+
await client.feedback.report(message_id=msg_id, conversation_id=conv_id, reason="inaccurate")
|
|
156
|
+
await client.feedback.report_bug(title="SDK crash on resume", description="...")
|
|
157
|
+
|
|
158
|
+
key = await client.api_keys.create(name="ci") # secret returned exactly once
|
|
159
|
+
await client.api_keys.revoke(key_id)
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
`api_keys` hits core-auth at `{origin}/api/auth/api-keys` — the origin is
|
|
163
|
+
derived from `base_url`, so a `base_url` of `https://host/api` works as-is.
|
|
164
|
+
|
|
165
|
+
## Errors
|
|
166
|
+
|
|
167
|
+
`MatildaError` is the base class. Non-2xx responses raise `MatildaAPIError`
|
|
168
|
+
(`QuotaExceededError` on 429 with a structured quota body). Stream errors are
|
|
169
|
+
`ResponseError` events on `stream`/`create`, and raise as `MatildaStreamError`
|
|
170
|
+
on the text/object paths; `SafetyReplaceError` fires there when the server
|
|
171
|
+
replaces in-flight output. Uploads raise `UploadError` after retries are
|
|
172
|
+
exhausted.
|
|
173
|
+
|
|
174
|
+
If `config.get_token` (an async token provider) is configured, a 401 triggers
|
|
175
|
+
one automatic retry with a force-refreshed token.
|
|
176
|
+
|
|
177
|
+
## Configuration
|
|
178
|
+
|
|
179
|
+
| Parameter | Default | Notes |
|
|
180
|
+
|---|---|---|
|
|
181
|
+
| `chunk_size` | 8 MiB | Part size (server is authoritative) |
|
|
182
|
+
| `chunked_threshold` | 16 MiB | Files ≥ this use chunked |
|
|
183
|
+
| `part_concurrency` | 4 | Parallel parts per file |
|
|
184
|
+
| `max_part_retries` | 3 | Per-part retry count |
|
|
185
|
+
| `timeout` | 120 s | httpx request timeout |
|
|
186
|
+
| `api_version` | `2026-06-23` | Sent as `X-Matilda-API-Version` |
|
|
187
|
+
|
|
188
|
+
## Development
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
python -m venv .venv && .venv/bin/pip install -e ".[dev]"
|
|
192
|
+
.venv/bin/python -m pytest tests # includes a contract-drift guard against @matilda/contracts
|
|
193
|
+
.venv/bin/ruff check src tests
|
|
194
|
+
```
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "matilda-client"
|
|
3
|
+
version = "0.3.0"
|
|
4
|
+
description = "Public Python client SDK for the Matilda API: OAuth login (PKCE + device flow), streaming chat, structured output, chunked file uploads, conversations, feedback, and API keys."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
authors = [{name = "Maincode"}]
|
|
8
|
+
keywords = ["matilda", "sdk", "ai", "llm", "streaming"]
|
|
9
|
+
classifiers = [
|
|
10
|
+
"Development Status :: 4 - Beta",
|
|
11
|
+
"Intended Audience :: Developers",
|
|
12
|
+
"Operating System :: OS Independent",
|
|
13
|
+
"Programming Language :: Python :: 3",
|
|
14
|
+
"Programming Language :: Python :: 3.12",
|
|
15
|
+
"Programming Language :: Python :: 3.13",
|
|
16
|
+
"Programming Language :: Python :: 3.14",
|
|
17
|
+
"Typing :: Typed",
|
|
18
|
+
]
|
|
19
|
+
dependencies = [
|
|
20
|
+
"httpx>=0.27",
|
|
21
|
+
"h11>=0.16.0", # CVE-2025-43859: explicit floor on transitive HTTP/1.1 parser
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.optional-dependencies]
|
|
25
|
+
dev = [
|
|
26
|
+
"pytest>=8.0",
|
|
27
|
+
"pytest-asyncio>=0.23",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.scripts]
|
|
31
|
+
matilda-key = "matilda_client.cli:main"
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
Repository = "https://github.com/MaincodeHQ/matilda-core/tree/main/packages/python/matilda-client"
|
|
35
|
+
|
|
36
|
+
# Release toolchain is pinned exactly so builds can't drift between releases
|
|
37
|
+
# (same rule as uv/ruff/build/twine in .github/workflows/publish-pypi.yml).
|
|
38
|
+
[build-system]
|
|
39
|
+
requires = ["setuptools==84.0.0"]
|
|
40
|
+
build-backend = "setuptools.build_meta"
|
|
41
|
+
|
|
42
|
+
[tool.setuptools.packages.find]
|
|
43
|
+
where = ["src"]
|
|
44
|
+
|
|
45
|
+
[tool.setuptools.package-data]
|
|
46
|
+
matilda_client = ["py.typed"]
|
|
47
|
+
|
|
48
|
+
[tool.setuptools.exclude-package-data]
|
|
49
|
+
"*" = ["tests", "tests.*"]
|
|
50
|
+
|
|
51
|
+
[tool.pytest.ini_options]
|
|
52
|
+
asyncio_mode = "auto"
|
|
53
|
+
pythonpath = ["src"]
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""Public Python client SDK for the Matilda API.
|
|
2
|
+
|
|
3
|
+
Async-first (httpx) client mirroring the TypeScript
|
|
4
|
+
``@maincode-ai/matilda-client-sdk``: streaming chat with typed events,
|
|
5
|
+
grammar-constrained structured output, robust chunked parallel file uploads,
|
|
6
|
+
conversations, feedback (including SDK bug reports), and API-key management.
|
|
7
|
+
|
|
8
|
+
Minimal dependencies: httpx + stdlib only, per ADR-025's SDK-boundary rule.
|
|
9
|
+
|
|
10
|
+
async with MatildaClient(token="eyJ...") as client:
|
|
11
|
+
response = await client.chat.create(input="G'day")
|
|
12
|
+
print(response.output_text)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from ._client import MatildaClient, PublicRuntimeConfig
|
|
16
|
+
from ._config import (
|
|
17
|
+
DEFAULT_BASE_URL,
|
|
18
|
+
DEFAULT_CHUNK_SIZE,
|
|
19
|
+
DEFAULT_CHUNKED_THRESHOLD,
|
|
20
|
+
DEFAULT_MAX_PART_RETRIES,
|
|
21
|
+
DEFAULT_PART_CONCURRENCY,
|
|
22
|
+
MAX_FILE_COUNT,
|
|
23
|
+
MatildaConfig,
|
|
24
|
+
TokenProvider,
|
|
25
|
+
)
|
|
26
|
+
from ._errors import (
|
|
27
|
+
CHAT_ERROR_CODES,
|
|
28
|
+
ChatErrorCode,
|
|
29
|
+
MatildaAPIError,
|
|
30
|
+
MatildaContentTypeError,
|
|
31
|
+
MatildaError,
|
|
32
|
+
MatildaObjectParseError,
|
|
33
|
+
MatildaStreamError,
|
|
34
|
+
QuotaExceededError,
|
|
35
|
+
SafetyReplaceError,
|
|
36
|
+
UploadError,
|
|
37
|
+
)
|
|
38
|
+
from ._events import (
|
|
39
|
+
ChatEvent,
|
|
40
|
+
Completed,
|
|
41
|
+
CursorEvent,
|
|
42
|
+
EventCallback,
|
|
43
|
+
GenerationStatus,
|
|
44
|
+
MatildaChatResponse,
|
|
45
|
+
MatildaObjectEvent,
|
|
46
|
+
MatildaObjectResponse,
|
|
47
|
+
ObjectEvent,
|
|
48
|
+
OutputTextDelta,
|
|
49
|
+
OutputTextReplace,
|
|
50
|
+
QueuedEvent,
|
|
51
|
+
ResponseCreated,
|
|
52
|
+
ResponseError,
|
|
53
|
+
StatusEvent,
|
|
54
|
+
StreamErrorInfo,
|
|
55
|
+
ToolCallCompleted,
|
|
56
|
+
ToolCallProgress,
|
|
57
|
+
ToolCallStarted,
|
|
58
|
+
Truncated,
|
|
59
|
+
Usage,
|
|
60
|
+
UsageEvent,
|
|
61
|
+
)
|
|
62
|
+
from ._sse import DEFAULT_STALL_TIMEOUT_MS
|
|
63
|
+
from ._version import (
|
|
64
|
+
MATILDA_API_VERSION_HEADER,
|
|
65
|
+
MATILDA_CURRENT_API_VERSION,
|
|
66
|
+
sdk_version,
|
|
67
|
+
)
|
|
68
|
+
from .auth import (
|
|
69
|
+
AuthError,
|
|
70
|
+
AuthServerMetadata,
|
|
71
|
+
BeginLoginResult,
|
|
72
|
+
DeviceAuthorization,
|
|
73
|
+
MemoryStorage,
|
|
74
|
+
StorageAdapter,
|
|
75
|
+
TokenManager,
|
|
76
|
+
TokenSet,
|
|
77
|
+
assert_state,
|
|
78
|
+
base64_url_encode,
|
|
79
|
+
begin_login,
|
|
80
|
+
build_authorize_url,
|
|
81
|
+
complete_login,
|
|
82
|
+
create_pkce_pair,
|
|
83
|
+
create_token_manager,
|
|
84
|
+
exchange_code_for_tokens,
|
|
85
|
+
fetch_auth_server_metadata,
|
|
86
|
+
memory_storage,
|
|
87
|
+
poll_device_token,
|
|
88
|
+
random_state,
|
|
89
|
+
refresh_token_grant,
|
|
90
|
+
request_device_code,
|
|
91
|
+
)
|
|
92
|
+
from .auth_local import (
|
|
93
|
+
DEFAULT_SUCCESS_REDIRECT,
|
|
94
|
+
AuthorizeUrlEvent,
|
|
95
|
+
AuthResource,
|
|
96
|
+
FileTokenStore,
|
|
97
|
+
LoginFlowEvent,
|
|
98
|
+
LoopbackReceiver,
|
|
99
|
+
StateEvent,
|
|
100
|
+
UserCodeEvent,
|
|
101
|
+
create_file_token_store,
|
|
102
|
+
create_loopback_receiver,
|
|
103
|
+
default_device_on_event,
|
|
104
|
+
run_device_login_flow,
|
|
105
|
+
run_loopback_login_flow,
|
|
106
|
+
)
|
|
107
|
+
from .feedback import FeedbackPlatform, ReportReason
|
|
108
|
+
from .files import S3_MIN_CHUNK_SIZE, FileComplete
|
|
109
|
+
|
|
110
|
+
__all__ = [
|
|
111
|
+
"CHAT_ERROR_CODES",
|
|
112
|
+
"DEFAULT_BASE_URL",
|
|
113
|
+
"DEFAULT_CHUNK_SIZE",
|
|
114
|
+
"DEFAULT_CHUNKED_THRESHOLD",
|
|
115
|
+
"DEFAULT_MAX_PART_RETRIES",
|
|
116
|
+
"DEFAULT_PART_CONCURRENCY",
|
|
117
|
+
"DEFAULT_STALL_TIMEOUT_MS",
|
|
118
|
+
"DEFAULT_SUCCESS_REDIRECT",
|
|
119
|
+
"MATILDA_API_VERSION_HEADER",
|
|
120
|
+
"MATILDA_CURRENT_API_VERSION",
|
|
121
|
+
"MAX_FILE_COUNT",
|
|
122
|
+
"S3_MIN_CHUNK_SIZE",
|
|
123
|
+
"AuthError",
|
|
124
|
+
"AuthResource",
|
|
125
|
+
"AuthServerMetadata",
|
|
126
|
+
"AuthorizeUrlEvent",
|
|
127
|
+
"BeginLoginResult",
|
|
128
|
+
"ChatErrorCode",
|
|
129
|
+
"ChatEvent",
|
|
130
|
+
"Completed",
|
|
131
|
+
"CursorEvent",
|
|
132
|
+
"DeviceAuthorization",
|
|
133
|
+
"EventCallback",
|
|
134
|
+
"FeedbackPlatform",
|
|
135
|
+
"FileComplete",
|
|
136
|
+
"FileTokenStore",
|
|
137
|
+
"GenerationStatus",
|
|
138
|
+
"LoginFlowEvent",
|
|
139
|
+
"LoopbackReceiver",
|
|
140
|
+
"MatildaAPIError",
|
|
141
|
+
"MatildaChatResponse",
|
|
142
|
+
"MatildaClient",
|
|
143
|
+
"MatildaConfig",
|
|
144
|
+
"MatildaContentTypeError",
|
|
145
|
+
"MatildaError",
|
|
146
|
+
"MatildaObjectEvent",
|
|
147
|
+
"MatildaObjectParseError",
|
|
148
|
+
"MatildaObjectResponse",
|
|
149
|
+
"MatildaStreamError",
|
|
150
|
+
"MemoryStorage",
|
|
151
|
+
"ObjectEvent",
|
|
152
|
+
"OutputTextDelta",
|
|
153
|
+
"OutputTextReplace",
|
|
154
|
+
"PublicRuntimeConfig",
|
|
155
|
+
"QueuedEvent",
|
|
156
|
+
"QuotaExceededError",
|
|
157
|
+
"ReportReason",
|
|
158
|
+
"ResponseCreated",
|
|
159
|
+
"ResponseError",
|
|
160
|
+
"SafetyReplaceError",
|
|
161
|
+
"StateEvent",
|
|
162
|
+
"StatusEvent",
|
|
163
|
+
"StorageAdapter",
|
|
164
|
+
"StreamErrorInfo",
|
|
165
|
+
"TokenManager",
|
|
166
|
+
"TokenProvider",
|
|
167
|
+
"TokenSet",
|
|
168
|
+
"ToolCallCompleted",
|
|
169
|
+
"ToolCallProgress",
|
|
170
|
+
"ToolCallStarted",
|
|
171
|
+
"Truncated",
|
|
172
|
+
"UploadError",
|
|
173
|
+
"Usage",
|
|
174
|
+
"UsageEvent",
|
|
175
|
+
"UserCodeEvent",
|
|
176
|
+
"assert_state",
|
|
177
|
+
"base64_url_encode",
|
|
178
|
+
"begin_login",
|
|
179
|
+
"build_authorize_url",
|
|
180
|
+
"complete_login",
|
|
181
|
+
"create_file_token_store",
|
|
182
|
+
"create_loopback_receiver",
|
|
183
|
+
"create_pkce_pair",
|
|
184
|
+
"create_token_manager",
|
|
185
|
+
"default_device_on_event",
|
|
186
|
+
"exchange_code_for_tokens",
|
|
187
|
+
"fetch_auth_server_metadata",
|
|
188
|
+
"memory_storage",
|
|
189
|
+
"poll_device_token",
|
|
190
|
+
"random_state",
|
|
191
|
+
"refresh_token_grant",
|
|
192
|
+
"request_device_code",
|
|
193
|
+
"run_device_login_flow",
|
|
194
|
+
"run_loopback_login_flow",
|
|
195
|
+
"sdk_version",
|
|
196
|
+
]
|