matelab-python-sdk 0.1.0a1__py3-none-any.whl

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.
@@ -0,0 +1,538 @@
1
+ Metadata-Version: 2.4
2
+ Name: matelab-python-sdk
3
+ Version: 0.1.0a1
4
+ Summary: Reusable async Python client for the Matelab Integration Contract
5
+ Author-email: 朱天念 <zhutiannian@gmail.com>
6
+ License-Expression: Apache-2.0
7
+ License-File: LICENSE
8
+ License-File: NOTICE
9
+ Keywords: asyncio,electronic-lab-notebook,eln,matelab,sdk
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Framework :: AsyncIO
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Topic :: Scientific/Engineering
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.11
23
+ Requires-Dist: httpx<1,>=0.27.2
24
+ Requires-Dist: pydantic<3,>=2.12.5
25
+ Description-Content-Type: text/markdown
26
+
27
+ # matelab-python-sdk
28
+
29
+ Reusable async Python client for the Matelab Integration Contract.
30
+
31
+ The current alpha is `0.1.0a1`. `[project].version` in `pyproject.toml` is the sole SDK version source;
32
+ `uv.lock` only mirrors that source.
33
+
34
+ The SDK is pinned to the immutable `matelab-spec v0.1.0` Contract Release. The sole release pin is
35
+ `contracts/matelab-integration-v1.lock.json`, which records
36
+ the source tag, commit, OpenAPI path, local snapshot path, and SHA-256.
37
+
38
+ ## Installation
39
+
40
+ Python 3.11 or newer is required. Install the alpha from a package index with either:
41
+
42
+ ```bash
43
+ uv add matelab-python-sdk
44
+ ```
45
+
46
+ ```bash
47
+ python -m pip install matelab-python-sdk
48
+ ```
49
+
50
+ Development installs use the locked checkout:
51
+
52
+ ```bash
53
+ uv sync --frozen
54
+ ```
55
+
56
+ To test the same artifact a downstream Consumer will install, build and install the wheel:
57
+
58
+ ```bash
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
61
+ ```
62
+
63
+ Do not infer Provider compatibility from the SDK version alone. A release is also bound to the Contract
64
+ tag, commit, and checksum recorded below.
65
+
66
+ ## Design
67
+
68
+ The public module is intentionally small:
69
+
70
+ ```python
71
+ from matelab import AsyncMatelab
72
+
73
+ async with AsyncMatelab() as client:
74
+ session = await client.authenticate("user@example.org", "password")
75
+ assert client.session is session
76
+ notebooks = await client.notebooks.list()
77
+ notebook = notebooks.owned[0].ref
78
+ records = await client.records.list(notebook=notebook)
79
+ record = await client.records.read(notebook=notebook, record=records.records[0].ref)
80
+ ```
81
+
82
+ `AsyncMatelab()` uses `https://matelab.iphy.ac.cn/api` by default. Pass another Provider API root
83
+ explicitly when needed, for example `AsyncMatelab("https://custom.example/api")`.
84
+
85
+ ### Session ownership
86
+
87
+ Each `AsyncMatelab` instance owns at most one current, process-local `Session`. The SDK injects its bearer
88
+ token, refreshes it under a per-instance async lock, performs bounded safe retries, and exposes every token
89
+ rotation through `client.session`. If refresh succeeds but the subsequent business request fails,
90
+ `client.session` still contains the refreshed token pair.
91
+
92
+ | SDK responsibility | Integrator responsibility |
93
+ |---|---|
94
+ | Bearer injection, expiry checks, refresh and bounded retry | Redis/database/file persistence and encryption |
95
+ | Per-instance, in-process refresh serialization | Cross-process locking and conflict handling |
96
+ | Contract validation of token and identity responses | Mapping `userid`/`session_id` to a persisted `Session` |
97
+ | Latest immutable `Session` through `client.session` | Revocation, cleanup, and saving after each call |
98
+
99
+ `Session`, `Token`, and `Identity` are immutable value objects, and token values are excluded from their
100
+ representations. The SDK does not read tokens from environment variables and does not provide a session store.
101
+
102
+ Credential authentication installs the returned Session on the client:
103
+
104
+ ```python
105
+ async with AsyncMatelab() as client:
106
+ session = await client.authenticate(username, password)
107
+ assert client.session is session
108
+ ```
109
+
110
+ External token exchange validates the original access-token identity, refreshes the supplied refresh token,
111
+ validates the refreshed access-token identity, and rejects a userid mismatch. The existing client Session is
112
+ replaced only after all three steps succeed:
113
+
114
+ ```python
115
+ async with AsyncMatelab() as client:
116
+ session = await client.bind_external_tokens(access_token, refresh_token)
117
+ assert client.session is session
118
+ ```
119
+
120
+ Restore a previously validated Session by passing it to the constructor. Construction performs no network request:
121
+
122
+ ```python
123
+ persisted_session = await session_store.load(userid, session_id)
124
+
125
+ async with AsyncMatelab(session=persisted_session) as client:
126
+ result = await handle_request(client)
127
+ ```
128
+
129
+ A Web or MCP integration should create one client for one logical session, then save the latest Session in
130
+ `finally`, including when a business call fails after refresh:
131
+
132
+ ```python
133
+ persisted_session = await session_store.load(userid, session_id)
134
+ client = AsyncMatelab(session=persisted_session, http_client=shared_http_client)
135
+
136
+ try:
137
+ result = await handle_request(client)
138
+ finally:
139
+ latest_session = client.session
140
+ try:
141
+ if latest_session is not None:
142
+ await session_store.save(userid, session_id, latest_session)
143
+ finally:
144
+ await client.aclose()
145
+ ```
146
+
147
+ If several processes can use the same persisted session, the integration must place its own distributed lock
148
+ around load, use, and save. The SDK lock only coordinates refreshes inside one `AsyncMatelab` instance.
149
+
150
+ Different logical sessions require different clients. They may reuse the same externally managed HTTP connection
151
+ pool, but must never share one global `AsyncMatelab` singleton:
152
+
153
+ ```python
154
+ alice_client = AsyncMatelab(session=alice_session, http_client=shared_http_client)
155
+ bob_client = AsyncMatelab(session=bob_session, http_client=shared_http_client)
156
+ ```
157
+
158
+ `src/matelab/_generated` is a private wire layer. Applications should not depend on its file
159
+ layout or generated class names. The distribution includes `py.typed`, so type checkers can consume the
160
+ public annotations directly from an installed wheel.
161
+
162
+ The current public domain scope includes authentication, group/user discovery, template and notebook
163
+ lifecycle operations, record discovery/lifecycle operations, comment reads, and streaming record or
164
+ comment attachment downloads, resumable file staging, literature discovery/lifecycle workflows, and
165
+ personal cloud-drive management.
166
+
167
+ ## Implementation roadmap
168
+
169
+ `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
171
+ must be added, records Provider-risk gates, and specifies the final completion checks.
172
+
173
+ The SDK exposes all 71 Contract operations through public domain interfaces. It deliberately excludes MCP
174
+ migration, adjacent-repository changes, external
175
+ publishing, and automatic mutation against a real Provider.
176
+
177
+ Machine-readable status lives in
178
+ `docs/operation-coverage.yaml`. An exact-coverage test keeps its 71
179
+ operation IDs, methods, paths, work packages, and Provider issue references aligned with the pinned
180
+ OpenAPI snapshot.
181
+
182
+ | Domain | Implemented | Planned | Current public surface |
183
+ |---|---:|---:|---|
184
+ | Authentication | 4 | 0 | `authenticate`, `bind_external_tokens`, `refresh`, `resolve_identity`, `exchange_chat_sso_code` |
185
+ | Groups and users | 2 | 0 | `groups.list`, `users.search` |
186
+ | Notebooks | 6 | 0 | `notebooks.list/create/update/shares/share/update_share/unshare` |
187
+ | Records | 18 | 0 | Discovery, reads, lifecycle, typed patch/attachments, relations, and downloads |
188
+ | Comments | 5 | 0 | Read, staged attachment upload, create/update/delete, and download |
189
+ | Templates | 13 | 0 | Discovery, content, lifecycle, sharing, groups, and marketplace |
190
+ | File staging | 1 | 0 | Resumable fragment staging and compensating abort request |
191
+ | Literature | 13 | 0 | Libraries, items, canonical metadata, comments, sharing, PDF lifecycle and streaming |
192
+ | Cloud drive | 9 | 0 | Personal root/folders/files, staged binding, metadata, move/delete and streaming |
193
+ | **Total** | **71** | **0** | No operation is intentionally unexposed |
194
+
195
+ ### Stability and known capability limits
196
+
197
+ Coverage currently contains 15 `stable` and 56 `experimental` operations. The stable operation IDs are
198
+ `resolveCurrentIdentity`, `loginTokenSet`, `refreshTokenSet`, `exchangeChatSsoCode`,
199
+ `shareMultipleTemplatesWithUsers`, `removeTemplateFromGroup`, `deleteNotebookShare`, `listNotebooks`,
200
+ `listNotebookRecords`, `exportRecords`, `deleteRecordsByUid`, `copyRecord`, `readRecord`,
201
+ `deletePersonalLiteratureItem`, and `readLiteratureCreateTemplate`.
202
+
203
+ Every other implemented operation is explicitly `experimental`; the exact per-operation list and its
204
+ PVD/PCG references live in
205
+ `docs/operation-coverage.yaml`. There are no `intentionally_unexposed`
206
+ operations and no `planned` operations. Experimental support means the SDK validates and exposes the
207
+ pinned Contract while preserving limitations such as unstable ordering/pagination, incomplete mutation
208
+ acknowledgements, missing batch atomicity or idempotency, weak attachment ownership binding, and known
209
+ Provider authorization gaps. It does not turn those limitations into SDK guarantees.
210
+
211
+ Chat iframe SSO consumes a one-time code and shared key. Both arguments are treated as secrets, the request is never
212
+ automatically retried, and the returned token set is stored in the same in-memory `Session` shape as credential login:
213
+
214
+ ```python
215
+ session = await client.exchange_chat_sso_code(code="chat-sanitizedcode123", key="sanitized-shared-key")
216
+ ```
217
+
218
+ Group and user discovery expose sharing identities without inventing Provider pagination:
219
+
220
+ ```python
221
+ from matelab import UserSearchScope
222
+
223
+ groups = await client.groups.list()
224
+ targets = await client.users.search("Example Researcher", scope=UserSearchScope.SAME_INSTITUTE)
225
+ ```
226
+
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
229
+ Provider returns members for an unstable first group and user search is unpaged, unordered, and not field-minimized
230
+ (PVD-006, PVD-029, PCG-011).
231
+
232
+ Notebook create/update and direct sharing keep write acknowledgement separate from what a readback can prove:
233
+
234
+ ```python
235
+ from matelab import NotebookMetadata, NotebookSharePermissionGrant
236
+
237
+ created = await client.notebooks.create(NotebookMetadata(title="Example Notebook"))
238
+ 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
+ )
242
+ ```
243
+
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.
248
+
249
+ Template discovery keeps a template database identity separate from direct-share, market-acquisition, and group
250
+ relation identities:
251
+
252
+ ```python
253
+ templates = await client.templates.list()
254
+ market = await client.templates.search_market("calibration", page=1, page_size=20)
255
+ content = await client.templates.read(templates.owned[0].ref)
256
+ ```
257
+
258
+ 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
260
+ modules are mapped to public `TemplateModule` values and retain additive module attributes. Template reads remain
261
+ experimental because Provider discovery ordering/pagination and historical `images` compatibility are not fully
262
+ stable (PCG-003, PCG-009, PVD-013, PVD-022).
263
+
264
+ 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).
271
+
272
+ Extended record reads stay behind the same `records` interface:
273
+
274
+ ```python
275
+ from matelab import RecordFieldExtraction, RecordLocator
276
+
277
+ exported = await client.records.export([RecordLocator(notebook=notebook, record=record)])
278
+ matches = await client.records.search(
279
+ notebooks=[notebook], extractions=[RecordFieldExtraction(alias="notes", path=("Notes",))]
280
+ )
281
+ page = await client.records.page(notebook)
282
+ deleted = await client.records.recycle_bin(notebook)
283
+ relations = await client.records.relations(notebook=notebook, record=record)
284
+ ```
285
+
286
+ `records.page` fixes the legacy request to `page_size=0&default=1`, preventing the known owner-preference writes
287
+ described by PVD-039; its total is derived from the Provider's complete matching ID list. Public catalog records and
288
+ deleted records use identities distinct from active `RecordRef`. Relation targets separately expose declared and
289
+ resolved notebook IDs because the Provider may return dangling or incomplete identities. Search and relation order
290
+ remain unspecified, and no continuation token is invented.
291
+
292
+ Record creation keeps blank creation and structured import as separate capabilities:
293
+
294
+ ```python
295
+ from matelab import RecordImportItem, RecordImportTemplate, TemplateRef
296
+
297
+ blank = await client.records.create_blank(notebook=notebook, title="Blank Record", record_uid="caller-generated-uid")
298
+ imported = await client.records.import_dataset(
299
+ notebook=notebook,
300
+ template=RecordImportTemplate(template=TemplateRef(template_id=8), title="Example Template"),
301
+ items=[RecordImportItem(record_uid="import-uid", title="Imported", data={"Notes": "value"})],
302
+ )
303
+ ```
304
+
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
307
+ IDs to individual inputs or promise atomicity (PCG-008). Delete means moving records into the recycle bin, not
308
+ permanent deletion. Delete and restore results classify only post-write observations; restore never treats an active
309
+ row with the same UID but a different database ID as proof of success (PCG-005). Record mutations are not
310
+ automatically retried.
311
+
312
+ Record patching exposes a deliberately narrower capability than the raw Provider operation. Scalar/module changes
313
+ cannot smuggle Provider-native attachment strings; staged attachments use separate form-removal, table-replacement,
314
+ files append/replace/remove, and rich-text types. Unsafe form replacement and table-file removal are absent, while a
315
+ 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
318
+ remain distinct, and mutation retries stay disabled.
319
+
320
+ 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.
324
+
325
+ 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.
329
+
330
+ Attachment bytes are streamed and must be consumed or closed explicitly:
331
+
332
+ ```python
333
+ from matelab import ByteRange
334
+
335
+ comments = await client.records.comments(notebook=notebook, record=record)
336
+ attachment = comments.comments[0].attachments[0]
337
+ async with await client.records.download_comment_attachment(attachment, byte_range=ByteRange.from_start(0)) as download:
338
+ async for chunk in download:
339
+ consume(chunk)
340
+ ```
341
+
342
+ `DownloadStream` exposes status, content type, length, range, and disposition metadata without buffering the
343
+ complete file. Streams are not automatically replayed. `ByteRange` deliberately rejects `bytes=0-0` (PVD-002).
344
+ Comment attachment refs preserve the notebook/record/comment context where they were observed, but they are not
345
+ Provider authorization credentials: current Providers do not verify that association (PVD-038).
346
+
347
+ Cross-domain staging keeps resumable state and completed-file identity separate:
348
+
349
+ ```python
350
+ import hashlib
351
+
352
+ from matelab import StagedFile
353
+
354
+ pdf_bytes = b"sanitized PDF bytes"
355
+ staged = await client.uploads.stage(
356
+ pdf_bytes,
357
+ filename="example.pdf",
358
+ fragment_size=len(pdf_bytes),
359
+ complete_sha256=hashlib.sha256(pdf_bytes).hexdigest(),
360
+ )
361
+ assert isinstance(staged, StagedFile)
362
+ ```
363
+
364
+ For multiple fragments, pass `StagedFileFragment.session` into the next call. `next_offset` is explicitly a
365
+ caller-side total derived from declared fragment sizes; the Provider does not confirm an offset. A final result
366
+ contains the Provider hash, size, temporary row identity and fresh hidden binding value, but does not claim that a
367
+ 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).
369
+ Staging mutations are never automatically retried.
370
+
371
+ Literature identities distinguish the personal library, shared libraries and pending incoming copies:
372
+
373
+ ```python
374
+ from matelab import DoiMetadataSource, LiteratureMetadata
375
+
376
+ libraries = await client.literature.libraries()
377
+ page = await client.literature.list(libraries.personal.ref)
378
+ detail = await client.literature.read(page.items[0].ref)
379
+ schema = await client.literature.creation_schema()
380
+
381
+ if schema.metadata_extraction_available:
382
+ candidates = await client.literature.extract_metadata(DoiMetadataSource("10.0000/example"))
383
+
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
388
+ ```
389
+
390
+ Create never guesses the new item from list position because the Provider returns no ID. Canonical update reads the
391
+ item first and refuses to drop source/hidden fields unless `allow_source_metadata_loss=True` is explicit (PVD-027).
392
+ 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).
396
+
397
+ Literature comments use one public save intent: detail is read first, an existing caller-owned comment is edited, and
398
+ otherwise a comment is created. Multiple caller-owned comments are rejected as ambiguous (PVD-035). A staged
399
+ attachment can replace one `matelab-staged-file` marker; raw temporary URLs are rejected. These checks contain common
400
+ misuse but do not repair the Provider's cross-user UID lookup (PVD-026). Shared-library reads and writes remain
401
+ experimental because the Provider permission JOIN is not scoped to the current user (PVD-011); successful SDK calls
402
+ must not be treated as independent authorization proof. Literature PDF downloads reuse `DownloadStream` and the
403
+ stable `ByteRange` subset.
404
+
405
+ The personal cloud-drive surface keeps root, folder, final file and temporary staging identities separate:
406
+
407
+ ```python
408
+ from matelab import CloudFolderMetadata
409
+
410
+ listing = await client.cloud_drive.list()
411
+ 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)
413
+
414
+ if bound.observed is not None:
415
+ renamed = await client.cloud_drive.update_file(
416
+ bound.observed, filename="example-renamed.pdf", description="Sanitized description"
417
+ )
418
+ ```
419
+
420
+ `CloudDriveListing` contains a typed file page, complete folder tree, quota usage and personal-root permissions rather
421
+ than flattening them into one ambiguous collection. Folder browse results retain their location; filename searches
422
+ are explicitly root-wide and return `location=None` because the Provider omits each match's folder ID. Ordering has
423
+ no stable ID tie-breaker (PCG-003, PVD-013).
424
+
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
428
+ 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
431
+ resolve bytes from the final file identity and reuse `DownloadStream`, thumbnail/preview choices and the PVD-002-safe
432
+ range subset. Cloud mutations are not automatically retried.
433
+
434
+ To run the implementation as a persistent Codex goal, start a task in this repository and use:
435
+
436
+ > 完整阅读并严格遵循 `AGENTS.md`、`README.md` 和 `docs/roadmap.md`。创建并持续执行一个 goal:
437
+ > 只修改当前仓库,按照 roadmap 从第一个未完成 work package 开始,完成 71-operation 精确覆盖和全部
438
+ > SDK 领域 interface;每个 package 通过局部验证后自动继续,最终让 generation `--check`、Ruff、
439
+ > Ruff format、Basedpyright、Pytest 和 package build 全部通过。不要修改相邻仓库,不执行生产 Provider
440
+ > mutation,不 commit、push、tag 或发布。
441
+
442
+ Owned/shared `NotebookRef`, public `PublicNotebookRef`, `RecordRef`, and `RecordVersionRef` keep
443
+ Provider identifiers distinct. Historical reads first re-read the authorized current record and confirm
444
+ that the requested version is still present in its `modify_log`; both reads write Provider audit entries.
445
+
446
+ Errors are separated into Provider business errors, authentication errors, HTTP/transport errors,
447
+ Integration Contract response errors, and client-side usage errors. Token values and sensitive response
448
+ fields are redacted from error text and retained diagnostic payloads.
449
+
450
+ ## Development
451
+
452
+ ```bash
453
+ uv sync
454
+ uv run python scripts/generate_models.py
455
+ uv run python scripts/generate_models.py --check
456
+ uv run ruff check .
457
+ uv run ruff format --check .
458
+ uv run basedpyright
459
+ uv run pytest
460
+ uv build
461
+ ```
462
+
463
+ The generator first verifies the contract lock, OpenAPI release metadata, and snapshot digest. It then
464
+ creates a temporary OpenAPI 3.1 generation projection, resolves references without network access, and
465
+ generates private component, operation-response, and parameter models. The projection only flattens pure
466
+ object inheritance that the generator cannot otherwise preserve correctly; the checked-in release
467
+ snapshot remains unchanged. `--check` performs the same validation and deterministic generation without
468
+ 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
471
+ against the complete test suite on the supported Python boundary versions rather than inferred from
472
+ `uv.lock`. The exact toolchain remains locked for development and release builds. Basedpyright and its
473
+ Node wheel retain the compatible exact pair `basedpyright==1.39.9` and
474
+ `nodejs-wheel-binaries==22.20.0`.
475
+
476
+ ## Opt-in Provider consumer smoke
477
+
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:
487
+
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:
495
+
496
+ ```dotenv
497
+ MATELAB_PROVIDER_SMOKE_PRODUCTION_ACKNOWLEDGED=1
498
+ ```
499
+
500
+ Fill the git-ignored local `.env.test` with exactly one target acknowledgement and the fixture:
501
+
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`
509
+
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:
512
+
513
+ ```bash
514
+ uv run --env-file .env.test pytest -m provider tests/provider/test_provider_smoke.py
515
+ ```
516
+
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.
519
+
520
+ ## Reproducible release build
521
+
522
+ Build from a clean release commit (or its tag) and set the archive timestamp to that commit's
523
+ committer timestamp. `pyproject.toml` declares the supported Hatchling range, while `uv.lock` supplies the
524
+ exact version used by the frozen, no-build-isolation release environment:
525
+
526
+ ```bash
527
+ export SOURCE_DATE_EPOCH="$(git show -s --format=%ct HEAD)"
528
+ uv sync --frozen
529
+ uv run python scripts/generate_models.py --check
530
+ uv build --no-build-isolation --out-dir dist/release
531
+ uv run python scripts/check_release.py dist/release/*.whl dist/release/*.tar.gz
532
+ (cd dist/release && sha256sum *.whl *.tar.gz > SHA256SUMS)
533
+ ```
534
+
535
+ 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`.
@@ -0,0 +1,22 @@
1
+ matelab/__init__.py,sha256=3iX8xyk434Yh8j_tsinsgW1bnMwgrv_dqGQEIspSFf8,9741
2
+ matelab/_transport.py,sha256=qMaioZcP5RfIxl5rr2FzY2Gn_zHAa-vJbjDNxapii-4,20223
3
+ matelab/client.py,sha256=U54F-s8APVXppCjaMo-ALcQQCFM-VOzWoB_taeCdhXM,8168
4
+ matelab/cloud_drive.py,sha256=NDVobVI-3MDRbbN_M6uRgM4apI6RUglk3ZAEdeaP90g,33306
5
+ matelab/errors.py,sha256=7o5h5cjbGkUfpL-8W5hF9WaYJMlBH9xN8Uk2grSZwjg,1209
6
+ matelab/groups.py,sha256=w0OgFtWwBcQqFUCvO_p52uIxKch-Soh3lFIgm0MNj94,2471
7
+ matelab/literature.py,sha256=uMAXqDowj5JwSMed6b3koGHZBHF22GVqExO1kv2fmCY,51970
8
+ matelab/models.py,sha256=AuIlyERpIVUYF2DgIhtC7LsmG4hLpHz28ae4hvyrEik,655
9
+ matelab/notebooks.py,sha256=ykmpvziHGMtYt9vrMe97NSwcVTHsc4FRBStLNAhyDvc,19154
10
+ matelab/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
11
+ matelab/records.py,sha256=9HiFdu9c8VqlbgSYhayOOXxZBSZeOMMHbkNm0a0-Xd4,107223
12
+ matelab/streaming.py,sha256=KcRx8QiBpaH198OY3YKdIFyx3bAikNf1sGHXXUKvoU4,3056
13
+ matelab/templates.py,sha256=YpnCZ6WIMI7JEPYfdKNIzfuCMA9iwQx2qA473AvWpfk,31584
14
+ matelab/uploads.py,sha256=_H6WdE_hvPdJeWdRdeh-0g9pm8EdxdcKKsv6hPMxObc,10182
15
+ matelab/users.py,sha256=oU-FU0RNyNaSu6MBEL_2uP627AI7ZZWzQM7irwyJ1WY,2528
16
+ matelab/_generated/__init__.py,sha256=jmGbXLLe3JorsGU7OUdF8iKGewFMRqkCzgyFAw7C15w,79
17
+ matelab/_generated/models.py,sha256=uAsP0DYFGcanOxhn5-ISEXkoi_MDHhJylqwow-4MOBc,92551
18
+ matelab_python_sdk-0.1.0a1.dist-info/METADATA,sha256=YMcK5wX333aTqUf4QrNsRiUUrUHctCe79BJCHKjSjAQ,27968
19
+ matelab_python_sdk-0.1.0a1.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
20
+ matelab_python_sdk-0.1.0a1.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
21
+ matelab_python_sdk-0.1.0a1.dist-info/licenses/NOTICE,sha256=Hc4scCJTof5VL3JCPI2aaewnnzCejIHUGuBVYBf-klc,98
22
+ matelab_python_sdk-0.1.0a1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.31.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any