matelab-python-sdk 0.1.0a3__tar.gz → 0.1.0a5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- matelab_python_sdk-0.1.0a5/CHANGELOG.md +61 -0
- {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/PKG-INFO +196 -48
- {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/README.md +195 -47
- {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/contracts/matelab-integration-v1.lock.json +3 -3
- {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/contracts/matelab-integration-v1.openapi.yaml +299 -16
- matelab_python_sdk-0.1.0a5/docs/attachment-write-contract-gaps.md +103 -0
- {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/docs/operation-coverage.yaml +1 -1
- {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/docs/roadmap.md +33 -7
- {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/pyproject.toml +1 -2
- {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/scripts/check_installed_package.py +2 -4
- {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/scripts/check_release.py +61 -22
- {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/scripts/generate_models.py +88 -3
- {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/src/matelab/__init__.py +39 -66
- {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/src/matelab/_generated/models.py +937 -761
- {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/src/matelab/_transport.py +89 -70
- matelab_python_sdk-0.1.0a5/src/matelab/_wire_validation.py +214 -0
- {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/src/matelab/client.py +32 -44
- matelab_python_sdk-0.1.0a5/src/matelab/domains/__init__.py +1 -0
- {matelab_python_sdk-0.1.0a3/src/matelab → matelab_python_sdk-0.1.0a5/src/matelab/domains}/cloud_drive.py +62 -116
- {matelab_python_sdk-0.1.0a3/src/matelab → matelab_python_sdk-0.1.0a5/src/matelab/domains}/groups.py +6 -2
- matelab_python_sdk-0.1.0a3/src/matelab/literature/__init__.py → matelab_python_sdk-0.1.0a5/src/matelab/domains/literature.py +95 -126
- {matelab_python_sdk-0.1.0a3/src/matelab → matelab_python_sdk-0.1.0a5/src/matelab/domains}/notebooks.py +84 -68
- matelab_python_sdk-0.1.0a3/src/matelab/records/__init__.py → matelab_python_sdk-0.1.0a5/src/matelab/domains/records.py +1030 -379
- {matelab_python_sdk-0.1.0a3/src/matelab → matelab_python_sdk-0.1.0a5/src/matelab/domains}/templates.py +76 -100
- {matelab_python_sdk-0.1.0a3/src/matelab → matelab_python_sdk-0.1.0a5/src/matelab/domains}/uploads.py +5 -14
- {matelab_python_sdk-0.1.0a3/src/matelab → matelab_python_sdk-0.1.0a5/src/matelab/domains}/users.py +9 -23
- {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/src/matelab/streaming.py +23 -5
- {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/tests/test_client.py +51 -1
- {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/tests/test_cloud_drive.py +6 -9
- {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/tests/test_comments_and_downloads.py +3 -3
- {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/tests/test_extended_records.py +9 -13
- {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/tests/test_generation.py +161 -1
- {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/tests/test_groups.py +22 -0
- {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/tests/test_notebook_lifecycle.py +31 -31
- {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/tests/test_operation_coverage.py +4 -1
- {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/tests/test_record_lifecycle.py +23 -12
- {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/tests/test_record_mutations.py +907 -108
- {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/tests/test_release.py +89 -12
- {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/tests/test_streaming.py +74 -4
- {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/tests/test_template_lifecycle.py +6 -6
- {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/tests/test_templates.py +12 -12
- {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/tests/test_transport.py +112 -66
- {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/tests/test_uploads_and_literature.py +45 -21
- {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/tests/test_users.py +10 -11
- matelab_python_sdk-0.1.0a5/tests/typing/consumer.py +83 -0
- {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/uv.lock +1 -1
- matelab_python_sdk-0.1.0a3/CHANGELOG.md +0 -34
- matelab_python_sdk-0.1.0a3/tests/typing/consumer.py +0 -47
- {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/.env.example +0 -0
- {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/.github/workflows/release.yml +0 -0
- {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/.gitignore +0 -0
- {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/AGENTS.md +0 -0
- {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/LICENSE +0 -0
- {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/NOTICE +0 -0
- {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/src/matelab/_generated/__init__.py +0 -0
- {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/src/matelab/errors.py +0 -0
- {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/src/matelab/py.typed +0 -0
- /matelab_python_sdk-0.1.0a3/src/matelab/models.py → /matelab_python_sdk-0.1.0a5/src/matelab/session.py +0 -0
- {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/tests/conftest.py +0 -0
- {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/tests/provider/test_provider_smoke.py +0 -0
- {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/tests/test_consumer_smoke.py +0 -0
- {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/tests/test_notebooks.py +0 -0
- {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/tests/test_records.py +0 -0
- {matelab_python_sdk-0.1.0a3 → matelab_python_sdk-0.1.0a5}/tests/typing/pyproject.toml +0 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0a5 - 2026-07-29
|
|
4
|
+
|
|
5
|
+
- Adopts immutable `matelab-spec v0.1.2` at commit `6dd674ac001ae61197495ace5eba489d585cb825` and OpenAPI
|
|
6
|
+
SHA-256 `7ccaf3116c8dcec7a6e49ff51fb1a5ecc83a140e013ec600516f5c2ffb3b5378`.
|
|
7
|
+
- Adds notebook-scoped staged finalizers for a null table file cell, an attachment-bearing appended table row,
|
|
8
|
+
files-module root append, and table/files occurrence replacement, with exact provenance, one-shot transport, and
|
|
9
|
+
canonical readback checks.
|
|
10
|
+
- Adds typed intents for an empty file table column and complete form file-field/table file-column deletion.
|
|
11
|
+
- Allows `records.update(..., module_deletions=...)` to delete a canonical module even when the current record read
|
|
12
|
+
observes attachments in it, verifies absence on persisted readback, and does not claim attachment-quote or byte
|
|
13
|
+
cleanup beyond the pinned Contract.
|
|
14
|
+
- Rejects cross-scope staged attachment handles before the record read, preserving the public static types while
|
|
15
|
+
avoiding the Provider's read-audit side effect for invalid runtime input.
|
|
16
|
+
- Keeps notebook-staged rich-text binding and attachment-bearing multi-record import explicitly unsupported because
|
|
17
|
+
v0.1.2 Provider Verification does not provide safe one-shot semantics for either.
|
|
18
|
+
|
|
19
|
+
## 0.1.0a4 - 2026-07-28
|
|
20
|
+
|
|
21
|
+
- Further reduces the alpha public surface from 150 to 126 symbols by accepting direct keyword arguments and existing
|
|
22
|
+
domain values instead of request-shaped wrapper dataclasses.
|
|
23
|
+
- Centralizes safe request-model validation, strengthens generated Contract constraints, and preserves structured field
|
|
24
|
+
diagnostics without exposing credentials, tokens, or sensitive payload values.
|
|
25
|
+
- Tightens Session replacement, refresh races, multipart retry safety, streaming cleanup, and exact HTTP success
|
|
26
|
+
handling while retaining stable transport, authentication, Provider, protocol, usage, and verification errors.
|
|
27
|
+
- Collects all business capabilities under `matelab.domains`, keeps client/session/error/transport primitives at the
|
|
28
|
+
package root, and hardens generation and release-artifact audits.
|
|
29
|
+
|
|
30
|
+
## 0.1.0a3 - 2026-07-28
|
|
31
|
+
|
|
32
|
+
- Replaces HTTPX with HTTPX2 2.9.1 and raises the Pydantic compatibility floor to 2.13.4. Injected clients must now
|
|
33
|
+
be `httpx2.AsyncClient` instances; HTTPX and HTTPX2 objects are not interchangeable. Default TLS verification now
|
|
34
|
+
uses the operating system trust store.
|
|
35
|
+
- Shrinks the alpha public surface from 184 to 150 symbols by returning existing domain values, tuples, booleans, or
|
|
36
|
+
`None` from simple mutations instead of defining endpoint-shaped result dataclasses. Contract invariants remain
|
|
37
|
+
documented operation semantics rather than repeated constant fields.
|
|
38
|
+
- Reorganizes the record and literature implementations into domain packages and keeps generated wire imports behind
|
|
39
|
+
one private namespace without changing the generated-model boundary.
|
|
40
|
+
- Aligns the opt-in SDK Provider smoke with the isolated test service's shared `MATELAB_PROVIDER_*` environment
|
|
41
|
+
variables and adds a safe `.env.example`.
|
|
42
|
+
|
|
43
|
+
## 0.1.0a2 - 2026-07-27
|
|
44
|
+
|
|
45
|
+
- Adopts the immutable `matelab-spec v0.1.1` release.
|
|
46
|
+
- Adds notebook-scoped, uploader-bound record attachment staging before a target record exists.
|
|
47
|
+
- Finalizes staged attachments through a single-record import or one new form-file-field update without exposing
|
|
48
|
+
Provider-native attachment syntax or private transport details.
|
|
49
|
+
- Enforces exact notebook/user provenance and one finalization attempt per staged handle in one SDK client.
|
|
50
|
+
|
|
51
|
+
## 0.1.0a1 - 2026-07-27
|
|
52
|
+
|
|
53
|
+
First public alpha candidate of the asynchronous Matelab Integration Contract SDK.
|
|
54
|
+
|
|
55
|
+
- Binds the immutable `matelab-spec v0.1.0` release.
|
|
56
|
+
- Exposes all 71 pinned Contract operations through public domain interfaces: 15 are stable and 56 remain
|
|
57
|
+
experimental.
|
|
58
|
+
- Provides instance-owned immutable Sessions, bounded token refresh, typed public models, streaming downloads,
|
|
59
|
+
and generated private wire validation.
|
|
60
|
+
- Ships as a typed package for Python 3.11 through 3.14.
|
|
61
|
+
- 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.
|
|
3
|
+
Version: 0.1.0a5
|
|
4
4
|
Summary: Reusable async Python client for the Matelab Integration Contract
|
|
5
5
|
Author-email: 朱天念 <zhutiannian@gmail.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -28,10 +28,10 @@ Description-Content-Type: text/markdown
|
|
|
28
28
|
|
|
29
29
|
Reusable async Python client for the Matelab Integration Contract.
|
|
30
30
|
|
|
31
|
-
The current alpha is `0.1.
|
|
31
|
+
The current alpha is `0.1.0a5`. `[project].version` in `pyproject.toml` is the sole SDK version source;
|
|
32
32
|
`uv.lock` only mirrors that source.
|
|
33
33
|
|
|
34
|
-
The SDK is pinned to the immutable `matelab-spec v0.1.
|
|
34
|
+
The SDK is pinned to the immutable `matelab-spec v0.1.2` Contract Release. The sole release pin is
|
|
35
35
|
`contracts/matelab-integration-v1.lock.json`, which records
|
|
36
36
|
the source tag, commit, OpenAPI path, local snapshot path, and SHA-256.
|
|
37
37
|
|
|
@@ -57,7 +57,7 @@ To test the same artifact a downstream Consumer will install, build and install
|
|
|
57
57
|
|
|
58
58
|
```bash
|
|
59
59
|
uv build --no-build-isolation --out-dir dist/release
|
|
60
|
-
python -m pip install dist/release/matelab_python_sdk-0.1.
|
|
60
|
+
python -m pip install dist/release/matelab_python_sdk-0.1.0a5-py3-none-any.whl
|
|
61
61
|
```
|
|
62
62
|
|
|
63
63
|
Do not infer Provider compatibility from the SDK version alone. A release is also bound to the Contract
|
|
@@ -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
|
|
@@ -176,7 +194,7 @@ notebook selector used for upload:
|
|
|
176
194
|
```python
|
|
177
195
|
import hashlib
|
|
178
196
|
|
|
179
|
-
from matelab import RecordImportItem,
|
|
197
|
+
from matelab import RecordImportItem, TemplateRef
|
|
180
198
|
|
|
181
199
|
content = b"measurement data"
|
|
182
200
|
staged = await client.records.stage_attachment(
|
|
@@ -188,7 +206,8 @@ staged = await client.records.stage_attachment(
|
|
|
188
206
|
)
|
|
189
207
|
result = await client.records.import_dataset(
|
|
190
208
|
notebook=notebook,
|
|
191
|
-
template=
|
|
209
|
+
template=TemplateRef(template_id=8),
|
|
210
|
+
template_title="Example Template",
|
|
192
211
|
items=(
|
|
193
212
|
RecordImportItem(
|
|
194
213
|
record_uid="REC-IMPORT-001",
|
|
@@ -199,38 +218,163 @@ result = await client.records.import_dataset(
|
|
|
199
218
|
)
|
|
200
219
|
```
|
|
201
220
|
|
|
202
|
-
The same staged handle may instead be consumed by one safe update
|
|
203
|
-
module:
|
|
221
|
+
The same staged handle may instead be consumed by one safe update finalizer. For example, add a new file field to an
|
|
222
|
+
existing form module:
|
|
204
223
|
|
|
205
224
|
```python
|
|
206
|
-
from matelab import RecordFormAttachmentFieldAddition
|
|
225
|
+
from matelab import RecordFormAttachmentFieldAddition
|
|
207
226
|
|
|
208
227
|
# Alternative to the import above; do not run both with the same staged handle.
|
|
209
228
|
result = await client.records.update(
|
|
210
229
|
source,
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
),
|
|
218
|
-
)
|
|
230
|
+
attachment_changes=(
|
|
231
|
+
RecordFormAttachmentFieldAddition(
|
|
232
|
+
module="Attachments",
|
|
233
|
+
name="Measurement",
|
|
234
|
+
attachment=staged,
|
|
235
|
+
),
|
|
219
236
|
),
|
|
220
237
|
)
|
|
221
238
|
```
|
|
222
239
|
|
|
223
|
-
Choose exactly one finalizer. A staged name may occur once in either a single-record import or one update
|
|
224
|
-
|
|
225
|
-
attachment references, cross-user or cross-notebook
|
|
226
|
-
and a second finalization attempt through the same
|
|
227
|
-
|
|
228
|
-
|
|
240
|
+
Choose exactly one finalizer. A staged name may occur once in either a single-record import or one update operation;
|
|
241
|
+
do not reuse it after transport starts, even when the Provider outcome is unknown. A locally rejected target or
|
|
242
|
+
argument does not spend the handle. The SDK rejects raw Provider attachment references, cross-user or cross-notebook
|
|
243
|
+
handles, unsafe combinations, duplicate use in one request, and a second finalization attempt through the same
|
|
244
|
+
client. A Session imported without identity must first call `await client.resolve_identity()`.
|
|
245
|
+
|
|
246
|
+
The Provider supplies no staging status, abort, TTL, atomicity, idempotency, or retry guarantee. Integrations that
|
|
247
|
+
persist handles must durably claim their own `uploaded -> finalizing/indeterminate` transition before calling a
|
|
248
|
+
finalizer.
|
|
249
|
+
|
|
250
|
+
### Attachment-bearing record update boundary
|
|
251
|
+
|
|
252
|
+
The v0.1.2 Contract adds dedicated, verified notebook-staged update shapes. They remain public intent objects; callers
|
|
253
|
+
never construct Provider paths or attachment strings:
|
|
254
|
+
|
|
255
|
+
```python
|
|
256
|
+
from matelab import (
|
|
257
|
+
RecordFilesAttachmentRootAppend,
|
|
258
|
+
RecordTableFileCellSet,
|
|
259
|
+
RecordTableRowAttachmentAppend,
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
# Each example is a separate finalizer; never run several with the same handle.
|
|
263
|
+
await client.records.update(
|
|
264
|
+
source,
|
|
265
|
+
attachment_changes=(
|
|
266
|
+
RecordTableFileCellSet(
|
|
267
|
+
table="Measurements",
|
|
268
|
+
column="Evidence",
|
|
269
|
+
row=0,
|
|
270
|
+
attachment=staged,
|
|
271
|
+
),
|
|
272
|
+
),
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
await client.records.update(
|
|
276
|
+
source,
|
|
277
|
+
attachment_changes=(
|
|
278
|
+
RecordTableRowAttachmentAppend(
|
|
279
|
+
table="Measurements",
|
|
280
|
+
file_column="Evidence",
|
|
281
|
+
values={"Label": "Sample C"},
|
|
282
|
+
attachment=another_staged,
|
|
283
|
+
),
|
|
284
|
+
),
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
await client.records.update(
|
|
288
|
+
source,
|
|
289
|
+
attachment_changes=(
|
|
290
|
+
RecordFilesAttachmentRootAppend(
|
|
291
|
+
module="Files",
|
|
292
|
+
caption="Evidence",
|
|
293
|
+
attachment=third_staged,
|
|
294
|
+
),
|
|
295
|
+
),
|
|
296
|
+
)
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
`RecordTableFileCellSet` requires an existing file column and a cell whose immediate canonical value is exactly
|
|
300
|
+
`null`. The row-append intent addresses the new row by the row count from the SDK's immediate read and supports one
|
|
301
|
+
staged file column. Files append is root-only and requires a string caption.
|
|
302
|
+
|
|
303
|
+
Replacement starts with an occurrence returned by `records.read()`; applications must not fabricate a
|
|
304
|
+
`RecordAttachmentRef`:
|
|
305
|
+
|
|
306
|
+
```python
|
|
307
|
+
from matelab import RecordFilesAttachmentReplacement
|
|
308
|
+
|
|
309
|
+
record = await client.records.read(notebook=source.notebook, record=source.record)
|
|
310
|
+
existing = next(
|
|
311
|
+
attachment
|
|
312
|
+
for attachment in record.attachments
|
|
313
|
+
if attachment.location is not None and attachment.location.kind == "files_module"
|
|
314
|
+
)
|
|
315
|
+
await client.records.update(
|
|
316
|
+
source,
|
|
317
|
+
attachment_changes=(RecordFilesAttachmentReplacement(existing=existing, replacement=staged, caption=None),),
|
|
318
|
+
)
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
For a files occurrence, `caption=None` preserves the observed string caption (an observed null caption normalizes to
|
|
322
|
+
the required empty string). Canonical readback must preserve the Provider `uid` and folder. Table replacement requires
|
|
323
|
+
exactly one current attachment in the selected cell and uses `RecordTableFileAttachmentReplacement`. Both replacement
|
|
324
|
+
forms require the new hash to differ.
|
|
325
|
+
|
|
326
|
+
Row/index-based finalizers use an immediate SDK read and must not be called while a concurrent editor is known to be
|
|
327
|
+
active. The Provider offers no expected hash or revision. `expected_content_sha256` is only a client-side prewrite
|
|
328
|
+
check, not Provider compare-and-swap.
|
|
329
|
+
|
|
330
|
+
File-bearing structure changes do not consume staging:
|
|
331
|
+
|
|
332
|
+
```python
|
|
333
|
+
from matelab import (
|
|
334
|
+
RecordFormFileFieldDeletion,
|
|
335
|
+
RecordTableFileColumnAddition,
|
|
336
|
+
RecordTableFileColumnDeletion,
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
await client.records.update(
|
|
340
|
+
source,
|
|
341
|
+
file_structure_changes=(
|
|
342
|
+
RecordTableFileColumnAddition(table="Measurements", name="Additional evidence"),
|
|
343
|
+
RecordFormFileFieldDeletion(form="Attachments", name="Obsolete evidence"),
|
|
344
|
+
RecordTableFileColumnDeletion(table="Measurements", name="Old evidence"),
|
|
345
|
+
),
|
|
346
|
+
)
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
The SDK emits the v0.1.2 dedicated operations: a new file column omits wire `data` and must read back as `null` for
|
|
350
|
+
every existing row; whole form-field/table-column deletion uses a strict two-segment target. It does not expose a
|
|
351
|
+
three-segment delete as table-cell clearing.
|
|
352
|
+
|
|
353
|
+
Deleting a complete canonical module is also supported even when the current read observes attachments in it:
|
|
354
|
+
|
|
355
|
+
```python
|
|
356
|
+
result = await client.records.update(
|
|
357
|
+
source,
|
|
358
|
+
module_deletions=("Raw files",),
|
|
359
|
+
expected_content_sha256=record.content_sha256,
|
|
360
|
+
)
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
This requests only a canonical record-content mutation. Readback proves the module is absent; it does not prove
|
|
364
|
+
attachment-quote cleanup or byte deletion. Collaboration-pending has no readback and is not reported as persisted.
|
|
365
|
+
|
|
366
|
+
Notebook-staged rich-text binding remains unsupported. Provider Verification shows that `#file{name}` is stored as
|
|
367
|
+
plain text and that a hash-based URI can fall back to an existing quote, so neither is a one-shot staged finalizer.
|
|
368
|
+
Existing `RecordRichTextUpdate` remains legal only with record-scoped `StagedRecordAttachment`. Attachment-bearing
|
|
369
|
+
multi-record import is also forbidden; callers must split it into single-record finalizers.
|
|
370
|
+
|
|
371
|
+
See `docs/attachment-write-contract-gaps.md` in the source repository for the exact support matrix, integration
|
|
372
|
+
lifecycle requirements, and minimum Contract changes needed to expose the remaining operations.
|
|
229
373
|
|
|
230
374
|
## Implementation roadmap
|
|
231
375
|
|
|
232
376
|
`docs/roadmap.md` is the complete SDK-only execution plan. It assigns all 71
|
|
233
|
-
`matelab-spec v0.1.
|
|
377
|
+
`matelab-spec v0.1.2` operations to ordered work packages, defines the machine-readable coverage that
|
|
234
378
|
must be added, records Provider-risk gates, and specifies the final completion checks.
|
|
235
379
|
|
|
236
380
|
The SDK exposes all 71 Contract operations through public domain interfaces. It deliberately excludes MCP
|
|
@@ -295,11 +439,9 @@ Provider returns members for an unstable first group and user search is unpaged,
|
|
|
295
439
|
Notebook create/update and direct sharing keep write acknowledgement separate from what a readback can prove:
|
|
296
440
|
|
|
297
441
|
```python
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
await client.notebooks.create(NotebookMetadata(title="Example Notebook"))
|
|
442
|
+
await client.notebooks.create(title="Example Notebook")
|
|
301
443
|
shares = await client.notebooks.share(notebook, [target.ref])
|
|
302
|
-
updated = await client.notebooks.update_share(shares[0].ref,
|
|
444
|
+
updated = await client.notebooks.update_share(shares[0].ref, write=True, create=True)
|
|
303
445
|
```
|
|
304
446
|
|
|
305
447
|
Create returns `None` because the Provider returns no identity. Owned updates return the notebook row observed by
|
|
@@ -314,7 +456,7 @@ relation identities:
|
|
|
314
456
|
```python
|
|
315
457
|
templates = await client.templates.list()
|
|
316
458
|
market = await client.templates.search_market("calibration", page=1, page_size=20)
|
|
317
|
-
|
|
459
|
+
modules = await client.templates.read(templates.owned[0].ref)
|
|
318
460
|
```
|
|
319
461
|
|
|
320
462
|
The market result reports the Provider `total_count`, the requested and effective page sizes, and a `has_more`
|
|
@@ -334,12 +476,10 @@ limitations are documented operation semantics rather than constant fields on ev
|
|
|
334
476
|
Extended record reads stay behind the same `records` interface:
|
|
335
477
|
|
|
336
478
|
```python
|
|
337
|
-
from matelab import
|
|
479
|
+
from matelab import RecordLocator
|
|
338
480
|
|
|
339
481
|
exported = await client.records.export([RecordLocator(notebook=notebook, record=record)])
|
|
340
|
-
matches = await client.records.search(
|
|
341
|
-
notebooks=[notebook], extractions=[RecordFieldExtraction(alias="notes", path=("Notes",))]
|
|
342
|
-
)
|
|
482
|
+
matches = await client.records.search(notebooks=[notebook], extractions={"notes": ("Notes",)})
|
|
343
483
|
page = await client.records.page(notebook)
|
|
344
484
|
deleted = await client.records.recycle_bin(notebook)
|
|
345
485
|
relations = await client.records.relations(notebook=notebook, record=record)
|
|
@@ -354,12 +494,13 @@ remain unspecified, and no continuation token is invented.
|
|
|
354
494
|
Record creation keeps blank creation and structured import as separate capabilities:
|
|
355
495
|
|
|
356
496
|
```python
|
|
357
|
-
from matelab import RecordImportItem,
|
|
497
|
+
from matelab import RecordImportItem, TemplateRef
|
|
358
498
|
|
|
359
499
|
blank = await client.records.create_blank(notebook=notebook, title="Blank Record", record_uid="caller-generated-uid")
|
|
360
500
|
imported = await client.records.import_dataset(
|
|
361
501
|
notebook=notebook,
|
|
362
|
-
template=
|
|
502
|
+
template=TemplateRef(template_id=8),
|
|
503
|
+
template_title="Example Template",
|
|
363
504
|
items=[RecordImportItem(record_uid="import-uid", title="Imported", data={"Notes": "value"})],
|
|
364
505
|
)
|
|
365
506
|
```
|
|
@@ -397,7 +538,7 @@ Attachment bytes are streamed and must be consumed or closed explicitly:
|
|
|
397
538
|
from matelab import ByteRange
|
|
398
539
|
|
|
399
540
|
comments = await client.records.comments(notebook=notebook, record=record)
|
|
400
|
-
attachment = comments
|
|
541
|
+
attachment = comments[0].attachments[0]
|
|
401
542
|
async with await client.records.download_comment_attachment(attachment, byte_range=ByteRange.from_start(0)) as download:
|
|
402
543
|
async for chunk in download:
|
|
403
544
|
consume(chunk)
|
|
@@ -436,7 +577,7 @@ Staging mutations are never automatically retried.
|
|
|
436
577
|
Literature identities distinguish the personal library, shared libraries and pending incoming copies:
|
|
437
578
|
|
|
438
579
|
```python
|
|
439
|
-
from matelab import
|
|
580
|
+
from matelab import LiteratureMetadata
|
|
440
581
|
|
|
441
582
|
libraries = await client.literature.libraries()
|
|
442
583
|
page = await client.literature.list(libraries.personal.ref)
|
|
@@ -444,7 +585,7 @@ detail = await client.literature.read(page.items[0].ref)
|
|
|
444
585
|
schema = await client.literature.creation_schema()
|
|
445
586
|
|
|
446
587
|
if schema.metadata_extraction_available:
|
|
447
|
-
candidates = await client.literature.extract_metadata(
|
|
588
|
+
candidates = await client.literature.extract_metadata(doi="10.0000/example")
|
|
448
589
|
|
|
449
590
|
await client.literature.create(LiteratureMetadata(title="Example import", doi="10.0000/example"), staged_pdf=staged)
|
|
450
591
|
```
|
|
@@ -467,10 +608,8 @@ stable `ByteRange` subset.
|
|
|
467
608
|
The personal cloud-drive surface keeps root, folder, final file and temporary staging identities separate:
|
|
468
609
|
|
|
469
610
|
```python
|
|
470
|
-
from matelab import CloudFolderMetadata
|
|
471
|
-
|
|
472
611
|
listing = await client.cloud_drive.list()
|
|
473
|
-
folder_result = await client.cloud_drive.create_folder(
|
|
612
|
+
folder_result = await client.cloud_drive.create_folder(name="Example data")
|
|
474
613
|
matches = await client.cloud_drive.bind_staged_file(staged, target=folder_result.folder)
|
|
475
614
|
|
|
476
615
|
if len(matches) == 1:
|
|
@@ -479,13 +618,13 @@ if len(matches) == 1:
|
|
|
479
618
|
)
|
|
480
619
|
```
|
|
481
620
|
|
|
482
|
-
`CloudDriveListing` contains a typed file page, complete folder
|
|
621
|
+
`CloudDriveListing` contains a typed file page, complete folder snapshot, quota usage and personal-root permissions rather
|
|
483
622
|
than flattening them into one ambiguous collection. Folder browse results retain their location; filename searches
|
|
484
623
|
are explicitly root-wide and return `location=None` because the Provider omits each match's folder ID. Ordering has
|
|
485
624
|
no stable ID tie-breaker (PCG-003, PVD-013).
|
|
486
625
|
|
|
487
626
|
Folder create returns the Provider ID plus an optional observed folder, and all folder mutations read back the
|
|
488
|
-
complete
|
|
627
|
+
complete folder snapshot. Staged finalize accepts a completed `StagedFile`, then paginates the target folder and returns all
|
|
489
628
|
exact filename/hash/size matches; zero, one, or multiple results preserve missing and ambiguous observations without
|
|
490
629
|
another result model. This is useful evidence, not an uploader-ownership guarantee: the Provider binds by
|
|
491
630
|
temporary row ID without checking its owner (PVD-031), and finalize atomicity/idempotency remain absent (PCG-012).
|
|
@@ -568,8 +707,17 @@ agree. With the environment prepared:
|
|
|
568
707
|
uv run --env-file .env.test pytest -m provider tests/provider/test_provider_smoke.py
|
|
569
708
|
```
|
|
570
709
|
|
|
571
|
-
|
|
572
|
-
|
|
710
|
+
This command runs only the SDK public-interface smoke; it is not the 71-operation Provider Verification. To reuse the
|
|
711
|
+
same `.env.test` for the complete Contract suite, also populate the optional share user, secondary account, record
|
|
712
|
+
staging opt-in, and Chat SSO settings documented in `.env.example`, then run from sibling checkouts:
|
|
713
|
+
|
|
714
|
+
```bash
|
|
715
|
+
cd ../matelab-spec
|
|
716
|
+
uv run --env-file ../matelab-python-sdk/.env.test pytest
|
|
717
|
+
```
|
|
718
|
+
|
|
719
|
+
The files are never loaded implicitly, so normal test runs remain safely skipped. Do not use either flow against
|
|
720
|
+
production, and never commit Provider credentials.
|
|
573
721
|
|
|
574
722
|
## Reproducible release build
|
|
575
723
|
|
|
@@ -587,6 +735,6 @@ uv run python scripts/check_release.py dist/release/*.whl dist/release/*.tar.gz
|
|
|
587
735
|
```
|
|
588
736
|
|
|
589
737
|
Rebuilding the same commit with the same locked environment and `SOURCE_DATE_EPOCH` must produce
|
|
590
|
-
byte-identical wheel and source distribution hashes. The release is bound to `matelab-spec v0.1.
|
|
591
|
-
commit `
|
|
592
|
-
`
|
|
738
|
+
byte-identical wheel and source distribution hashes. The release is bound to `matelab-spec v0.1.2`,
|
|
739
|
+
commit `6dd674ac001ae61197495ace5eba489d585cb825`, and OpenAPI SHA-256
|
|
740
|
+
`7ccaf3116c8dcec7a6e49ff51fb1a5ecc83a140e013ec600516f5c2ffb3b5378`.
|