matelab-python-sdk 0.1.0a4__tar.gz → 0.1.0a6__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.0a6/.env.example +14 -0
- {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/AGENTS.md +5 -2
- {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/CHANGELOG.md +24 -0
- {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/PKG-INFO +205 -113
- {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/README.md +204 -112
- {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/contracts/matelab-integration-v1.lock.json +3 -3
- {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/contracts/matelab-integration-v1.openapi.yaml +333 -49
- {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/docs/operation-coverage.yaml +7 -5
- {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/docs/roadmap.md +54 -40
- {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/pyproject.toml +3 -1
- {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/scripts/generate_models.py +1 -5
- {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/src/matelab/__init__.py +22 -34
- {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/src/matelab/_generated/models.py +106 -3
- matelab_python_sdk-0.1.0a6/src/matelab/_transport.py +276 -0
- {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/src/matelab/_wire_validation.py +38 -35
- matelab_python_sdk-0.1.0a6/src/matelab/client.py +120 -0
- matelab_python_sdk-0.1.0a6/src/matelab/domains/cloud_drive.py +356 -0
- {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/src/matelab/domains/groups.py +7 -9
- {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/src/matelab/domains/literature.py +120 -341
- {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/src/matelab/domains/notebooks.py +18 -66
- {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/src/matelab/domains/records.py +499 -308
- {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/src/matelab/domains/templates.py +52 -152
- {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/src/matelab/domains/uploads.py +3 -14
- {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/src/matelab/domains/users.py +4 -17
- {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/src/matelab/errors.py +3 -11
- matelab_python_sdk-0.1.0a6/src/matelab/session.py +32 -0
- {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/src/matelab/streaming.py +11 -14
- {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/tests/conftest.py +4 -2
- {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/tests/provider/test_provider_smoke.py +12 -26
- matelab_python_sdk-0.1.0a6/tests/test_client.py +394 -0
- {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/tests/test_cloud_drive.py +101 -148
- {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/tests/test_comments_and_downloads.py +17 -27
- {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/tests/test_consumer_smoke.py +36 -46
- {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/tests/test_extended_records.py +40 -69
- {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/tests/test_generation.py +51 -14
- {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/tests/test_groups.py +16 -27
- matelab_python_sdk-0.1.0a6/tests/test_notebook_lifecycle.py +306 -0
- {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/tests/test_notebooks.py +13 -22
- {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/tests/test_operation_coverage.py +17 -8
- {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/tests/test_record_lifecycle.py +64 -235
- matelab_python_sdk-0.1.0a6/tests/test_record_mutations.py +1416 -0
- {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/tests/test_records.py +36 -58
- {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/tests/test_release.py +1 -5
- {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/tests/test_streaming.py +61 -84
- {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/tests/test_template_lifecycle.py +95 -256
- {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/tests/test_templates.py +37 -58
- {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/tests/test_transport.py +128 -188
- {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/tests/test_uploads_and_literature.py +124 -187
- {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/tests/test_users.py +27 -45
- matelab_python_sdk-0.1.0a6/tests/typing/consumer.py +75 -0
- {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/uv.lock +1 -1
- matelab_python_sdk-0.1.0a4/.env.example +0 -6
- matelab_python_sdk-0.1.0a4/src/matelab/_transport.py +0 -500
- matelab_python_sdk-0.1.0a4/src/matelab/client.py +0 -172
- matelab_python_sdk-0.1.0a4/src/matelab/domains/cloud_drive.py +0 -573
- matelab_python_sdk-0.1.0a4/src/matelab/session.py +0 -27
- matelab_python_sdk-0.1.0a4/tests/test_client.py +0 -570
- matelab_python_sdk-0.1.0a4/tests/test_notebook_lifecycle.py +0 -454
- matelab_python_sdk-0.1.0a4/tests/test_record_mutations.py +0 -994
- matelab_python_sdk-0.1.0a4/tests/typing/consumer.py +0 -44
- {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/.github/workflows/release.yml +0 -0
- {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/.gitignore +0 -0
- {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/LICENSE +0 -0
- {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/NOTICE +0 -0
- {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/scripts/check_installed_package.py +0 -0
- {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/scripts/check_release.py +0 -0
- {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/src/matelab/_generated/__init__.py +0 -0
- {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/src/matelab/domains/__init__.py +0 -0
- {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/src/matelab/py.typed +0 -0
- {matelab_python_sdk-0.1.0a4 → matelab_python_sdk-0.1.0a6}/tests/typing/pyproject.toml +0 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# SDK Provider smoke must target the confirmed isolated test service.
|
|
2
|
+
# Authentication and external-token refresh persist Provider token state.
|
|
3
|
+
# Keep these names aligned with matelab-spec Provider Verification.
|
|
4
|
+
MATELAB_PROVIDER_BASE_URL=
|
|
5
|
+
MATELAB_PROVIDER_USERNAME=
|
|
6
|
+
MATELAB_PROVIDER_PASSWORD=
|
|
7
|
+
|
|
8
|
+
# The SDK smoke does not read the settings below. Populate them only when the
|
|
9
|
+
# same .env.test will also run the complete matelab-spec Provider Verification.
|
|
10
|
+
MATELAB_PROVIDER_SHARE_USER_ID=
|
|
11
|
+
MATELAB_PROVIDER_SECONDARY_USERNAME=
|
|
12
|
+
MATELAB_PROVIDER_SECONDARY_PASSWORD=
|
|
13
|
+
MATELAB_PROVIDER_RECORD_STAGING_MUTATIONS=
|
|
14
|
+
MATELAB_PROVIDER_CHAT_SSO_KEY=
|
|
@@ -50,8 +50,11 @@
|
|
|
50
50
|
- `base_url` 表示 Provider API 根。query、JSON、form-urlencoded、multipart 和 streaming 必须按各 operation
|
|
51
51
|
定义分别实现。
|
|
52
52
|
- bearer 只用于要求认证的 operation;成功码按 operation 配置,不能全局接受 `{0, 10}`。
|
|
53
|
-
- 每个 `AsyncMatelab` 实例最多持有一个进程内 Session,包含 access/refresh token、毫秒 expiration
|
|
54
|
-
identity
|
|
53
|
+
- 每个 `AsyncMatelab` 实例最多持有一个进程内 Session,包含 access/refresh token、毫秒 expiration 和已验证
|
|
54
|
+
identity。SDK 不提供裸 access/refresh token 绑定接口;构造传入完整 Session 的真实性和身份归属验证属于
|
|
55
|
+
集成方。
|
|
56
|
+
- `Token`、`Identity` 和 `Session` 是冻结、拒绝额外字段的 Pydantic public model,组成稳定的规范化 Session
|
|
57
|
+
JSON 契约。token 在 repr 中隐藏,但显式序列化必须保留真实凭证;序列化结果不得进入日志或发送给不可信方。
|
|
55
58
|
- `client.session` 暴露实例内最新 Session。持久化、加密、撤销、`userid/session_id` 关联和跨进程锁属于
|
|
56
59
|
集成方;不同逻辑 Session 不得共享同一个 `AsyncMatelab`,但多个实例可以共享注入的 `httpx.AsyncClient`。
|
|
57
60
|
- access 到期前按 skew 刷新;并发刷新使用单个异步锁;已认证且可安全重试的请求遇到 `code=5` 最多强制
|
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.0a6 - 2026-08-01
|
|
4
|
+
|
|
5
|
+
- Adopts the immutable `matelab-spec v0.3.0` Contract release and its form-encoding updates.
|
|
6
|
+
- Tightens the public authentication/session contract around complete, serializable Pydantic `Session` models while
|
|
7
|
+
retaining automatic access-token refresh and Provider SSO exchange.
|
|
8
|
+
- Simplifies transport and domain interfaces, removes external-token binding and identity-attachment seams, and
|
|
9
|
+
restores strict runtime validation for byte-range inputs.
|
|
10
|
+
|
|
11
|
+
## 0.1.0a5 - 2026-07-29
|
|
12
|
+
|
|
13
|
+
- Adopts immutable `matelab-spec v0.1.2` at commit `6dd674ac001ae61197495ace5eba489d585cb825` and OpenAPI
|
|
14
|
+
SHA-256 `7ccaf3116c8dcec7a6e49ff51fb1a5ecc83a140e013ec600516f5c2ffb3b5378`.
|
|
15
|
+
- Adds notebook-scoped staged finalizers for a null table file cell, an attachment-bearing appended table row,
|
|
16
|
+
files-module root append, and table/files occurrence replacement, with exact provenance, one-shot transport, and
|
|
17
|
+
canonical readback checks.
|
|
18
|
+
- Adds typed intents for an empty file table column and complete form file-field/table file-column deletion.
|
|
19
|
+
- Allows `records.update(..., module_deletions=...)` to delete a canonical module even when the current record read
|
|
20
|
+
observes attachments in it, verifies absence on persisted readback, and does not claim attachment-quote or byte
|
|
21
|
+
cleanup beyond the pinned Contract.
|
|
22
|
+
- Rejects cross-scope staged attachment handles before the record read, preserving the public static types while
|
|
23
|
+
avoiding the Provider's read-audit side effect for invalid runtime input.
|
|
24
|
+
- Keeps notebook-staged rich-text binding and attachment-bearing multi-record import explicitly unsupported because
|
|
25
|
+
v0.1.2 Provider Verification does not provide safe one-shot semantics for either.
|
|
26
|
+
|
|
3
27
|
## 0.1.0a4 - 2026-07-28
|
|
4
28
|
|
|
5
29
|
- Further reduces the alpha public surface from 150 to 126 symbols by accepting direct keyword arguments and existing
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: matelab-python-sdk
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.0a6
|
|
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.0a6`. `[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.
|
|
34
|
+
The SDK is pinned to the immutable `matelab-spec v0.3.0` 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.0a6-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
|
|
@@ -92,13 +92,10 @@ Catch `MatelabError` for one application-level fallback, or a specific subclass
|
|
|
92
92
|
| `MatelabAuthenticationError` | The Session is missing, expired, invalid, or rejected; refresh or authenticate again as appropriate. |
|
|
93
93
|
| `MatelabProviderError` | The Provider rejected a valid request with a business error; inspect `code` and do not assume a mutation was applied. |
|
|
94
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. |
|
|
95
|
+
| `MatelabProtocolError` | The Provider response does not match the pinned Contract; treat it as Provider drift or an SDK defect. A mutation may already have been applied. |
|
|
97
96
|
|
|
98
|
-
`code
|
|
99
|
-
|
|
100
|
-
documented scalar attributes. Never automatically retry a mutation solely because it raised a transport or
|
|
101
|
-
verification error.
|
|
97
|
+
`code` and `status_code` are the stable scalar diagnostics. Provider response bodies and caller inputs are never
|
|
98
|
+
attached to exceptions. Never automatically retry a mutation solely because it raised a transport or protocol error.
|
|
102
99
|
|
|
103
100
|
### Session ownership
|
|
104
101
|
|
|
@@ -114,24 +111,28 @@ rotation through `client.session`. If refresh succeeds but the subsequent busine
|
|
|
114
111
|
| Contract validation of token and identity responses | Mapping `userid`/`session_id` to a persisted `Session` |
|
|
115
112
|
| Latest immutable `Session` through `client.session` | Revocation, cleanup, and saving after each call |
|
|
116
113
|
|
|
117
|
-
`Session`, `Token`, and `Identity` are
|
|
118
|
-
|
|
114
|
+
`Session`, `Token`, and `Identity` are frozen Pydantic models and form the stable, normalized SDK Session contract.
|
|
115
|
+
Their JSON fields are `access`, `refresh`, and `identity`; tokens contain `value` and `expires_at_ms`, while identity
|
|
116
|
+
contains `userid`, `username`, and `email`. Provider envelope fields are not part of this model. An `Identity` requires
|
|
117
|
+
a positive userid and non-empty username; its email may be `None`.
|
|
119
118
|
|
|
120
|
-
|
|
119
|
+
Token values are excluded from model representations but intentionally remain present in `model_dump()` and
|
|
120
|
+
`model_dump_json()` so an integration can persist and restore the complete Session:
|
|
121
121
|
|
|
122
122
|
```python
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
123
|
+
serialized = session.model_dump_json()
|
|
124
|
+
restored = Session.model_validate_json(serialized)
|
|
125
|
+
assert restored == session
|
|
126
126
|
```
|
|
127
127
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
128
|
+
The serialized result contains live credentials. Encrypt it at rest and never write it to logs or send it to an
|
|
129
|
+
untrusted party. The SDK does not read tokens from environment variables and does not provide a session store.
|
|
130
|
+
|
|
131
|
+
Credential authentication installs the returned Session on the client:
|
|
131
132
|
|
|
132
133
|
```python
|
|
133
134
|
async with AsyncMatelab() as client:
|
|
134
|
-
session = await client.
|
|
135
|
+
session = await client.authenticate(username, password)
|
|
135
136
|
assert client.session is session
|
|
136
137
|
```
|
|
137
138
|
|
|
@@ -144,6 +145,10 @@ async with AsyncMatelab(session=persisted_session) as client:
|
|
|
144
145
|
result = await handle_request(client)
|
|
145
146
|
```
|
|
146
147
|
|
|
148
|
+
Every Session contains a validated identity, and refreshing a restored Session preserves it. Integrations that
|
|
149
|
+
construct a Session from an external assertion are responsible for validating that assertion and the identity-token
|
|
150
|
+
association before passing the complete Session to the SDK; the SDK does not accept bare external token pairs.
|
|
151
|
+
|
|
147
152
|
A Web or MCP integration should create one client for one logical session, then save the latest Session in
|
|
148
153
|
`finally`, including when a business call fails after refresh:
|
|
149
154
|
|
|
@@ -194,7 +199,7 @@ notebook selector used for upload:
|
|
|
194
199
|
```python
|
|
195
200
|
import hashlib
|
|
196
201
|
|
|
197
|
-
from matelab import RecordImportItem
|
|
202
|
+
from matelab import RecordImportItem
|
|
198
203
|
|
|
199
204
|
content = b"measurement data"
|
|
200
205
|
staged = await client.records.stage_attachment(
|
|
@@ -206,20 +211,17 @@ staged = await client.records.stage_attachment(
|
|
|
206
211
|
)
|
|
207
212
|
result = await client.records.import_dataset(
|
|
208
213
|
notebook=notebook,
|
|
209
|
-
template=TemplateRef(template_id=8),
|
|
210
214
|
template_title="Example Template",
|
|
211
215
|
items=(
|
|
212
216
|
RecordImportItem(
|
|
213
|
-
record_uid="REC-IMPORT-001",
|
|
214
|
-
title="Imported measurement",
|
|
215
|
-
data={"Attachments": {"File": [staged]}},
|
|
217
|
+
record_uid="REC-IMPORT-001", title="Imported measurement", data={"Attachments": {"File": [staged]}}
|
|
216
218
|
),
|
|
217
219
|
),
|
|
218
220
|
)
|
|
219
221
|
```
|
|
220
222
|
|
|
221
|
-
The same staged handle may instead be consumed by one safe update
|
|
222
|
-
module:
|
|
223
|
+
The same staged handle may instead be consumed by one safe update finalizer. For example, add a new file field to an
|
|
224
|
+
existing form module:
|
|
223
225
|
|
|
224
226
|
```python
|
|
225
227
|
from matelab import RecordFormAttachmentFieldAddition
|
|
@@ -228,30 +230,124 @@ from matelab import RecordFormAttachmentFieldAddition
|
|
|
228
230
|
result = await client.records.update(
|
|
229
231
|
source,
|
|
230
232
|
attachment_changes=(
|
|
231
|
-
RecordFormAttachmentFieldAddition(
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
233
|
+
RecordFormAttachmentFieldAddition(module="Attachments", name="Measurement", attachment=staged),
|
|
234
|
+
),
|
|
235
|
+
)
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Choose exactly one finalizer. A staged name may occur once in either a single-record import or one update operation;
|
|
239
|
+
do not reuse it after transport starts, even when the Provider outcome is unknown. A locally rejected target or
|
|
240
|
+
argument does not spend the handle. The SDK rejects raw Provider attachment references, cross-user or cross-notebook
|
|
241
|
+
handles, unsafe combinations, duplicate use in one request, and a second finalization attempt through the same
|
|
242
|
+
client.
|
|
243
|
+
|
|
244
|
+
The Provider supplies no staging status, abort, TTL, atomicity, idempotency, or retry guarantee. Integrations that
|
|
245
|
+
persist handles must durably claim their own `uploaded -> finalizing/indeterminate` transition before calling a
|
|
246
|
+
finalizer.
|
|
247
|
+
|
|
248
|
+
### Attachment-bearing record update boundary
|
|
249
|
+
|
|
250
|
+
The v0.1.2 Contract adds dedicated, verified notebook-staged update shapes. They remain public intent objects; callers
|
|
251
|
+
never construct Provider paths or attachment strings:
|
|
252
|
+
|
|
253
|
+
```python
|
|
254
|
+
from matelab import RecordFilesAttachmentRootAppend, RecordTableFileCellSet, RecordTableRowAttachmentAppend
|
|
255
|
+
|
|
256
|
+
# Each example is a separate finalizer; never run several with the same handle.
|
|
257
|
+
await client.records.update(
|
|
258
|
+
source,
|
|
259
|
+
attachment_changes=(RecordTableFileCellSet(table="Measurements", column="Evidence", row=0, attachment=staged),),
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
await client.records.update(
|
|
263
|
+
source,
|
|
264
|
+
attachment_changes=(
|
|
265
|
+
RecordTableRowAttachmentAppend(
|
|
266
|
+
table="Measurements", file_column="Evidence", values={"Label": "Sample C"}, attachment=another_staged
|
|
235
267
|
),
|
|
236
268
|
),
|
|
237
269
|
)
|
|
270
|
+
|
|
271
|
+
await client.records.update(
|
|
272
|
+
source,
|
|
273
|
+
attachment_changes=(RecordFilesAttachmentRootAppend(module="Files", caption="Evidence", attachment=third_staged),),
|
|
274
|
+
)
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
`RecordTableFileCellSet` requires an existing file column and a cell whose immediate canonical value is exactly
|
|
278
|
+
`null`. The row-append intent addresses the new row by the row count from the SDK's immediate read and supports one
|
|
279
|
+
staged file column. Files append is root-only and requires a string caption.
|
|
280
|
+
|
|
281
|
+
Replacement starts with an occurrence returned by `records.read()`; applications must not fabricate a
|
|
282
|
+
`RecordAttachmentRef`:
|
|
283
|
+
|
|
284
|
+
```python
|
|
285
|
+
from matelab import RecordFilesAttachmentReplacement
|
|
286
|
+
|
|
287
|
+
record = await client.records.read(notebook=source.notebook, record=source.record)
|
|
288
|
+
existing = next(
|
|
289
|
+
attachment
|
|
290
|
+
for attachment in record.attachments
|
|
291
|
+
if attachment.location is not None and attachment.location.kind == "files_module"
|
|
292
|
+
)
|
|
293
|
+
await client.records.update(
|
|
294
|
+
source, attachment_changes=(RecordFilesAttachmentReplacement(existing=existing, replacement=staged, caption=None),)
|
|
295
|
+
)
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
For a files occurrence, `caption=None` preserves the observed string caption (an observed null caption normalizes to
|
|
299
|
+
the required empty string). The update preserves the Provider `uid` and folder in the submitted content. Table
|
|
300
|
+
replacement requires exactly one current attachment in the selected cell and uses
|
|
301
|
+
`RecordTableFileAttachmentReplacement`. Both replacement forms require the new hash to differ.
|
|
302
|
+
|
|
303
|
+
Row/index-based finalizers use an immediate SDK read and must not be called while a concurrent editor is known to be
|
|
304
|
+
active. The Provider offers no expected hash or revision. `expected_content_sha256` is only a client-side prewrite
|
|
305
|
+
check, not Provider compare-and-swap.
|
|
306
|
+
|
|
307
|
+
File-bearing structure changes do not consume staging:
|
|
308
|
+
|
|
309
|
+
```python
|
|
310
|
+
from matelab import RecordFormFileFieldDeletion, RecordTableFileColumnAddition, RecordTableFileColumnDeletion
|
|
311
|
+
|
|
312
|
+
await client.records.update(
|
|
313
|
+
source,
|
|
314
|
+
file_structure_changes=(
|
|
315
|
+
RecordTableFileColumnAddition(table="Measurements", name="Additional evidence"),
|
|
316
|
+
RecordFormFileFieldDeletion(form="Attachments", name="Obsolete evidence"),
|
|
317
|
+
RecordTableFileColumnDeletion(table="Measurements", name="Old evidence"),
|
|
318
|
+
),
|
|
319
|
+
)
|
|
238
320
|
```
|
|
239
321
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
322
|
+
The SDK emits the v0.1.2 dedicated operations: a new file column omits wire `data`; whole
|
|
323
|
+
form-field/table-column deletion uses a strict two-segment target. It does not expose a three-segment delete as
|
|
324
|
+
table-cell clearing.
|
|
325
|
+
|
|
326
|
+
Deleting a complete canonical module is also supported even when the current read observes attachments in it:
|
|
327
|
+
|
|
328
|
+
```python
|
|
329
|
+
result = await client.records.update(
|
|
330
|
+
source, module_deletions=("Raw files",), expected_content_sha256=record.content_sha256
|
|
331
|
+
)
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
This requests only a canonical record-content mutation. A successful response acknowledges the submitted mutation;
|
|
335
|
+
it does not prove attachment-quote cleanup or byte deletion. Collaboration-pending is reported as
|
|
336
|
+
`"pending_browser_save"`, not persisted.
|
|
337
|
+
|
|
338
|
+
Notebook-staged rich-text binding remains unsupported. Provider Verification shows that `#file{name}` is stored as
|
|
339
|
+
plain text and that a hash-based URI can fall back to an existing quote, so neither is a one-shot staged finalizer.
|
|
340
|
+
Existing `RecordRichTextUpdate` remains legal only with record-scoped `StagedRecordAttachment`. Attachment-bearing
|
|
341
|
+
multi-record import is also forbidden; callers must split it into single-record finalizers.
|
|
246
342
|
|
|
247
343
|
## Implementation roadmap
|
|
248
344
|
|
|
249
345
|
`docs/roadmap.md` is the complete SDK-only execution plan. It assigns all 71
|
|
250
|
-
`matelab-spec v0.
|
|
346
|
+
`matelab-spec v0.3.0` operations to ordered work packages, defines the machine-readable coverage that
|
|
251
347
|
must be added, records Provider-risk gates, and specifies the final completion checks.
|
|
252
348
|
|
|
253
|
-
The SDK
|
|
254
|
-
migration, adjacent-repository changes, external
|
|
349
|
+
The SDK tracks all 71 Contract operations and exposes 70 through public domain interfaces; one identity-bootstrap
|
|
350
|
+
operation is intentionally unexposed. It deliberately excludes MCP migration, adjacent-repository changes, external
|
|
255
351
|
publishing, and automatic mutation against a real Provider.
|
|
256
352
|
|
|
257
353
|
Machine-readable status lives in
|
|
@@ -261,7 +357,7 @@ OpenAPI snapshot.
|
|
|
261
357
|
|
|
262
358
|
| Domain | Implemented | Planned | Current public surface |
|
|
263
359
|
|---|---:|---:|---|
|
|
264
|
-
| Authentication |
|
|
360
|
+
| Authentication | 3 | 0 | `authenticate`, `refresh`, `exchange_chat_sso_code`; identity bootstrap intentionally unexposed |
|
|
265
361
|
| Groups and users | 2 | 0 | `groups.list`, `users.search` |
|
|
266
362
|
| Notebooks | 6 | 0 | `notebooks.list/create/update/shares/share/update_share/unshare` |
|
|
267
363
|
| Records | 18 | 0 | Discovery, reads, lifecycle, typed patch/attachments, relations, and downloads |
|
|
@@ -270,20 +366,21 @@ OpenAPI snapshot.
|
|
|
270
366
|
| File staging | 1 | 0 | Resumable fragment staging and compensating abort request |
|
|
271
367
|
| Literature | 13 | 0 | Libraries, items, canonical metadata, comments, sharing, PDF lifecycle and streaming |
|
|
272
368
|
| Cloud drive | 9 | 0 | Personal root/folders/files, staged binding, metadata, move/delete and streaming |
|
|
273
|
-
| **Total** | **
|
|
369
|
+
| **Total** | **70** | **0** | One operation is intentionally unexposed |
|
|
274
370
|
|
|
275
371
|
### Stability and known capability limits
|
|
276
372
|
|
|
277
|
-
Coverage currently contains
|
|
278
|
-
|
|
373
|
+
Coverage currently contains 14 `stable`, 56 `experimental`, and one `not_applicable` operation. The stable operation
|
|
374
|
+
IDs are `loginTokenSet`, `refreshTokenSet`, `exchangeChatSsoCode`,
|
|
279
375
|
`shareMultipleTemplatesWithUsers`, `removeTemplateFromGroup`, `deleteNotebookShare`, `listNotebooks`,
|
|
280
376
|
`listNotebookRecords`, `exportRecords`, `deleteRecordsByUid`, `copyRecord`, `readRecord`,
|
|
281
377
|
`deletePersonalLiteratureItem`, and `readLiteratureCreateTemplate`.
|
|
282
378
|
|
|
283
379
|
Every other implemented operation is explicitly `experimental`; the exact per-operation list and its
|
|
284
380
|
PVD/PCG references live in
|
|
285
|
-
`docs/operation-coverage.yaml`.
|
|
286
|
-
|
|
381
|
+
`docs/operation-coverage.yaml`. `resolveCurrentIdentity` is intentionally unexposed because the SDK accepts only
|
|
382
|
+
complete, integration-validated Sessions and does not bind bare external token pairs. There are no `planned`
|
|
383
|
+
operations. Experimental support means the SDK validates and exposes the
|
|
287
384
|
pinned Contract while preserving limitations such as unstable ordering/pagination, incomplete mutation
|
|
288
385
|
acknowledgements, missing batch atomicity or idempotency, weak attachment ownership binding, and known
|
|
289
386
|
Provider authorization gaps. It does not turn those limitations into SDK guarantees.
|
|
@@ -298,10 +395,8 @@ session = await client.exchange_chat_sso_code(code="chat-sanitizedcode123", key=
|
|
|
298
395
|
Group and user discovery expose sharing identities without inventing Provider pagination:
|
|
299
396
|
|
|
300
397
|
```python
|
|
301
|
-
from matelab import UserSearchScope
|
|
302
|
-
|
|
303
398
|
groups = await client.groups.list()
|
|
304
|
-
targets = await client.users.search("Example Researcher",
|
|
399
|
+
targets = await client.users.search("Example Researcher", global_scope=False)
|
|
305
400
|
```
|
|
306
401
|
|
|
307
402
|
Ordering remains Provider-unspecified and is documented rather than repeated as a constant result field. Group
|
|
@@ -309,19 +404,18 @@ members belong only to `groups.members_for`, not to every returned group. These
|
|
|
309
404
|
Provider returns members for an unstable first group and user search is unpaged, unordered, and not field-minimized
|
|
310
405
|
(PVD-006, PVD-029, PCG-011).
|
|
311
406
|
|
|
312
|
-
Notebook create/update and direct sharing
|
|
407
|
+
Notebook create/update and direct sharing use the Provider acknowledgement without an automatic follow-up read:
|
|
313
408
|
|
|
314
409
|
```python
|
|
315
410
|
await client.notebooks.create(title="Example Notebook")
|
|
316
|
-
shares = await client.notebooks.
|
|
317
|
-
|
|
411
|
+
shares = await client.notebooks.shares(notebook)
|
|
412
|
+
await client.notebooks.share(notebook, [target.ref])
|
|
413
|
+
await client.notebooks.update_share(shares.shares[0].ref, write=True, create=True)
|
|
318
414
|
```
|
|
319
415
|
|
|
320
|
-
Create
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
or `None`. A stored share mask of zero still has effective read access (PVD-010), and share-list order remains
|
|
324
|
-
unspecified.
|
|
416
|
+
Create, update, share, permission update, and unshare return `None` because their Provider responses contain no new
|
|
417
|
+
resource representation. Call `list()` or `shares()` explicitly when the application needs current state. A stored
|
|
418
|
+
share mask of zero still has effective read access (PVD-010), and share-list order remains unspecified.
|
|
325
419
|
|
|
326
420
|
Template discovery keeps a template database identity separate from direct-share, market-acquisition, and group
|
|
327
421
|
relation identities:
|
|
@@ -339,9 +433,9 @@ experimental because Provider discovery ordering/pagination and historical `imag
|
|
|
339
433
|
stable (PCG-003, PCG-009, PVD-013, PVD-022).
|
|
340
434
|
|
|
341
435
|
Template writes remain separate operations: metadata, canonical modules, and usage HTML are not presented as one
|
|
342
|
-
transaction. Metadata create returns the
|
|
343
|
-
return `None` because the Provider supplies no new identity or
|
|
344
|
-
|
|
436
|
+
transaction. Metadata create/update returns the `TemplateRef` built from the Provider template ID. Other mutations
|
|
437
|
+
return `None` because the Provider supplies no new identity or resource representation; callers can explicitly list
|
|
438
|
+
or read when they need current state.
|
|
345
439
|
Direct-share, market-acquisition, and group relation refs remain distinct. Marketplace revision and uploader-binding
|
|
346
440
|
limitations are documented operation semantics rather than constant fields on every result (PVD-021, PVD-026).
|
|
347
441
|
`UploadBindingRef.new()` creates the fresh hidden correlation value required by intro attachment binding.
|
|
@@ -367,43 +461,40 @@ remain unspecified, and no continuation token is invented.
|
|
|
367
461
|
Record creation keeps blank creation and structured import as separate capabilities:
|
|
368
462
|
|
|
369
463
|
```python
|
|
370
|
-
from matelab import RecordImportItem
|
|
464
|
+
from matelab import RecordImportItem
|
|
371
465
|
|
|
372
|
-
|
|
466
|
+
await client.records.create_blank(notebook=notebook, title="Blank Record", record_uid="caller-generated-uid")
|
|
373
467
|
imported = await client.records.import_dataset(
|
|
374
468
|
notebook=notebook,
|
|
375
|
-
template=TemplateRef(template_id=8),
|
|
376
469
|
template_title="Example Template",
|
|
377
470
|
items=[RecordImportItem(record_uid="import-uid", title="Imported", data={"Notes": "value"})],
|
|
378
471
|
)
|
|
379
472
|
```
|
|
380
473
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
row with the same UID but a different database ID as proof of success (PCG-005). Record mutations are not
|
|
386
|
-
automatically retried.
|
|
474
|
+
Blank-record creation returns `None` because the Provider returns no record identity, even when the caller supplies a
|
|
475
|
+
UID. Import validates the complete batch with generated wire models but cannot map returned database IDs to
|
|
476
|
+
individual inputs or promise atomicity (PCG-008). Delete means moving records into the recycle bin, not permanent
|
|
477
|
+
deletion. Delete and restore return `None`; record mutations are not automatically retried.
|
|
387
478
|
|
|
388
479
|
Record patching exposes a deliberately narrower capability than the raw Provider operation. Scalar/module changes
|
|
389
480
|
cannot smuggle Provider-native attachment strings; staged attachments use separate form-removal, table-replacement,
|
|
390
481
|
files append/replace/remove, and rich-text types. Unsafe form replacement and table-file removal are absent, while a
|
|
391
482
|
files/images removal is rejected when the observed module contains the same hash more than once (PVD-014 through
|
|
392
483
|
PVD-016). `Record.content_sha256` can be supplied as a client-side precondition, documented as advisory
|
|
393
|
-
read-before-write rather than Provider CAS. `
|
|
394
|
-
|
|
395
|
-
remain distinct, and mutation
|
|
484
|
+
read-before-write rather than Provider CAS. `records.update()` returns the acknowledgement classification
|
|
485
|
+
`"provider_reported_persisted"`, `"pending_browser_save"`, or `"provider_acknowledged_unclassified"`; it does not
|
|
486
|
+
issue a post-write read. Database, active-browser, and unclassified acknowledgements remain distinct, and mutation
|
|
487
|
+
retries stay disabled.
|
|
396
488
|
|
|
397
489
|
Relation addition reads both endpoints and checks their resolved data server before writing; this reduces PVD-019
|
|
398
|
-
risk but is not an atomic Provider authorization guarantee. Relation
|
|
399
|
-
|
|
400
|
-
|
|
490
|
+
risk but is not an atomic Provider authorization guarantee. Relation deletion refuses an observed cross-notebook
|
|
491
|
+
target-ID collision because the Provider ignores target notebook identity (PVD-020). Both mutations return `None`
|
|
492
|
+
after acknowledgement.
|
|
401
493
|
|
|
402
494
|
Comment upload follows the Provider's literal one-request `upload` field, not the incompatible Front fragment
|
|
403
|
-
protocol (PVD-037). Comment
|
|
404
|
-
comment is
|
|
405
|
-
|
|
406
|
-
binding remains affected by PVD-026.
|
|
495
|
+
protocol (PVD-037). Comment mutations return `None` after acknowledgement. Edit and delete first verify that the
|
|
496
|
+
selected comment is currently observed and caller-owned, but do not perform a post-write read (PVD-004). Staged
|
|
497
|
+
comment attachments have no Contract abort operation, and binding remains affected by PVD-026.
|
|
407
498
|
|
|
408
499
|
Attachment bytes are streamed and must be consumed or closed explicitly:
|
|
409
500
|
|
|
@@ -465,10 +556,10 @@ await client.literature.create(LiteratureMetadata(title="Example import", doi="1
|
|
|
465
556
|
|
|
466
557
|
Create returns `None` and never guesses the new item from list position because the Provider returns no ID. Canonical update reads the
|
|
467
558
|
item first and refuses to drop source/hidden fields unless `allow_source_metadata_loss=True` is explicit (PVD-027).
|
|
468
|
-
PDF replace/delete are separate
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
559
|
+
PDF replace/delete are separate acknowledged mutations and are not presented as atomic with metadata (PCG-010).
|
|
560
|
+
Permanent personal deletion is named `permanently_delete`, returns `None`, and is non-recoverable. Sharing requires
|
|
561
|
+
list-observed item summaries, user-search summaries and a
|
|
562
|
+
valid caller identity, then returns `None` because the Provider supplies no per-recipient IDs (PVD-012, PVD-036).
|
|
472
563
|
|
|
473
564
|
Literature comments use one public save intent: detail is read first, an existing caller-owned comment is edited, and
|
|
474
565
|
otherwise a comment is created. Multiple caller-owned comments are rejected as ambiguous (PVD-035). A staged
|
|
@@ -482,13 +573,8 @@ The personal cloud-drive surface keeps root, folder, final file and temporary st
|
|
|
482
573
|
|
|
483
574
|
```python
|
|
484
575
|
listing = await client.cloud_drive.list()
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
if len(matches) == 1:
|
|
489
|
-
renamed = await client.cloud_drive.update_file(
|
|
490
|
-
matches[0], filename="example-renamed.pdf", description="Sanitized description"
|
|
491
|
-
)
|
|
576
|
+
folder = await client.cloud_drive.create_folder(name="Example data")
|
|
577
|
+
await client.cloud_drive.bind_staged_file(staged, target=folder)
|
|
492
578
|
```
|
|
493
579
|
|
|
494
580
|
`CloudDriveListing` contains a typed file page, complete folder snapshot, quota usage and personal-root permissions rather
|
|
@@ -496,14 +582,11 @@ than flattening them into one ambiguous collection. Folder browse results retain
|
|
|
496
582
|
are explicitly root-wide and return `location=None` because the Provider omits each match's folder ID. Ordering has
|
|
497
583
|
no stable ID tie-breaker (PCG-003, PVD-013).
|
|
498
584
|
|
|
499
|
-
Folder create returns the Provider ID
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
Batch move returns identities observed in the target, while permanent delete returns identities confirmed absent;
|
|
505
|
-
neither claims Provider per-item results or atomicity. Permanent deletion is named `permanently_delete_files` and is
|
|
506
|
-
non-recoverable. Cloud downloads
|
|
585
|
+
Folder create returns a `CloudFolderRef` built from the Provider ID. Other folder and file mutations return `None`
|
|
586
|
+
after acknowledgement and do not automatically list the drive. Staged finalize accepts a completed `StagedFile`;
|
|
587
|
+
the Provider binds by temporary row ID without checking its owner (PVD-031), and finalize atomicity/idempotency remain
|
|
588
|
+
absent (PCG-012). Batch move and permanent delete do not claim Provider per-item results or atomicity. Permanent
|
|
589
|
+
deletion is named `permanently_delete_files` and is non-recoverable. Cloud downloads
|
|
507
590
|
resolve bytes from the final file identity and reuse `DownloadStream`, thumbnail/preview choices and the PVD-002-safe
|
|
508
591
|
range subset. Cloud mutations are not automatically retried.
|
|
509
592
|
|
|
@@ -520,8 +603,8 @@ Provider identifiers distinct. Historical reads first re-read the authorized cur
|
|
|
520
603
|
that the requested version is still present in its `modify_log`; both reads write Provider audit entries.
|
|
521
604
|
|
|
522
605
|
Errors are separated into Provider business errors, authentication errors, HTTP/transport errors,
|
|
523
|
-
Integration Contract response errors, and client-side usage errors.
|
|
524
|
-
|
|
606
|
+
Integration Contract response errors, and client-side usage errors. Provider response bodies and caller inputs do not
|
|
607
|
+
enter exceptions.
|
|
525
608
|
|
|
526
609
|
## Development
|
|
527
610
|
|
|
@@ -552,9 +635,9 @@ Node wheel retain the compatible exact pair `basedpyright==1.39.9` and
|
|
|
552
635
|
## Opt-in Provider consumer smoke
|
|
553
636
|
|
|
554
637
|
`tests/provider/test_provider_smoke.py` exercises the consumer flow through only the public SDK interface. Its base
|
|
555
|
-
scenario covers credential authentication,
|
|
556
|
-
`AsyncMatelab
|
|
557
|
-
|
|
638
|
+
scenario covers credential authentication, persistence of the complete returned Session, restoration through a new
|
|
639
|
+
`AsyncMatelab` instance, refresh with identity preservation, and notebook discovery. It is not Provider Verification
|
|
640
|
+
and is skipped by default.
|
|
558
641
|
|
|
559
642
|
Raw Provider conformance remains the responsibility of `matelab-spec`, which sends direct HTTP requests and validates
|
|
560
643
|
the unmodified responses. The SDK does not repeat its route-by-route, cross-account, sharing, or attachment-isolation
|
|
@@ -562,9 +645,9 @@ verification. Representative SDK adapter tests instead feed the pinned OpenAPI's
|
|
|
562
645
|
`MockTransport` and assert the resulting public values; synthetic fixtures remain where SDK-specific encoding,
|
|
563
646
|
error, retry, and compatibility boundaries require evidence beyond those examples.
|
|
564
647
|
|
|
565
|
-
Provider smoke is restricted to the confirmed isolated test service. Authentication and
|
|
566
|
-
|
|
567
|
-
|
|
648
|
+
Provider smoke is restricted to the confirmed isolated test service. Authentication and refresh persist Provider
|
|
649
|
+
token state. This side effect is inherent to the tested Provider operations; it cannot be disabled by a test setting.
|
|
650
|
+
Explicitly loading `.env.test` and selecting the `provider` marker is the opt-in for this flow.
|
|
568
651
|
|
|
569
652
|
Copy `.env.example` to the git-ignored local `.env.test`, then fill in the shared Provider connection settings:
|
|
570
653
|
|
|
@@ -573,15 +656,24 @@ Copy `.env.example` to the git-ignored local `.env.test`, then fill in the share
|
|
|
573
656
|
- `MATELAB_PROVIDER_PASSWORD`
|
|
574
657
|
|
|
575
658
|
These names intentionally match `matelab-spec` Provider Verification. The isolated target may copy them from the spec
|
|
576
|
-
`.env` into this repository's `.env.test`.
|
|
577
|
-
|
|
659
|
+
`.env` into this repository's `.env.test`. Refreshing the restored Session must preserve its authenticated identity.
|
|
660
|
+
With the environment prepared:
|
|
578
661
|
|
|
579
662
|
```bash
|
|
580
663
|
uv run --env-file .env.test pytest -m provider tests/provider/test_provider_smoke.py
|
|
581
664
|
```
|
|
582
665
|
|
|
583
|
-
|
|
584
|
-
|
|
666
|
+
This command runs only the SDK public-interface smoke; it is not the 71-operation Provider Verification. To reuse the
|
|
667
|
+
same `.env.test` for the complete Contract suite, also populate the optional share user, secondary account, record
|
|
668
|
+
staging opt-in, and Chat SSO settings documented in `.env.example`, then run from sibling checkouts:
|
|
669
|
+
|
|
670
|
+
```bash
|
|
671
|
+
cd ../matelab-spec
|
|
672
|
+
uv run --env-file ../matelab-python-sdk/.env.test pytest
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
The files are never loaded implicitly, so normal test runs remain safely skipped. Do not use either flow against
|
|
676
|
+
production, and never commit Provider credentials.
|
|
585
677
|
|
|
586
678
|
## Reproducible release build
|
|
587
679
|
|
|
@@ -599,6 +691,6 @@ uv run python scripts/check_release.py dist/release/*.whl dist/release/*.tar.gz
|
|
|
599
691
|
```
|
|
600
692
|
|
|
601
693
|
Rebuilding the same commit with the same locked environment and `SOURCE_DATE_EPOCH` must produce
|
|
602
|
-
byte-identical wheel and source distribution hashes. The release is bound to `matelab-spec v0.
|
|
603
|
-
commit `
|
|
604
|
-
`
|
|
694
|
+
byte-identical wheel and source distribution hashes. The release is bound to `matelab-spec v0.3.0`,
|
|
695
|
+
commit `0b5612588708b4639a42c7983ee2f08c350994bf`, and OpenAPI SHA-256
|
|
696
|
+
`ebf5d446e3a5866d773cf8cfffe1db28f27015635fbb621bc091673e865ef70e`.
|