matelab-python-sdk 0.1.0a4__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.
Files changed (70) hide show
  1. matelab_python_sdk-0.1.0a6/.env.example +14 -0
  2. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/AGENTS.md +5 -2
  3. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/CHANGELOG.md +24 -0
  4. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/PKG-INFO +205 -113
  5. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/README.md +204 -112
  6. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/contracts/matelab-integration-v1.lock.json +3 -3
  7. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/contracts/matelab-integration-v1.openapi.yaml +333 -49
  8. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/docs/operation-coverage.yaml +7 -5
  9. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/docs/roadmap.md +54 -40
  10. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/pyproject.toml +3 -1
  11. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/scripts/generate_models.py +1 -5
  12. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/src/matelab/__init__.py +22 -34
  13. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/src/matelab/_generated/models.py +106 -3
  14. matelab_python_sdk-0.1.0a6/src/matelab/_transport.py +276 -0
  15. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/src/matelab/_wire_validation.py +38 -35
  16. matelab_python_sdk-0.1.0a6/src/matelab/client.py +120 -0
  17. matelab_python_sdk-0.1.0a6/src/matelab/domains/cloud_drive.py +356 -0
  18. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/src/matelab/domains/groups.py +7 -9
  19. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/src/matelab/domains/literature.py +120 -341
  20. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/src/matelab/domains/notebooks.py +18 -66
  21. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/src/matelab/domains/records.py +499 -308
  22. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/src/matelab/domains/templates.py +52 -152
  23. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/src/matelab/domains/uploads.py +3 -14
  24. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/src/matelab/domains/users.py +4 -17
  25. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/src/matelab/errors.py +3 -11
  26. matelab_python_sdk-0.1.0a6/src/matelab/session.py +32 -0
  27. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/src/matelab/streaming.py +11 -14
  28. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/tests/conftest.py +4 -2
  29. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/tests/provider/test_provider_smoke.py +12 -26
  30. matelab_python_sdk-0.1.0a6/tests/test_client.py +394 -0
  31. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/tests/test_cloud_drive.py +101 -148
  32. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/tests/test_comments_and_downloads.py +17 -27
  33. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/tests/test_consumer_smoke.py +36 -46
  34. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/tests/test_extended_records.py +40 -69
  35. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/tests/test_generation.py +51 -14
  36. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/tests/test_groups.py +16 -27
  37. matelab_python_sdk-0.1.0a6/tests/test_notebook_lifecycle.py +306 -0
  38. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/tests/test_notebooks.py +13 -22
  39. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/tests/test_operation_coverage.py +17 -8
  40. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/tests/test_record_lifecycle.py +64 -235
  41. matelab_python_sdk-0.1.0a6/tests/test_record_mutations.py +1416 -0
  42. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/tests/test_records.py +36 -58
  43. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/tests/test_release.py +1 -5
  44. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/tests/test_streaming.py +61 -84
  45. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/tests/test_template_lifecycle.py +95 -256
  46. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/tests/test_templates.py +37 -58
  47. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/tests/test_transport.py +128 -188
  48. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/tests/test_uploads_and_literature.py +124 -187
  49. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/tests/test_users.py +27 -45
  50. matelab_python_sdk-0.1.0a6/tests/typing/consumer.py +75 -0
  51. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/uv.lock +1 -1
  52. matelab_python_sdk-0.1.0a4/.env.example +0 -6
  53. matelab_python_sdk-0.1.0a4/src/matelab/_transport.py +0 -500
  54. matelab_python_sdk-0.1.0a4/src/matelab/client.py +0 -172
  55. matelab_python_sdk-0.1.0a4/src/matelab/domains/cloud_drive.py +0 -573
  56. matelab_python_sdk-0.1.0a4/src/matelab/session.py +0 -27
  57. matelab_python_sdk-0.1.0a4/tests/test_client.py +0 -570
  58. matelab_python_sdk-0.1.0a4/tests/test_notebook_lifecycle.py +0 -454
  59. matelab_python_sdk-0.1.0a4/tests/test_record_mutations.py +0 -994
  60. matelab_python_sdk-0.1.0a4/tests/typing/consumer.py +0 -44
  61. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/.github/workflows/release.yml +0 -0
  62. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/.gitignore +0 -0
  63. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/LICENSE +0 -0
  64. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/NOTICE +0 -0
  65. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/scripts/check_installed_package.py +0 -0
  66. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/scripts/check_release.py +0 -0
  67. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/src/matelab/_generated/__init__.py +0 -0
  68. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/src/matelab/domains/__init__.py +0 -0
  69. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/src/matelab/py.typed +0 -0
  70. {matelab_python_sdk-0.1.0a4 → 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;不同用户的 token pair 必须拒绝绑定,失败时不得替换现有 Session。
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,29 @@
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
+
11
+ ## 0.1.0a5 - 2026-07-29
12
+
13
+ - Adopts immutable `matelab-spec v0.1.2` at commit `6dd674ac001ae61197495ace5eba489d585cb825` and OpenAPI
14
+ SHA-256 `7ccaf3116c8dcec7a6e49ff51fb1a5ecc83a140e013ec600516f5c2ffb3b5378`.
15
+ - Adds notebook-scoped staged finalizers for a null table file cell, an attachment-bearing appended table row,
16
+ files-module root append, and table/files occurrence replacement, with exact provenance, one-shot transport, and
17
+ canonical readback checks.
18
+ - Adds typed intents for an empty file table column and complete form file-field/table file-column deletion.
19
+ - Allows `records.update(..., module_deletions=...)` to delete a canonical module even when the current record read
20
+ observes attachments in it, verifies absence on persisted readback, and does not claim attachment-quote or byte
21
+ cleanup beyond the pinned Contract.
22
+ - Rejects cross-scope staged attachment handles before the record read, preserving the public static types while
23
+ avoiding the Provider's read-audit side effect for invalid runtime input.
24
+ - Keeps notebook-staged rich-text binding and attachment-bearing multi-record import explicitly unsupported because
25
+ v0.1.2 Provider Verification does not provide safe one-shot semantics for either.
26
+
3
27
  ## 0.1.0a4 - 2026-07-28
4
28
 
5
29
  - Further reduces the alpha public surface from 150 to 126 symbols by accepting direct keyword arguments and existing
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: matelab-python-sdk
3
- Version: 0.1.0a4
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.0a4`. `[project].version` in `pyproject.toml` is the sole SDK version source;
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.1.1` Contract Release. The sole release pin is
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.0a4-py3-none-any.whl
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`, `status_code`, and the sanitized `payload` are diagnostics. The `payload` shape may contain private wire
99
- field names and is not a stable Consumer interface; application control flow should use the exception class and
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 immutable value objects, and token values are excluded from their
118
- representations. The SDK does not read tokens from environment variables and does not provide a session store.
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
- Credential authentication installs the returned Session on the client:
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
- async with AsyncMatelab() as client:
124
- session = await client.authenticate(username, password)
125
- assert client.session is session
123
+ serialized = session.model_dump_json()
124
+ restored = Session.model_validate_json(serialized)
125
+ assert restored == session
126
126
  ```
127
127
 
128
- External token exchange validates the original access-token identity, refreshes the supplied refresh token,
129
- validates the refreshed access-token identity, and rejects a userid mismatch. The existing client Session is
130
- replaced only after all three steps succeed:
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.bind_external_tokens(access_token, refresh_token)
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, TemplateRef
202
+ from matelab import RecordImportItem
198
203
 
199
204
  content = b"measurement data"
200
205
  staged = await client.records.stage_attachment(
@@ -206,20 +211,17 @@ 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
  )
219
221
  ```
220
222
 
221
- The same staged handle may instead be consumed by one safe update that adds a new file field to an existing form
222
- module:
223
+ The same staged handle may instead be consumed by one safe update finalizer. For example, add a new file field to an
224
+ existing form module:
223
225
 
224
226
  ```python
225
227
  from matelab import RecordFormAttachmentFieldAddition
@@ -228,30 +230,124 @@ 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,
233
+ RecordFormAttachmentFieldAddition(module="Attachments", name="Measurement", attachment=staged),
234
+ ),
235
+ )
236
+ ```
237
+
238
+ Choose exactly one finalizer. A staged name may occur once in either a single-record import or one update operation;
239
+ do not reuse it after transport starts, even when the Provider outcome is unknown. A locally rejected target or
240
+ argument does not spend the handle. The SDK rejects raw Provider attachment references, cross-user or cross-notebook
241
+ handles, unsafe combinations, duplicate use in one request, and a second finalization attempt through the same
242
+ client.
243
+
244
+ The Provider supplies no staging status, abort, TTL, atomicity, idempotency, or retry guarantee. Integrations that
245
+ persist handles must durably claim their own `uploaded -> finalizing/indeterminate` transition before calling a
246
+ finalizer.
247
+
248
+ ### Attachment-bearing record update boundary
249
+
250
+ The v0.1.2 Contract adds dedicated, verified notebook-staged update shapes. They remain public intent objects; callers
251
+ never construct Provider paths or attachment strings:
252
+
253
+ ```python
254
+ from matelab import RecordFilesAttachmentRootAppend, RecordTableFileCellSet, RecordTableRowAttachmentAppend
255
+
256
+ # Each example is a separate finalizer; never run several with the same handle.
257
+ await client.records.update(
258
+ source,
259
+ attachment_changes=(RecordTableFileCellSet(table="Measurements", column="Evidence", row=0, attachment=staged),),
260
+ )
261
+
262
+ await client.records.update(
263
+ source,
264
+ attachment_changes=(
265
+ RecordTableRowAttachmentAppend(
266
+ table="Measurements", file_column="Evidence", values={"Label": "Sample C"}, attachment=another_staged
235
267
  ),
236
268
  ),
237
269
  )
270
+
271
+ await client.records.update(
272
+ source,
273
+ attachment_changes=(RecordFilesAttachmentRootAppend(module="Files", caption="Evidence", attachment=third_staged),),
274
+ )
275
+ ```
276
+
277
+ `RecordTableFileCellSet` requires an existing file column and a cell whose immediate canonical value is exactly
278
+ `null`. The row-append intent addresses the new row by the row count from the SDK's immediate read and supports one
279
+ staged file column. Files append is root-only and requires a string caption.
280
+
281
+ Replacement starts with an occurrence returned by `records.read()`; applications must not fabricate a
282
+ `RecordAttachmentRef`:
283
+
284
+ ```python
285
+ from matelab import RecordFilesAttachmentReplacement
286
+
287
+ record = await client.records.read(notebook=source.notebook, record=source.record)
288
+ existing = next(
289
+ attachment
290
+ for attachment in record.attachments
291
+ if attachment.location is not None and attachment.location.kind == "files_module"
292
+ )
293
+ await client.records.update(
294
+ source, attachment_changes=(RecordFilesAttachmentReplacement(existing=existing, replacement=staged, caption=None),)
295
+ )
296
+ ```
297
+
298
+ For a files occurrence, `caption=None` preserves the observed string caption (an observed null caption normalizes to
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.
302
+
303
+ Row/index-based finalizers use an immediate SDK read and must not be called while a concurrent editor is known to be
304
+ active. The Provider offers no expected hash or revision. `expected_content_sha256` is only a client-side prewrite
305
+ check, not Provider compare-and-swap.
306
+
307
+ File-bearing structure changes do not consume staging:
308
+
309
+ ```python
310
+ from matelab import RecordFormFileFieldDeletion, RecordTableFileColumnAddition, RecordTableFileColumnDeletion
311
+
312
+ await client.records.update(
313
+ source,
314
+ file_structure_changes=(
315
+ RecordTableFileColumnAddition(table="Measurements", name="Additional evidence"),
316
+ RecordFormFileFieldDeletion(form="Attachments", name="Obsolete evidence"),
317
+ RecordTableFileColumnDeletion(table="Measurements", name="Old evidence"),
318
+ ),
319
+ )
238
320
  ```
239
321
 
240
- Choose exactly one finalizer. A staged name may occur once in either a single-record import or one update
241
- operation; do not reuse it, even after an error whose Provider outcome is unknown. The SDK rejects raw Provider
242
- attachment references, cross-user or cross-notebook handles, unsafe update shapes, duplicate use in one request,
243
- and a second finalization attempt through the same client. A Session imported without identity must first call
244
- `await client.resolve_identity()`. The Provider supplies no staging status, abort, TTL, atomicity, or retry
245
- guarantee, so callers must discard the handle as soon as a finalization request starts.
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.
325
+
326
+ Deleting a complete canonical module is also supported even when the current read observes attachments in it:
327
+
328
+ ```python
329
+ result = await client.records.update(
330
+ source, module_deletions=("Raw files",), expected_content_sha256=record.content_sha256
331
+ )
332
+ ```
333
+
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.
337
+
338
+ Notebook-staged rich-text binding remains unsupported. Provider Verification shows that `#file{name}` is stored as
339
+ plain text and that a hash-based URI can fall back to an existing quote, so neither is a one-shot staged finalizer.
340
+ Existing `RecordRichTextUpdate` remains legal only with record-scoped `StagedRecordAttachment`. Attachment-bearing
341
+ multi-record import is also forbidden; callers must split it into single-record finalizers.
246
342
 
247
343
  ## Implementation roadmap
248
344
 
249
345
  `docs/roadmap.md` is the complete SDK-only execution plan. It assigns all 71
250
- `matelab-spec v0.1.1` operations to ordered work packages, defines the machine-readable coverage that
346
+ `matelab-spec v0.3.0` operations to ordered work packages, defines the machine-readable coverage that
251
347
  must be added, records Provider-risk gates, and specifies the final completion checks.
252
348
 
253
- The SDK exposes all 71 Contract operations through public domain interfaces. It deliberately excludes MCP
254
- 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
255
351
  publishing, and automatic mutation against a real Provider.
256
352
 
257
353
  Machine-readable status lives in
@@ -261,7 +357,7 @@ OpenAPI snapshot.
261
357
 
262
358
  | Domain | Implemented | Planned | Current public surface |
263
359
  |---|---:|---:|---|
264
- | Authentication | 4 | 0 | `authenticate`, `bind_external_tokens`, `refresh`, `resolve_identity`, `exchange_chat_sso_code` |
360
+ | Authentication | 3 | 0 | `authenticate`, `refresh`, `exchange_chat_sso_code`; identity bootstrap intentionally unexposed |
265
361
  | Groups and users | 2 | 0 | `groups.list`, `users.search` |
266
362
  | Notebooks | 6 | 0 | `notebooks.list/create/update/shares/share/update_share/unshare` |
267
363
  | Records | 18 | 0 | Discovery, reads, lifecycle, typed patch/attachments, relations, and downloads |
@@ -270,20 +366,21 @@ OpenAPI snapshot.
270
366
  | File staging | 1 | 0 | Resumable fragment staging and compensating abort request |
271
367
  | Literature | 13 | 0 | Libraries, items, canonical metadata, comments, sharing, PDF lifecycle and streaming |
272
368
  | Cloud drive | 9 | 0 | Personal root/folders/files, staged binding, metadata, move/delete and streaming |
273
- | **Total** | **71** | **0** | No operation is intentionally unexposed |
369
+ | **Total** | **70** | **0** | One operation is intentionally unexposed |
274
370
 
275
371
  ### Stability and known capability limits
276
372
 
277
- Coverage currently contains 15 `stable` and 56 `experimental` operations. The stable operation IDs are
278
- `resolveCurrentIdentity`, `loginTokenSet`, `refreshTokenSet`, `exchangeChatSsoCode`,
373
+ Coverage currently contains 14 `stable`, 56 `experimental`, and one `not_applicable` operation. The stable operation
374
+ IDs are `loginTokenSet`, `refreshTokenSet`, `exchangeChatSsoCode`,
279
375
  `shareMultipleTemplatesWithUsers`, `removeTemplateFromGroup`, `deleteNotebookShare`, `listNotebooks`,
280
376
  `listNotebookRecords`, `exportRecords`, `deleteRecordsByUid`, `copyRecord`, `readRecord`,
281
377
  `deletePersonalLiteratureItem`, and `readLiteratureCreateTemplate`.
282
378
 
283
379
  Every other implemented operation is explicitly `experimental`; the exact per-operation list and its
284
380
  PVD/PCG references live in
285
- `docs/operation-coverage.yaml`. There are no `intentionally_unexposed`
286
- operations and no `planned` operations. Experimental support means the SDK validates and exposes the
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
287
384
  pinned Contract while preserving limitations such as unstable ordering/pagination, incomplete mutation
288
385
  acknowledgements, missing batch atomicity or idempotency, weak attachment ownership binding, and known
289
386
  Provider authorization gaps. It does not turn those limitations into SDK guarantees.
@@ -298,10 +395,8 @@ session = await client.exchange_chat_sso_code(code="chat-sanitizedcode123", key=
298
395
  Group and user discovery expose sharing identities without inventing Provider pagination:
299
396
 
300
397
  ```python
301
- from matelab import UserSearchScope
302
-
303
398
  groups = await client.groups.list()
304
- targets = await client.users.search("Example Researcher", scope=UserSearchScope.SAME_INSTITUTE)
399
+ targets = await client.users.search("Example Researcher", global_scope=False)
305
400
  ```
306
401
 
307
402
  Ordering remains Provider-unspecified and is documented rather than repeated as a constant result field. Group
@@ -309,19 +404,18 @@ members belong only to `groups.members_for`, not to every returned group. These
309
404
  Provider returns members for an unstable first group and user search is unpaged, unordered, and not field-minimized
310
405
  (PVD-006, PVD-029, PCG-011).
311
406
 
312
- Notebook create/update and direct sharing keep write acknowledgement separate from what a readback can prove:
407
+ Notebook create/update and direct sharing use the Provider acknowledgement without an automatic follow-up read:
313
408
 
314
409
  ```python
315
410
  await client.notebooks.create(title="Example Notebook")
316
- shares = await client.notebooks.share(notebook, [target.ref])
317
- updated = await client.notebooks.update_share(shares[0].ref, write=True, create=True)
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)
318
414
  ```
319
415
 
320
- Create returns `None` because the Provider returns no identity. Owned updates return the notebook row observed by
321
- database ID, or `None` when it is missing, containing PVD-003 false success without claiming an atomic guarantee.
322
- Share returns only requested relations visible during readback, and permission updates return the observed relation
323
- or `None`. A stored share mask of zero still has effective read access (PVD-010), and share-list order remains
324
- 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.
325
419
 
326
420
  Template discovery keeps a template database identity separate from direct-share, market-acquisition, and group
327
421
  relation identities:
@@ -339,9 +433,9 @@ experimental because Provider discovery ordering/pagination and historical `imag
339
433
  stable (PCG-003, PCG-009, PVD-013, PVD-022).
340
434
 
341
435
  Template writes remain separate operations: metadata, canonical modules, and usage HTML are not presented as one
342
- transaction. Metadata create returns the Provider template ID plus an optional observed summary; copy and share
343
- return `None` because the Provider supplies no new identity or per-recipient rows. Group addition and market
344
- acquisition return the observed relation or `None`, while removal returns readback-confirmed absence as `bool`.
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.
345
439
  Direct-share, market-acquisition, and group relation refs remain distinct. Marketplace revision and uploader-binding
346
440
  limitations are documented operation semantics rather than constant fields on every result (PVD-021, PVD-026).
347
441
  `UploadBindingRef.new()` creates the fresh hidden correlation value required by intro attachment binding.
@@ -367,43 +461,40 @@ remain unspecified, and no continuation token is invented.
367
461
  Record creation keeps blank creation and structured import as separate capabilities:
368
462
 
369
463
  ```python
370
- from matelab import RecordImportItem, TemplateRef
464
+ from matelab import RecordImportItem
371
465
 
372
- blank = await client.records.create_blank(notebook=notebook, title="Blank Record", record_uid="caller-generated-uid")
466
+ await client.records.create_blank(notebook=notebook, title="Blank Record", record_uid="caller-generated-uid")
373
467
  imported = await client.records.import_dataset(
374
468
  notebook=notebook,
375
- template=TemplateRef(template_id=8),
376
469
  template_title="Example Template",
377
470
  items=[RecordImportItem(record_uid="import-uid", title="Imported", data={"Notes": "value"})],
378
471
  )
379
472
  ```
380
473
 
381
- A caller-supplied blank-record UID returns its matching readback; when the Provider generates it, create returns
382
- `None` rather than inventing identity. Import validates the complete batch with generated wire models but cannot map returned database
383
- IDs to individual inputs or promise atomicity (PCG-008). Delete means moving records into the recycle bin, not
384
- permanent deletion. Delete and restore results classify only post-write observations; restore never treats an active
385
- row with the same UID but a different database ID as proof of success (PCG-005). Record mutations are not
386
- 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.
387
478
 
388
479
  Record patching exposes a deliberately narrower capability than the raw Provider operation. Scalar/module changes
389
480
  cannot smuggle Provider-native attachment strings; staged attachments use separate form-removal, table-replacement,
390
481
  files append/replace/remove, and rich-text types. Unsafe form replacement and table-file removal are absent, while a
391
482
  files/images removal is rejected when the observed module contains the same hash more than once (PVD-014 through
392
483
  PVD-016). `Record.content_sha256` can be supplied as a client-side precondition, documented as advisory
393
- read-before-write rather than Provider CAS. `RecordUpdateResult` retains only the before/after evidence and whether
394
- the acknowledgement means persisted, pending browser save, or unclassified. Database, active-browser, and unclassified acknowledgements
395
- remain distinct, and mutation retries stay disabled.
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.
396
488
 
397
489
  Relation addition reads both endpoints and checks their resolved data server before writing; this reduces PVD-019
398
- risk but is not an atomic Provider authorization guarantee. Relation addition returns matching relations observed
399
- afterward. Relation deletion refuses an observed cross-notebook target-ID collision because the Provider ignores
400
- target notebook identity (PVD-020), then returns the complete post-write relation snapshot.
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.
401
493
 
402
494
  Comment upload follows the Provider's literal one-request `upload` field, not the incompatible Front fragment
403
- protocol (PVD-037). Comment create/update return matching post-write comment rows; delete returns whether the selected
404
- comment is absent. Edit and delete require a currently observed caller-owned comment and read it back, containing the
405
- Provider's edit false-success behavior (PVD-004). Staged comment attachments have no Contract abort operation, and
406
- 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.
407
498
 
408
499
  Attachment bytes are streamed and must be consumed or closed explicitly:
409
500
 
@@ -465,10 +556,10 @@ await client.literature.create(LiteratureMetadata(title="Example import", doi="1
465
556
 
466
557
  Create returns `None` and never guesses the new item from list position because the Provider returns no ID. Canonical update reads the
467
558
  item first and refuses to drop source/hidden fields unless `allow_source_metadata_loss=True` is explicit (PVD-027).
468
- PDF replace/delete are separate, read-back-verified mutations and are not presented as atomic with metadata
469
- (PCG-010). Permanent personal deletion is named `permanently_delete` and returns snapshot-confirmed absence as a
470
- `bool`; the operation is non-recoverable. Sharing requires list-observed item summaries, user-search summaries and a
471
- resolved caller identity, then returns `None` because the Provider supplies no per-recipient IDs (PVD-012, PVD-036).
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).
472
563
 
473
564
  Literature comments use one public save intent: detail is read first, an existing caller-owned comment is edited, and
474
565
  otherwise a comment is created. Multiple caller-owned comments are rejected as ambiguous (PVD-035). A staged
@@ -482,13 +573,8 @@ The personal cloud-drive surface keeps root, folder, final file and temporary st
482
573
 
483
574
  ```python
484
575
  listing = await client.cloud_drive.list()
485
- folder_result = await client.cloud_drive.create_folder(name="Example data")
486
- matches = await client.cloud_drive.bind_staged_file(staged, target=folder_result.folder)
487
-
488
- if len(matches) == 1:
489
- renamed = await client.cloud_drive.update_file(
490
- matches[0], filename="example-renamed.pdf", description="Sanitized description"
491
- )
576
+ folder = await client.cloud_drive.create_folder(name="Example data")
577
+ await client.cloud_drive.bind_staged_file(staged, target=folder)
492
578
  ```
493
579
 
494
580
  `CloudDriveListing` contains a typed file page, complete folder snapshot, quota usage and personal-root permissions rather
@@ -496,14 +582,11 @@ than flattening them into one ambiguous collection. Folder browse results retain
496
582
  are explicitly root-wide and return `location=None` because the Provider omits each match's folder ID. Ordering has
497
583
  no stable ID tie-breaker (PCG-003, PVD-013).
498
584
 
499
- Folder create returns the Provider ID plus an optional observed folder, and all folder mutations read back the
500
- complete folder snapshot. Staged finalize accepts a completed `StagedFile`, then paginates the target folder and returns all
501
- exact filename/hash/size matches; zero, one, or multiple results preserve missing and ambiguous observations without
502
- another result model. This is useful evidence, not an uploader-ownership guarantee: the Provider binds by
503
- temporary row ID without checking its owner (PVD-031), and finalize atomicity/idempotency remain absent (PCG-012).
504
- Batch move returns identities observed in the target, while permanent delete returns identities confirmed absent;
505
- neither claims Provider per-item results or atomicity. Permanent deletion is named `permanently_delete_files` and is
506
- 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
507
590
  resolve bytes from the final file identity and reuse `DownloadStream`, thumbnail/preview choices and the PVD-002-safe
508
591
  range subset. Cloud mutations are not automatically retried.
509
592
 
@@ -520,8 +603,8 @@ Provider identifiers distinct. Historical reads first re-read the authorized cur
520
603
  that the requested version is still present in its `modify_log`; both reads write Provider audit entries.
521
604
 
522
605
  Errors are separated into Provider business errors, authentication errors, HTTP/transport errors,
523
- Integration Contract response errors, and client-side usage errors. Token values and sensitive response
524
- fields are redacted from error text and retained diagnostic payloads.
606
+ Integration Contract response errors, and client-side usage errors. Provider response bodies and caller inputs do not
607
+ enter exceptions.
525
608
 
526
609
  ## Development
527
610
 
@@ -552,9 +635,9 @@ Node wheel retain the compatible exact pair `basedpyright==1.39.9` and
552
635
  ## Opt-in Provider consumer smoke
553
636
 
554
637
  `tests/provider/test_provider_smoke.py` exercises the consumer flow through only the public SDK interface. Its base
555
- scenario covers credential authentication, extraction of the returned token pair, exchange through a new
556
- `AsyncMatelab.bind_external_tokens` instance, userid consistency checks, and notebook discovery. It is not Provider
557
- Verification and is skipped by default.
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.
558
641
 
559
642
  Raw Provider conformance remains the responsibility of `matelab-spec`, which sends direct HTTP requests and validates
560
643
  the unmodified responses. The SDK does not repeat its route-by-route, cross-account, sharing, or attachment-isolation
@@ -562,9 +645,9 @@ verification. Representative SDK adapter tests instead feed the pinned OpenAPI's
562
645
  `MockTransport` and assert the resulting public values; synthetic fixtures remain where SDK-specific encoding,
563
646
  error, retry, and compatibility boundaries require evidence beyond those examples.
564
647
 
565
- Provider smoke is restricted to the confirmed isolated test service. Authentication and external-token refresh
566
- persist Provider token state. This side effect is inherent to the tested Provider operations; it cannot be disabled
567
- by a test setting. Explicitly loading `.env.test` and selecting the `provider` marker is the opt-in for this flow.
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.
568
651
 
569
652
  Copy `.env.example` to the git-ignored local `.env.test`, then fill in the shared Provider connection settings:
570
653
 
@@ -573,15 +656,24 @@ Copy `.env.example` to the git-ignored local `.env.test`, then fill in the share
573
656
  - `MATELAB_PROVIDER_PASSWORD`
574
657
 
575
658
  These names intentionally match `matelab-spec` Provider Verification. The isolated target may copy them from the spec
576
- `.env` into this repository's `.env.test`. The identities returned by authentication and external token binding must
577
- agree. With the environment prepared:
659
+ `.env` into this repository's `.env.test`. Refreshing the restored Session must preserve its authenticated identity.
660
+ With the environment prepared:
578
661
 
579
662
  ```bash
580
663
  uv run --env-file .env.test pytest -m provider tests/provider/test_provider_smoke.py
581
664
  ```
582
665
 
583
- The file is never loaded implicitly, so the normal test suite remains safely skipped. Do not use this flow against
584
- production, and never commit its credentials.
666
+ This command runs only the SDK public-interface smoke; it is not the 71-operation Provider Verification. To reuse the
667
+ same `.env.test` for the complete Contract suite, also populate the optional share user, secondary account, record
668
+ staging opt-in, and Chat SSO settings documented in `.env.example`, then run from sibling checkouts:
669
+
670
+ ```bash
671
+ cd ../matelab-spec
672
+ uv run --env-file ../matelab-python-sdk/.env.test pytest
673
+ ```
674
+
675
+ The files are never loaded implicitly, so normal test runs remain safely skipped. Do not use either flow against
676
+ production, and never commit Provider credentials.
585
677
 
586
678
  ## Reproducible release build
587
679
 
@@ -599,6 +691,6 @@ uv run python scripts/check_release.py dist/release/*.whl dist/release/*.tar.gz
599
691
  ```
600
692
 
601
693
  Rebuilding the same commit with the same locked environment and `SOURCE_DATE_EPOCH` must produce
602
- byte-identical wheel and source distribution hashes. The release is bound to `matelab-spec v0.1.1`,
603
- commit `047746ad37d827a85f93d947942f1e5fab80d54c`, and OpenAPI SHA-256
604
- `1d437b071968d2df165c712092fba4a283832e0ac19bc791e0d5af82294d1cca`.
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`.