matelab-python-sdk 0.1.0a1__tar.gz → 0.1.0a3__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.0a3/.env.example +6 -0
  2. matelab_python_sdk-0.1.0a3/CHANGELOG.md +34 -0
  3. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a3}/PKG-INFO +141 -87
  4. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a3}/README.md +138 -84
  5. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a3}/contracts/matelab-integration-v1.lock.json +3 -3
  6. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a3}/contracts/matelab-integration-v1.openapi.yaml +147 -16
  7. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a3}/docs/operation-coverage.yaml +5 -5
  8. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a3}/docs/roadmap.md +55 -29
  9. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a3}/pyproject.toml +4 -9
  10. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a3}/scripts/check_installed_package.py +1 -1
  11. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a3}/scripts/check_release.py +7 -3
  12. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a3}/src/matelab/__init__.py +4 -68
  13. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a3}/src/matelab/_generated/models.py +24 -7
  14. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a3}/src/matelab/_transport.py +12 -12
  15. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a3}/src/matelab/client.py +17 -24
  16. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a3}/src/matelab/cloud_drive.py +48 -210
  17. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a3}/src/matelab/groups.py +3 -5
  18. matelab_python_sdk-0.1.0a1/src/matelab/literature.py → matelab_python_sdk-0.1.0a3/src/matelab/literature/__init__.py +64 -233
  19. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a3}/src/matelab/notebooks.py +33 -124
  20. matelab_python_sdk-0.1.0a1/src/matelab/records.py → matelab_python_sdk-0.1.0a3/src/matelab/records/__init__.py +369 -363
  21. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a3}/src/matelab/streaming.py +3 -3
  22. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a3}/src/matelab/templates.py +62 -188
  23. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a3}/src/matelab/uploads.py +13 -38
  24. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a3}/src/matelab/users.py +3 -6
  25. matelab_python_sdk-0.1.0a3/tests/conftest.py +58 -0
  26. matelab_python_sdk-0.1.0a3/tests/provider/test_provider_smoke.py +68 -0
  27. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a3}/tests/test_client.py +72 -80
  28. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a3}/tests/test_cloud_drive.py +56 -66
  29. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a3}/tests/test_comments_and_downloads.py +21 -22
  30. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a3}/tests/test_consumer_smoke.py +7 -7
  31. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a3}/tests/test_extended_records.py +51 -56
  32. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a3}/tests/test_generation.py +1 -1
  33. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a3}/tests/test_groups.py +13 -14
  34. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a3}/tests/test_notebook_lifecycle.py +74 -88
  35. matelab_python_sdk-0.1.0a3/tests/test_notebooks.py +71 -0
  36. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a3}/tests/test_operation_coverage.py +2 -2
  37. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a3}/tests/test_record_lifecycle.py +191 -65
  38. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a3}/tests/test_record_mutations.py +335 -93
  39. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a3}/tests/test_records.py +82 -64
  40. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a3}/tests/test_release.py +38 -4
  41. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a3}/tests/test_streaming.py +41 -41
  42. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a3}/tests/test_template_lifecycle.py +96 -112
  43. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a3}/tests/test_templates.py +39 -40
  44. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a3}/tests/test_transport.py +87 -87
  45. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a3}/tests/test_uploads_and_literature.py +90 -106
  46. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a3}/tests/test_users.py +30 -31
  47. matelab_python_sdk-0.1.0a3/tests/typing/consumer.py +47 -0
  48. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a3}/uv.lock +27 -31
  49. matelab_python_sdk-0.1.0a1/CHANGELOG.md +0 -13
  50. matelab_python_sdk-0.1.0a1/tests/conftest.py +0 -20
  51. matelab_python_sdk-0.1.0a1/tests/provider/test_provider_smoke.py +0 -128
  52. matelab_python_sdk-0.1.0a1/tests/test_notebooks.py +0 -128
  53. matelab_python_sdk-0.1.0a1/tests/typing/consumer.py +0 -7
  54. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a3}/.github/workflows/release.yml +0 -0
  55. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a3}/.gitignore +0 -0
  56. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a3}/AGENTS.md +0 -0
  57. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a3}/LICENSE +0 -0
  58. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a3}/NOTICE +0 -0
  59. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a3}/scripts/generate_models.py +0 -0
  60. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a3}/src/matelab/_generated/__init__.py +0 -0
  61. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a3}/src/matelab/errors.py +0 -0
  62. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a3}/src/matelab/models.py +0 -0
  63. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a3}/src/matelab/py.typed +0 -0
  64. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a3}/tests/typing/pyproject.toml +0 -0
@@ -0,0 +1,6 @@
1
+ # Provider consumer tests 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=
@@ -0,0 +1,34 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0a3 - 2026-07-28
4
+
5
+ - Replaces HTTPX with HTTPX2 2.9.1 and raises the Pydantic compatibility floor to 2.13.4. Injected clients must now
6
+ be `httpx2.AsyncClient` instances; HTTPX and HTTPX2 objects are not interchangeable. Default TLS verification now
7
+ uses the operating system trust store.
8
+ - Shrinks the alpha public surface from 184 to 150 symbols by returning existing domain values, tuples, booleans, or
9
+ `None` from simple mutations instead of defining endpoint-shaped result dataclasses. Contract invariants remain
10
+ documented operation semantics rather than repeated constant fields.
11
+ - Reorganizes the record and literature implementations into domain packages and keeps generated wire imports behind
12
+ one private namespace without changing the generated-model boundary.
13
+ - Aligns the opt-in SDK Provider smoke with the isolated test service's shared `MATELAB_PROVIDER_*` environment
14
+ variables and adds a safe `.env.example`.
15
+
16
+ ## 0.1.0a2 - 2026-07-27
17
+
18
+ - Adopts the immutable `matelab-spec v0.1.1` release.
19
+ - Adds notebook-scoped, uploader-bound record attachment staging before a target record exists.
20
+ - Finalizes staged attachments through a single-record import or one new form-file-field update without exposing
21
+ Provider-native attachment syntax or private transport details.
22
+ - Enforces exact notebook/user provenance and one finalization attempt per staged handle in one SDK client.
23
+
24
+ ## 0.1.0a1 - 2026-07-27
25
+
26
+ First public alpha candidate of the asynchronous Matelab Integration Contract SDK.
27
+
28
+ - Binds the immutable `matelab-spec v0.1.0` release.
29
+ - Exposes all 71 pinned Contract operations through public domain interfaces: 15 are stable and 56 remain
30
+ experimental.
31
+ - Provides instance-owned immutable Sessions, bounded token refresh, typed public models, streaming downloads,
32
+ and generated private wire validation.
33
+ - Ships as a typed package for Python 3.11 through 3.14.
34
+ - 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.0a1
3
+ Version: 0.1.0a3
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
@@ -20,18 +20,18 @@ Classifier: Topic :: Scientific/Engineering
20
20
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
21
  Classifier: Typing :: Typed
22
22
  Requires-Python: >=3.11
23
- Requires-Dist: httpx<1,>=0.27.2
24
- Requires-Dist: pydantic<3,>=2.12.5
23
+ Requires-Dist: httpx2<3,>=2.9.1
24
+ Requires-Dist: pydantic<3,>=2.13.4
25
25
  Description-Content-Type: text/markdown
26
26
 
27
27
  # matelab-python-sdk
28
28
 
29
29
  Reusable async Python client for the Matelab Integration Contract.
30
30
 
31
- The current alpha is `0.1.0a1`. `[project].version` in `pyproject.toml` is the sole SDK version source;
31
+ The current alpha is `0.1.0a3`. `[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.0` Contract Release. The sole release pin is
34
+ The SDK is pinned to the immutable `matelab-spec v0.1.1` 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.0a1-py3-none-any.whl
60
+ python -m pip install dist/release/matelab_python_sdk-0.1.0a3-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
@@ -155,6 +155,9 @@ alice_client = AsyncMatelab(session=alice_session, http_client=shared_http_clien
155
155
  bob_client = AsyncMatelab(session=bob_session, http_client=shared_http_client)
156
156
  ```
157
157
 
158
+ An injected `http_client` must be an `httpx2.AsyncClient`. The independently distributed `httpx.AsyncClient` has
159
+ similar methods but uses incompatible request, response, transport, and exception types.
160
+
158
161
  `src/matelab/_generated` is a private wire layer. Applications should not depend on its file
159
162
  layout or generated class names. The distribution includes `py.typed`, so type checkers can consume the
160
163
  public annotations directly from an installed wheel.
@@ -164,10 +167,70 @@ lifecycle operations, record discovery/lifecycle operations, comment reads, and
164
167
  comment attachment downloads, resumable file staging, literature discovery/lifecycle workflows, and
165
168
  personal cloud-drive management.
166
169
 
170
+ ### Staging a record attachment before record creation
171
+
172
+ `records.stage_attachment` supports the Contract's pre-upload workflow without inventing a target record UID.
173
+ The returned `StagedNotebookAttachment` is scoped by the SDK to the resolved authenticated user and the exact
174
+ notebook selector used for upload:
175
+
176
+ ```python
177
+ import hashlib
178
+
179
+ from matelab import RecordImportItem, RecordImportTemplate, TemplateRef
180
+
181
+ content = b"measurement data"
182
+ staged = await client.records.stage_attachment(
183
+ notebook=notebook,
184
+ filename="measurement.csv",
185
+ content=content,
186
+ size=len(content),
187
+ sha256=hashlib.sha256(content).hexdigest(),
188
+ )
189
+ result = await client.records.import_dataset(
190
+ notebook=notebook,
191
+ template=RecordImportTemplate(template=TemplateRef(template_id=8), title="Example Template"),
192
+ items=(
193
+ RecordImportItem(
194
+ record_uid="REC-IMPORT-001",
195
+ title="Imported measurement",
196
+ data={"Attachments": {"File": [staged]}},
197
+ ),
198
+ ),
199
+ )
200
+ ```
201
+
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:
204
+
205
+ ```python
206
+ from matelab import RecordFormAttachmentFieldAddition, RecordPatch
207
+
208
+ # Alternative to the import above; do not run both with the same staged handle.
209
+ result = await client.records.update(
210
+ source,
211
+ RecordPatch(
212
+ attachment_changes=(
213
+ RecordFormAttachmentFieldAddition(
214
+ module="Attachments",
215
+ name="Measurement",
216
+ attachment=staged,
217
+ ),
218
+ )
219
+ ),
220
+ )
221
+ ```
222
+
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.
229
+
167
230
  ## Implementation roadmap
168
231
 
169
232
  `docs/roadmap.md` is the complete SDK-only execution plan. It assigns all 71
170
- `matelab-spec v0.1.0` operations to ordered work packages, defines the machine-readable coverage that
233
+ `matelab-spec v0.1.1` operations to ordered work packages, defines the machine-readable coverage that
171
234
  must be added, records Provider-risk gates, and specifies the final completion checks.
172
235
 
173
236
  The SDK exposes all 71 Contract operations through public domain interfaces. It deliberately excludes MCP
@@ -224,8 +287,8 @@ groups = await client.groups.list()
224
287
  targets = await client.users.search("Example Researcher", scope=UserSearchScope.SAME_INSTITUTE)
225
288
  ```
226
289
 
227
- Both results explicitly report `provider_unspecified` ordering. Group members belong only to
228
- `groups.members_for`, not to every returned group. These two discovery interfaces are experimental because the
290
+ Ordering remains Provider-unspecified and is documented rather than repeated as a constant result field. Group
291
+ members belong only to `groups.members_for`, not to every returned group. These two discovery interfaces are experimental because the
229
292
  Provider returns members for an unstable first group and user search is unpaged, unordered, and not field-minimized
230
293
  (PVD-006, PVD-029, PCG-011).
231
294
 
@@ -234,17 +297,16 @@ Notebook create/update and direct sharing keep write acknowledgement separate fr
234
297
  ```python
235
298
  from matelab import NotebookMetadata, NotebookSharePermissionGrant
236
299
 
237
- created = await client.notebooks.create(NotebookMetadata(title="Example Notebook"))
300
+ await client.notebooks.create(NotebookMetadata(title="Example Notebook"))
238
301
  shares = await client.notebooks.share(notebook, [target.ref])
239
- updated = await client.notebooks.update_share(
240
- shares.visible_after_write[0].ref, NotebookSharePermissionGrant(write=True, create=True)
241
- )
302
+ updated = await client.notebooks.update_share(shares[0].ref, NotebookSharePermissionGrant(write=True, create=True))
242
303
  ```
243
304
 
244
- Create cannot return a notebook ref because the Provider returns no identity. Owned updates are read back by database
245
- ID and report matching, mismatched, or missing observations, containing PVD-003 false success without claiming an
246
- atomic guarantee. Share results similarly report post-write visibility rather than invented affected rows. A stored
247
- share mask of zero still has effective read access (PVD-010), and share-list order remains unspecified.
305
+ Create returns `None` because the Provider returns no identity. Owned updates return the notebook row observed by
306
+ database ID, or `None` when it is missing, containing PVD-003 false success without claiming an atomic guarantee.
307
+ Share returns only requested relations visible during readback, and permission updates return the observed relation
308
+ or `None`. A stored share mask of zero still has effective read access (PVD-010), and share-list order remains
309
+ unspecified.
248
310
 
249
311
  Template discovery keeps a template database identity separate from direct-share, market-acquisition, and group
250
312
  relation identities:
@@ -256,18 +318,18 @@ content = await client.templates.read(templates.owned[0].ref)
256
318
  ```
257
319
 
258
320
  The market result reports the Provider `total_count`, the requested and effective page sizes, and a `has_more`
259
- value explicitly marked as `derived_from_total`; it does not claim a stable order or continuation token. Canonical
321
+ value derived from the total; it does not claim a stable order or continuation token. Canonical
260
322
  modules are mapped to public `TemplateModule` values and retain additive module attributes. Template reads remain
261
323
  experimental because Provider discovery ordering/pagination and historical `images` compatibility are not fully
262
324
  stable (PCG-003, PCG-009, PVD-013, PVD-022).
263
325
 
264
326
  Template writes remain separate operations: metadata, canonical modules, and usage HTML are not presented as one
265
- transaction. Metadata create returns the Provider template ID; copy explicitly returns no new identity because the
266
- Provider supplies none. Direct-share, market-acquisition, and group relation refs are distinct and are required by
267
- their matching removal methods. Mutation results report readback observations without inventing affected rows.
268
- Metadata and intro results explicitly report that they do not advance the marketplace revision (PVD-021).
269
- `UploadBindingRef.new()` creates the fresh hidden correlation value required by intro attachment binding, while the
270
- result still records that the Provider does not verify the uploader (PVD-026).
327
+ transaction. Metadata create returns the Provider template ID plus an optional observed summary; copy and share
328
+ return `None` because the Provider supplies no new identity or per-recipient rows. Group addition and market
329
+ acquisition return the observed relation or `None`, while removal returns readback-confirmed absence as `bool`.
330
+ Direct-share, market-acquisition, and group relation refs remain distinct. Marketplace revision and uploader-binding
331
+ limitations are documented operation semantics rather than constant fields on every result (PVD-021, PVD-026).
332
+ `UploadBindingRef.new()` creates the fresh hidden correlation value required by intro attachment binding.
271
333
 
272
334
  Extended record reads stay behind the same `records` interface:
273
335
 
@@ -302,8 +364,8 @@ imported = await client.records.import_dataset(
302
364
  )
303
365
  ```
304
366
 
305
- A caller-supplied blank-record UID is read back; when the Provider generates it, the result explicitly has no
306
- invented identity. Import validates the complete batch with generated wire models but cannot map returned database
367
+ A caller-supplied blank-record UID returns its matching readback; when the Provider generates it, create returns
368
+ `None` rather than inventing identity. Import validates the complete batch with generated wire models but cannot map returned database
307
369
  IDs to individual inputs or promise atomicity (PCG-008). Delete means moving records into the recycle bin, not
308
370
  permanent deletion. Delete and restore results classify only post-write observations; restore never treats an active
309
371
  row with the same UID but a different database ID as proof of success (PCG-005). Record mutations are not
@@ -313,19 +375,21 @@ Record patching exposes a deliberately narrower capability than the raw Provider
313
375
  cannot smuggle Provider-native attachment strings; staged attachments use separate form-removal, table-replacement,
314
376
  files append/replace/remove, and rich-text types. Unsafe form replacement and table-file removal are absent, while a
315
377
  files/images removal is rejected when the observed module contains the same hash more than once (PVD-014 through
316
- PVD-016). `Record.content_sha256` can be supplied as a client-side precondition, but results explicitly label it as
317
- advisory read-before-write rather than Provider CAS. Database, active-browser, and unclassified acknowledgements
378
+ PVD-016). `Record.content_sha256` can be supplied as a client-side precondition, documented as advisory
379
+ read-before-write rather than Provider CAS. `RecordUpdateResult` retains only the before/after evidence and whether
380
+ the acknowledgement means persisted, pending browser save, or unclassified. Database, active-browser, and unclassified acknowledgements
318
381
  remain distinct, and mutation retries stay disabled.
319
382
 
320
383
  Relation addition reads both endpoints and checks their resolved data server before writing; this reduces PVD-019
321
- risk but is not an atomic Provider authorization guarantee. Relation deletion refuses an observed cross-notebook
322
- target-ID collision because the Provider ignores target notebook identity (PVD-020), then reports readback and any
323
- other relation that disappeared without inventing an affected-row count.
384
+ risk but is not an atomic Provider authorization guarantee. Relation addition returns matching relations observed
385
+ afterward. Relation deletion refuses an observed cross-notebook target-ID collision because the Provider ignores
386
+ target notebook identity (PVD-020), then returns the complete post-write relation snapshot.
324
387
 
325
388
  Comment upload follows the Provider's literal one-request `upload` field, not the incompatible Front fragment
326
- protocol (PVD-037). Comment creation derives an ID only from a unique post-write observation. Edit and delete require
327
- a currently observed caller-owned comment and read it back, containing the Provider's edit false-success behavior
328
- (PVD-004). Staged comment attachments have no Contract abort operation, and binding remains affected by PVD-026.
389
+ protocol (PVD-037). Comment create/update return matching post-write comment rows; delete returns whether the selected
390
+ comment is absent. Edit and delete require a currently observed caller-owned comment and read it back, containing the
391
+ Provider's edit false-success behavior (PVD-004). Staged comment attachments have no Contract abort operation, and
392
+ binding remains affected by PVD-026.
329
393
 
330
394
  Attachment bytes are streamed and must be consumed or closed explicitly:
331
395
 
@@ -365,7 +429,8 @@ For multiple fragments, pass `StagedFileFragment.session` into the next call. `n
365
429
  caller-side total derived from declared fragment sizes; the Provider does not confirm an offset. A final result
366
430
  contains the Provider hash, size, temporary row identity and fresh hidden binding value, but does not claim that a
367
431
  later literature/cloud operation checks the uploader or consumes the file exactly once. `uploads.abort` exposes the
368
- Provider's legacy code-2 cancellation signal as compensating cleanup that is not independently verified (PVD-028).
432
+ Provider's legacy code-2 cancellation signal as a `None`-returning compensating cleanup that is not independently
433
+ verified (PVD-028).
369
434
  Staging mutations are never automatically retried.
370
435
 
371
436
  Literature identities distinguish the personal library, shared libraries and pending incoming copies:
@@ -381,18 +446,15 @@ schema = await client.literature.creation_schema()
381
446
  if schema.metadata_extraction_available:
382
447
  candidates = await client.literature.extract_metadata(DoiMetadataSource("10.0000/example"))
383
448
 
384
- created = await client.literature.create(
385
- LiteratureMetadata(title="Example import", doi="10.0000/example"), staged_pdf=staged
386
- )
387
- assert created.created_item is None
449
+ await client.literature.create(LiteratureMetadata(title="Example import", doi="10.0000/example"), staged_pdf=staged)
388
450
  ```
389
451
 
390
- Create never guesses the new item from list position because the Provider returns no ID. Canonical update reads the
452
+ Create returns `None` and never guesses the new item from list position because the Provider returns no ID. Canonical update reads the
391
453
  item first and refuses to drop source/hidden fields unless `allow_source_metadata_loss=True` is explicit (PVD-027).
392
454
  PDF replace/delete are separate, read-back-verified mutations and are not presented as atomic with metadata
393
- (PCG-010). Permanent personal deletion is named `permanently_delete`, reports snapshot verification, and is marked
394
- non-recoverable. Sharing requires list-observed item summaries, user-search summaries and a resolved caller identity;
395
- its result deliberately contains no invented per-recipient IDs or batch atomicity (PVD-012, PVD-036).
455
+ (PCG-010). Permanent personal deletion is named `permanently_delete` and returns snapshot-confirmed absence as a
456
+ `bool`; the operation is non-recoverable. Sharing requires list-observed item summaries, user-search summaries and a
457
+ resolved caller identity, then returns `None` because the Provider supplies no per-recipient IDs (PVD-012, PVD-036).
396
458
 
397
459
  Literature comments use one public save intent: detail is read first, an existing caller-owned comment is edited, and
398
460
  otherwise a comment is created. Multiple caller-owned comments are rejected as ambiguous (PVD-035). A staged
@@ -409,11 +471,11 @@ from matelab import CloudFolderMetadata
409
471
 
410
472
  listing = await client.cloud_drive.list()
411
473
  folder_result = await client.cloud_drive.create_folder(CloudFolderMetadata(name="Example data"))
412
- bound = await client.cloud_drive.bind_staged_file(staged, target=folder_result.folder)
474
+ matches = await client.cloud_drive.bind_staged_file(staged, target=folder_result.folder)
413
475
 
414
- if bound.observed is not None:
476
+ if len(matches) == 1:
415
477
  renamed = await client.cloud_drive.update_file(
416
- bound.observed, filename="example-renamed.pdf", description="Sanitized description"
478
+ matches[0], filename="example-renamed.pdf", description="Sanitized description"
417
479
  )
418
480
  ```
419
481
 
@@ -422,12 +484,14 @@ than flattening them into one ambiguous collection. Folder browse results retain
422
484
  are explicitly root-wide and return `location=None` because the Provider omits each match's folder ID. Ordering has
423
485
  no stable ID tie-breaker (PCG-003, PVD-013).
424
486
 
425
- Folder create returns the Provider ID and all folder mutations read back the complete tree. Staged finalize accepts a
426
- completed `StagedFile`, then paginates the target folder and returns a final `CloudFileRef` only for one exact
427
- filename/hash/size observation. This is useful evidence, not an uploader-ownership guarantee: the Provider binds by
487
+ 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
489
+ exact filename/hash/size matches; zero, one, or multiple results preserve missing and ambiguous observations without
490
+ another result model. This is useful evidence, not an uploader-ownership guarantee: the Provider binds by
428
491
  temporary row ID without checking its owner (PVD-031), and finalize atomicity/idempotency remain absent (PCG-012).
429
- Batch move and permanent delete keep Provider item results as `None`, report only post-write observations and do not
430
- claim atomicity. Permanent deletion is named `permanently_delete_files` and marked non-recoverable. Cloud downloads
492
+ Batch move returns identities observed in the target, while permanent delete returns identities confirmed absent;
493
+ neither claims Provider per-item results or atomicity. Permanent deletion is named `permanently_delete_files` and is
494
+ non-recoverable. Cloud downloads
431
495
  resolve bytes from the final file identity and reuse `DownloadStream`, thumbnail/preview choices and the PVD-002-safe
432
496
  range subset. Cloud mutations are not automatically retried.
433
497
 
@@ -466,8 +530,8 @@ generates private component, operation-response, and parameter models. The proje
466
530
  object inheritance that the generator cannot otherwise preserve correctly; the checked-in release
467
531
  snapshot remains unchanged. `--check` performs the same validation and deterministic generation without
468
532
  writing the checked-in models. The current lock resolves `datamodel-code-generator 0.71.0` and
469
- `hatchling 1.31.0`. Published metadata requires `httpx>=0.27.2,<1` and
470
- `pydantic>=2.12.5,<3`; the build backend requires `hatchling>=1.27,<2`. These lower bounds are verified
533
+ `hatchling 1.31.0`. Published metadata requires `httpx2>=2.9.1,<3` and
534
+ `pydantic>=2.13.4,<3`; the build backend requires `hatchling>=1.27,<2`. These lower bounds are verified
471
535
  against the complete test suite on the supported Python boundary versions rather than inferred from
472
536
  `uv.lock`. The exact toolchain remains locked for development and release builds. Basedpyright and its
473
537
  Node wheel retain the compatible exact pair `basedpyright==1.39.9` and
@@ -475,47 +539,37 @@ Node wheel retain the compatible exact pair `basedpyright==1.39.9` and
475
539
 
476
540
  ## Opt-in Provider consumer smoke
477
541
 
478
- `tests/provider/test_provider_smoke.py` exercises the consumer flow through only the public SDK interface:
479
- credential authentication, extraction of the returned token pair, exchange through a new
480
- `AsyncMatelab.bind_external_tokens` instance, userid consistency checks, owned/shared notebook discovery,
481
- record listing, and current-record reading. It is not Provider Verification and is skipped by default.
482
-
483
- Prefer an explicitly isolated Provider. A production target requires explicit authorization for the current run
484
- and a dedicated test fixture. Authentication and external-token refresh persist Provider token state, and
485
- `GET /eln_items/item_view` writes a Provider audit entry even though it is a read operation. The smoke therefore
486
- requires explicit target and write acknowledgements:
542
+ `tests/provider/test_provider_smoke.py` exercises the consumer flow through only the public SDK interface. Its base
543
+ scenario covers credential authentication, extraction of the returned token pair, exchange through a new
544
+ `AsyncMatelab.bind_external_tokens` instance, userid consistency checks, and notebook discovery. It is not Provider
545
+ Verification and is skipped by default.
487
546
 
488
- ```dotenv
489
- MATELAB_PROVIDER_SMOKE=1
490
- MATELAB_PROVIDER_SMOKE_ISOLATED=1
491
- MATELAB_PROVIDER_SMOKE_ALLOW_WRITES=1
492
- ```
493
-
494
- For an explicitly authorized production test fixture, leave `MATELAB_PROVIDER_SMOKE_ISOLATED` empty and use:
547
+ Raw Provider conformance remains the responsibility of `matelab-spec`, which sends direct HTTP requests and validates
548
+ the unmodified responses. The SDK does not repeat its route-by-route, cross-account, sharing, or attachment-isolation
549
+ verification. Representative SDK adapter tests instead feed the pinned OpenAPI's sanitized response examples through
550
+ `MockTransport` and assert the resulting public values; synthetic fixtures remain where SDK-specific encoding,
551
+ error, retry, and compatibility boundaries require evidence beyond those examples.
495
552
 
496
- ```dotenv
497
- MATELAB_PROVIDER_SMOKE_PRODUCTION_ACKNOWLEDGED=1
498
- ```
553
+ Provider smoke is restricted to the confirmed isolated test service. Authentication and external-token refresh
554
+ persist Provider token state. This side effect is inherent to the tested Provider operations; it cannot be disabled
555
+ by a test setting. Explicitly loading `.env.test` and selecting the `provider` marker is the opt-in for this flow.
499
556
 
500
- Fill the git-ignored local `.env.test` with exactly one target acknowledgement and the fixture:
557
+ Copy `.env.example` to the git-ignored local `.env.test`, then fill in the shared Provider connection settings:
501
558
 
502
- - `MATELAB_PROVIDER_SMOKE_BASE_URL`
503
- - `MATELAB_PROVIDER_SMOKE_USERNAME`
504
- - `MATELAB_PROVIDER_SMOKE_PASSWORD`
505
- - `MATELAB_PROVIDER_SMOKE_EXPECTED_USERID`
506
- - `MATELAB_PROVIDER_SMOKE_NOTEBOOK_ID`
507
- - `MATELAB_PROVIDER_SMOKE_RECORD_DATABASE_ID`
508
- - `MATELAB_PROVIDER_SMOKE_RECORD_UID`
559
+ - `MATELAB_PROVIDER_BASE_URL`
560
+ - `MATELAB_PROVIDER_USERNAME`
561
+ - `MATELAB_PROVIDER_PASSWORD`
509
562
 
510
- The userid and both record identifiers are checked before the record read so an ambiguous or incorrect
511
- fixture fails safely. With the environment prepared:
563
+ These names intentionally match `matelab-spec` Provider Verification. The isolated target may copy them from the spec
564
+ `.env` into this repository's `.env.test`. The identities returned by authentication and external token binding must
565
+ agree. With the environment prepared:
512
566
 
513
567
  ```bash
514
568
  uv run --env-file .env.test pytest -m provider tests/provider/test_provider_smoke.py
515
569
  ```
516
570
 
517
- The file is never loaded implicitly, so the normal test suite remains safely skipped. Never use production without
518
- explicit authorization for that run, and never commit its credentials.
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.
519
573
 
520
574
  ## Reproducible release build
521
575
 
@@ -533,6 +587,6 @@ uv run python scripts/check_release.py dist/release/*.whl dist/release/*.tar.gz
533
587
  ```
534
588
 
535
589
  Rebuilding the same commit with the same locked environment and `SOURCE_DATE_EPOCH` must produce
536
- byte-identical wheel and source distribution hashes. The release is bound to `matelab-spec v0.1.0`,
537
- commit `236795db1aa7a784ad9df936e9547e8d529be53f`, and OpenAPI SHA-256
538
- `62ca07dcd7a0b3f80eb381a0d1447316f69faa751bafb8ab7c6a751e619d8164`.
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`.