matelab-python-sdk 0.1.0a2__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 (62) hide show
  1. matelab_python_sdk-0.1.0a3/.env.example +6 -0
  2. {matelab_python_sdk-0.1.0a2 → matelab_python_sdk-0.1.0a3}/CHANGELOG.md +13 -0
  3. {matelab_python_sdk-0.1.0a2 → matelab_python_sdk-0.1.0a3}/PKG-INFO +76 -82
  4. {matelab_python_sdk-0.1.0a2 → matelab_python_sdk-0.1.0a3}/README.md +73 -79
  5. {matelab_python_sdk-0.1.0a2 → matelab_python_sdk-0.1.0a3}/docs/roadmap.md +35 -24
  6. {matelab_python_sdk-0.1.0a2 → matelab_python_sdk-0.1.0a3}/pyproject.toml +4 -9
  7. {matelab_python_sdk-0.1.0a2 → matelab_python_sdk-0.1.0a3}/scripts/check_installed_package.py +1 -1
  8. {matelab_python_sdk-0.1.0a2 → matelab_python_sdk-0.1.0a3}/scripts/check_release.py +7 -3
  9. {matelab_python_sdk-0.1.0a2 → matelab_python_sdk-0.1.0a3}/src/matelab/__init__.py +0 -68
  10. {matelab_python_sdk-0.1.0a2 → matelab_python_sdk-0.1.0a3}/src/matelab/_transport.py +12 -12
  11. {matelab_python_sdk-0.1.0a2 → matelab_python_sdk-0.1.0a3}/src/matelab/client.py +17 -24
  12. {matelab_python_sdk-0.1.0a2 → matelab_python_sdk-0.1.0a3}/src/matelab/cloud_drive.py +48 -210
  13. {matelab_python_sdk-0.1.0a2 → matelab_python_sdk-0.1.0a3}/src/matelab/groups.py +3 -5
  14. matelab_python_sdk-0.1.0a2/src/matelab/literature.py → matelab_python_sdk-0.1.0a3/src/matelab/literature/__init__.py +64 -233
  15. {matelab_python_sdk-0.1.0a2 → matelab_python_sdk-0.1.0a3}/src/matelab/notebooks.py +33 -124
  16. matelab_python_sdk-0.1.0a2/src/matelab/records.py → matelab_python_sdk-0.1.0a3/src/matelab/records/__init__.py +100 -350
  17. {matelab_python_sdk-0.1.0a2 → matelab_python_sdk-0.1.0a3}/src/matelab/streaming.py +3 -3
  18. {matelab_python_sdk-0.1.0a2 → matelab_python_sdk-0.1.0a3}/src/matelab/templates.py +62 -188
  19. {matelab_python_sdk-0.1.0a2 → matelab_python_sdk-0.1.0a3}/src/matelab/uploads.py +13 -38
  20. {matelab_python_sdk-0.1.0a2 → matelab_python_sdk-0.1.0a3}/src/matelab/users.py +3 -6
  21. matelab_python_sdk-0.1.0a3/tests/conftest.py +58 -0
  22. matelab_python_sdk-0.1.0a3/tests/provider/test_provider_smoke.py +68 -0
  23. {matelab_python_sdk-0.1.0a2 → matelab_python_sdk-0.1.0a3}/tests/test_client.py +72 -80
  24. {matelab_python_sdk-0.1.0a2 → matelab_python_sdk-0.1.0a3}/tests/test_cloud_drive.py +55 -65
  25. {matelab_python_sdk-0.1.0a2 → matelab_python_sdk-0.1.0a3}/tests/test_comments_and_downloads.py +21 -22
  26. {matelab_python_sdk-0.1.0a2 → matelab_python_sdk-0.1.0a3}/tests/test_consumer_smoke.py +7 -7
  27. {matelab_python_sdk-0.1.0a2 → matelab_python_sdk-0.1.0a3}/tests/test_extended_records.py +51 -56
  28. {matelab_python_sdk-0.1.0a2 → matelab_python_sdk-0.1.0a3}/tests/test_groups.py +13 -14
  29. {matelab_python_sdk-0.1.0a2 → matelab_python_sdk-0.1.0a3}/tests/test_notebook_lifecycle.py +74 -88
  30. matelab_python_sdk-0.1.0a3/tests/test_notebooks.py +71 -0
  31. {matelab_python_sdk-0.1.0a2 → matelab_python_sdk-0.1.0a3}/tests/test_operation_coverage.py +2 -2
  32. {matelab_python_sdk-0.1.0a2 → matelab_python_sdk-0.1.0a3}/tests/test_record_lifecycle.py +61 -74
  33. {matelab_python_sdk-0.1.0a2 → matelab_python_sdk-0.1.0a3}/tests/test_record_mutations.py +109 -140
  34. {matelab_python_sdk-0.1.0a2 → matelab_python_sdk-0.1.0a3}/tests/test_records.py +82 -64
  35. {matelab_python_sdk-0.1.0a2 → matelab_python_sdk-0.1.0a3}/tests/test_release.py +36 -4
  36. {matelab_python_sdk-0.1.0a2 → matelab_python_sdk-0.1.0a3}/tests/test_streaming.py +41 -41
  37. {matelab_python_sdk-0.1.0a2 → matelab_python_sdk-0.1.0a3}/tests/test_template_lifecycle.py +96 -112
  38. {matelab_python_sdk-0.1.0a2 → matelab_python_sdk-0.1.0a3}/tests/test_templates.py +39 -40
  39. {matelab_python_sdk-0.1.0a2 → matelab_python_sdk-0.1.0a3}/tests/test_transport.py +87 -87
  40. {matelab_python_sdk-0.1.0a2 → matelab_python_sdk-0.1.0a3}/tests/test_uploads_and_literature.py +90 -106
  41. {matelab_python_sdk-0.1.0a2 → matelab_python_sdk-0.1.0a3}/tests/test_users.py +30 -31
  42. {matelab_python_sdk-0.1.0a2 → matelab_python_sdk-0.1.0a3}/uv.lock +27 -31
  43. matelab_python_sdk-0.1.0a2/tests/conftest.py +0 -20
  44. matelab_python_sdk-0.1.0a2/tests/provider/test_provider_smoke.py +0 -128
  45. matelab_python_sdk-0.1.0a2/tests/test_notebooks.py +0 -128
  46. {matelab_python_sdk-0.1.0a2 → matelab_python_sdk-0.1.0a3}/.github/workflows/release.yml +0 -0
  47. {matelab_python_sdk-0.1.0a2 → matelab_python_sdk-0.1.0a3}/.gitignore +0 -0
  48. {matelab_python_sdk-0.1.0a2 → matelab_python_sdk-0.1.0a3}/AGENTS.md +0 -0
  49. {matelab_python_sdk-0.1.0a2 → matelab_python_sdk-0.1.0a3}/LICENSE +0 -0
  50. {matelab_python_sdk-0.1.0a2 → matelab_python_sdk-0.1.0a3}/NOTICE +0 -0
  51. {matelab_python_sdk-0.1.0a2 → matelab_python_sdk-0.1.0a3}/contracts/matelab-integration-v1.lock.json +0 -0
  52. {matelab_python_sdk-0.1.0a2 → matelab_python_sdk-0.1.0a3}/contracts/matelab-integration-v1.openapi.yaml +0 -0
  53. {matelab_python_sdk-0.1.0a2 → matelab_python_sdk-0.1.0a3}/docs/operation-coverage.yaml +0 -0
  54. {matelab_python_sdk-0.1.0a2 → matelab_python_sdk-0.1.0a3}/scripts/generate_models.py +0 -0
  55. {matelab_python_sdk-0.1.0a2 → matelab_python_sdk-0.1.0a3}/src/matelab/_generated/__init__.py +0 -0
  56. {matelab_python_sdk-0.1.0a2 → matelab_python_sdk-0.1.0a3}/src/matelab/_generated/models.py +0 -0
  57. {matelab_python_sdk-0.1.0a2 → matelab_python_sdk-0.1.0a3}/src/matelab/errors.py +0 -0
  58. {matelab_python_sdk-0.1.0a2 → matelab_python_sdk-0.1.0a3}/src/matelab/models.py +0 -0
  59. {matelab_python_sdk-0.1.0a2 → matelab_python_sdk-0.1.0a3}/src/matelab/py.typed +0 -0
  60. {matelab_python_sdk-0.1.0a2 → matelab_python_sdk-0.1.0a3}/tests/test_generation.py +0 -0
  61. {matelab_python_sdk-0.1.0a2 → matelab_python_sdk-0.1.0a3}/tests/typing/consumer.py +0 -0
  62. {matelab_python_sdk-0.1.0a2 → 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=
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
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
+
3
16
  ## 0.1.0a2 - 2026-07-27
4
17
 
5
18
  - Adopts the immutable `matelab-spec v0.1.1` release.
@@ -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.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,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.0a3`. `[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.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.
@@ -284,8 +287,8 @@ groups = await client.groups.list()
284
287
  targets = await client.users.search("Example Researcher", scope=UserSearchScope.SAME_INSTITUTE)
285
288
  ```
286
289
 
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
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
289
292
  Provider returns members for an unstable first group and user search is unpaged, unordered, and not field-minimized
290
293
  (PVD-006, PVD-029, PCG-011).
291
294
 
@@ -294,17 +297,16 @@ Notebook create/update and direct sharing keep write acknowledgement separate fr
294
297
  ```python
295
298
  from matelab import NotebookMetadata, NotebookSharePermissionGrant
296
299
 
297
- created = await client.notebooks.create(NotebookMetadata(title="Example Notebook"))
300
+ await client.notebooks.create(NotebookMetadata(title="Example Notebook"))
298
301
  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
- )
302
+ updated = await client.notebooks.update_share(shares[0].ref, NotebookSharePermissionGrant(write=True, create=True))
302
303
  ```
303
304
 
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.
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.
308
310
 
309
311
  Template discovery keeps a template database identity separate from direct-share, market-acquisition, and group
310
312
  relation identities:
@@ -316,18 +318,18 @@ content = await client.templates.read(templates.owned[0].ref)
316
318
  ```
317
319
 
318
320
  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
321
+ value derived from the total; it does not claim a stable order or continuation token. Canonical
320
322
  modules are mapped to public `TemplateModule` values and retain additive module attributes. Template reads remain
321
323
  experimental because Provider discovery ordering/pagination and historical `images` compatibility are not fully
322
324
  stable (PCG-003, PCG-009, PVD-013, PVD-022).
323
325
 
324
326
  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).
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.
331
333
 
332
334
  Extended record reads stay behind the same `records` interface:
333
335
 
@@ -362,8 +364,8 @@ imported = await client.records.import_dataset(
362
364
  )
363
365
  ```
364
366
 
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
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
367
369
  IDs to individual inputs or promise atomicity (PCG-008). Delete means moving records into the recycle bin, not
368
370
  permanent deletion. Delete and restore results classify only post-write observations; restore never treats an active
369
371
  row with the same UID but a different database ID as proof of success (PCG-005). Record mutations are not
@@ -373,19 +375,21 @@ Record patching exposes a deliberately narrower capability than the raw Provider
373
375
  cannot smuggle Provider-native attachment strings; staged attachments use separate form-removal, table-replacement,
374
376
  files append/replace/remove, and rich-text types. Unsafe form replacement and table-file removal are absent, while a
375
377
  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
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
378
381
  remain distinct, and mutation retries stay disabled.
379
382
 
380
383
  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.
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.
384
387
 
385
388
  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.
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.
389
393
 
390
394
  Attachment bytes are streamed and must be consumed or closed explicitly:
391
395
 
@@ -425,7 +429,8 @@ For multiple fragments, pass `StagedFileFragment.session` into the next call. `n
425
429
  caller-side total derived from declared fragment sizes; the Provider does not confirm an offset. A final result
426
430
  contains the Provider hash, size, temporary row identity and fresh hidden binding value, but does not claim that a
427
431
  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).
432
+ Provider's legacy code-2 cancellation signal as a `None`-returning compensating cleanup that is not independently
433
+ verified (PVD-028).
429
434
  Staging mutations are never automatically retried.
430
435
 
431
436
  Literature identities distinguish the personal library, shared libraries and pending incoming copies:
@@ -441,18 +446,15 @@ schema = await client.literature.creation_schema()
441
446
  if schema.metadata_extraction_available:
442
447
  candidates = await client.literature.extract_metadata(DoiMetadataSource("10.0000/example"))
443
448
 
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
449
+ await client.literature.create(LiteratureMetadata(title="Example import", doi="10.0000/example"), staged_pdf=staged)
448
450
  ```
449
451
 
450
- 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
451
453
  item first and refuses to drop source/hidden fields unless `allow_source_metadata_loss=True` is explicit (PVD-027).
452
454
  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).
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).
456
458
 
457
459
  Literature comments use one public save intent: detail is read first, an existing caller-owned comment is edited, and
458
460
  otherwise a comment is created. Multiple caller-owned comments are rejected as ambiguous (PVD-035). A staged
@@ -469,11 +471,11 @@ from matelab import CloudFolderMetadata
469
471
 
470
472
  listing = await client.cloud_drive.list()
471
473
  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)
474
+ matches = await client.cloud_drive.bind_staged_file(staged, target=folder_result.folder)
473
475
 
474
- if bound.observed is not None:
476
+ if len(matches) == 1:
475
477
  renamed = await client.cloud_drive.update_file(
476
- bound.observed, filename="example-renamed.pdf", description="Sanitized description"
478
+ matches[0], filename="example-renamed.pdf", description="Sanitized description"
477
479
  )
478
480
  ```
479
481
 
@@ -482,12 +484,14 @@ than flattening them into one ambiguous collection. Folder browse results retain
482
484
  are explicitly root-wide and return `location=None` because the Provider omits each match's folder ID. Ordering has
483
485
  no stable ID tie-breaker (PCG-003, PVD-013).
484
486
 
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
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
488
491
  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
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
491
495
  resolve bytes from the final file identity and reuse `DownloadStream`, thumbnail/preview choices and the PVD-002-safe
492
496
  range subset. Cloud mutations are not automatically retried.
493
497
 
@@ -526,8 +530,8 @@ generates private component, operation-response, and parameter models. The proje
526
530
  object inheritance that the generator cannot otherwise preserve correctly; the checked-in release
527
531
  snapshot remains unchanged. `--check` performs the same validation and deterministic generation without
528
532
  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
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
531
535
  against the complete test suite on the supported Python boundary versions rather than inferred from
532
536
  `uv.lock`. The exact toolchain remains locked for development and release builds. Basedpyright and its
533
537
  Node wheel retain the compatible exact pair `basedpyright==1.39.9` and
@@ -535,47 +539,37 @@ Node wheel retain the compatible exact pair `basedpyright==1.39.9` and
535
539
 
536
540
  ## Opt-in Provider consumer smoke
537
541
 
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.
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.
542
546
 
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:
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.
547
552
 
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
- ```
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.
559
556
 
560
- 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:
561
558
 
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`
559
+ - `MATELAB_PROVIDER_BASE_URL`
560
+ - `MATELAB_PROVIDER_USERNAME`
561
+ - `MATELAB_PROVIDER_PASSWORD`
569
562
 
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:
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:
572
566
 
573
567
  ```bash
574
568
  uv run --env-file .env.test pytest -m provider tests/provider/test_provider_smoke.py
575
569
  ```
576
570
 
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.
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.
579
573
 
580
574
  ## Reproducible release build
581
575
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  Reusable async Python client for the Matelab Integration Contract.
4
4
 
5
- The current alpha is `0.1.0a2`. `[project].version` in `pyproject.toml` is the sole SDK version source;
5
+ The current alpha is `0.1.0a3`. `[project].version` in `pyproject.toml` is the sole SDK version source;
6
6
  `uv.lock` only mirrors that source.
7
7
 
8
8
  The SDK is pinned to the immutable `matelab-spec v0.1.1` Contract Release. The sole release pin is
@@ -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.0a2-py3-none-any.whl
34
+ python -m pip install dist/release/matelab_python_sdk-0.1.0a3-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
@@ -129,6 +129,9 @@ alice_client = AsyncMatelab(session=alice_session, http_client=shared_http_clien
129
129
  bob_client = AsyncMatelab(session=bob_session, http_client=shared_http_client)
130
130
  ```
131
131
 
132
+ An injected `http_client` must be an `httpx2.AsyncClient`. The independently distributed `httpx.AsyncClient` has
133
+ similar methods but uses incompatible request, response, transport, and exception types.
134
+
132
135
  `src/matelab/_generated` is a private wire layer. Applications should not depend on its file
133
136
  layout or generated class names. The distribution includes `py.typed`, so type checkers can consume the
134
137
  public annotations directly from an installed wheel.
@@ -258,8 +261,8 @@ groups = await client.groups.list()
258
261
  targets = await client.users.search("Example Researcher", scope=UserSearchScope.SAME_INSTITUTE)
259
262
  ```
260
263
 
261
- Both results explicitly report `provider_unspecified` ordering. Group members belong only to
262
- `groups.members_for`, not to every returned group. These two discovery interfaces are experimental because the
264
+ Ordering remains Provider-unspecified and is documented rather than repeated as a constant result field. Group
265
+ members belong only to `groups.members_for`, not to every returned group. These two discovery interfaces are experimental because the
263
266
  Provider returns members for an unstable first group and user search is unpaged, unordered, and not field-minimized
264
267
  (PVD-006, PVD-029, PCG-011).
265
268
 
@@ -268,17 +271,16 @@ Notebook create/update and direct sharing keep write acknowledgement separate fr
268
271
  ```python
269
272
  from matelab import NotebookMetadata, NotebookSharePermissionGrant
270
273
 
271
- created = await client.notebooks.create(NotebookMetadata(title="Example Notebook"))
274
+ await client.notebooks.create(NotebookMetadata(title="Example Notebook"))
272
275
  shares = await client.notebooks.share(notebook, [target.ref])
273
- updated = await client.notebooks.update_share(
274
- shares.visible_after_write[0].ref, NotebookSharePermissionGrant(write=True, create=True)
275
- )
276
+ updated = await client.notebooks.update_share(shares[0].ref, NotebookSharePermissionGrant(write=True, create=True))
276
277
  ```
277
278
 
278
- Create cannot return a notebook ref because the Provider returns no identity. Owned updates are read back by database
279
- ID and report matching, mismatched, or missing observations, containing PVD-003 false success without claiming an
280
- atomic guarantee. Share results similarly report post-write visibility rather than invented affected rows. A stored
281
- share mask of zero still has effective read access (PVD-010), and share-list order remains unspecified.
279
+ Create returns `None` because the Provider returns no identity. Owned updates return the notebook row observed by
280
+ database ID, or `None` when it is missing, containing PVD-003 false success without claiming an atomic guarantee.
281
+ Share returns only requested relations visible during readback, and permission updates return the observed relation
282
+ or `None`. A stored share mask of zero still has effective read access (PVD-010), and share-list order remains
283
+ unspecified.
282
284
 
283
285
  Template discovery keeps a template database identity separate from direct-share, market-acquisition, and group
284
286
  relation identities:
@@ -290,18 +292,18 @@ content = await client.templates.read(templates.owned[0].ref)
290
292
  ```
291
293
 
292
294
  The market result reports the Provider `total_count`, the requested and effective page sizes, and a `has_more`
293
- value explicitly marked as `derived_from_total`; it does not claim a stable order or continuation token. Canonical
295
+ value derived from the total; it does not claim a stable order or continuation token. Canonical
294
296
  modules are mapped to public `TemplateModule` values and retain additive module attributes. Template reads remain
295
297
  experimental because Provider discovery ordering/pagination and historical `images` compatibility are not fully
296
298
  stable (PCG-003, PCG-009, PVD-013, PVD-022).
297
299
 
298
300
  Template writes remain separate operations: metadata, canonical modules, and usage HTML are not presented as one
299
- transaction. Metadata create returns the Provider template ID; copy explicitly returns no new identity because the
300
- Provider supplies none. Direct-share, market-acquisition, and group relation refs are distinct and are required by
301
- their matching removal methods. Mutation results report readback observations without inventing affected rows.
302
- Metadata and intro results explicitly report that they do not advance the marketplace revision (PVD-021).
303
- `UploadBindingRef.new()` creates the fresh hidden correlation value required by intro attachment binding, while the
304
- result still records that the Provider does not verify the uploader (PVD-026).
301
+ transaction. Metadata create returns the Provider template ID plus an optional observed summary; copy and share
302
+ return `None` because the Provider supplies no new identity or per-recipient rows. Group addition and market
303
+ acquisition return the observed relation or `None`, while removal returns readback-confirmed absence as `bool`.
304
+ Direct-share, market-acquisition, and group relation refs remain distinct. Marketplace revision and uploader-binding
305
+ limitations are documented operation semantics rather than constant fields on every result (PVD-021, PVD-026).
306
+ `UploadBindingRef.new()` creates the fresh hidden correlation value required by intro attachment binding.
305
307
 
306
308
  Extended record reads stay behind the same `records` interface:
307
309
 
@@ -336,8 +338,8 @@ imported = await client.records.import_dataset(
336
338
  )
337
339
  ```
338
340
 
339
- A caller-supplied blank-record UID is read back; when the Provider generates it, the result explicitly has no
340
- invented identity. Import validates the complete batch with generated wire models but cannot map returned database
341
+ A caller-supplied blank-record UID returns its matching readback; when the Provider generates it, create returns
342
+ `None` rather than inventing identity. Import validates the complete batch with generated wire models but cannot map returned database
341
343
  IDs to individual inputs or promise atomicity (PCG-008). Delete means moving records into the recycle bin, not
342
344
  permanent deletion. Delete and restore results classify only post-write observations; restore never treats an active
343
345
  row with the same UID but a different database ID as proof of success (PCG-005). Record mutations are not
@@ -347,19 +349,21 @@ Record patching exposes a deliberately narrower capability than the raw Provider
347
349
  cannot smuggle Provider-native attachment strings; staged attachments use separate form-removal, table-replacement,
348
350
  files append/replace/remove, and rich-text types. Unsafe form replacement and table-file removal are absent, while a
349
351
  files/images removal is rejected when the observed module contains the same hash more than once (PVD-014 through
350
- PVD-016). `Record.content_sha256` can be supplied as a client-side precondition, but results explicitly label it as
351
- advisory read-before-write rather than Provider CAS. Database, active-browser, and unclassified acknowledgements
352
+ PVD-016). `Record.content_sha256` can be supplied as a client-side precondition, documented as advisory
353
+ read-before-write rather than Provider CAS. `RecordUpdateResult` retains only the before/after evidence and whether
354
+ the acknowledgement means persisted, pending browser save, or unclassified. Database, active-browser, and unclassified acknowledgements
352
355
  remain distinct, and mutation retries stay disabled.
353
356
 
354
357
  Relation addition reads both endpoints and checks their resolved data server before writing; this reduces PVD-019
355
- risk but is not an atomic Provider authorization guarantee. Relation deletion refuses an observed cross-notebook
356
- target-ID collision because the Provider ignores target notebook identity (PVD-020), then reports readback and any
357
- other relation that disappeared without inventing an affected-row count.
358
+ risk but is not an atomic Provider authorization guarantee. Relation addition returns matching relations observed
359
+ afterward. Relation deletion refuses an observed cross-notebook target-ID collision because the Provider ignores
360
+ target notebook identity (PVD-020), then returns the complete post-write relation snapshot.
358
361
 
359
362
  Comment upload follows the Provider's literal one-request `upload` field, not the incompatible Front fragment
360
- protocol (PVD-037). Comment creation derives an ID only from a unique post-write observation. Edit and delete require
361
- a currently observed caller-owned comment and read it back, containing the Provider's edit false-success behavior
362
- (PVD-004). Staged comment attachments have no Contract abort operation, and binding remains affected by PVD-026.
363
+ protocol (PVD-037). Comment create/update return matching post-write comment rows; delete returns whether the selected
364
+ comment is absent. Edit and delete require a currently observed caller-owned comment and read it back, containing the
365
+ Provider's edit false-success behavior (PVD-004). Staged comment attachments have no Contract abort operation, and
366
+ binding remains affected by PVD-026.
363
367
 
364
368
  Attachment bytes are streamed and must be consumed or closed explicitly:
365
369
 
@@ -399,7 +403,8 @@ For multiple fragments, pass `StagedFileFragment.session` into the next call. `n
399
403
  caller-side total derived from declared fragment sizes; the Provider does not confirm an offset. A final result
400
404
  contains the Provider hash, size, temporary row identity and fresh hidden binding value, but does not claim that a
401
405
  later literature/cloud operation checks the uploader or consumes the file exactly once. `uploads.abort` exposes the
402
- Provider's legacy code-2 cancellation signal as compensating cleanup that is not independently verified (PVD-028).
406
+ Provider's legacy code-2 cancellation signal as a `None`-returning compensating cleanup that is not independently
407
+ verified (PVD-028).
403
408
  Staging mutations are never automatically retried.
404
409
 
405
410
  Literature identities distinguish the personal library, shared libraries and pending incoming copies:
@@ -415,18 +420,15 @@ schema = await client.literature.creation_schema()
415
420
  if schema.metadata_extraction_available:
416
421
  candidates = await client.literature.extract_metadata(DoiMetadataSource("10.0000/example"))
417
422
 
418
- created = await client.literature.create(
419
- LiteratureMetadata(title="Example import", doi="10.0000/example"), staged_pdf=staged
420
- )
421
- assert created.created_item is None
423
+ await client.literature.create(LiteratureMetadata(title="Example import", doi="10.0000/example"), staged_pdf=staged)
422
424
  ```
423
425
 
424
- Create never guesses the new item from list position because the Provider returns no ID. Canonical update reads the
426
+ Create returns `None` and never guesses the new item from list position because the Provider returns no ID. Canonical update reads the
425
427
  item first and refuses to drop source/hidden fields unless `allow_source_metadata_loss=True` is explicit (PVD-027).
426
428
  PDF replace/delete are separate, read-back-verified mutations and are not presented as atomic with metadata
427
- (PCG-010). Permanent personal deletion is named `permanently_delete`, reports snapshot verification, and is marked
428
- non-recoverable. Sharing requires list-observed item summaries, user-search summaries and a resolved caller identity;
429
- its result deliberately contains no invented per-recipient IDs or batch atomicity (PVD-012, PVD-036).
429
+ (PCG-010). Permanent personal deletion is named `permanently_delete` and returns snapshot-confirmed absence as a
430
+ `bool`; the operation is non-recoverable. Sharing requires list-observed item summaries, user-search summaries and a
431
+ resolved caller identity, then returns `None` because the Provider supplies no per-recipient IDs (PVD-012, PVD-036).
430
432
 
431
433
  Literature comments use one public save intent: detail is read first, an existing caller-owned comment is edited, and
432
434
  otherwise a comment is created. Multiple caller-owned comments are rejected as ambiguous (PVD-035). A staged
@@ -443,11 +445,11 @@ from matelab import CloudFolderMetadata
443
445
 
444
446
  listing = await client.cloud_drive.list()
445
447
  folder_result = await client.cloud_drive.create_folder(CloudFolderMetadata(name="Example data"))
446
- bound = await client.cloud_drive.bind_staged_file(staged, target=folder_result.folder)
448
+ matches = await client.cloud_drive.bind_staged_file(staged, target=folder_result.folder)
447
449
 
448
- if bound.observed is not None:
450
+ if len(matches) == 1:
449
451
  renamed = await client.cloud_drive.update_file(
450
- bound.observed, filename="example-renamed.pdf", description="Sanitized description"
452
+ matches[0], filename="example-renamed.pdf", description="Sanitized description"
451
453
  )
452
454
  ```
453
455
 
@@ -456,12 +458,14 @@ than flattening them into one ambiguous collection. Folder browse results retain
456
458
  are explicitly root-wide and return `location=None` because the Provider omits each match's folder ID. Ordering has
457
459
  no stable ID tie-breaker (PCG-003, PVD-013).
458
460
 
459
- Folder create returns the Provider ID and all folder mutations read back the complete tree. Staged finalize accepts a
460
- completed `StagedFile`, then paginates the target folder and returns a final `CloudFileRef` only for one exact
461
- filename/hash/size observation. This is useful evidence, not an uploader-ownership guarantee: the Provider binds by
461
+ Folder create returns the Provider ID plus an optional observed folder, and all folder mutations read back the
462
+ complete tree. Staged finalize accepts a completed `StagedFile`, then paginates the target folder and returns all
463
+ exact filename/hash/size matches; zero, one, or multiple results preserve missing and ambiguous observations without
464
+ another result model. This is useful evidence, not an uploader-ownership guarantee: the Provider binds by
462
465
  temporary row ID without checking its owner (PVD-031), and finalize atomicity/idempotency remain absent (PCG-012).
463
- Batch move and permanent delete keep Provider item results as `None`, report only post-write observations and do not
464
- claim atomicity. Permanent deletion is named `permanently_delete_files` and marked non-recoverable. Cloud downloads
466
+ Batch move returns identities observed in the target, while permanent delete returns identities confirmed absent;
467
+ neither claims Provider per-item results or atomicity. Permanent deletion is named `permanently_delete_files` and is
468
+ non-recoverable. Cloud downloads
465
469
  resolve bytes from the final file identity and reuse `DownloadStream`, thumbnail/preview choices and the PVD-002-safe
466
470
  range subset. Cloud mutations are not automatically retried.
467
471
 
@@ -500,8 +504,8 @@ generates private component, operation-response, and parameter models. The proje
500
504
  object inheritance that the generator cannot otherwise preserve correctly; the checked-in release
501
505
  snapshot remains unchanged. `--check` performs the same validation and deterministic generation without
502
506
  writing the checked-in models. The current lock resolves `datamodel-code-generator 0.71.0` and
503
- `hatchling 1.31.0`. Published metadata requires `httpx>=0.27.2,<1` and
504
- `pydantic>=2.12.5,<3`; the build backend requires `hatchling>=1.27,<2`. These lower bounds are verified
507
+ `hatchling 1.31.0`. Published metadata requires `httpx2>=2.9.1,<3` and
508
+ `pydantic>=2.13.4,<3`; the build backend requires `hatchling>=1.27,<2`. These lower bounds are verified
505
509
  against the complete test suite on the supported Python boundary versions rather than inferred from
506
510
  `uv.lock`. The exact toolchain remains locked for development and release builds. Basedpyright and its
507
511
  Node wheel retain the compatible exact pair `basedpyright==1.39.9` and
@@ -509,47 +513,37 @@ Node wheel retain the compatible exact pair `basedpyright==1.39.9` and
509
513
 
510
514
  ## Opt-in Provider consumer smoke
511
515
 
512
- `tests/provider/test_provider_smoke.py` exercises the consumer flow through only the public SDK interface:
513
- credential authentication, extraction of the returned token pair, exchange through a new
514
- `AsyncMatelab.bind_external_tokens` instance, userid consistency checks, owned/shared notebook discovery,
515
- record listing, and current-record reading. It is not Provider Verification and is skipped by default.
516
+ `tests/provider/test_provider_smoke.py` exercises the consumer flow through only the public SDK interface. Its base
517
+ scenario covers credential authentication, extraction of the returned token pair, exchange through a new
518
+ `AsyncMatelab.bind_external_tokens` instance, userid consistency checks, and notebook discovery. It is not Provider
519
+ Verification and is skipped by default.
516
520
 
517
- Prefer an explicitly isolated Provider. A production target requires explicit authorization for the current run
518
- and a dedicated test fixture. Authentication and external-token refresh persist Provider token state, and
519
- `GET /eln_items/item_view` writes a Provider audit entry even though it is a read operation. The smoke therefore
520
- requires explicit target and write acknowledgements:
521
+ Raw Provider conformance remains the responsibility of `matelab-spec`, which sends direct HTTP requests and validates
522
+ the unmodified responses. The SDK does not repeat its route-by-route, cross-account, sharing, or attachment-isolation
523
+ verification. Representative SDK adapter tests instead feed the pinned OpenAPI's sanitized response examples through
524
+ `MockTransport` and assert the resulting public values; synthetic fixtures remain where SDK-specific encoding,
525
+ error, retry, and compatibility boundaries require evidence beyond those examples.
521
526
 
522
- ```dotenv
523
- MATELAB_PROVIDER_SMOKE=1
524
- MATELAB_PROVIDER_SMOKE_ISOLATED=1
525
- MATELAB_PROVIDER_SMOKE_ALLOW_WRITES=1
526
- ```
527
-
528
- For an explicitly authorized production test fixture, leave `MATELAB_PROVIDER_SMOKE_ISOLATED` empty and use:
529
-
530
- ```dotenv
531
- MATELAB_PROVIDER_SMOKE_PRODUCTION_ACKNOWLEDGED=1
532
- ```
527
+ Provider smoke is restricted to the confirmed isolated test service. Authentication and external-token refresh
528
+ persist Provider token state. This side effect is inherent to the tested Provider operations; it cannot be disabled
529
+ by a test setting. Explicitly loading `.env.test` and selecting the `provider` marker is the opt-in for this flow.
533
530
 
534
- Fill the git-ignored local `.env.test` with exactly one target acknowledgement and the fixture:
531
+ Copy `.env.example` to the git-ignored local `.env.test`, then fill in the shared Provider connection settings:
535
532
 
536
- - `MATELAB_PROVIDER_SMOKE_BASE_URL`
537
- - `MATELAB_PROVIDER_SMOKE_USERNAME`
538
- - `MATELAB_PROVIDER_SMOKE_PASSWORD`
539
- - `MATELAB_PROVIDER_SMOKE_EXPECTED_USERID`
540
- - `MATELAB_PROVIDER_SMOKE_NOTEBOOK_ID`
541
- - `MATELAB_PROVIDER_SMOKE_RECORD_DATABASE_ID`
542
- - `MATELAB_PROVIDER_SMOKE_RECORD_UID`
533
+ - `MATELAB_PROVIDER_BASE_URL`
534
+ - `MATELAB_PROVIDER_USERNAME`
535
+ - `MATELAB_PROVIDER_PASSWORD`
543
536
 
544
- The userid and both record identifiers are checked before the record read so an ambiguous or incorrect
545
- fixture fails safely. With the environment prepared:
537
+ These names intentionally match `matelab-spec` Provider Verification. The isolated target may copy them from the spec
538
+ `.env` into this repository's `.env.test`. The identities returned by authentication and external token binding must
539
+ agree. With the environment prepared:
546
540
 
547
541
  ```bash
548
542
  uv run --env-file .env.test pytest -m provider tests/provider/test_provider_smoke.py
549
543
  ```
550
544
 
551
- The file is never loaded implicitly, so the normal test suite remains safely skipped. Never use production without
552
- explicit authorization for that run, and never commit its credentials.
545
+ The file is never loaded implicitly, so the normal test suite remains safely skipped. Do not use this flow against
546
+ production, and never commit its credentials.
553
547
 
554
548
  ## Reproducible release build
555
549