matelab-python-sdk 0.1.0a3__tar.gz → 0.1.0a5__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 (64) hide show
  1. matelab_python_sdk-0.1.0a5/CHANGELOG.md +61 -0
  2. {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/PKG-INFO +196 -48
  3. {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/README.md +195 -47
  4. {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/contracts/matelab-integration-v1.lock.json +3 -3
  5. {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/contracts/matelab-integration-v1.openapi.yaml +299 -16
  6. matelab_python_sdk-0.1.0a5/docs/attachment-write-contract-gaps.md +103 -0
  7. {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/docs/operation-coverage.yaml +1 -1
  8. {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/docs/roadmap.md +33 -7
  9. {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/pyproject.toml +1 -2
  10. {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/scripts/check_installed_package.py +2 -4
  11. {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/scripts/check_release.py +61 -22
  12. {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/scripts/generate_models.py +88 -3
  13. {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/src/matelab/__init__.py +39 -66
  14. {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/src/matelab/_generated/models.py +937 -761
  15. {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/src/matelab/_transport.py +89 -70
  16. matelab_python_sdk-0.1.0a5/src/matelab/_wire_validation.py +214 -0
  17. {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/src/matelab/client.py +32 -44
  18. matelab_python_sdk-0.1.0a5/src/matelab/domains/__init__.py +1 -0
  19. {matelab_python_sdk-0.1.0a3/src/matelab → matelab_python_sdk-0.1.0a5/src/matelab/domains}/cloud_drive.py +62 -116
  20. {matelab_python_sdk-0.1.0a3/src/matelab → matelab_python_sdk-0.1.0a5/src/matelab/domains}/groups.py +6 -2
  21. matelab_python_sdk-0.1.0a3/src/matelab/literature/__init__.py → matelab_python_sdk-0.1.0a5/src/matelab/domains/literature.py +95 -126
  22. {matelab_python_sdk-0.1.0a3/src/matelab → matelab_python_sdk-0.1.0a5/src/matelab/domains}/notebooks.py +84 -68
  23. matelab_python_sdk-0.1.0a3/src/matelab/records/__init__.py → matelab_python_sdk-0.1.0a5/src/matelab/domains/records.py +1030 -379
  24. {matelab_python_sdk-0.1.0a3/src/matelab → matelab_python_sdk-0.1.0a5/src/matelab/domains}/templates.py +76 -100
  25. {matelab_python_sdk-0.1.0a3/src/matelab → matelab_python_sdk-0.1.0a5/src/matelab/domains}/uploads.py +5 -14
  26. {matelab_python_sdk-0.1.0a3/src/matelab → matelab_python_sdk-0.1.0a5/src/matelab/domains}/users.py +9 -23
  27. {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/src/matelab/streaming.py +23 -5
  28. {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/tests/test_client.py +51 -1
  29. {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/tests/test_cloud_drive.py +6 -9
  30. {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/tests/test_comments_and_downloads.py +3 -3
  31. {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/tests/test_extended_records.py +9 -13
  32. {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/tests/test_generation.py +161 -1
  33. {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/tests/test_groups.py +22 -0
  34. {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/tests/test_notebook_lifecycle.py +31 -31
  35. {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/tests/test_operation_coverage.py +4 -1
  36. {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/tests/test_record_lifecycle.py +23 -12
  37. {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/tests/test_record_mutations.py +907 -108
  38. {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/tests/test_release.py +89 -12
  39. {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/tests/test_streaming.py +74 -4
  40. {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/tests/test_template_lifecycle.py +6 -6
  41. {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/tests/test_templates.py +12 -12
  42. {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/tests/test_transport.py +112 -66
  43. {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/tests/test_uploads_and_literature.py +45 -21
  44. {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/tests/test_users.py +10 -11
  45. matelab_python_sdk-0.1.0a5/tests/typing/consumer.py +83 -0
  46. {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/uv.lock +1 -1
  47. matelab_python_sdk-0.1.0a3/CHANGELOG.md +0 -34
  48. matelab_python_sdk-0.1.0a3/tests/typing/consumer.py +0 -47
  49. {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/.env.example +0 -0
  50. {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/.github/workflows/release.yml +0 -0
  51. {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/.gitignore +0 -0
  52. {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/AGENTS.md +0 -0
  53. {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/LICENSE +0 -0
  54. {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/NOTICE +0 -0
  55. {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/src/matelab/_generated/__init__.py +0 -0
  56. {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/src/matelab/errors.py +0 -0
  57. {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/src/matelab/py.typed +0 -0
  58. /matelab_python_sdk-0.1.0a3/src/matelab/models.py → /matelab_python_sdk-0.1.0a5/src/matelab/session.py +0 -0
  59. {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/tests/conftest.py +0 -0
  60. {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/tests/provider/test_provider_smoke.py +0 -0
  61. {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/tests/test_consumer_smoke.py +0 -0
  62. {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/tests/test_notebooks.py +0 -0
  63. {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/tests/test_records.py +0 -0
  64. {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/tests/typing/pyproject.toml +0 -0
@@ -0,0 +1,61 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0a5 - 2026-07-29
4
+
5
+ - Adopts immutable `matelab-spec v0.1.2` at commit `6dd674ac001ae61197495ace5eba489d585cb825` and OpenAPI
6
+ SHA-256 `7ccaf3116c8dcec7a6e49ff51fb1a5ecc83a140e013ec600516f5c2ffb3b5378`.
7
+ - Adds notebook-scoped staged finalizers for a null table file cell, an attachment-bearing appended table row,
8
+ files-module root append, and table/files occurrence replacement, with exact provenance, one-shot transport, and
9
+ canonical readback checks.
10
+ - Adds typed intents for an empty file table column and complete form file-field/table file-column deletion.
11
+ - Allows `records.update(..., module_deletions=...)` to delete a canonical module even when the current record read
12
+ observes attachments in it, verifies absence on persisted readback, and does not claim attachment-quote or byte
13
+ cleanup beyond the pinned Contract.
14
+ - Rejects cross-scope staged attachment handles before the record read, preserving the public static types while
15
+ avoiding the Provider's read-audit side effect for invalid runtime input.
16
+ - Keeps notebook-staged rich-text binding and attachment-bearing multi-record import explicitly unsupported because
17
+ v0.1.2 Provider Verification does not provide safe one-shot semantics for either.
18
+
19
+ ## 0.1.0a4 - 2026-07-28
20
+
21
+ - Further reduces the alpha public surface from 150 to 126 symbols by accepting direct keyword arguments and existing
22
+ domain values instead of request-shaped wrapper dataclasses.
23
+ - Centralizes safe request-model validation, strengthens generated Contract constraints, and preserves structured field
24
+ diagnostics without exposing credentials, tokens, or sensitive payload values.
25
+ - Tightens Session replacement, refresh races, multipart retry safety, streaming cleanup, and exact HTTP success
26
+ handling while retaining stable transport, authentication, Provider, protocol, usage, and verification errors.
27
+ - Collects all business capabilities under `matelab.domains`, keeps client/session/error/transport primitives at the
28
+ package root, and hardens generation and release-artifact audits.
29
+
30
+ ## 0.1.0a3 - 2026-07-28
31
+
32
+ - Replaces HTTPX with HTTPX2 2.9.1 and raises the Pydantic compatibility floor to 2.13.4. Injected clients must now
33
+ be `httpx2.AsyncClient` instances; HTTPX and HTTPX2 objects are not interchangeable. Default TLS verification now
34
+ uses the operating system trust store.
35
+ - Shrinks the alpha public surface from 184 to 150 symbols by returning existing domain values, tuples, booleans, or
36
+ `None` from simple mutations instead of defining endpoint-shaped result dataclasses. Contract invariants remain
37
+ documented operation semantics rather than repeated constant fields.
38
+ - Reorganizes the record and literature implementations into domain packages and keeps generated wire imports behind
39
+ one private namespace without changing the generated-model boundary.
40
+ - Aligns the opt-in SDK Provider smoke with the isolated test service's shared `MATELAB_PROVIDER_*` environment
41
+ variables and adds a safe `.env.example`.
42
+
43
+ ## 0.1.0a2 - 2026-07-27
44
+
45
+ - Adopts the immutable `matelab-spec v0.1.1` release.
46
+ - Adds notebook-scoped, uploader-bound record attachment staging before a target record exists.
47
+ - Finalizes staged attachments through a single-record import or one new form-file-field update without exposing
48
+ Provider-native attachment syntax or private transport details.
49
+ - Enforces exact notebook/user provenance and one finalization attempt per staged handle in one SDK client.
50
+
51
+ ## 0.1.0a1 - 2026-07-27
52
+
53
+ First public alpha candidate of the asynchronous Matelab Integration Contract SDK.
54
+
55
+ - Binds the immutable `matelab-spec v0.1.0` release.
56
+ - Exposes all 71 pinned Contract operations through public domain interfaces: 15 are stable and 56 remain
57
+ experimental.
58
+ - Provides instance-owned immutable Sessions, bounded token refresh, typed public models, streaming downloads,
59
+ and generated private wire validation.
60
+ - Ships as a typed package for Python 3.11 through 3.14.
61
+ - Uses the Apache License 2.0 with PEP 639 package metadata.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: matelab-python-sdk
3
- Version: 0.1.0a3
3
+ Version: 0.1.0a5
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.0a3`. `[project].version` in `pyproject.toml` is the sole SDK version source;
31
+ The current alpha is `0.1.0a5`. `[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.1.2` 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.0a3-py3-none-any.whl
60
+ python -m pip install dist/release/matelab_python_sdk-0.1.0a5-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
@@ -82,6 +82,24 @@ async with AsyncMatelab() as client:
82
82
  `AsyncMatelab()` uses `https://matelab.iphy.ac.cn/api` by default. Pass another Provider API root
83
83
  explicitly when needed, for example `AsyncMatelab("https://custom.example/api")`.
84
84
 
85
+ ### Error handling
86
+
87
+ Catch `MatelabError` for one application-level fallback, or a specific subclass when recovery differs:
88
+
89
+ | Error | Meaning and normal response |
90
+ |---|---|
91
+ | `MatelabUsageError` | The call cannot be represented safely; correct its arguments. |
92
+ | `MatelabAuthenticationError` | The Session is missing, expired, invalid, or rejected; refresh or authenticate again as appropriate. |
93
+ | `MatelabProviderError` | The Provider rejected a valid request with a business error; inspect `code` and do not assume a mutation was applied. |
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. |
97
+
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.
102
+
85
103
  ### Session ownership
86
104
 
87
105
  Each `AsyncMatelab` instance owns at most one current, process-local `Session`. The SDK injects its bearer
@@ -176,7 +194,7 @@ notebook selector used for upload:
176
194
  ```python
177
195
  import hashlib
178
196
 
179
- from matelab import RecordImportItem, RecordImportTemplate, TemplateRef
197
+ from matelab import RecordImportItem, TemplateRef
180
198
 
181
199
  content = b"measurement data"
182
200
  staged = await client.records.stage_attachment(
@@ -188,7 +206,8 @@ staged = await client.records.stage_attachment(
188
206
  )
189
207
  result = await client.records.import_dataset(
190
208
  notebook=notebook,
191
- template=RecordImportTemplate(template=TemplateRef(template_id=8), title="Example Template"),
209
+ template=TemplateRef(template_id=8),
210
+ template_title="Example Template",
192
211
  items=(
193
212
  RecordImportItem(
194
213
  record_uid="REC-IMPORT-001",
@@ -199,38 +218,163 @@ result = await client.records.import_dataset(
199
218
  )
200
219
  ```
201
220
 
202
- The same staged handle may instead be consumed by one safe update that adds a new file field to an existing form
203
- module:
221
+ The same staged handle may instead be consumed by one safe update finalizer. For example, add a new file field to an
222
+ existing form module:
204
223
 
205
224
  ```python
206
- from matelab import RecordFormAttachmentFieldAddition, RecordPatch
225
+ from matelab import RecordFormAttachmentFieldAddition
207
226
 
208
227
  # Alternative to the import above; do not run both with the same staged handle.
209
228
  result = await client.records.update(
210
229
  source,
211
- RecordPatch(
212
- attachment_changes=(
213
- RecordFormAttachmentFieldAddition(
214
- module="Attachments",
215
- name="Measurement",
216
- attachment=staged,
217
- ),
218
- )
230
+ attachment_changes=(
231
+ RecordFormAttachmentFieldAddition(
232
+ module="Attachments",
233
+ name="Measurement",
234
+ attachment=staged,
235
+ ),
219
236
  ),
220
237
  )
221
238
  ```
222
239
 
223
- Choose exactly one finalizer. A staged name may occur once in either a single-record import or one update
224
- operation; do not reuse it, even after an error whose Provider outcome is unknown. The SDK rejects raw Provider
225
- attachment references, cross-user or cross-notebook handles, unsafe update shapes, duplicate use in one request,
226
- and a second finalization attempt through the same client. A Session imported without identity must first call
227
- `await client.resolve_identity()`. The Provider supplies no staging status, abort, TTL, atomicity, or retry
228
- guarantee, so callers must discard the handle as soon as a finalization request starts.
240
+ Choose exactly one finalizer. A staged name may occur once in either a single-record import or one update operation;
241
+ do not reuse it after transport starts, even when the Provider outcome is unknown. A locally rejected target or
242
+ argument does not spend the handle. The SDK rejects raw Provider attachment references, cross-user or cross-notebook
243
+ 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()`.
245
+
246
+ The Provider supplies no staging status, abort, TTL, atomicity, idempotency, or retry guarantee. Integrations that
247
+ persist handles must durably claim their own `uploaded -> finalizing/indeterminate` transition before calling a
248
+ finalizer.
249
+
250
+ ### Attachment-bearing record update boundary
251
+
252
+ The v0.1.2 Contract adds dedicated, verified notebook-staged update shapes. They remain public intent objects; callers
253
+ never construct Provider paths or attachment strings:
254
+
255
+ ```python
256
+ from matelab import (
257
+ RecordFilesAttachmentRootAppend,
258
+ RecordTableFileCellSet,
259
+ RecordTableRowAttachmentAppend,
260
+ )
261
+
262
+ # Each example is a separate finalizer; never run several with the same handle.
263
+ await client.records.update(
264
+ source,
265
+ attachment_changes=(
266
+ RecordTableFileCellSet(
267
+ table="Measurements",
268
+ column="Evidence",
269
+ row=0,
270
+ attachment=staged,
271
+ ),
272
+ ),
273
+ )
274
+
275
+ await client.records.update(
276
+ source,
277
+ attachment_changes=(
278
+ RecordTableRowAttachmentAppend(
279
+ table="Measurements",
280
+ file_column="Evidence",
281
+ values={"Label": "Sample C"},
282
+ attachment=another_staged,
283
+ ),
284
+ ),
285
+ )
286
+
287
+ await client.records.update(
288
+ source,
289
+ attachment_changes=(
290
+ RecordFilesAttachmentRootAppend(
291
+ module="Files",
292
+ caption="Evidence",
293
+ attachment=third_staged,
294
+ ),
295
+ ),
296
+ )
297
+ ```
298
+
299
+ `RecordTableFileCellSet` requires an existing file column and a cell whose immediate canonical value is exactly
300
+ `null`. The row-append intent addresses the new row by the row count from the SDK's immediate read and supports one
301
+ staged file column. Files append is root-only and requires a string caption.
302
+
303
+ Replacement starts with an occurrence returned by `records.read()`; applications must not fabricate a
304
+ `RecordAttachmentRef`:
305
+
306
+ ```python
307
+ from matelab import RecordFilesAttachmentReplacement
308
+
309
+ record = await client.records.read(notebook=source.notebook, record=source.record)
310
+ existing = next(
311
+ attachment
312
+ for attachment in record.attachments
313
+ if attachment.location is not None and attachment.location.kind == "files_module"
314
+ )
315
+ await client.records.update(
316
+ source,
317
+ attachment_changes=(RecordFilesAttachmentReplacement(existing=existing, replacement=staged, caption=None),),
318
+ )
319
+ ```
320
+
321
+ 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.
325
+
326
+ Row/index-based finalizers use an immediate SDK read and must not be called while a concurrent editor is known to be
327
+ active. The Provider offers no expected hash or revision. `expected_content_sha256` is only a client-side prewrite
328
+ check, not Provider compare-and-swap.
329
+
330
+ File-bearing structure changes do not consume staging:
331
+
332
+ ```python
333
+ from matelab import (
334
+ RecordFormFileFieldDeletion,
335
+ RecordTableFileColumnAddition,
336
+ RecordTableFileColumnDeletion,
337
+ )
338
+
339
+ await client.records.update(
340
+ source,
341
+ file_structure_changes=(
342
+ RecordTableFileColumnAddition(table="Measurements", name="Additional evidence"),
343
+ RecordFormFileFieldDeletion(form="Attachments", name="Obsolete evidence"),
344
+ RecordTableFileColumnDeletion(table="Measurements", name="Old evidence"),
345
+ ),
346
+ )
347
+ ```
348
+
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.
352
+
353
+ Deleting a complete canonical module is also supported even when the current read observes attachments in it:
354
+
355
+ ```python
356
+ result = await client.records.update(
357
+ source,
358
+ module_deletions=("Raw files",),
359
+ expected_content_sha256=record.content_sha256,
360
+ )
361
+ ```
362
+
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.
365
+
366
+ Notebook-staged rich-text binding remains unsupported. Provider Verification shows that `#file{name}` is stored as
367
+ plain text and that a hash-based URI can fall back to an existing quote, so neither is a one-shot staged finalizer.
368
+ Existing `RecordRichTextUpdate` remains legal only with record-scoped `StagedRecordAttachment`. Attachment-bearing
369
+ multi-record import is also forbidden; callers must split it into single-record finalizers.
370
+
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.
229
373
 
230
374
  ## Implementation roadmap
231
375
 
232
376
  `docs/roadmap.md` is the complete SDK-only execution plan. It assigns all 71
233
- `matelab-spec v0.1.1` operations to ordered work packages, defines the machine-readable coverage that
377
+ `matelab-spec v0.1.2` operations to ordered work packages, defines the machine-readable coverage that
234
378
  must be added, records Provider-risk gates, and specifies the final completion checks.
235
379
 
236
380
  The SDK exposes all 71 Contract operations through public domain interfaces. It deliberately excludes MCP
@@ -295,11 +439,9 @@ Provider returns members for an unstable first group and user search is unpaged,
295
439
  Notebook create/update and direct sharing keep write acknowledgement separate from what a readback can prove:
296
440
 
297
441
  ```python
298
- from matelab import NotebookMetadata, NotebookSharePermissionGrant
299
-
300
- await client.notebooks.create(NotebookMetadata(title="Example Notebook"))
442
+ await client.notebooks.create(title="Example Notebook")
301
443
  shares = await client.notebooks.share(notebook, [target.ref])
302
- updated = await client.notebooks.update_share(shares[0].ref, NotebookSharePermissionGrant(write=True, create=True))
444
+ updated = await client.notebooks.update_share(shares[0].ref, write=True, create=True)
303
445
  ```
304
446
 
305
447
  Create returns `None` because the Provider returns no identity. Owned updates return the notebook row observed by
@@ -314,7 +456,7 @@ relation identities:
314
456
  ```python
315
457
  templates = await client.templates.list()
316
458
  market = await client.templates.search_market("calibration", page=1, page_size=20)
317
- content = await client.templates.read(templates.owned[0].ref)
459
+ modules = await client.templates.read(templates.owned[0].ref)
318
460
  ```
319
461
 
320
462
  The market result reports the Provider `total_count`, the requested and effective page sizes, and a `has_more`
@@ -334,12 +476,10 @@ limitations are documented operation semantics rather than constant fields on ev
334
476
  Extended record reads stay behind the same `records` interface:
335
477
 
336
478
  ```python
337
- from matelab import RecordFieldExtraction, RecordLocator
479
+ from matelab import RecordLocator
338
480
 
339
481
  exported = await client.records.export([RecordLocator(notebook=notebook, record=record)])
340
- matches = await client.records.search(
341
- notebooks=[notebook], extractions=[RecordFieldExtraction(alias="notes", path=("Notes",))]
342
- )
482
+ matches = await client.records.search(notebooks=[notebook], extractions={"notes": ("Notes",)})
343
483
  page = await client.records.page(notebook)
344
484
  deleted = await client.records.recycle_bin(notebook)
345
485
  relations = await client.records.relations(notebook=notebook, record=record)
@@ -354,12 +494,13 @@ remain unspecified, and no continuation token is invented.
354
494
  Record creation keeps blank creation and structured import as separate capabilities:
355
495
 
356
496
  ```python
357
- from matelab import RecordImportItem, RecordImportTemplate, TemplateRef
497
+ from matelab import RecordImportItem, TemplateRef
358
498
 
359
499
  blank = await client.records.create_blank(notebook=notebook, title="Blank Record", record_uid="caller-generated-uid")
360
500
  imported = await client.records.import_dataset(
361
501
  notebook=notebook,
362
- template=RecordImportTemplate(template=TemplateRef(template_id=8), title="Example Template"),
502
+ template=TemplateRef(template_id=8),
503
+ template_title="Example Template",
363
504
  items=[RecordImportItem(record_uid="import-uid", title="Imported", data={"Notes": "value"})],
364
505
  )
365
506
  ```
@@ -397,7 +538,7 @@ Attachment bytes are streamed and must be consumed or closed explicitly:
397
538
  from matelab import ByteRange
398
539
 
399
540
  comments = await client.records.comments(notebook=notebook, record=record)
400
- attachment = comments.comments[0].attachments[0]
541
+ attachment = comments[0].attachments[0]
401
542
  async with await client.records.download_comment_attachment(attachment, byte_range=ByteRange.from_start(0)) as download:
402
543
  async for chunk in download:
403
544
  consume(chunk)
@@ -436,7 +577,7 @@ Staging mutations are never automatically retried.
436
577
  Literature identities distinguish the personal library, shared libraries and pending incoming copies:
437
578
 
438
579
  ```python
439
- from matelab import DoiMetadataSource, LiteratureMetadata
580
+ from matelab import LiteratureMetadata
440
581
 
441
582
  libraries = await client.literature.libraries()
442
583
  page = await client.literature.list(libraries.personal.ref)
@@ -444,7 +585,7 @@ detail = await client.literature.read(page.items[0].ref)
444
585
  schema = await client.literature.creation_schema()
445
586
 
446
587
  if schema.metadata_extraction_available:
447
- candidates = await client.literature.extract_metadata(DoiMetadataSource("10.0000/example"))
588
+ candidates = await client.literature.extract_metadata(doi="10.0000/example")
448
589
 
449
590
  await client.literature.create(LiteratureMetadata(title="Example import", doi="10.0000/example"), staged_pdf=staged)
450
591
  ```
@@ -467,10 +608,8 @@ stable `ByteRange` subset.
467
608
  The personal cloud-drive surface keeps root, folder, final file and temporary staging identities separate:
468
609
 
469
610
  ```python
470
- from matelab import CloudFolderMetadata
471
-
472
611
  listing = await client.cloud_drive.list()
473
- folder_result = await client.cloud_drive.create_folder(CloudFolderMetadata(name="Example data"))
612
+ folder_result = await client.cloud_drive.create_folder(name="Example data")
474
613
  matches = await client.cloud_drive.bind_staged_file(staged, target=folder_result.folder)
475
614
 
476
615
  if len(matches) == 1:
@@ -479,13 +618,13 @@ if len(matches) == 1:
479
618
  )
480
619
  ```
481
620
 
482
- `CloudDriveListing` contains a typed file page, complete folder tree, quota usage and personal-root permissions rather
621
+ `CloudDriveListing` contains a typed file page, complete folder snapshot, quota usage and personal-root permissions rather
483
622
  than flattening them into one ambiguous collection. Folder browse results retain their location; filename searches
484
623
  are explicitly root-wide and return `location=None` because the Provider omits each match's folder ID. Ordering has
485
624
  no stable ID tie-breaker (PCG-003, PVD-013).
486
625
 
487
626
  Folder create returns the Provider ID plus an optional observed folder, and all folder mutations read back the
488
- complete tree. Staged finalize accepts a completed `StagedFile`, then paginates the target folder and returns all
627
+ complete folder snapshot. Staged finalize accepts a completed `StagedFile`, then paginates the target folder and returns all
489
628
  exact filename/hash/size matches; zero, one, or multiple results preserve missing and ambiguous observations without
490
629
  another result model. This is useful evidence, not an uploader-ownership guarantee: the Provider binds by
491
630
  temporary row ID without checking its owner (PVD-031), and finalize atomicity/idempotency remain absent (PCG-012).
@@ -568,8 +707,17 @@ agree. With the environment prepared:
568
707
  uv run --env-file .env.test pytest -m provider tests/provider/test_provider_smoke.py
569
708
  ```
570
709
 
571
- The file is never loaded implicitly, so the normal test suite remains safely skipped. Do not use this flow against
572
- production, and never commit its credentials.
710
+ This command runs only the SDK public-interface smoke; it is not the 71-operation Provider Verification. To reuse the
711
+ same `.env.test` for the complete Contract suite, also populate the optional share user, secondary account, record
712
+ staging opt-in, and Chat SSO settings documented in `.env.example`, then run from sibling checkouts:
713
+
714
+ ```bash
715
+ cd ../matelab-spec
716
+ uv run --env-file ../matelab-python-sdk/.env.test pytest
717
+ ```
718
+
719
+ The files are never loaded implicitly, so normal test runs remain safely skipped. Do not use either flow against
720
+ production, and never commit Provider credentials.
573
721
 
574
722
  ## Reproducible release build
575
723
 
@@ -587,6 +735,6 @@ uv run python scripts/check_release.py dist/release/*.whl dist/release/*.tar.gz
587
735
  ```
588
736
 
589
737
  Rebuilding the same commit with the same locked environment and `SOURCE_DATE_EPOCH` must produce
590
- byte-identical wheel and source distribution hashes. The release is bound to `matelab-spec v0.1.1`,
591
- commit `047746ad37d827a85f93d947942f1e5fab80d54c`, and OpenAPI SHA-256
592
- `1d437b071968d2df165c712092fba4a283832e0ac19bc791e0d5af82294d1cca`.
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`.