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.
Files changed (69) hide show
  1. matelab_python_sdk-0.1.0a6/.env.example +14 -0
  2. {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/AGENTS.md +5 -2
  3. {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/CHANGELOG.md +8 -0
  4. {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/PKG-INFO +106 -150
  5. {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/README.md +105 -149
  6. {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/contracts/matelab-integration-v1.lock.json +3 -3
  7. {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/contracts/matelab-integration-v1.openapi.yaml +35 -34
  8. {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/docs/operation-coverage.yaml +7 -5
  9. {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/docs/roadmap.md +40 -42
  10. {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/pyproject.toml +3 -1
  11. {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/scripts/generate_models.py +1 -5
  12. {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/src/matelab/__init__.py +2 -34
  13. {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/src/matelab/_generated/models.py +3 -3
  14. matelab_python_sdk-0.1.0a6/src/matelab/_transport.py +276 -0
  15. {matelab_python_sdk-0.1.0a5 → 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.0a5 → matelab_python_sdk-0.1.0a6}/src/matelab/domains/groups.py +7 -9
  19. {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/src/matelab/domains/literature.py +120 -341
  20. {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/src/matelab/domains/notebooks.py +18 -66
  21. {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/src/matelab/domains/records.py +77 -606
  22. {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/src/matelab/domains/templates.py +52 -152
  23. {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/src/matelab/domains/uploads.py +3 -14
  24. {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/src/matelab/domains/users.py +4 -17
  25. {matelab_python_sdk-0.1.0a5 → 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.0a5 → matelab_python_sdk-0.1.0a6}/src/matelab/streaming.py +11 -14
  28. {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/tests/conftest.py +4 -2
  29. {matelab_python_sdk-0.1.0a5 → 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.0a5 → matelab_python_sdk-0.1.0a6}/tests/test_cloud_drive.py +101 -148
  32. {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/tests/test_comments_and_downloads.py +17 -27
  33. {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/tests/test_consumer_smoke.py +36 -46
  34. {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/tests/test_extended_records.py +40 -69
  35. {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/tests/test_generation.py +14 -31
  36. {matelab_python_sdk-0.1.0a5 → 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.0a5 → matelab_python_sdk-0.1.0a6}/tests/test_notebooks.py +13 -22
  39. {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/tests/test_operation_coverage.py +17 -8
  40. {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/tests/test_record_lifecycle.py +64 -235
  41. {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/tests/test_record_mutations.py +223 -553
  42. {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/tests/test_records.py +36 -58
  43. {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/tests/test_release.py +1 -5
  44. {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/tests/test_streaming.py +61 -84
  45. {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/tests/test_template_lifecycle.py +95 -256
  46. {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/tests/test_templates.py +37 -58
  47. {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/tests/test_transport.py +128 -188
  48. {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/tests/test_uploads_and_literature.py +124 -187
  49. {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/tests/test_users.py +27 -45
  50. {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/tests/typing/consumer.py +8 -16
  51. {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/uv.lock +1 -1
  52. matelab_python_sdk-0.1.0a5/.env.example +0 -6
  53. matelab_python_sdk-0.1.0a5/docs/attachment-write-contract-gaps.md +0 -103
  54. matelab_python_sdk-0.1.0a5/src/matelab/_transport.py +0 -500
  55. matelab_python_sdk-0.1.0a5/src/matelab/client.py +0 -172
  56. matelab_python_sdk-0.1.0a5/src/matelab/domains/cloud_drive.py +0 -573
  57. matelab_python_sdk-0.1.0a5/src/matelab/session.py +0 -27
  58. matelab_python_sdk-0.1.0a5/tests/test_client.py +0 -570
  59. matelab_python_sdk-0.1.0a5/tests/test_notebook_lifecycle.py +0 -454
  60. {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/.github/workflows/release.yml +0 -0
  61. {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/.gitignore +0 -0
  62. {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/LICENSE +0 -0
  63. {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/NOTICE +0 -0
  64. {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/scripts/check_installed_package.py +0 -0
  65. {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/scripts/check_release.py +0 -0
  66. {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/src/matelab/_generated/__init__.py +0 -0
  67. {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/src/matelab/domains/__init__.py +0 -0
  68. {matelab_python_sdk-0.1.0a5 → matelab_python_sdk-0.1.0a6}/src/matelab/py.typed +0 -0
  69. {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;不同用户的 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,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.0a5
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.0a5`. `[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.2` 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.0a5-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,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. A Session imported without identity must first call `await client.resolve_identity()`.
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). Canonical readback must preserve the Provider `uid` and folder. Table replacement requires
323
- exactly one current attachment in the selected cell and uses `RecordTableFileAttachmentReplacement`. Both replacement
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` and must read back as `null` for
350
- every existing row; whole form-field/table-column deletion uses a strict two-segment target. It does not expose a
351
- three-segment delete as table-cell clearing.
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. Readback proves the module is absent; it does not prove
364
- attachment-quote cleanup or byte deletion. Collaboration-pending has no readback and is not reported as persisted.
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.1.2` 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
378
347
  must be added, records Provider-risk gates, and specifies the final completion checks.
379
348
 
380
- The SDK exposes all 71 Contract operations through public domain interfaces. It deliberately excludes MCP
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 | 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 |
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** | **71** | **0** | No operation is intentionally unexposed |
369
+ | **Total** | **70** | **0** | One operation is intentionally unexposed |
401
370
 
402
371
  ### Stability and known capability limits
403
372
 
404
- Coverage currently contains 15 `stable` and 56 `experimental` operations. The stable operation IDs are
405
- `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`,
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`. There are no `intentionally_unexposed`
413
- 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
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", scope=UserSearchScope.SAME_INSTITUTE)
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 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:
440
408
 
441
409
  ```python
442
410
  await client.notebooks.create(title="Example Notebook")
443
- shares = await client.notebooks.share(notebook, [target.ref])
444
- 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)
445
414
  ```
446
415
 
447
- Create returns `None` because the Provider returns no identity. Owned updates return the notebook row observed by
448
- database ID, or `None` when it is missing, containing PVD-003 false success without claiming an atomic guarantee.
449
- Share returns only requested relations visible during readback, and permission updates return the observed relation
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 Provider template ID plus an optional observed summary; copy and share
470
- return `None` because the Provider supplies no new identity or per-recipient rows. Group addition and market
471
- 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.
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, TemplateRef
464
+ from matelab import RecordImportItem
498
465
 
499
- 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")
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
- A caller-supplied blank-record UID returns its matching readback; when the Provider generates it, create returns
509
- `None` rather than inventing identity. Import validates the complete batch with generated wire models but cannot map returned database
510
- IDs to individual inputs or promise atomicity (PCG-008). Delete means moving records into the recycle bin, not
511
- permanent deletion. Delete and restore results classify only post-write observations; restore never treats an active
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. `RecordUpdateResult` retains only the before/after evidence and whether
521
- the acknowledgement means persisted, pending browser save, or unclassified. Database, active-browser, and unclassified acknowledgements
522
- 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.
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 addition returns matching relations observed
526
- afterward. Relation deletion refuses an observed cross-notebook target-ID collision because the Provider ignores
527
- 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.
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 create/update return matching post-write comment rows; delete returns whether the selected
531
- comment is absent. Edit and delete require a currently observed caller-owned comment and read it back, containing the
532
- Provider's edit false-success behavior (PVD-004). Staged comment attachments have no Contract abort operation, and
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, read-back-verified mutations and are not presented as atomic with metadata
596
- (PCG-010). Permanent personal deletion is named `permanently_delete` and returns snapshot-confirmed absence as a
597
- `bool`; the operation is non-recoverable. Sharing requires list-observed item summaries, user-search summaries and a
598
- 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).
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
- folder_result = await client.cloud_drive.create_folder(name="Example data")
613
- matches = await client.cloud_drive.bind_staged_file(staged, target=folder_result.folder)
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 plus an optional observed folder, and all folder mutations read back the
627
- complete folder snapshot. Staged finalize accepts a completed `StagedFile`, then paginates the target folder and returns all
628
- exact filename/hash/size matches; zero, one, or multiple results preserve missing and ambiguous observations without
629
- another result model. This is useful evidence, not an uploader-ownership guarantee: the Provider binds by
630
- temporary row ID without checking its owner (PVD-031), and finalize atomicity/idempotency remain absent (PCG-012).
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. Token values and sensitive response
651
- 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.
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, extraction of the returned token pair, exchange through a new
683
- `AsyncMatelab.bind_external_tokens` instance, userid consistency checks, and notebook discovery. It is not Provider
684
- 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.
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 external-token refresh
693
- persist Provider token state. This side effect is inherent to the tested Provider operations; it cannot be disabled
694
- 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.
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`. The identities returned by authentication and external token binding must
704
- 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:
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.1.2`,
739
- commit `6dd674ac001ae61197495ace5eba489d585cb825`, and OpenAPI SHA-256
740
- `7ccaf3116c8dcec7a6e49ff51fb1a5ecc83a140e013ec600516f5c2ffb3b5378`.
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`.