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.
- {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/CHANGELOG.md +8 -0
- {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/PKG-INFO +68 -8
- {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/README.md +67 -7
- {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/contracts/matelab-integration-v1.lock.json +3 -3
- {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/contracts/matelab-integration-v1.openapi.yaml +147 -16
- {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/docs/operation-coverage.yaml +5 -5
- {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/docs/roadmap.md +22 -7
- {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/pyproject.toml +1 -1
- {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/scripts/check_installed_package.py +1 -1
- {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/scripts/check_release.py +1 -1
- {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/src/matelab/__init__.py +4 -0
- {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/src/matelab/_generated/models.py +24 -7
- {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/src/matelab/records.py +294 -38
- {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/tests/test_cloud_drive.py +1 -1
- {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/tests/test_generation.py +1 -1
- {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/tests/test_record_lifecycle.py +140 -1
- {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/tests/test_record_mutations.py +273 -0
- {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/tests/test_release.py +4 -2
- matelab_python_sdk-0.1.0a2/tests/typing/consumer.py +47 -0
- {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/uv.lock +1 -1
- matelab_python_sdk-0.1.0a1/tests/typing/consumer.py +0 -7
- {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/.github/workflows/release.yml +0 -0
- {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/.gitignore +0 -0
- {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/AGENTS.md +0 -0
- {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/LICENSE +0 -0
- {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/NOTICE +0 -0
- {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/scripts/generate_models.py +0 -0
- {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/src/matelab/_generated/__init__.py +0 -0
- {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/src/matelab/_transport.py +0 -0
- {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/src/matelab/client.py +0 -0
- {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/src/matelab/cloud_drive.py +0 -0
- {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/src/matelab/errors.py +0 -0
- {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/src/matelab/groups.py +0 -0
- {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/src/matelab/literature.py +0 -0
- {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/src/matelab/models.py +0 -0
- {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/src/matelab/notebooks.py +0 -0
- {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/src/matelab/py.typed +0 -0
- {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/src/matelab/streaming.py +0 -0
- {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/src/matelab/templates.py +0 -0
- {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/src/matelab/uploads.py +0 -0
- {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/src/matelab/users.py +0 -0
- {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/tests/conftest.py +0 -0
- {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/tests/provider/test_provider_smoke.py +0 -0
- {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/tests/test_client.py +0 -0
- {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/tests/test_comments_and_downloads.py +0 -0
- {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/tests/test_consumer_smoke.py +0 -0
- {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/tests/test_extended_records.py +0 -0
- {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/tests/test_groups.py +0 -0
- {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/tests/test_notebook_lifecycle.py +0 -0
- {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/tests/test_notebooks.py +0 -0
- {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/tests/test_operation_coverage.py +0 -0
- {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/tests/test_records.py +0 -0
- {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/tests/test_streaming.py +0 -0
- {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/tests/test_template_lifecycle.py +0 -0
- {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/tests/test_templates.py +0 -0
- {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/tests/test_transport.py +0 -0
- {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/tests/test_uploads_and_literature.py +0 -0
- {matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/tests/test_users.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
537
|
-
commit `
|
|
538
|
-
`
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
511
|
-
commit `
|
|
512
|
-
`
|
|
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`.
|
{matelab_python_sdk-0.1.0a1 → matelab_python_sdk-0.1.0a2}/contracts/matelab-integration-v1.lock.json
RENAMED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
"schema_version": 1,
|
|
3
3
|
"contract_line": "integration-v1",
|
|
4
4
|
"release": {
|
|
5
|
-
"tag": "v0.1.
|
|
6
|
-
"commit": "
|
|
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": "
|
|
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.
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
3906
|
-
|
|
3907
|
-
description:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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.
|
|
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.
|
|
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.
|
|
49
|
-
- Contract commit:`
|
|
50
|
-
- OpenAPI SHA-256:`
|
|
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.
|
|
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.
|
|
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.
|
|
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 基线。
|
|
@@ -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.
|
|
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"}
|