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