matelab-python-sdk 0.1.0a5__tar.gz → 0.1.0a6__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.
- matelab_python_sdk-0.1.0a6/.env.example +14 -0
- {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/AGENTS.md +5 -2
- {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/CHANGELOG.md +8 -0
- {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/PKG-INFO +106 -150
- {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/README.md +105 -149
- {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/contracts/matelab-integration-v1.lock.json +3 -3
- {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/contracts/matelab-integration-v1.openapi.yaml +35 -34
- {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/docs/operation-coverage.yaml +7 -5
- {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/docs/roadmap.md +40 -42
- {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/pyproject.toml +3 -1
- {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/scripts/generate_models.py +1 -5
- {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/src/matelab/__init__.py +2 -34
- {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/src/matelab/_generated/models.py +3 -3
- matelab_python_sdk-0.1.0a6/src/matelab/_transport.py +276 -0
- {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/src/matelab/_wire_validation.py +38 -35
- matelab_python_sdk-0.1.0a6/src/matelab/client.py +120 -0
- matelab_python_sdk-0.1.0a6/src/matelab/domains/cloud_drive.py +356 -0
- {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/src/matelab/domains/groups.py +7 -9
- {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/src/matelab/domains/literature.py +120 -341
- {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/src/matelab/domains/notebooks.py +18 -66
- {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/src/matelab/domains/records.py +77 -606
- {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/src/matelab/domains/templates.py +52 -152
- {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/src/matelab/domains/uploads.py +3 -14
- {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/src/matelab/domains/users.py +4 -17
- {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/src/matelab/errors.py +3 -11
- matelab_python_sdk-0.1.0a6/src/matelab/session.py +32 -0
- {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/src/matelab/streaming.py +11 -14
- {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/tests/conftest.py +4 -2
- {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/tests/provider/test_provider_smoke.py +12 -26
- matelab_python_sdk-0.1.0a6/tests/test_client.py +394 -0
- {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/tests/test_cloud_drive.py +101 -148
- {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/tests/test_comments_and_downloads.py +17 -27
- {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/tests/test_consumer_smoke.py +36 -46
- {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/tests/test_extended_records.py +40 -69
- {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/tests/test_generation.py +14 -31
- {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/tests/test_groups.py +16 -27
- matelab_python_sdk-0.1.0a6/tests/test_notebook_lifecycle.py +306 -0
- {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/tests/test_notebooks.py +13 -22
- {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/tests/test_operation_coverage.py +17 -8
- {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/tests/test_record_lifecycle.py +64 -235
- {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/tests/test_record_mutations.py +223 -553
- {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/tests/test_records.py +36 -58
- {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/tests/test_release.py +1 -5
- {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/tests/test_streaming.py +61 -84
- {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/tests/test_template_lifecycle.py +95 -256
- {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/tests/test_templates.py +37 -58
- {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/tests/test_transport.py +128 -188
- {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/tests/test_uploads_and_literature.py +124 -187
- {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/tests/test_users.py +27 -45
- {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/tests/typing/consumer.py +8 -16
- {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/uv.lock +1 -1
- matelab_python_sdk-0.1.0a5/.env.example +0 -6
- matelab_python_sdk-0.1.0a5/docs/attachment-write-contract-gaps.md +0 -103
- matelab_python_sdk-0.1.0a5/src/matelab/_transport.py +0 -500
- matelab_python_sdk-0.1.0a5/src/matelab/client.py +0 -172
- matelab_python_sdk-0.1.0a5/src/matelab/domains/cloud_drive.py +0 -573
- matelab_python_sdk-0.1.0a5/src/matelab/session.py +0 -27
- matelab_python_sdk-0.1.0a5/tests/test_client.py +0 -570
- matelab_python_sdk-0.1.0a5/tests/test_notebook_lifecycle.py +0 -454
- {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/.github/workflows/release.yml +0 -0
- {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/.gitignore +0 -0
- {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/LICENSE +0 -0
- {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/NOTICE +0 -0
- {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/scripts/check_installed_package.py +0 -0
- {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/scripts/check_release.py +0 -0
- {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/src/matelab/_generated/__init__.py +0 -0
- {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/src/matelab/domains/__init__.py +0 -0
- {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/src/matelab/py.typed +0 -0
- {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/tests/typing/pyproject.toml +0 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# SDK Provider smoke must target the confirmed isolated test service.
|
|
2
|
+
# Authentication and external-token refresh persist Provider token state.
|
|
3
|
+
# Keep these names aligned with matelab-spec Provider Verification.
|
|
4
|
+
MATELAB_PROVIDER_BASE_URL=
|
|
5
|
+
MATELAB_PROVIDER_USERNAME=
|
|
6
|
+
MATELAB_PROVIDER_PASSWORD=
|
|
7
|
+
|
|
8
|
+
# The SDK smoke does not read the settings below. Populate them only when the
|
|
9
|
+
# same .env.test will also run the complete matelab-spec Provider Verification.
|
|
10
|
+
MATELAB_PROVIDER_SHARE_USER_ID=
|
|
11
|
+
MATELAB_PROVIDER_SECONDARY_USERNAME=
|
|
12
|
+
MATELAB_PROVIDER_SECONDARY_PASSWORD=
|
|
13
|
+
MATELAB_PROVIDER_RECORD_STAGING_MUTATIONS=
|
|
14
|
+
MATELAB_PROVIDER_CHAT_SSO_KEY=
|
|
@@ -50,8 +50,11 @@
|
|
|
50
50
|
- `base_url` 表示 Provider API 根。query、JSON、form-urlencoded、multipart 和 streaming 必须按各 operation
|
|
51
51
|
定义分别实现。
|
|
52
52
|
- bearer 只用于要求认证的 operation;成功码按 operation 配置,不能全局接受 `{0, 10}`。
|
|
53
|
-
- 每个 `AsyncMatelab` 实例最多持有一个进程内 Session,包含 access/refresh token、毫秒 expiration
|
|
54
|
-
identity
|
|
53
|
+
- 每个 `AsyncMatelab` 实例最多持有一个进程内 Session,包含 access/refresh token、毫秒 expiration 和已验证
|
|
54
|
+
identity。SDK 不提供裸 access/refresh token 绑定接口;构造传入完整 Session 的真实性和身份归属验证属于
|
|
55
|
+
集成方。
|
|
56
|
+
- `Token`、`Identity` 和 `Session` 是冻结、拒绝额外字段的 Pydantic public model,组成稳定的规范化 Session
|
|
57
|
+
JSON 契约。token 在 repr 中隐藏,但显式序列化必须保留真实凭证;序列化结果不得进入日志或发送给不可信方。
|
|
55
58
|
- `client.session` 暴露实例内最新 Session。持久化、加密、撤销、`userid/session_id` 关联和跨进程锁属于
|
|
56
59
|
集成方;不同逻辑 Session 不得共享同一个 `AsyncMatelab`,但多个实例可以共享注入的 `httpx.AsyncClient`。
|
|
57
60
|
- access 到期前按 skew 刷新;并发刷新使用单个异步锁;已认证且可安全重试的请求遇到 `code=5` 最多强制
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.0a6 - 2026-08-01
|
|
4
|
+
|
|
5
|
+
- Adopts the immutable `matelab-spec v0.3.0` Contract release and its form-encoding updates.
|
|
6
|
+
- Tightens the public authentication/session contract around complete, serializable Pydantic `Session` models while
|
|
7
|
+
retaining automatic access-token refresh and Provider SSO exchange.
|
|
8
|
+
- Simplifies transport and domain interfaces, removes external-token binding and identity-attachment seams, and
|
|
9
|
+
restores strict runtime validation for byte-range inputs.
|
|
10
|
+
|
|
3
11
|
## 0.1.0a5 - 2026-07-29
|
|
4
12
|
|
|
5
13
|
- Adopts immutable `matelab-spec v0.1.2` at commit `6dd674ac001ae61197495ace5eba489d585cb825` and OpenAPI
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: matelab-python-sdk
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.0a6
|
|
4
4
|
Summary: Reusable async Python client for the Matelab Integration Contract
|
|
5
5
|
Author-email: 朱天念 <zhutiannian@gmail.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -28,10 +28,10 @@ Description-Content-Type: text/markdown
|
|
|
28
28
|
|
|
29
29
|
Reusable async Python client for the Matelab Integration Contract.
|
|
30
30
|
|
|
31
|
-
The current alpha is `0.1.
|
|
31
|
+
The current alpha is `0.1.0a6`. `[project].version` in `pyproject.toml` is the sole SDK version source;
|
|
32
32
|
`uv.lock` only mirrors that source.
|
|
33
33
|
|
|
34
|
-
The SDK is pinned to the immutable `matelab-spec v0.
|
|
34
|
+
The SDK is pinned to the immutable `matelab-spec v0.3.0` Contract Release. The sole release pin is
|
|
35
35
|
`contracts/matelab-integration-v1.lock.json`, which records
|
|
36
36
|
the source tag, commit, OpenAPI path, local snapshot path, and SHA-256.
|
|
37
37
|
|
|
@@ -57,7 +57,7 @@ To test the same artifact a downstream Consumer will install, build and install
|
|
|
57
57
|
|
|
58
58
|
```bash
|
|
59
59
|
uv build --no-build-isolation --out-dir dist/release
|
|
60
|
-
python -m pip install dist/release/matelab_python_sdk-0.1.
|
|
60
|
+
python -m pip install dist/release/matelab_python_sdk-0.1.0a6-py3-none-any.whl
|
|
61
61
|
```
|
|
62
62
|
|
|
63
63
|
Do not infer Provider compatibility from the SDK version alone. A release is also bound to the Contract
|
|
@@ -92,13 +92,10 @@ Catch `MatelabError` for one application-level fallback, or a specific subclass
|
|
|
92
92
|
| `MatelabAuthenticationError` | The Session is missing, expired, invalid, or rejected; refresh or authenticate again as appropriate. |
|
|
93
93
|
| `MatelabProviderError` | The Provider rejected a valid request with a business error; inspect `code` and do not assume a mutation was applied. |
|
|
94
94
|
| `MatelabTransportError` | The HTTP exchange failed; `status_code` is present for HTTP failures, and a mutation outcome may be unknown. |
|
|
95
|
-
| `MatelabProtocolError` | The Provider response does not match the pinned Contract; treat it as Provider drift or an SDK defect. |
|
|
96
|
-
| `MatelabVerificationError` | A mutation was acknowledged but its readback failed; the mutation may already have persisted. |
|
|
95
|
+
| `MatelabProtocolError` | The Provider response does not match the pinned Contract; treat it as Provider drift or an SDK defect. A mutation may already have been applied. |
|
|
97
96
|
|
|
98
|
-
`code
|
|
99
|
-
|
|
100
|
-
documented scalar attributes. Never automatically retry a mutation solely because it raised a transport or
|
|
101
|
-
verification error.
|
|
97
|
+
`code` and `status_code` are the stable scalar diagnostics. Provider response bodies and caller inputs are never
|
|
98
|
+
attached to exceptions. Never automatically retry a mutation solely because it raised a transport or protocol error.
|
|
102
99
|
|
|
103
100
|
### Session ownership
|
|
104
101
|
|
|
@@ -114,24 +111,28 @@ rotation through `client.session`. If refresh succeeds but the subsequent busine
|
|
|
114
111
|
| Contract validation of token and identity responses | Mapping `userid`/`session_id` to a persisted `Session` |
|
|
115
112
|
| Latest immutable `Session` through `client.session` | Revocation, cleanup, and saving after each call |
|
|
116
113
|
|
|
117
|
-
`Session`, `Token`, and `Identity` are
|
|
118
|
-
|
|
114
|
+
`Session`, `Token`, and `Identity` are frozen Pydantic models and form the stable, normalized SDK Session contract.
|
|
115
|
+
Their JSON fields are `access`, `refresh`, and `identity`; tokens contain `value` and `expires_at_ms`, while identity
|
|
116
|
+
contains `userid`, `username`, and `email`. Provider envelope fields are not part of this model. An `Identity` requires
|
|
117
|
+
a positive userid and non-empty username; its email may be `None`.
|
|
119
118
|
|
|
120
|
-
|
|
119
|
+
Token values are excluded from model representations but intentionally remain present in `model_dump()` and
|
|
120
|
+
`model_dump_json()` so an integration can persist and restore the complete Session:
|
|
121
121
|
|
|
122
122
|
```python
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
123
|
+
serialized = session.model_dump_json()
|
|
124
|
+
restored = Session.model_validate_json(serialized)
|
|
125
|
+
assert restored == session
|
|
126
126
|
```
|
|
127
127
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
128
|
+
The serialized result contains live credentials. Encrypt it at rest and never write it to logs or send it to an
|
|
129
|
+
untrusted party. The SDK does not read tokens from environment variables and does not provide a session store.
|
|
130
|
+
|
|
131
|
+
Credential authentication installs the returned Session on the client:
|
|
131
132
|
|
|
132
133
|
```python
|
|
133
134
|
async with AsyncMatelab() as client:
|
|
134
|
-
session = await client.
|
|
135
|
+
session = await client.authenticate(username, password)
|
|
135
136
|
assert client.session is session
|
|
136
137
|
```
|
|
137
138
|
|
|
@@ -144,6 +145,10 @@ async with AsyncMatelab(session=persisted_session) as client:
|
|
|
144
145
|
result = await handle_request(client)
|
|
145
146
|
```
|
|
146
147
|
|
|
148
|
+
Every Session contains a validated identity, and refreshing a restored Session preserves it. Integrations that
|
|
149
|
+
construct a Session from an external assertion are responsible for validating that assertion and the identity-token
|
|
150
|
+
association before passing the complete Session to the SDK; the SDK does not accept bare external token pairs.
|
|
151
|
+
|
|
147
152
|
A Web or MCP integration should create one client for one logical session, then save the latest Session in
|
|
148
153
|
`finally`, including when a business call fails after refresh:
|
|
149
154
|
|
|
@@ -194,7 +199,7 @@ notebook selector used for upload:
|
|
|
194
199
|
```python
|
|
195
200
|
import hashlib
|
|
196
201
|
|
|
197
|
-
from matelab import RecordImportItem
|
|
202
|
+
from matelab import RecordImportItem
|
|
198
203
|
|
|
199
204
|
content = b"measurement data"
|
|
200
205
|
staged = await client.records.stage_attachment(
|
|
@@ -206,13 +211,10 @@ staged = await client.records.stage_attachment(
|
|
|
206
211
|
)
|
|
207
212
|
result = await client.records.import_dataset(
|
|
208
213
|
notebook=notebook,
|
|
209
|
-
template=TemplateRef(template_id=8),
|
|
210
214
|
template_title="Example Template",
|
|
211
215
|
items=(
|
|
212
216
|
RecordImportItem(
|
|
213
|
-
record_uid="REC-IMPORT-001",
|
|
214
|
-
title="Imported measurement",
|
|
215
|
-
data={"Attachments": {"File": [staged]}},
|
|
217
|
+
record_uid="REC-IMPORT-001", title="Imported measurement", data={"Attachments": {"File": [staged]}}
|
|
216
218
|
),
|
|
217
219
|
),
|
|
218
220
|
)
|
|
@@ -228,11 +230,7 @@ from matelab import RecordFormAttachmentFieldAddition
|
|
|
228
230
|
result = await client.records.update(
|
|
229
231
|
source,
|
|
230
232
|
attachment_changes=(
|
|
231
|
-
RecordFormAttachmentFieldAddition(
|
|
232
|
-
module="Attachments",
|
|
233
|
-
name="Measurement",
|
|
234
|
-
attachment=staged,
|
|
235
|
-
),
|
|
233
|
+
RecordFormAttachmentFieldAddition(module="Attachments", name="Measurement", attachment=staged),
|
|
236
234
|
),
|
|
237
235
|
)
|
|
238
236
|
```
|
|
@@ -241,7 +239,7 @@ Choose exactly one finalizer. A staged name may occur once in either a single-re
|
|
|
241
239
|
do not reuse it after transport starts, even when the Provider outcome is unknown. A locally rejected target or
|
|
242
240
|
argument does not spend the handle. The SDK rejects raw Provider attachment references, cross-user or cross-notebook
|
|
243
241
|
handles, unsafe combinations, duplicate use in one request, and a second finalization attempt through the same
|
|
244
|
-
client.
|
|
242
|
+
client.
|
|
245
243
|
|
|
246
244
|
The Provider supplies no staging status, abort, TTL, atomicity, idempotency, or retry guarantee. Integrations that
|
|
247
245
|
persist handles must durably claim their own `uploaded -> finalizing/indeterminate` transition before calling a
|
|
@@ -253,46 +251,26 @@ The v0.1.2 Contract adds dedicated, verified notebook-staged update shapes. They
|
|
|
253
251
|
never construct Provider paths or attachment strings:
|
|
254
252
|
|
|
255
253
|
```python
|
|
256
|
-
from matelab import
|
|
257
|
-
RecordFilesAttachmentRootAppend,
|
|
258
|
-
RecordTableFileCellSet,
|
|
259
|
-
RecordTableRowAttachmentAppend,
|
|
260
|
-
)
|
|
254
|
+
from matelab import RecordFilesAttachmentRootAppend, RecordTableFileCellSet, RecordTableRowAttachmentAppend
|
|
261
255
|
|
|
262
256
|
# Each example is a separate finalizer; never run several with the same handle.
|
|
263
257
|
await client.records.update(
|
|
264
258
|
source,
|
|
265
|
-
attachment_changes=(
|
|
266
|
-
RecordTableFileCellSet(
|
|
267
|
-
table="Measurements",
|
|
268
|
-
column="Evidence",
|
|
269
|
-
row=0,
|
|
270
|
-
attachment=staged,
|
|
271
|
-
),
|
|
272
|
-
),
|
|
259
|
+
attachment_changes=(RecordTableFileCellSet(table="Measurements", column="Evidence", row=0, attachment=staged),),
|
|
273
260
|
)
|
|
274
261
|
|
|
275
262
|
await client.records.update(
|
|
276
263
|
source,
|
|
277
264
|
attachment_changes=(
|
|
278
265
|
RecordTableRowAttachmentAppend(
|
|
279
|
-
table="Measurements",
|
|
280
|
-
file_column="Evidence",
|
|
281
|
-
values={"Label": "Sample C"},
|
|
282
|
-
attachment=another_staged,
|
|
266
|
+
table="Measurements", file_column="Evidence", values={"Label": "Sample C"}, attachment=another_staged
|
|
283
267
|
),
|
|
284
268
|
),
|
|
285
269
|
)
|
|
286
270
|
|
|
287
271
|
await client.records.update(
|
|
288
272
|
source,
|
|
289
|
-
attachment_changes=(
|
|
290
|
-
RecordFilesAttachmentRootAppend(
|
|
291
|
-
module="Files",
|
|
292
|
-
caption="Evidence",
|
|
293
|
-
attachment=third_staged,
|
|
294
|
-
),
|
|
295
|
-
),
|
|
273
|
+
attachment_changes=(RecordFilesAttachmentRootAppend(module="Files", caption="Evidence", attachment=third_staged),),
|
|
296
274
|
)
|
|
297
275
|
```
|
|
298
276
|
|
|
@@ -313,15 +291,14 @@ existing = next(
|
|
|
313
291
|
if attachment.location is not None and attachment.location.kind == "files_module"
|
|
314
292
|
)
|
|
315
293
|
await client.records.update(
|
|
316
|
-
source,
|
|
317
|
-
attachment_changes=(RecordFilesAttachmentReplacement(existing=existing, replacement=staged, caption=None),),
|
|
294
|
+
source, attachment_changes=(RecordFilesAttachmentReplacement(existing=existing, replacement=staged, caption=None),)
|
|
318
295
|
)
|
|
319
296
|
```
|
|
320
297
|
|
|
321
298
|
For a files occurrence, `caption=None` preserves the observed string caption (an observed null caption normalizes to
|
|
322
|
-
the required empty string).
|
|
323
|
-
exactly one current attachment in the selected cell and uses
|
|
324
|
-
forms require the new hash to differ.
|
|
299
|
+
the required empty string). The update preserves the Provider `uid` and folder in the submitted content. Table
|
|
300
|
+
replacement requires exactly one current attachment in the selected cell and uses
|
|
301
|
+
`RecordTableFileAttachmentReplacement`. Both replacement forms require the new hash to differ.
|
|
325
302
|
|
|
326
303
|
Row/index-based finalizers use an immediate SDK read and must not be called while a concurrent editor is known to be
|
|
327
304
|
active. The Provider offers no expected hash or revision. `expected_content_sha256` is only a client-side prewrite
|
|
@@ -330,11 +307,7 @@ check, not Provider compare-and-swap.
|
|
|
330
307
|
File-bearing structure changes do not consume staging:
|
|
331
308
|
|
|
332
309
|
```python
|
|
333
|
-
from matelab import
|
|
334
|
-
RecordFormFileFieldDeletion,
|
|
335
|
-
RecordTableFileColumnAddition,
|
|
336
|
-
RecordTableFileColumnDeletion,
|
|
337
|
-
)
|
|
310
|
+
from matelab import RecordFormFileFieldDeletion, RecordTableFileColumnAddition, RecordTableFileColumnDeletion
|
|
338
311
|
|
|
339
312
|
await client.records.update(
|
|
340
313
|
source,
|
|
@@ -346,39 +319,35 @@ await client.records.update(
|
|
|
346
319
|
)
|
|
347
320
|
```
|
|
348
321
|
|
|
349
|
-
The SDK emits the v0.1.2 dedicated operations: a new file column omits wire `data
|
|
350
|
-
|
|
351
|
-
|
|
322
|
+
The SDK emits the v0.1.2 dedicated operations: a new file column omits wire `data`; whole
|
|
323
|
+
form-field/table-column deletion uses a strict two-segment target. It does not expose a three-segment delete as
|
|
324
|
+
table-cell clearing.
|
|
352
325
|
|
|
353
326
|
Deleting a complete canonical module is also supported even when the current read observes attachments in it:
|
|
354
327
|
|
|
355
328
|
```python
|
|
356
329
|
result = await client.records.update(
|
|
357
|
-
source,
|
|
358
|
-
module_deletions=("Raw files",),
|
|
359
|
-
expected_content_sha256=record.content_sha256,
|
|
330
|
+
source, module_deletions=("Raw files",), expected_content_sha256=record.content_sha256
|
|
360
331
|
)
|
|
361
332
|
```
|
|
362
333
|
|
|
363
|
-
This requests only a canonical record-content mutation.
|
|
364
|
-
attachment-quote cleanup or byte deletion. Collaboration-pending
|
|
334
|
+
This requests only a canonical record-content mutation. A successful response acknowledges the submitted mutation;
|
|
335
|
+
it does not prove attachment-quote cleanup or byte deletion. Collaboration-pending is reported as
|
|
336
|
+
`"pending_browser_save"`, not persisted.
|
|
365
337
|
|
|
366
338
|
Notebook-staged rich-text binding remains unsupported. Provider Verification shows that `#file{name}` is stored as
|
|
367
339
|
plain text and that a hash-based URI can fall back to an existing quote, so neither is a one-shot staged finalizer.
|
|
368
340
|
Existing `RecordRichTextUpdate` remains legal only with record-scoped `StagedRecordAttachment`. Attachment-bearing
|
|
369
341
|
multi-record import is also forbidden; callers must split it into single-record finalizers.
|
|
370
342
|
|
|
371
|
-
See `docs/attachment-write-contract-gaps.md` in the source repository for the exact support matrix, integration
|
|
372
|
-
lifecycle requirements, and minimum Contract changes needed to expose the remaining operations.
|
|
373
|
-
|
|
374
343
|
## Implementation roadmap
|
|
375
344
|
|
|
376
345
|
`docs/roadmap.md` is the complete SDK-only execution plan. It assigns all 71
|
|
377
|
-
`matelab-spec v0.
|
|
346
|
+
`matelab-spec v0.3.0` operations to ordered work packages, defines the machine-readable coverage that
|
|
378
347
|
must be added, records Provider-risk gates, and specifies the final completion checks.
|
|
379
348
|
|
|
380
|
-
The SDK
|
|
381
|
-
migration, adjacent-repository changes, external
|
|
349
|
+
The SDK tracks all 71 Contract operations and exposes 70 through public domain interfaces; one identity-bootstrap
|
|
350
|
+
operation is intentionally unexposed. It deliberately excludes MCP migration, adjacent-repository changes, external
|
|
382
351
|
publishing, and automatic mutation against a real Provider.
|
|
383
352
|
|
|
384
353
|
Machine-readable status lives in
|
|
@@ -388,7 +357,7 @@ OpenAPI snapshot.
|
|
|
388
357
|
|
|
389
358
|
| Domain | Implemented | Planned | Current public surface |
|
|
390
359
|
|---|---:|---:|---|
|
|
391
|
-
| Authentication |
|
|
360
|
+
| Authentication | 3 | 0 | `authenticate`, `refresh`, `exchange_chat_sso_code`; identity bootstrap intentionally unexposed |
|
|
392
361
|
| Groups and users | 2 | 0 | `groups.list`, `users.search` |
|
|
393
362
|
| Notebooks | 6 | 0 | `notebooks.list/create/update/shares/share/update_share/unshare` |
|
|
394
363
|
| Records | 18 | 0 | Discovery, reads, lifecycle, typed patch/attachments, relations, and downloads |
|
|
@@ -397,20 +366,21 @@ OpenAPI snapshot.
|
|
|
397
366
|
| File staging | 1 | 0 | Resumable fragment staging and compensating abort request |
|
|
398
367
|
| Literature | 13 | 0 | Libraries, items, canonical metadata, comments, sharing, PDF lifecycle and streaming |
|
|
399
368
|
| Cloud drive | 9 | 0 | Personal root/folders/files, staged binding, metadata, move/delete and streaming |
|
|
400
|
-
| **Total** | **
|
|
369
|
+
| **Total** | **70** | **0** | One operation is intentionally unexposed |
|
|
401
370
|
|
|
402
371
|
### Stability and known capability limits
|
|
403
372
|
|
|
404
|
-
Coverage currently contains
|
|
405
|
-
|
|
373
|
+
Coverage currently contains 14 `stable`, 56 `experimental`, and one `not_applicable` operation. The stable operation
|
|
374
|
+
IDs are `loginTokenSet`, `refreshTokenSet`, `exchangeChatSsoCode`,
|
|
406
375
|
`shareMultipleTemplatesWithUsers`, `removeTemplateFromGroup`, `deleteNotebookShare`, `listNotebooks`,
|
|
407
376
|
`listNotebookRecords`, `exportRecords`, `deleteRecordsByUid`, `copyRecord`, `readRecord`,
|
|
408
377
|
`deletePersonalLiteratureItem`, and `readLiteratureCreateTemplate`.
|
|
409
378
|
|
|
410
379
|
Every other implemented operation is explicitly `experimental`; the exact per-operation list and its
|
|
411
380
|
PVD/PCG references live in
|
|
412
|
-
`docs/operation-coverage.yaml`.
|
|
413
|
-
|
|
381
|
+
`docs/operation-coverage.yaml`. `resolveCurrentIdentity` is intentionally unexposed because the SDK accepts only
|
|
382
|
+
complete, integration-validated Sessions and does not bind bare external token pairs. There are no `planned`
|
|
383
|
+
operations. Experimental support means the SDK validates and exposes the
|
|
414
384
|
pinned Contract while preserving limitations such as unstable ordering/pagination, incomplete mutation
|
|
415
385
|
acknowledgements, missing batch atomicity or idempotency, weak attachment ownership binding, and known
|
|
416
386
|
Provider authorization gaps. It does not turn those limitations into SDK guarantees.
|
|
@@ -425,10 +395,8 @@ session = await client.exchange_chat_sso_code(code="chat-sanitizedcode123", key=
|
|
|
425
395
|
Group and user discovery expose sharing identities without inventing Provider pagination:
|
|
426
396
|
|
|
427
397
|
```python
|
|
428
|
-
from matelab import UserSearchScope
|
|
429
|
-
|
|
430
398
|
groups = await client.groups.list()
|
|
431
|
-
targets = await client.users.search("Example Researcher",
|
|
399
|
+
targets = await client.users.search("Example Researcher", global_scope=False)
|
|
432
400
|
```
|
|
433
401
|
|
|
434
402
|
Ordering remains Provider-unspecified and is documented rather than repeated as a constant result field. Group
|
|
@@ -436,19 +404,18 @@ members belong only to `groups.members_for`, not to every returned group. These
|
|
|
436
404
|
Provider returns members for an unstable first group and user search is unpaged, unordered, and not field-minimized
|
|
437
405
|
(PVD-006, PVD-029, PCG-011).
|
|
438
406
|
|
|
439
|
-
Notebook create/update and direct sharing
|
|
407
|
+
Notebook create/update and direct sharing use the Provider acknowledgement without an automatic follow-up read:
|
|
440
408
|
|
|
441
409
|
```python
|
|
442
410
|
await client.notebooks.create(title="Example Notebook")
|
|
443
|
-
shares = await client.notebooks.
|
|
444
|
-
|
|
411
|
+
shares = await client.notebooks.shares(notebook)
|
|
412
|
+
await client.notebooks.share(notebook, [target.ref])
|
|
413
|
+
await client.notebooks.update_share(shares.shares[0].ref, write=True, create=True)
|
|
445
414
|
```
|
|
446
415
|
|
|
447
|
-
Create
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
or `None`. A stored share mask of zero still has effective read access (PVD-010), and share-list order remains
|
|
451
|
-
unspecified.
|
|
416
|
+
Create, update, share, permission update, and unshare return `None` because their Provider responses contain no new
|
|
417
|
+
resource representation. Call `list()` or `shares()` explicitly when the application needs current state. A stored
|
|
418
|
+
share mask of zero still has effective read access (PVD-010), and share-list order remains unspecified.
|
|
452
419
|
|
|
453
420
|
Template discovery keeps a template database identity separate from direct-share, market-acquisition, and group
|
|
454
421
|
relation identities:
|
|
@@ -466,9 +433,9 @@ experimental because Provider discovery ordering/pagination and historical `imag
|
|
|
466
433
|
stable (PCG-003, PCG-009, PVD-013, PVD-022).
|
|
467
434
|
|
|
468
435
|
Template writes remain separate operations: metadata, canonical modules, and usage HTML are not presented as one
|
|
469
|
-
transaction. Metadata create returns the
|
|
470
|
-
return `None` because the Provider supplies no new identity or
|
|
471
|
-
|
|
436
|
+
transaction. Metadata create/update returns the `TemplateRef` built from the Provider template ID. Other mutations
|
|
437
|
+
return `None` because the Provider supplies no new identity or resource representation; callers can explicitly list
|
|
438
|
+
or read when they need current state.
|
|
472
439
|
Direct-share, market-acquisition, and group relation refs remain distinct. Marketplace revision and uploader-binding
|
|
473
440
|
limitations are documented operation semantics rather than constant fields on every result (PVD-021, PVD-026).
|
|
474
441
|
`UploadBindingRef.new()` creates the fresh hidden correlation value required by intro attachment binding.
|
|
@@ -494,43 +461,40 @@ remain unspecified, and no continuation token is invented.
|
|
|
494
461
|
Record creation keeps blank creation and structured import as separate capabilities:
|
|
495
462
|
|
|
496
463
|
```python
|
|
497
|
-
from matelab import RecordImportItem
|
|
464
|
+
from matelab import RecordImportItem
|
|
498
465
|
|
|
499
|
-
|
|
466
|
+
await client.records.create_blank(notebook=notebook, title="Blank Record", record_uid="caller-generated-uid")
|
|
500
467
|
imported = await client.records.import_dataset(
|
|
501
468
|
notebook=notebook,
|
|
502
|
-
template=TemplateRef(template_id=8),
|
|
503
469
|
template_title="Example Template",
|
|
504
470
|
items=[RecordImportItem(record_uid="import-uid", title="Imported", data={"Notes": "value"})],
|
|
505
471
|
)
|
|
506
472
|
```
|
|
507
473
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
row with the same UID but a different database ID as proof of success (PCG-005). Record mutations are not
|
|
513
|
-
automatically retried.
|
|
474
|
+
Blank-record creation returns `None` because the Provider returns no record identity, even when the caller supplies a
|
|
475
|
+
UID. Import validates the complete batch with generated wire models but cannot map returned database IDs to
|
|
476
|
+
individual inputs or promise atomicity (PCG-008). Delete means moving records into the recycle bin, not permanent
|
|
477
|
+
deletion. Delete and restore return `None`; record mutations are not automatically retried.
|
|
514
478
|
|
|
515
479
|
Record patching exposes a deliberately narrower capability than the raw Provider operation. Scalar/module changes
|
|
516
480
|
cannot smuggle Provider-native attachment strings; staged attachments use separate form-removal, table-replacement,
|
|
517
481
|
files append/replace/remove, and rich-text types. Unsafe form replacement and table-file removal are absent, while a
|
|
518
482
|
files/images removal is rejected when the observed module contains the same hash more than once (PVD-014 through
|
|
519
483
|
PVD-016). `Record.content_sha256` can be supplied as a client-side precondition, documented as advisory
|
|
520
|
-
read-before-write rather than Provider CAS. `
|
|
521
|
-
|
|
522
|
-
remain distinct, and mutation
|
|
484
|
+
read-before-write rather than Provider CAS. `records.update()` returns the acknowledgement classification
|
|
485
|
+
`"provider_reported_persisted"`, `"pending_browser_save"`, or `"provider_acknowledged_unclassified"`; it does not
|
|
486
|
+
issue a post-write read. Database, active-browser, and unclassified acknowledgements remain distinct, and mutation
|
|
487
|
+
retries stay disabled.
|
|
523
488
|
|
|
524
489
|
Relation addition reads both endpoints and checks their resolved data server before writing; this reduces PVD-019
|
|
525
|
-
risk but is not an atomic Provider authorization guarantee. Relation
|
|
526
|
-
|
|
527
|
-
|
|
490
|
+
risk but is not an atomic Provider authorization guarantee. Relation deletion refuses an observed cross-notebook
|
|
491
|
+
target-ID collision because the Provider ignores target notebook identity (PVD-020). Both mutations return `None`
|
|
492
|
+
after acknowledgement.
|
|
528
493
|
|
|
529
494
|
Comment upload follows the Provider's literal one-request `upload` field, not the incompatible Front fragment
|
|
530
|
-
protocol (PVD-037). Comment
|
|
531
|
-
comment is
|
|
532
|
-
|
|
533
|
-
binding remains affected by PVD-026.
|
|
495
|
+
protocol (PVD-037). Comment mutations return `None` after acknowledgement. Edit and delete first verify that the
|
|
496
|
+
selected comment is currently observed and caller-owned, but do not perform a post-write read (PVD-004). Staged
|
|
497
|
+
comment attachments have no Contract abort operation, and binding remains affected by PVD-026.
|
|
534
498
|
|
|
535
499
|
Attachment bytes are streamed and must be consumed or closed explicitly:
|
|
536
500
|
|
|
@@ -592,10 +556,10 @@ await client.literature.create(LiteratureMetadata(title="Example import", doi="1
|
|
|
592
556
|
|
|
593
557
|
Create returns `None` and never guesses the new item from list position because the Provider returns no ID. Canonical update reads the
|
|
594
558
|
item first and refuses to drop source/hidden fields unless `allow_source_metadata_loss=True` is explicit (PVD-027).
|
|
595
|
-
PDF replace/delete are separate
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
559
|
+
PDF replace/delete are separate acknowledged mutations and are not presented as atomic with metadata (PCG-010).
|
|
560
|
+
Permanent personal deletion is named `permanently_delete`, returns `None`, and is non-recoverable. Sharing requires
|
|
561
|
+
list-observed item summaries, user-search summaries and a
|
|
562
|
+
valid caller identity, then returns `None` because the Provider supplies no per-recipient IDs (PVD-012, PVD-036).
|
|
599
563
|
|
|
600
564
|
Literature comments use one public save intent: detail is read first, an existing caller-owned comment is edited, and
|
|
601
565
|
otherwise a comment is created. Multiple caller-owned comments are rejected as ambiguous (PVD-035). A staged
|
|
@@ -609,13 +573,8 @@ The personal cloud-drive surface keeps root, folder, final file and temporary st
|
|
|
609
573
|
|
|
610
574
|
```python
|
|
611
575
|
listing = await client.cloud_drive.list()
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
if len(matches) == 1:
|
|
616
|
-
renamed = await client.cloud_drive.update_file(
|
|
617
|
-
matches[0], filename="example-renamed.pdf", description="Sanitized description"
|
|
618
|
-
)
|
|
576
|
+
folder = await client.cloud_drive.create_folder(name="Example data")
|
|
577
|
+
await client.cloud_drive.bind_staged_file(staged, target=folder)
|
|
619
578
|
```
|
|
620
579
|
|
|
621
580
|
`CloudDriveListing` contains a typed file page, complete folder snapshot, quota usage and personal-root permissions rather
|
|
@@ -623,14 +582,11 @@ than flattening them into one ambiguous collection. Folder browse results retain
|
|
|
623
582
|
are explicitly root-wide and return `location=None` because the Provider omits each match's folder ID. Ordering has
|
|
624
583
|
no stable ID tie-breaker (PCG-003, PVD-013).
|
|
625
584
|
|
|
626
|
-
Folder create returns the Provider ID
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
Batch move returns identities observed in the target, while permanent delete returns identities confirmed absent;
|
|
632
|
-
neither claims Provider per-item results or atomicity. Permanent deletion is named `permanently_delete_files` and is
|
|
633
|
-
non-recoverable. Cloud downloads
|
|
585
|
+
Folder create returns a `CloudFolderRef` built from the Provider ID. Other folder and file mutations return `None`
|
|
586
|
+
after acknowledgement and do not automatically list the drive. Staged finalize accepts a completed `StagedFile`;
|
|
587
|
+
the Provider binds by temporary row ID without checking its owner (PVD-031), and finalize atomicity/idempotency remain
|
|
588
|
+
absent (PCG-012). Batch move and permanent delete do not claim Provider per-item results or atomicity. Permanent
|
|
589
|
+
deletion is named `permanently_delete_files` and is non-recoverable. Cloud downloads
|
|
634
590
|
resolve bytes from the final file identity and reuse `DownloadStream`, thumbnail/preview choices and the PVD-002-safe
|
|
635
591
|
range subset. Cloud mutations are not automatically retried.
|
|
636
592
|
|
|
@@ -647,8 +603,8 @@ Provider identifiers distinct. Historical reads first re-read the authorized cur
|
|
|
647
603
|
that the requested version is still present in its `modify_log`; both reads write Provider audit entries.
|
|
648
604
|
|
|
649
605
|
Errors are separated into Provider business errors, authentication errors, HTTP/transport errors,
|
|
650
|
-
Integration Contract response errors, and client-side usage errors.
|
|
651
|
-
|
|
606
|
+
Integration Contract response errors, and client-side usage errors. Provider response bodies and caller inputs do not
|
|
607
|
+
enter exceptions.
|
|
652
608
|
|
|
653
609
|
## Development
|
|
654
610
|
|
|
@@ -679,9 +635,9 @@ Node wheel retain the compatible exact pair `basedpyright==1.39.9` and
|
|
|
679
635
|
## Opt-in Provider consumer smoke
|
|
680
636
|
|
|
681
637
|
`tests/provider/test_provider_smoke.py` exercises the consumer flow through only the public SDK interface. Its base
|
|
682
|
-
scenario covers credential authentication,
|
|
683
|
-
`AsyncMatelab
|
|
684
|
-
|
|
638
|
+
scenario covers credential authentication, persistence of the complete returned Session, restoration through a new
|
|
639
|
+
`AsyncMatelab` instance, refresh with identity preservation, and notebook discovery. It is not Provider Verification
|
|
640
|
+
and is skipped by default.
|
|
685
641
|
|
|
686
642
|
Raw Provider conformance remains the responsibility of `matelab-spec`, which sends direct HTTP requests and validates
|
|
687
643
|
the unmodified responses. The SDK does not repeat its route-by-route, cross-account, sharing, or attachment-isolation
|
|
@@ -689,9 +645,9 @@ verification. Representative SDK adapter tests instead feed the pinned OpenAPI's
|
|
|
689
645
|
`MockTransport` and assert the resulting public values; synthetic fixtures remain where SDK-specific encoding,
|
|
690
646
|
error, retry, and compatibility boundaries require evidence beyond those examples.
|
|
691
647
|
|
|
692
|
-
Provider smoke is restricted to the confirmed isolated test service. Authentication and
|
|
693
|
-
|
|
694
|
-
|
|
648
|
+
Provider smoke is restricted to the confirmed isolated test service. Authentication and refresh persist Provider
|
|
649
|
+
token state. This side effect is inherent to the tested Provider operations; it cannot be disabled by a test setting.
|
|
650
|
+
Explicitly loading `.env.test` and selecting the `provider` marker is the opt-in for this flow.
|
|
695
651
|
|
|
696
652
|
Copy `.env.example` to the git-ignored local `.env.test`, then fill in the shared Provider connection settings:
|
|
697
653
|
|
|
@@ -700,8 +656,8 @@ Copy `.env.example` to the git-ignored local `.env.test`, then fill in the share
|
|
|
700
656
|
- `MATELAB_PROVIDER_PASSWORD`
|
|
701
657
|
|
|
702
658
|
These names intentionally match `matelab-spec` Provider Verification. The isolated target may copy them from the spec
|
|
703
|
-
`.env` into this repository's `.env.test`.
|
|
704
|
-
|
|
659
|
+
`.env` into this repository's `.env.test`. Refreshing the restored Session must preserve its authenticated identity.
|
|
660
|
+
With the environment prepared:
|
|
705
661
|
|
|
706
662
|
```bash
|
|
707
663
|
uv run --env-file .env.test pytest -m provider tests/provider/test_provider_smoke.py
|
|
@@ -735,6 +691,6 @@ uv run python scripts/check_release.py dist/release/*.whl dist/release/*.tar.gz
|
|
|
735
691
|
```
|
|
736
692
|
|
|
737
693
|
Rebuilding the same commit with the same locked environment and `SOURCE_DATE_EPOCH` must produce
|
|
738
|
-
byte-identical wheel and source distribution hashes. The release is bound to `matelab-spec v0.
|
|
739
|
-
commit `
|
|
740
|
-
`
|
|
694
|
+
byte-identical wheel and source distribution hashes. The release is bound to `matelab-spec v0.3.0`,
|
|
695
|
+
commit `0b5612588708b4639a42c7983ee2f08c350994bf`, and OpenAPI SHA-256
|
|
696
|
+
`ebf5d446e3a5866d773cf8cfffe1db28f27015635fbb621bc091673e865ef70e`.
|