matelab-python-sdk 0.1.0a4__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 (63) hide show
  1. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a5}/CHANGELOG.md +16 -0
  2. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a5}/PKG-INFO +154 -18
  3. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a5}/README.md +153 -17
  4. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a5}/contracts/matelab-integration-v1.lock.json +3 -3
  5. {matelab_python_sdk-0.1.0a4 → 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.0a4 → matelab_python_sdk-0.1.0a5}/docs/operation-coverage.yaml +1 -1
  8. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a5}/docs/roadmap.md +23 -7
  9. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a5}/pyproject.toml +1 -1
  10. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a5}/src/matelab/__init__.py +20 -0
  11. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a5}/src/matelab/_generated/models.py +104 -1
  12. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a5}/src/matelab/domains/records.py +764 -44
  13. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a5}/tests/test_generation.py +55 -1
  14. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a5}/tests/test_record_mutations.py +832 -80
  15. matelab_python_sdk-0.1.0a5/tests/typing/consumer.py +83 -0
  16. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a5}/uv.lock +1 -1
  17. matelab_python_sdk-0.1.0a4/tests/typing/consumer.py +0 -44
  18. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a5}/.env.example +0 -0
  19. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a5}/.github/workflows/release.yml +0 -0
  20. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a5}/.gitignore +0 -0
  21. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a5}/AGENTS.md +0 -0
  22. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a5}/LICENSE +0 -0
  23. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a5}/NOTICE +0 -0
  24. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a5}/scripts/check_installed_package.py +0 -0
  25. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a5}/scripts/check_release.py +0 -0
  26. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a5}/scripts/generate_models.py +0 -0
  27. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a5}/src/matelab/_generated/__init__.py +0 -0
  28. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a5}/src/matelab/_transport.py +0 -0
  29. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a5}/src/matelab/_wire_validation.py +0 -0
  30. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a5}/src/matelab/client.py +0 -0
  31. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a5}/src/matelab/domains/__init__.py +0 -0
  32. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a5}/src/matelab/domains/cloud_drive.py +0 -0
  33. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a5}/src/matelab/domains/groups.py +0 -0
  34. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a5}/src/matelab/domains/literature.py +0 -0
  35. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a5}/src/matelab/domains/notebooks.py +0 -0
  36. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a5}/src/matelab/domains/templates.py +0 -0
  37. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a5}/src/matelab/domains/uploads.py +0 -0
  38. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a5}/src/matelab/domains/users.py +0 -0
  39. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a5}/src/matelab/errors.py +0 -0
  40. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a5}/src/matelab/py.typed +0 -0
  41. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a5}/src/matelab/session.py +0 -0
  42. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a5}/src/matelab/streaming.py +0 -0
  43. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a5}/tests/conftest.py +0 -0
  44. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a5}/tests/provider/test_provider_smoke.py +0 -0
  45. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a5}/tests/test_client.py +0 -0
  46. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a5}/tests/test_cloud_drive.py +0 -0
  47. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a5}/tests/test_comments_and_downloads.py +0 -0
  48. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a5}/tests/test_consumer_smoke.py +0 -0
  49. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a5}/tests/test_extended_records.py +0 -0
  50. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a5}/tests/test_groups.py +0 -0
  51. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a5}/tests/test_notebook_lifecycle.py +0 -0
  52. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a5}/tests/test_notebooks.py +0 -0
  53. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a5}/tests/test_operation_coverage.py +0 -0
  54. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a5}/tests/test_record_lifecycle.py +0 -0
  55. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a5}/tests/test_records.py +0 -0
  56. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a5}/tests/test_release.py +0 -0
  57. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a5}/tests/test_streaming.py +0 -0
  58. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a5}/tests/test_template_lifecycle.py +0 -0
  59. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a5}/tests/test_templates.py +0 -0
  60. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a5}/tests/test_transport.py +0 -0
  61. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a5}/tests/test_uploads_and_literature.py +0 -0
  62. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a5}/tests/test_users.py +0 -0
  63. {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a5}/tests/typing/pyproject.toml +0 -0
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
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
+
3
19
  ## 0.1.0a4 - 2026-07-28
4
20
 
5
21
  - 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.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.0a4`. `[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.0a4-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
@@ -218,8 +218,8 @@ result = await client.records.import_dataset(
218
218
  )
219
219
  ```
220
220
 
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:
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:
223
223
 
224
224
  ```python
225
225
  from matelab import RecordFormAttachmentFieldAddition
@@ -237,17 +237,144 @@ result = await client.records.update(
237
237
  )
238
238
  ```
239
239
 
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.
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.
246
373
 
247
374
  ## Implementation roadmap
248
375
 
249
376
  `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
377
+ `matelab-spec v0.1.2` operations to ordered work packages, defines the machine-readable coverage that
251
378
  must be added, records Provider-risk gates, and specifies the final completion checks.
252
379
 
253
380
  The SDK exposes all 71 Contract operations through public domain interfaces. It deliberately excludes MCP
@@ -580,8 +707,17 @@ agree. With the environment prepared:
580
707
  uv run --env-file .env.test pytest -m provider tests/provider/test_provider_smoke.py
581
708
  ```
582
709
 
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.
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.
585
721
 
586
722
  ## Reproducible release build
587
723
 
@@ -599,6 +735,6 @@ uv run python scripts/check_release.py dist/release/*.whl dist/release/*.tar.gz
599
735
  ```
600
736
 
601
737
  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`.
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`.
@@ -2,10 +2,10 @@
2
2
 
3
3
  Reusable async Python client for the Matelab Integration Contract.
4
4
 
5
- The current alpha is `0.1.0a4`. `[project].version` in `pyproject.toml` is the sole SDK version source;
5
+ The current alpha is `0.1.0a5`. `[project].version` in `pyproject.toml` is the sole SDK version source;
6
6
  `uv.lock` only mirrors that source.
7
7
 
8
- The SDK is pinned to the immutable `matelab-spec v0.1.1` Contract Release. The sole release pin is
8
+ The SDK is pinned to the immutable `matelab-spec v0.1.2` Contract Release. The sole release pin is
9
9
  `contracts/matelab-integration-v1.lock.json`, which records
10
10
  the source tag, commit, OpenAPI path, local snapshot path, and SHA-256.
11
11
 
@@ -31,7 +31,7 @@ To test the same artifact a downstream Consumer will install, build and install
31
31
 
32
32
  ```bash
33
33
  uv build --no-build-isolation --out-dir dist/release
34
- python -m pip install dist/release/matelab_python_sdk-0.1.0a4-py3-none-any.whl
34
+ python -m pip install dist/release/matelab_python_sdk-0.1.0a5-py3-none-any.whl
35
35
  ```
36
36
 
37
37
  Do not infer Provider compatibility from the SDK version alone. A release is also bound to the Contract
@@ -192,8 +192,8 @@ result = await client.records.import_dataset(
192
192
  )
193
193
  ```
194
194
 
195
- The same staged handle may instead be consumed by one safe update that adds a new file field to an existing form
196
- module:
195
+ The same staged handle may instead be consumed by one safe update finalizer. For example, add a new file field to an
196
+ existing form module:
197
197
 
198
198
  ```python
199
199
  from matelab import RecordFormAttachmentFieldAddition
@@ -211,17 +211,144 @@ result = await client.records.update(
211
211
  )
212
212
  ```
213
213
 
214
- Choose exactly one finalizer. A staged name may occur once in either a single-record import or one update
215
- operation; do not reuse it, even after an error whose Provider outcome is unknown. The SDK rejects raw Provider
216
- attachment references, cross-user or cross-notebook handles, unsafe update shapes, duplicate use in one request,
217
- and a second finalization attempt through the same client. A Session imported without identity must first call
218
- `await client.resolve_identity()`. The Provider supplies no staging status, abort, TTL, atomicity, or retry
219
- guarantee, so callers must discard the handle as soon as a finalization request starts.
214
+ Choose exactly one finalizer. A staged name may occur once in either a single-record import or one update operation;
215
+ do not reuse it after transport starts, even when the Provider outcome is unknown. A locally rejected target or
216
+ argument does not spend the handle. The SDK rejects raw Provider attachment references, cross-user or cross-notebook
217
+ handles, unsafe combinations, duplicate use in one request, and a second finalization attempt through the same
218
+ client. A Session imported without identity must first call `await client.resolve_identity()`.
219
+
220
+ The Provider supplies no staging status, abort, TTL, atomicity, idempotency, or retry guarantee. Integrations that
221
+ persist handles must durably claim their own `uploaded -> finalizing/indeterminate` transition before calling a
222
+ finalizer.
223
+
224
+ ### Attachment-bearing record update boundary
225
+
226
+ The v0.1.2 Contract adds dedicated, verified notebook-staged update shapes. They remain public intent objects; callers
227
+ never construct Provider paths or attachment strings:
228
+
229
+ ```python
230
+ from matelab import (
231
+ RecordFilesAttachmentRootAppend,
232
+ RecordTableFileCellSet,
233
+ RecordTableRowAttachmentAppend,
234
+ )
235
+
236
+ # Each example is a separate finalizer; never run several with the same handle.
237
+ await client.records.update(
238
+ source,
239
+ attachment_changes=(
240
+ RecordTableFileCellSet(
241
+ table="Measurements",
242
+ column="Evidence",
243
+ row=0,
244
+ attachment=staged,
245
+ ),
246
+ ),
247
+ )
248
+
249
+ await client.records.update(
250
+ source,
251
+ attachment_changes=(
252
+ RecordTableRowAttachmentAppend(
253
+ table="Measurements",
254
+ file_column="Evidence",
255
+ values={"Label": "Sample C"},
256
+ attachment=another_staged,
257
+ ),
258
+ ),
259
+ )
260
+
261
+ await client.records.update(
262
+ source,
263
+ attachment_changes=(
264
+ RecordFilesAttachmentRootAppend(
265
+ module="Files",
266
+ caption="Evidence",
267
+ attachment=third_staged,
268
+ ),
269
+ ),
270
+ )
271
+ ```
272
+
273
+ `RecordTableFileCellSet` requires an existing file column and a cell whose immediate canonical value is exactly
274
+ `null`. The row-append intent addresses the new row by the row count from the SDK's immediate read and supports one
275
+ staged file column. Files append is root-only and requires a string caption.
276
+
277
+ Replacement starts with an occurrence returned by `records.read()`; applications must not fabricate a
278
+ `RecordAttachmentRef`:
279
+
280
+ ```python
281
+ from matelab import RecordFilesAttachmentReplacement
282
+
283
+ record = await client.records.read(notebook=source.notebook, record=source.record)
284
+ existing = next(
285
+ attachment
286
+ for attachment in record.attachments
287
+ if attachment.location is not None and attachment.location.kind == "files_module"
288
+ )
289
+ await client.records.update(
290
+ source,
291
+ attachment_changes=(RecordFilesAttachmentReplacement(existing=existing, replacement=staged, caption=None),),
292
+ )
293
+ ```
294
+
295
+ For a files occurrence, `caption=None` preserves the observed string caption (an observed null caption normalizes to
296
+ the required empty string). Canonical readback must preserve the Provider `uid` and folder. Table replacement requires
297
+ exactly one current attachment in the selected cell and uses `RecordTableFileAttachmentReplacement`. Both replacement
298
+ forms require the new hash to differ.
299
+
300
+ Row/index-based finalizers use an immediate SDK read and must not be called while a concurrent editor is known to be
301
+ active. The Provider offers no expected hash or revision. `expected_content_sha256` is only a client-side prewrite
302
+ check, not Provider compare-and-swap.
303
+
304
+ File-bearing structure changes do not consume staging:
305
+
306
+ ```python
307
+ from matelab import (
308
+ RecordFormFileFieldDeletion,
309
+ RecordTableFileColumnAddition,
310
+ RecordTableFileColumnDeletion,
311
+ )
312
+
313
+ await client.records.update(
314
+ source,
315
+ file_structure_changes=(
316
+ RecordTableFileColumnAddition(table="Measurements", name="Additional evidence"),
317
+ RecordFormFileFieldDeletion(form="Attachments", name="Obsolete evidence"),
318
+ RecordTableFileColumnDeletion(table="Measurements", name="Old evidence"),
319
+ ),
320
+ )
321
+ ```
322
+
323
+ The SDK emits the v0.1.2 dedicated operations: a new file column omits wire `data` and must read back as `null` for
324
+ every existing row; whole form-field/table-column deletion uses a strict two-segment target. It does not expose a
325
+ three-segment delete as table-cell clearing.
326
+
327
+ Deleting a complete canonical module is also supported even when the current read observes attachments in it:
328
+
329
+ ```python
330
+ result = await client.records.update(
331
+ source,
332
+ module_deletions=("Raw files",),
333
+ expected_content_sha256=record.content_sha256,
334
+ )
335
+ ```
336
+
337
+ This requests only a canonical record-content mutation. Readback proves the module is absent; it does not prove
338
+ attachment-quote cleanup or byte deletion. Collaboration-pending has no readback and is not reported as persisted.
339
+
340
+ Notebook-staged rich-text binding remains unsupported. Provider Verification shows that `#file{name}` is stored as
341
+ plain text and that a hash-based URI can fall back to an existing quote, so neither is a one-shot staged finalizer.
342
+ Existing `RecordRichTextUpdate` remains legal only with record-scoped `StagedRecordAttachment`. Attachment-bearing
343
+ multi-record import is also forbidden; callers must split it into single-record finalizers.
344
+
345
+ See `docs/attachment-write-contract-gaps.md` in the source repository for the exact support matrix, integration
346
+ lifecycle requirements, and minimum Contract changes needed to expose the remaining operations.
220
347
 
221
348
  ## Implementation roadmap
222
349
 
223
350
  `docs/roadmap.md` is the complete SDK-only execution plan. It assigns all 71
224
- `matelab-spec v0.1.1` operations to ordered work packages, defines the machine-readable coverage that
351
+ `matelab-spec v0.1.2` operations to ordered work packages, defines the machine-readable coverage that
225
352
  must be added, records Provider-risk gates, and specifies the final completion checks.
226
353
 
227
354
  The SDK exposes all 71 Contract operations through public domain interfaces. It deliberately excludes MCP
@@ -554,8 +681,17 @@ agree. With the environment prepared:
554
681
  uv run --env-file .env.test pytest -m provider tests/provider/test_provider_smoke.py
555
682
  ```
556
683
 
557
- The file is never loaded implicitly, so the normal test suite remains safely skipped. Do not use this flow against
558
- production, and never commit its credentials.
684
+ This command runs only the SDK public-interface smoke; it is not the 71-operation Provider Verification. To reuse the
685
+ same `.env.test` for the complete Contract suite, also populate the optional share user, secondary account, record
686
+ staging opt-in, and Chat SSO settings documented in `.env.example`, then run from sibling checkouts:
687
+
688
+ ```bash
689
+ cd ../matelab-spec
690
+ uv run --env-file ../matelab-python-sdk/.env.test pytest
691
+ ```
692
+
693
+ The files are never loaded implicitly, so normal test runs remain safely skipped. Do not use either flow against
694
+ production, and never commit Provider credentials.
559
695
 
560
696
  ## Reproducible release build
561
697
 
@@ -573,6 +709,6 @@ uv run python scripts/check_release.py dist/release/*.whl dist/release/*.tar.gz
573
709
  ```
574
710
 
575
711
  Rebuilding the same commit with the same locked environment and `SOURCE_DATE_EPOCH` must produce
576
- byte-identical wheel and source distribution hashes. The release is bound to `matelab-spec v0.1.1`,
577
- commit `047746ad37d827a85f93d947942f1e5fab80d54c`, and OpenAPI SHA-256
578
- `1d437b071968d2df165c712092fba4a283832e0ac19bc791e0d5af82294d1cca`.
712
+ byte-identical wheel and source distribution hashes. The release is bound to `matelab-spec v0.1.2`,
713
+ commit `6dd674ac001ae61197495ace5eba489d585cb825`, and OpenAPI SHA-256
714
+ `7ccaf3116c8dcec7a6e49ff51fb1a5ecc83a140e013ec600516f5c2ffb3b5378`.
@@ -2,12 +2,12 @@
2
2
  "schema_version": 1,
3
3
  "contract_line": "integration-v1",
4
4
  "release": {
5
- "tag": "v0.1.1",
6
- "commit": "047746ad37d827a85f93d947942f1e5fab80d54c"
5
+ "tag": "v0.1.2",
6
+ "commit": "6dd674ac001ae61197495ace5eba489d585cb825"
7
7
  },
8
8
  "openapi": {
9
9
  "source_path": "openapi/matelab-integration-v1.openapi.yaml",
10
10
  "snapshot": "matelab-integration-v1.openapi.yaml",
11
- "sha256": "1d437b071968d2df165c712092fba4a283832e0ac19bc791e0d5af82294d1cca"
11
+ "sha256": "7ccaf3116c8dcec7a6e49ff51fb1a5ecc83a140e013ec600516f5c2ffb3b5378"
12
12
  }
13
13
  }