matelab-python-sdk 0.1.0a1__tar.gz → 0.1.0a2__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 (59) hide show
  1. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/CHANGELOG.md +8 -0
  2. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/PKG-INFO +68 -8
  3. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/README.md +67 -7
  4. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/contracts/matelab-integration-v1.lock.json +3 -3
  5. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/contracts/matelab-integration-v1.openapi.yaml +147 -16
  6. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/docs/operation-coverage.yaml +5 -5
  7. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/docs/roadmap.md +22 -7
  8. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/pyproject.toml +1 -1
  9. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/scripts/check_installed_package.py +1 -1
  10. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/scripts/check_release.py +1 -1
  11. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/src/matelab/__init__.py +4 -0
  12. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/src/matelab/_generated/models.py +24 -7
  13. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/src/matelab/records.py +294 -38
  14. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/tests/test_cloud_drive.py +1 -1
  15. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/tests/test_generation.py +1 -1
  16. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/tests/test_record_lifecycle.py +140 -1
  17. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/tests/test_record_mutations.py +273 -0
  18. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/tests/test_release.py +4 -2
  19. matelab_python_sdk-0.1.0a2/tests/typing/consumer.py +47 -0
  20. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/uv.lock +1 -1
  21. matelab_python_sdk-0.1.0a1/tests/typing/consumer.py +0 -7
  22. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/.github/workflows/release.yml +0 -0
  23. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/.gitignore +0 -0
  24. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/AGENTS.md +0 -0
  25. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/LICENSE +0 -0
  26. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/NOTICE +0 -0
  27. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/scripts/generate_models.py +0 -0
  28. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/src/matelab/_generated/__init__.py +0 -0
  29. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/src/matelab/_transport.py +0 -0
  30. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/src/matelab/client.py +0 -0
  31. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/src/matelab/cloud_drive.py +0 -0
  32. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/src/matelab/errors.py +0 -0
  33. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/src/matelab/groups.py +0 -0
  34. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/src/matelab/literature.py +0 -0
  35. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/src/matelab/models.py +0 -0
  36. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/src/matelab/notebooks.py +0 -0
  37. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/src/matelab/py.typed +0 -0
  38. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/src/matelab/streaming.py +0 -0
  39. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/src/matelab/templates.py +0 -0
  40. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/src/matelab/uploads.py +0 -0
  41. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/src/matelab/users.py +0 -0
  42. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/tests/conftest.py +0 -0
  43. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/tests/provider/test_provider_smoke.py +0 -0
  44. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/tests/test_client.py +0 -0
  45. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/tests/test_comments_and_downloads.py +0 -0
  46. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/tests/test_consumer_smoke.py +0 -0
  47. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/tests/test_extended_records.py +0 -0
  48. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/tests/test_groups.py +0 -0
  49. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/tests/test_notebook_lifecycle.py +0 -0
  50. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/tests/test_notebooks.py +0 -0
  51. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/tests/test_operation_coverage.py +0 -0
  52. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/tests/test_records.py +0 -0
  53. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/tests/test_streaming.py +0 -0
  54. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/tests/test_template_lifecycle.py +0 -0
  55. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/tests/test_templates.py +0 -0
  56. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/tests/test_transport.py +0 -0
  57. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/tests/test_uploads_and_literature.py +0 -0
  58. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/tests/test_users.py +0 -0
  59. {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/tests/typing/pyproject.toml +0 -0
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.0a2 - 2026-07-27
4
+
5
+ - Adopts the immutable `matelab-spec v0.1.1` release.
6
+ - Adds notebook-scoped, uploader-bound record attachment staging before a target record exists.
7
+ - Finalizes staged attachments through a single-record import or one new form-file-field update without exposing
8
+ Provider-native attachment syntax or private transport details.
9
+ - Enforces exact notebook/user provenance and one finalization attempt per staged handle in one SDK client.
10
+
3
11
  ## 0.1.0a1 - 2026-07-27
4
12
 
5
13
  First public alpha candidate of the asynchronous Matelab Integration Contract SDK.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: matelab-python-sdk
3
- Version: 0.1.0a1
3
+ Version: 0.1.0a2
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.0a1`. `[project].version` in `pyproject.toml` is the sole SDK version source;
31
+ The current alpha is `0.1.0a2`. `[project].version` in `pyproject.toml` is the sole SDK version source;
32
32
  `uv.lock` only mirrors that source.
33
33
 
34
- The SDK is pinned to the immutable `matelab-spec v0.1.0` Contract Release. The sole release pin is
34
+ The SDK is pinned to the immutable `matelab-spec v0.1.1` Contract Release. The sole release pin is
35
35
  `contracts/matelab-integration-v1.lock.json`, which records
36
36
  the source tag, commit, OpenAPI path, local snapshot path, and SHA-256.
37
37
 
@@ -57,7 +57,7 @@ To test the same artifact a downstream Consumer will install, build and install
57
57
 
58
58
  ```bash
59
59
  uv build --no-build-isolation --out-dir dist/release
60
- python -m pip install dist/release/matelab_python_sdk-0.1.0a1-py3-none-any.whl
60
+ python -m pip install dist/release/matelab_python_sdk-0.1.0a2-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
@@ -164,10 +164,70 @@ lifecycle operations, record discovery/lifecycle operations, comment reads, and
164
164
  comment attachment downloads, resumable file staging, literature discovery/lifecycle workflows, and
165
165
  personal cloud-drive management.
166
166
 
167
+ ### Staging a record attachment before record creation
168
+
169
+ `records.stage_attachment` supports the Contract's pre-upload workflow without inventing a target record UID.
170
+ The returned `StagedNotebookAttachment` is scoped by the SDK to the resolved authenticated user and the exact
171
+ notebook selector used for upload:
172
+
173
+ ```python
174
+ import hashlib
175
+
176
+ from matelab import RecordImportItem, RecordImportTemplate, TemplateRef
177
+
178
+ content = b"measurement data"
179
+ staged = await client.records.stage_attachment(
180
+ notebook=notebook,
181
+ filename="measurement.csv",
182
+ content=content,
183
+ size=len(content),
184
+ sha256=hashlib.sha256(content).hexdigest(),
185
+ )
186
+ result = await client.records.import_dataset(
187
+ notebook=notebook,
188
+ template=RecordImportTemplate(template=TemplateRef(template_id=8), title="Example Template"),
189
+ items=(
190
+ RecordImportItem(
191
+ record_uid="REC-IMPORT-001",
192
+ title="Imported measurement",
193
+ data={"Attachments": {"File": [staged]}},
194
+ ),
195
+ ),
196
+ )
197
+ ```
198
+
199
+ The same staged handle may instead be consumed by one safe update that adds a new file field to an existing form
200
+ module:
201
+
202
+ ```python
203
+ from matelab import RecordFormAttachmentFieldAddition, RecordPatch
204
+
205
+ # Alternative to the import above; do not run both with the same staged handle.
206
+ result = await client.records.update(
207
+ source,
208
+ RecordPatch(
209
+ attachment_changes=(
210
+ RecordFormAttachmentFieldAddition(
211
+ module="Attachments",
212
+ name="Measurement",
213
+ attachment=staged,
214
+ ),
215
+ )
216
+ ),
217
+ )
218
+ ```
219
+
220
+ Choose exactly one finalizer. A staged name may occur once in either a single-record import or one update
221
+ operation; do not reuse it, even after an error whose Provider outcome is unknown. The SDK rejects raw Provider
222
+ attachment references, cross-user or cross-notebook handles, unsafe update shapes, duplicate use in one request,
223
+ and a second finalization attempt through the same client. A Session imported without identity must first call
224
+ `await client.resolve_identity()`. The Provider supplies no staging status, abort, TTL, atomicity, or retry
225
+ guarantee, so callers must discard the handle as soon as a finalization request starts.
226
+
167
227
  ## Implementation roadmap
168
228
 
169
229
  `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
230
+ `matelab-spec v0.1.1` operations to ordered work packages, defines the machine-readable coverage that
171
231
  must be added, records Provider-risk gates, and specifies the final completion checks.
172
232
 
173
233
  The SDK exposes all 71 Contract operations through public domain interfaces. It deliberately excludes MCP
@@ -533,6 +593,6 @@ uv run python scripts/check_release.py dist/release/*.whl dist/release/*.tar.gz
533
593
  ```
534
594
 
535
595
  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`.
596
+ byte-identical wheel and source distribution hashes. The release is bound to `matelab-spec v0.1.1`,
597
+ commit `047746ad37d827a85f93d947942f1e5fab80d54c`, and OpenAPI SHA-256
598
+ `1d437b071968d2df165c712092fba4a283832e0ac19bc791e0d5af82294d1cca`.
@@ -2,10 +2,10 @@
2
2
 
3
3
  Reusable async Python client for the Matelab Integration Contract.
4
4
 
5
- The current alpha is `0.1.0a1`. `[project].version` in `pyproject.toml` is the sole SDK version source;
5
+ The current alpha is `0.1.0a2`. `[project].version` in `pyproject.toml` is the sole SDK version source;
6
6
  `uv.lock` only mirrors that source.
7
7
 
8
- The SDK is pinned to the immutable `matelab-spec v0.1.0` Contract Release. The sole release pin is
8
+ The SDK is pinned to the immutable `matelab-spec v0.1.1` Contract Release. The sole release pin is
9
9
  `contracts/matelab-integration-v1.lock.json`, which records
10
10
  the source tag, commit, OpenAPI path, local snapshot path, and SHA-256.
11
11
 
@@ -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.0a1-py3-none-any.whl
34
+ python -m pip install dist/release/matelab_python_sdk-0.1.0a2-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
@@ -138,10 +138,70 @@ lifecycle operations, record discovery/lifecycle operations, comment reads, and
138
138
  comment attachment downloads, resumable file staging, literature discovery/lifecycle workflows, and
139
139
  personal cloud-drive management.
140
140
 
141
+ ### Staging a record attachment before record creation
142
+
143
+ `records.stage_attachment` supports the Contract's pre-upload workflow without inventing a target record UID.
144
+ The returned `StagedNotebookAttachment` is scoped by the SDK to the resolved authenticated user and the exact
145
+ notebook selector used for upload:
146
+
147
+ ```python
148
+ import hashlib
149
+
150
+ from matelab import RecordImportItem, RecordImportTemplate, TemplateRef
151
+
152
+ content = b"measurement data"
153
+ staged = await client.records.stage_attachment(
154
+ notebook=notebook,
155
+ filename="measurement.csv",
156
+ content=content,
157
+ size=len(content),
158
+ sha256=hashlib.sha256(content).hexdigest(),
159
+ )
160
+ result = await client.records.import_dataset(
161
+ notebook=notebook,
162
+ template=RecordImportTemplate(template=TemplateRef(template_id=8), title="Example Template"),
163
+ items=(
164
+ RecordImportItem(
165
+ record_uid="REC-IMPORT-001",
166
+ title="Imported measurement",
167
+ data={"Attachments": {"File": [staged]}},
168
+ ),
169
+ ),
170
+ )
171
+ ```
172
+
173
+ The same staged handle may instead be consumed by one safe update that adds a new file field to an existing form
174
+ module:
175
+
176
+ ```python
177
+ from matelab import RecordFormAttachmentFieldAddition, RecordPatch
178
+
179
+ # Alternative to the import above; do not run both with the same staged handle.
180
+ result = await client.records.update(
181
+ source,
182
+ RecordPatch(
183
+ attachment_changes=(
184
+ RecordFormAttachmentFieldAddition(
185
+ module="Attachments",
186
+ name="Measurement",
187
+ attachment=staged,
188
+ ),
189
+ )
190
+ ),
191
+ )
192
+ ```
193
+
194
+ Choose exactly one finalizer. A staged name may occur once in either a single-record import or one update
195
+ operation; do not reuse it, even after an error whose Provider outcome is unknown. The SDK rejects raw Provider
196
+ attachment references, cross-user or cross-notebook handles, unsafe update shapes, duplicate use in one request,
197
+ and a second finalization attempt through the same client. A Session imported without identity must first call
198
+ `await client.resolve_identity()`. The Provider supplies no staging status, abort, TTL, atomicity, or retry
199
+ guarantee, so callers must discard the handle as soon as a finalization request starts.
200
+
141
201
  ## Implementation roadmap
142
202
 
143
203
  `docs/roadmap.md` is the complete SDK-only execution plan. It assigns all 71
144
- `matelab-spec v0.1.0` operations to ordered work packages, defines the machine-readable coverage that
204
+ `matelab-spec v0.1.1` operations to ordered work packages, defines the machine-readable coverage that
145
205
  must be added, records Provider-risk gates, and specifies the final completion checks.
146
206
 
147
207
  The SDK exposes all 71 Contract operations through public domain interfaces. It deliberately excludes MCP
@@ -507,6 +567,6 @@ uv run python scripts/check_release.py dist/release/*.whl dist/release/*.tar.gz
507
567
  ```
508
568
 
509
569
  Rebuilding the same commit with the same locked environment and `SOURCE_DATE_EPOCH` must produce
510
- byte-identical wheel and source distribution hashes. The release is bound to `matelab-spec v0.1.0`,
511
- commit `236795db1aa7a784ad9df936e9547e8d529be53f`, and OpenAPI SHA-256
512
- `62ca07dcd7a0b3f80eb381a0d1447316f69faa751bafb8ab7c6a751e619d8164`.
570
+ byte-identical wheel and source distribution hashes. The release is bound to `matelab-spec v0.1.1`,
571
+ commit `047746ad37d827a85f93d947942f1e5fab80d54c`, and OpenAPI SHA-256
572
+ `1d437b071968d2df165c712092fba4a283832e0ac19bc791e0d5af82294d1cca`.
@@ -2,12 +2,12 @@
2
2
  "schema_version": 1,
3
3
  "contract_line": "integration-v1",
4
4
  "release": {
5
- "tag": "v0.1.0",
6
- "commit": "236795db1aa7a784ad9df936e9547e8d529be53f"
5
+ "tag": "v0.1.1",
6
+ "commit": "047746ad37d827a85f93d947942f1e5fab80d54c"
7
7
  },
8
8
  "openapi": {
9
9
  "source_path": "openapi/matelab-integration-v1.openapi.yaml",
10
10
  "snapshot": "matelab-integration-v1.openapi.yaml",
11
- "sha256": "62ca07dcd7a0b3f80eb381a0d1447316f69faa751bafb8ab7c6a751e619d8164"
11
+ "sha256": "1d437b071968d2df165c712092fba4a283832e0ac19bc791e0d5af82294d1cca"
12
12
  }
13
13
  }
@@ -1,7 +1,7 @@
1
1
  openapi: 3.1.0
2
2
  info:
3
3
  title: Matelab Integration Contract
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  description: |
6
6
  Stable PHP wire contract for the `integration-v1` contract line.
7
7
  Business failures are returned as JSON envelopes over HTTP 200.
@@ -21,9 +21,13 @@ info:
21
21
  - controller/Recommend_fav.php
22
22
  - controller/Url.php
23
23
  - controller/Unlogin.php
24
+ - model/ElnItemsModel.php
25
+ - model/FileQuoteModel.php
24
26
  - model/FilesModel.php
25
27
  - traits/ElnTrait.php
26
28
  - traits/RecommendTrait.php
29
+ - service/ElnApiImport.php
30
+ - service/ElnApiUpdate.php
27
31
  - service/ElnServer.php
28
32
  - lib/Base.php
29
33
  - lib/Route.php
@@ -36,9 +40,16 @@ info:
36
40
  - repository: /home/tnzhu/projects/matelab-mcp
37
41
  commit: 94a129b2df0ef0fbcf179c10550fb7c9a5dd2e6f
38
42
  files:
43
+ - app/attachments/uploads.py
44
+ - app/resources/catalog.py
39
45
  - app/upstream/client.py
40
46
  - app/auth/pages.py
41
47
  note: |
48
+ The MCP prepare_upload flow generates an `mcp-upload-*` upload uid
49
+ before either create_records or update_record has selected a target
50
+ record and currently resolves its private staging state through a
51
+ native `#hash` reference.
52
+
42
53
  The MCP identity, ELN-group list, and notebook-list calls currently use
43
54
  non-contract POST and must migrate to GET.
44
55
  - repository: /home/tnzhu/projects/matelab-front
@@ -56,6 +67,42 @@ info:
56
67
  - src/views/recommend/fav.vue
57
68
  - src/views/recommend/favDetail.vue
58
69
  - src/views/recommend/favItems.vue
70
+ x-record-attachment-staging:
71
+ status: contract-contained
72
+ producerOperationId: uploadRecordAttachment
73
+ finalizerOperationIds: [importRecordDataset, updateRecordData]
74
+ reference:
75
+ syntax: "#file{name}"
76
+ sourceField: UploadAttachmentResponse.name
77
+ uploadEventUidIndependentOfRecordUid: true
78
+ providerScope:
79
+ uploader: authenticated-user
80
+ dataServer: selected-by-upload-notebook
81
+ notebook: not-bound
82
+ record: not-bound
83
+ safeConsumerScope:
84
+ uploader: same-authenticated-user
85
+ notebookSelector: same-eln-and-user-as-upload
86
+ dataServer: same
87
+ safeFinalizerShapes:
88
+ import:
89
+ datasetItems: 1
90
+ update:
91
+ operationArray: add
92
+ target: new-form-file-field
93
+ dataItems: 1
94
+ lifecycle:
95
+ finalizerChoice: exactly-one-of-import-or-update
96
+ targetRecords: 1
97
+ importDatasetItemsWhenFinalizing: 1
98
+ referencesPerStagedName: 1
99
+ secondConsumption: forbidden
100
+ uploadRetry: non-idempotent
101
+ finalizerRetryAfterError: forbidden
102
+ finalizerAtomicity: not-guaranteed
103
+ abortOperation: unsupported
104
+ statusOperation: unsupported
105
+ expiration: unspecified
59
106
  servers:
60
107
  - url: https://matelab.example.invalid
61
108
  description: Placeholder; consumers supply their Provider base URL.
@@ -1349,6 +1396,21 @@ paths:
1349
1396
  operationId: importRecordDataset
1350
1397
  tags: [Records]
1351
1398
  summary: Import one or more template-shaped records
1399
+ description: |
1400
+ Template-shaped file positions may contain `#file{name}`, where `name`
1401
+ is the completed `uploadRecordAttachment` response name. For the safe
1402
+ pre-upload workflow, the upload and import use the same authenticated
1403
+ user and the same `eln`/`user` notebook selector, `dataset` contains
1404
+ exactly one record, and the staged name occurs exactly once. The upload
1405
+ request `uid` is an upload-event key and need not equal `dataset[].uid`.
1406
+
1407
+ The Provider resolves staging by uploader and data server, not by
1408
+ notebook or future record identity. It consumes staging only after
1409
+ record writing begins, without an atomic transaction covering record
1410
+ creation, file quote creation, byte movement, and staging deletion.
1411
+ Consumers must not batch one staged name into multiple records or retry
1412
+ a failed finalize with the same name.
1413
+ x-known-provider-issues: [PVD-023, PVD-024, PVD-040]
1352
1414
  security: [{bearerAuth: []}]
1353
1415
  requestBody:
1354
1416
  required: true
@@ -1362,6 +1424,17 @@ paths:
1362
1424
  template: Example Template
1363
1425
  dataset:
1364
1426
  - {uid: GENERATED-WIRE-UID, title: Imported Example, data: {Notes: Sanitized}, keyword: calibration}
1427
+ preuploaded_attachment:
1428
+ summary: Upload-event UID and future record UID are deliberately different
1429
+ value:
1430
+ eln: Example Notebook
1431
+ template: Example Template
1432
+ dataset:
1433
+ - uid: REC-IMPORT-EXAMPLE-001
1434
+ title: Imported attachment example
1435
+ data:
1436
+ Attachments:
1437
+ File: ["#file{staged-attachment-example}"]
1365
1438
  nullable_optionals:
1366
1439
  value:
1367
1440
  eln: Example Notebook
@@ -1476,7 +1549,21 @@ paths:
1476
1549
  The Provider scans the incoming richtext value, resolves hashes against
1477
1550
  caller-owned staging or authorized existing file quotes, and binds the
1478
1551
  resulting attachments while saving the record.
1479
- x-known-provider-issues: [PVD-014, PVD-015, PVD-016, PVD-017]
1552
+
1553
+ The verified safe pre-upload shape adds one new field to an existing
1554
+ form module: the `add` item names a field that does not already exist,
1555
+ sets `type: file`, and supplies a one-element `data` array containing
1556
+ the `#file{name}` returned by `uploadRecordAttachment`. Appending to an
1557
+ existing form file field and replacing an existing file occurrence are
1558
+ outside this staging contract.
1559
+
1560
+ Upload and update use the same authenticated user and exact `eln`/`user`
1561
+ notebook selector, reference the staged name once, and target one
1562
+ existing record. The upload-event `uid` is independent of this
1563
+ operation's record `uid`. Record persistence and staging consumption
1564
+ are not atomic; after any finalize error the staged name is
1565
+ indeterminate and must not be retried.
1566
+ x-known-provider-issues: [PVD-014, PVD-015, PVD-016, PVD-017, PVD-040]
1480
1567
  security: [{bearerAuth: []}]
1481
1568
  requestBody:
1482
1569
  required: true
@@ -1486,6 +1573,16 @@ paths:
1486
1573
  examples:
1487
1574
  update:
1488
1575
  value: {eln: Example Notebook, uid: REC-EXAMPLE-001, modify: [{path: [Notes], data: Sanitized update}]}
1576
+ add_preuploaded_attachment_field:
1577
+ value:
1578
+ eln: Example Notebook
1579
+ uid: REC-EXAMPLE-001
1580
+ add:
1581
+ - module: Attachments
1582
+ name: Staged file
1583
+ type: file
1584
+ data:
1585
+ - "#file{staged-update-example}"
1489
1586
  no_op:
1490
1587
  summary: PHP accepts an update with no mutation arrays and rewrites the unchanged record
1491
1588
  value: {eln: Example Notebook, uid: REC-EXAMPLE-001}
@@ -1533,6 +1630,23 @@ paths:
1533
1630
  This integration operation deliberately contracts the one-fragment flow used
1534
1631
  by the consumer (`last=1`). The PHP provider also has an internal fragmented
1535
1632
  upload mode, but that mode is outside `integration-v1`.
1633
+
1634
+ The caller may upload before the target record exists. `uid` is an
1635
+ upload-event key stored with the temporary row; it is not a record UID
1636
+ and need not match a later import or update record UID. The resolved
1637
+ response `name` is the one-shot staging name consumed as `#file{name}`
1638
+ by exactly one later `importRecordDataset` or `updateRecordData` request.
1639
+
1640
+ The Provider checks write access to `eln` and uses it to choose a data
1641
+ server, but the resulting row is actually scoped only to the
1642
+ authenticated uploader and that data server. It is not bound to the
1643
+ notebook, a target record, or a finalizer operation. The safe Consumer
1644
+ workflow therefore preserves the exact upload `eln`/`user` selector and
1645
+ permits one occurrence in one target record. The operation has no
1646
+ contracted abort, status, expiration, or idempotent retry. A hash
1647
+ mismatch or ambiguous finalize error can leave staging behind, and
1648
+ retention after failures is not guaranteed.
1649
+ x-known-provider-issues: [PVD-040]
1536
1650
  security: [{bearerAuth: []}]
1537
1651
  requestBody:
1538
1652
  required: true
@@ -1547,8 +1661,8 @@ paths:
1547
1661
  upload:
1548
1662
  value:
1549
1663
  eln: Example Notebook
1550
- uid: REC-EXAMPLE-001
1551
- name: attachment-example
1664
+ uid: UPLOAD-EVENT-EXAMPLE
1665
+ name: staged-attachment-example
1552
1666
  hash: 74034daeef860e12596c34f7e83703e668d578f4a85e72ec1645117223d4e6a7
1553
1667
  last: 1
1554
1668
  file: Sanitized attachment bytes
@@ -1556,7 +1670,7 @@ paths:
1556
1670
  summary: Omitted name falls back to uid and omitted last selects the final fragment
1557
1671
  value:
1558
1672
  eln: Example Notebook
1559
- uid: REC-EXAMPLE-001
1673
+ uid: UPLOAD-EVENT-DEFAULT-NAME
1560
1674
  hash: 74034daeef860e12596c34f7e83703e668d578f4a85e72ec1645117223d4e6a7
1561
1675
  file: Sanitized attachment bytes
1562
1676
  responses:
@@ -1578,7 +1692,7 @@ paths:
1578
1692
  id: 7301
1579
1693
  path: url_eln/temp
1580
1694
  query: h=74034daeef860e12596c34f7e83703e668d578f4a85e72ec1645117223d4e6a7&id=7301&s=26&f=example.txt
1581
- name: attachment-example
1695
+ name: staged-attachment-example
1582
1696
  filename: example.txt
1583
1697
  size: 26
1584
1698
  hash: 74034daeef860e12596c34f7e83703e668d578f4a85e72ec1645117223d4e6a7
@@ -3436,7 +3550,7 @@ paths:
3436
3550
  - id: 501
3437
3551
  filename: measurement.csv
3438
3552
  hash: 8b9f9f89f4d7a86d1ea98d6ee312194f7a6ad47356c4c71e2b9421a4ec02f66b
3439
- size: "2048"
3553
+ size: 2048
3440
3554
  userid: 101
3441
3555
  date_time: "2026-01-10 09:00:00"
3442
3556
  comm: ""
@@ -3498,7 +3612,7 @@ paths:
3498
3612
  - id: 502
3499
3613
  filename: legacy.dat
3500
3614
  hash: 7fbc96d99fdaa807b29bdc2a0c6c1068e667b9bd294fb6d9e7c18d1044129dae
3501
- size: "0"
3615
+ size: 0
3502
3616
  userid: 101
3503
3617
  date_time: null
3504
3618
  comm: ""
@@ -3902,9 +4016,11 @@ components:
3902
4016
  filename: {type: string, minLength: 1, maxLength: 255}
3903
4017
  hash: {$ref: "#/components/schemas/Sha256"}
3904
4018
  size:
3905
- type: string
3906
- pattern: "^[0-9]+$"
3907
- description: Unsigned BIGINT serialized as a decimal string by this query.
4019
+ type: integer
4020
+ minimum: 0
4021
+ description: |
4022
+ File size in bytes. The Provider fetches the unsigned BIGINT with
4023
+ PDO::ATTR_STRINGIFY_FETCHES disabled and returns a JSON integer.
3908
4024
  userid: {type: integer, minimum: 1}
3909
4025
  date_time:
3910
4026
  type: [string, "null"]
@@ -4049,21 +4165,33 @@ components:
4049
4165
  eln:
4050
4166
  type: string
4051
4167
  minLength: 1
4052
- description: Notebook title used to select its data server.
4168
+ description: |
4169
+ Notebook title used to check write permission and select its data
4170
+ server. The value is not persisted on the staging row.
4053
4171
  user:
4054
4172
  type: integer
4055
4173
  minimum: 1
4056
- description: Optional notebook owner identifier for a non-owned notebook.
4174
+ description: |
4175
+ Optional notebook owner identifier for a non-owned notebook. It
4176
+ selects the notebook but never replaces the authenticated uploader
4177
+ as the staging owner.
4057
4178
  uid:
4058
4179
  type: string
4059
4180
  minLength: 1
4060
4181
  maxLength: 45
4061
- description: Record wire UID and upload-event identifier.
4182
+ description: |
4183
+ Caller-generated upload-event key stored in `upload_temp.data`.
4184
+ It is independent of, and need not equal, the UID of a record later
4185
+ created or updated with this staged file.
4062
4186
  name:
4063
4187
  type: string
4064
4188
  maxLength: 45
4065
4189
  default: ""
4066
- description: Missing or empty falls back to uid as the unique temporary upload name.
4190
+ description: |
4191
+ Caller-generated staging name stored in `upload_temp.unique_str`.
4192
+ Missing or empty falls back to `uid`. On success, use the response
4193
+ `name` once as `#file{name}` in the same user's same-notebook
4194
+ import or update workflow.
4067
4195
  hash:
4068
4196
  $ref: "#/components/schemas/Sha256"
4069
4197
  last:
@@ -4088,7 +4216,10 @@ components:
4088
4216
  query:
4089
4217
  type: string
4090
4218
  description: Provider-built legacy query string for the temporary file.
4091
- name: {type: string, minLength: 1}
4219
+ name:
4220
+ type: string
4221
+ minLength: 1
4222
+ description: Resolved one-shot staging name used by `#file{name}`.
4092
4223
  filename: {type: string, minLength: 1}
4093
4224
  size: {type: integer, minimum: 0}
4094
4225
  hash: {$ref: "#/components/schemas/Sha256"}
@@ -1,7 +1,7 @@
1
1
  schema_version: 1
2
2
  contract:
3
3
  line: integration-v1
4
- release: v0.1.0
4
+ release: v0.1.1
5
5
  operation_count: 71
6
6
 
7
7
  operations:
@@ -282,7 +282,7 @@ operations:
282
282
  state: implemented
283
283
  stability: experimental
284
284
  public_interface: AsyncMatelab.records.import_dataset
285
- known_issues: [PCG-008, PVD-023, PVD-024]
285
+ known_issues: [PCG-008, PCG-014, PVD-023, PVD-024, PVD-040]
286
286
  work_package: WP7
287
287
 
288
288
  - operation_id: exportRecords
@@ -312,7 +312,7 @@ operations:
312
312
  state: implemented
313
313
  stability: experimental
314
314
  public_interface: AsyncMatelab.records.update
315
- known_issues: [PCG-001, PCG-002, PVD-014, PVD-015, PVD-016, PVD-017]
315
+ known_issues: [PCG-001, PCG-002, PCG-014, PVD-014, PVD-015, PVD-016, PVD-017, PVD-040]
316
316
  work_package: WP8
317
317
 
318
318
  - operation_id: deleteRecordsByUid
@@ -331,8 +331,8 @@ operations:
331
331
  domain: records
332
332
  state: implemented
333
333
  stability: experimental
334
- public_interface: AsyncMatelab.records.upload_attachment
335
- known_issues: [PCG-002]
334
+ public_interface: AsyncMatelab.records.upload_attachment / AsyncMatelab.records.stage_attachment
335
+ known_issues: [PCG-002, PCG-014, PVD-040]
336
336
  work_package: WP8
337
337
 
338
338
  - operation_id: listPublicNotebookRecords
@@ -16,7 +16,7 @@
16
16
  目标是在当前仓库内完成一个可复用、异步、类型安全的 Matelab Python SDK:
17
17
 
18
18
  ```text
19
- matelab-spec v0.1.0
19
+ matelab-spec v0.1.1
20
20
 
21
21
 
22
22
  private generated wire models
@@ -45,11 +45,11 @@ small domain-oriented public interface
45
45
  ## 2. 固定基线
46
46
 
47
47
  - Contract line:`integration-v1`
48
- - Contract release:`matelab-spec v0.1.0`
49
- - Contract commit:`236795db1aa7a784ad9df936e9547e8d529be53f`
50
- - OpenAPI SHA-256:`62ca07dcd7a0b3f80eb381a0d1447316f69faa751bafb8ab7c6a751e619d8164`
48
+ - Contract release:`matelab-spec v0.1.1`
49
+ - Contract commit:`047746ad37d827a85f93d947942f1e5fab80d54c`
50
+ - OpenAPI SHA-256:`1d437b071968d2df165c712092fba4a283832e0ac19bc791e0d5af82294d1cca`
51
51
  - Contract operation:71
52
- - SDK 当前版本:`0.1.0a1`
52
+ - SDK 当前版本:`0.1.0a2`
53
53
  - Python:3.11+
54
54
  - 生成器:`datamodel-code-generator 0.71.0`
55
55
 
@@ -73,7 +73,7 @@ goal 期间不得跟踪 `matelab-spec/main`,也不得自动升级 Contract Rel
73
73
  领域 interface;
74
74
  - 71 条 Contract operation 的 public-interface、wire adapter、测试与 coverage;
75
75
  - opt-in 的隔离 Provider smoke;
76
- - reproducible `0.1.0a1` build。
76
+ - reproducible `0.1.0a2` build。
77
77
 
78
78
  当前 operation coverage:71 implemented / 0 planned。每条 operation 的领域入口、稳定性和已知问题以
79
79
  [`operation-coverage.yaml`](operation-coverage.yaml) 为准。
@@ -83,7 +83,7 @@ goal 期间不得跟踪 `matelab-spec/main`,也不得自动升级 Contract Rel
83
83
  在 `/home/tnzhu/projects/matelab-python-sdk` 启动新任务后,使用以下目标:
84
84
 
85
85
  > 完整阅读并严格遵循 `AGENTS.md`、`README.md` 和 `docs/roadmap.md`。创建并持续执行一个 goal:
86
- > 只修改当前 `matelab-python-sdk` 仓库,以 pinned `matelab-spec v0.1.0` 为唯一 Wire Contract,按照
86
+ > 只修改当前 `matelab-python-sdk` 仓库,以 pinned `matelab-spec v0.1.1` 为唯一 Wire Contract,按照
87
87
  > roadmap 从第一个未完成 work package 开始,建立 71-operation 精确覆盖,完成全部安全可表达的领域
88
88
  > interface,并对无法安全公开的 operation 给出有证据的 intentionally_unexposed 分类。每个 work
89
89
  > package 完成后立即运行其测试并继续下一项,不停在规划、部分实现或单个领域;最终必须让 generation
@@ -882,3 +882,18 @@ License 和公开 author metadata 已确认;内部 Gitee repository 不作为
882
882
  documentation/issues URL。author 同时负责维护,因此不重复声明 maintainer。release 通过私有 GitHub mirror
883
883
  的 tag workflow 构建,并使用 PyPI Trusted Publishing;Gitee `origin` 仍是内部主仓库。剩余人工步骤仅为在
884
884
  PyPI 登记 Pending Publisher,之后创建并推送 release tag。
885
+
886
+ ## 26. `0.1.0a2` Contract adoption 与 record attachment staging
887
+
888
+ 本阶段采用 immutable `matelab-spec v0.1.1`,保持 71-operation 覆盖不变,并落实 PCG-014 / PVD-040
889
+ 明确的创建前附件暂存语义:
890
+
891
+ - `/eln_api/upload` 的 `uid` 是独立 upload-event key,不再要求伪装成已有 record UID;
892
+ - `records.stage_attachment` 返回由 SDK 绑定 authenticated uploader 与精确 notebook selector 的公开句柄;
893
+ - import 只允许单记录、单次引用,update 只允许向已有 form module 新增一个 file field;
894
+ - Consumer 不接触 `#file{name}`、`#hash{...}`、PHP path 或 private transport;
895
+ - 同一 client 在 finalization 请求开始前记录一次性尝试;成功、业务错误、HTTP 错误或不确定结果后均禁止重试;
896
+ - Provider 未提供 status、abort、TTL、原子性和跨请求幂等保证,结果类型与文档不推测这些能力。
897
+
898
+ 发布准备将 SDK 版本推进到 `0.1.0a2`,更新生成模型、coverage、公开类型消费测试、release artifact 审计与
899
+ reproducible build 基线。
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "matelab-python-sdk"
7
- version = "0.1.0a1"
7
+ version = "0.1.0a2"
8
8
  description = "Reusable async Python client for the Matelab Integration Contract"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -5,7 +5,7 @@ from typing import cast
5
5
 
6
6
  import matelab
7
7
 
8
- _EXPECTED_PUBLIC_SYMBOLS = 182
8
+ _EXPECTED_PUBLIC_SYMBOLS = 184
9
9
 
10
10
 
11
11
  def main() -> None:
@@ -9,7 +9,7 @@ from pathlib import Path, PurePosixPath
9
9
  from typing import cast
10
10
  from zipfile import ZipFile
11
11
 
12
- _VERSION = "0.1.0a1"
12
+ _VERSION = "0.1.0a2"
13
13
  _DIST_INFO = f"matelab_python_sdk-{_VERSION}.dist-info"
14
14
  _EXPECTED_REQUIRES_DIST = {"httpx<1,>=0.27.2", "pydantic<3,>=2.12.5"}
15
15
  _EXPECTED_LICENSE_FILES = {"LICENSE", "NOTICE"}