matelab-python-sdk 0.2.0a8__tar.gz → 0.2.0a10__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.2.0a10/.agents/skills/matelab-sdk-release/SKILL.md +89 -0
- matelab_python_sdk-0.2.0a10/.agents/skills/matelab-sdk-release/agents/openai.yaml +4 -0
- matelab_python_sdk-0.2.0a10/.agents/skills/matelab-sdk-release/scripts/verify_pypi.py +165 -0
- {matelab_python_sdk-0.2.0a8 → matelab_python_sdk-0.2.0a10}/CONTEXT.md +2 -2
- {matelab_python_sdk-0.2.0a8 → matelab_python_sdk-0.2.0a10}/PKG-INFO +1 -1
- {matelab_python_sdk-0.2.0a8 → matelab_python_sdk-0.2.0a10}/contracts/matelab-integration-v1.lock.json +3 -3
- {matelab_python_sdk-0.2.0a8 → matelab_python_sdk-0.2.0a10}/contracts/matelab-integration-v1.openapi.yaml +80 -30
- {matelab_python_sdk-0.2.0a8 → matelab_python_sdk-0.2.0a10}/docs/api.md +55 -23
- {matelab_python_sdk-0.2.0a8 → matelab_python_sdk-0.2.0a10}/docs/operation-coverage.yaml +1 -1
- {matelab_python_sdk-0.2.0a8 → matelab_python_sdk-0.2.0a10}/pyproject.toml +1 -1
- {matelab_python_sdk-0.2.0a8 → matelab_python_sdk-0.2.0a10}/src/matelab/_generated/models.py +4 -4
- {matelab_python_sdk-0.2.0a8 → matelab_python_sdk-0.2.0a10}/src/matelab/domains/records.py +49 -22
- {matelab_python_sdk-0.2.0a8 → matelab_python_sdk-0.2.0a10}/src/matelab/domains/uploads.py +2 -2
- {matelab_python_sdk-0.2.0a8 → matelab_python_sdk-0.2.0a10}/tests/conftest.py +13 -1
- {matelab_python_sdk-0.2.0a8 → matelab_python_sdk-0.2.0a10}/tests/test_record_lifecycle.py +101 -4
- {matelab_python_sdk-0.2.0a8 → matelab_python_sdk-0.2.0a10}/tests/test_record_mutations.py +143 -1
- {matelab_python_sdk-0.2.0a8 → matelab_python_sdk-0.2.0a10}/tests/test_uploads_and_literature.py +1 -1
- {matelab_python_sdk-0.2.0a8 → matelab_python_sdk-0.2.0a10}/tests/typing/consumer.py +16 -0
- {matelab_python_sdk-0.2.0a8 → matelab_python_sdk-0.2.0a10}/uv.lock +1 -1
- {matelab_python_sdk-0.2.0a8 → matelab_python_sdk-0.2.0a10}/.env.example +0 -0
- {matelab_python_sdk-0.2.0a8 → matelab_python_sdk-0.2.0a10}/.github/workflows/release.yml +0 -0
- {matelab_python_sdk-0.2.0a8 → matelab_python_sdk-0.2.0a10}/.gitignore +0 -0
- {matelab_python_sdk-0.2.0a8 → matelab_python_sdk-0.2.0a10}/AGENTS.md +0 -0
- {matelab_python_sdk-0.2.0a8 → matelab_python_sdk-0.2.0a10}/LICENSE +0 -0
- {matelab_python_sdk-0.2.0a8 → matelab_python_sdk-0.2.0a10}/NOTICE +0 -0
- {matelab_python_sdk-0.2.0a8 → matelab_python_sdk-0.2.0a10}/README.md +0 -0
- {matelab_python_sdk-0.2.0a8 → matelab_python_sdk-0.2.0a10}/docs/agent-architecture.md +0 -0
- {matelab_python_sdk-0.2.0a8 → matelab_python_sdk-0.2.0a10}/docs/agent-workflows.md +0 -0
- {matelab_python_sdk-0.2.0a8 → matelab_python_sdk-0.2.0a10}/docs/development.md +0 -0
- {matelab_python_sdk-0.2.0a8 → matelab_python_sdk-0.2.0a10}/scripts/check_installed_package.py +0 -0
- {matelab_python_sdk-0.2.0a8 → matelab_python_sdk-0.2.0a10}/scripts/check_release.py +0 -0
- {matelab_python_sdk-0.2.0a8 → matelab_python_sdk-0.2.0a10}/scripts/generate_models.py +0 -0
- {matelab_python_sdk-0.2.0a8 → matelab_python_sdk-0.2.0a10}/src/matelab/__init__.py +0 -0
- {matelab_python_sdk-0.2.0a8 → matelab_python_sdk-0.2.0a10}/src/matelab/_generated/__init__.py +0 -0
- {matelab_python_sdk-0.2.0a8 → matelab_python_sdk-0.2.0a10}/src/matelab/_hashes.py +0 -0
- {matelab_python_sdk-0.2.0a8 → matelab_python_sdk-0.2.0a10}/src/matelab/_transport.py +0 -0
- {matelab_python_sdk-0.2.0a8 → matelab_python_sdk-0.2.0a10}/src/matelab/_wire_validation.py +0 -0
- {matelab_python_sdk-0.2.0a8 → matelab_python_sdk-0.2.0a10}/src/matelab/client.py +0 -0
- {matelab_python_sdk-0.2.0a8 → matelab_python_sdk-0.2.0a10}/src/matelab/domains/__init__.py +0 -0
- {matelab_python_sdk-0.2.0a8 → matelab_python_sdk-0.2.0a10}/src/matelab/domains/_attachment_content.py +0 -0
- {matelab_python_sdk-0.2.0a8 → matelab_python_sdk-0.2.0a10}/src/matelab/domains/cloud_drive.py +0 -0
- {matelab_python_sdk-0.2.0a8 → matelab_python_sdk-0.2.0a10}/src/matelab/domains/groups.py +0 -0
- {matelab_python_sdk-0.2.0a8 → matelab_python_sdk-0.2.0a10}/src/matelab/domains/literature.py +0 -0
- {matelab_python_sdk-0.2.0a8 → matelab_python_sdk-0.2.0a10}/src/matelab/domains/notebooks.py +0 -0
- {matelab_python_sdk-0.2.0a8 → matelab_python_sdk-0.2.0a10}/src/matelab/domains/templates.py +0 -0
- {matelab_python_sdk-0.2.0a8 → matelab_python_sdk-0.2.0a10}/src/matelab/domains/users.py +0 -0
- {matelab_python_sdk-0.2.0a8 → matelab_python_sdk-0.2.0a10}/src/matelab/errors.py +0 -0
- {matelab_python_sdk-0.2.0a8 → matelab_python_sdk-0.2.0a10}/src/matelab/py.typed +0 -0
- {matelab_python_sdk-0.2.0a8 → matelab_python_sdk-0.2.0a10}/src/matelab/session.py +0 -0
- {matelab_python_sdk-0.2.0a8 → matelab_python_sdk-0.2.0a10}/src/matelab/streaming.py +0 -0
- {matelab_python_sdk-0.2.0a8 → matelab_python_sdk-0.2.0a10}/tests/provider/test_provider_smoke.py +0 -0
- {matelab_python_sdk-0.2.0a8 → matelab_python_sdk-0.2.0a10}/tests/test_async_attachment_upload.py +0 -0
- {matelab_python_sdk-0.2.0a8 → matelab_python_sdk-0.2.0a10}/tests/test_client.py +0 -0
- {matelab_python_sdk-0.2.0a8 → matelab_python_sdk-0.2.0a10}/tests/test_cloud_drive.py +0 -0
- {matelab_python_sdk-0.2.0a8 → matelab_python_sdk-0.2.0a10}/tests/test_comments_and_downloads.py +0 -0
- {matelab_python_sdk-0.2.0a8 → matelab_python_sdk-0.2.0a10}/tests/test_consumer_smoke.py +0 -0
- {matelab_python_sdk-0.2.0a8 → matelab_python_sdk-0.2.0a10}/tests/test_errors.py +0 -0
- {matelab_python_sdk-0.2.0a8 → matelab_python_sdk-0.2.0a10}/tests/test_extended_records.py +0 -0
- {matelab_python_sdk-0.2.0a8 → matelab_python_sdk-0.2.0a10}/tests/test_generation.py +0 -0
- {matelab_python_sdk-0.2.0a8 → matelab_python_sdk-0.2.0a10}/tests/test_groups.py +0 -0
- {matelab_python_sdk-0.2.0a8 → matelab_python_sdk-0.2.0a10}/tests/test_notebook_lifecycle.py +0 -0
- {matelab_python_sdk-0.2.0a8 → matelab_python_sdk-0.2.0a10}/tests/test_notebooks.py +0 -0
- {matelab_python_sdk-0.2.0a8 → matelab_python_sdk-0.2.0a10}/tests/test_operation_coverage.py +0 -0
- {matelab_python_sdk-0.2.0a8 → matelab_python_sdk-0.2.0a10}/tests/test_records.py +0 -0
- {matelab_python_sdk-0.2.0a8 → matelab_python_sdk-0.2.0a10}/tests/test_release.py +0 -0
- {matelab_python_sdk-0.2.0a8 → matelab_python_sdk-0.2.0a10}/tests/test_streaming.py +0 -0
- {matelab_python_sdk-0.2.0a8 → matelab_python_sdk-0.2.0a10}/tests/test_template_lifecycle.py +0 -0
- {matelab_python_sdk-0.2.0a8 → matelab_python_sdk-0.2.0a10}/tests/test_templates.py +0 -0
- {matelab_python_sdk-0.2.0a8 → matelab_python_sdk-0.2.0a10}/tests/test_transport.py +0 -0
- {matelab_python_sdk-0.2.0a8 → matelab_python_sdk-0.2.0a10}/tests/test_users.py +0 -0
- {matelab_python_sdk-0.2.0a8 → matelab_python_sdk-0.2.0a10}/tests/typing/pyproject.toml +0 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: matelab-sdk-release
|
|
3
|
+
description: 完成 matelab-python-sdk 的 spec 增量同步、必要适配、完整验证、审查和 PyPI 发布。用于该项目的完整交付;仅同步、验证或提交推送时遵守用户限定的终点。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Matelab SDK 完整交付
|
|
7
|
+
|
|
8
|
+
在包含本 skill 的 `matelab-python-sdk` 仓库根目录执行。
|
|
9
|
+
以下项目路径均相对此仓库。先读适用 `AGENTS.md`;具体规范以仓库当前文件为准。
|
|
10
|
+
|
|
11
|
+
## 调用范围
|
|
12
|
+
|
|
13
|
+
- 用户要求执行 `$matelab-sdk-release` 且未限制阶段时,完成同步、适配、测试、审查、提交、推送、tag 和
|
|
14
|
+
PyPI 发布;可附目标版本。不重复索取已覆盖目标和副作用的授权。
|
|
15
|
+
- “只同步”“只验证”“仅提交推送”等限制优先,停在相应阶段;解释、制作或修改本技能不执行发布。
|
|
16
|
+
- 待发布内容包括相对上次已发布 tag 的提交及工作区改动,不能仅凭工作区干净就认定无变更。
|
|
17
|
+
无待发布内容且当前版本已发布时,核对状态后结束;目标 tag 已推送时,优先继续其 CI/PyPI 核验。
|
|
18
|
+
- 依赖更新仅在用户要求时进行。真实业务 mutation、生产访问及新增发布目标不属于默认范围。
|
|
19
|
+
|
|
20
|
+
## 1. 定界与 spec 增量
|
|
21
|
+
|
|
22
|
+
先检查 status、当前版本、Contract lock、最近发布 tag 和实际 remotes,保留用户已有改动。
|
|
23
|
+
按 `docs/agent-workflows.md#contract-adoption` 选择新的远端 annotated spec tag,固定 tag object、peeled commit、
|
|
24
|
+
snapshot 和 SHA-256。
|
|
25
|
+
|
|
26
|
+
比较相对当前 pin 的 request、response、security、examples、成功码及 PVD/PCG,只适配本次增量。
|
|
27
|
+
即使仅 description/examples 改变,也检查现有 public 输入能否表达新能力。专用 schema 与顶层请求可能服务不同
|
|
28
|
+
流程,先核对适用范围,再判断是否确有契约矛盾;把 SDK 尚未实现的能力如实归为 SDK 缺项。
|
|
29
|
+
|
|
30
|
+
从 release commit 复制快照、更新 lock、运行生成脚本,审查生成差异;同步受影响的 adapter、public 类型、测试、
|
|
31
|
+
coverage 和当前态文档。公开接口和生命周期沿 public intent → wire operation → Provider evidence 核对。
|
|
32
|
+
按需读 `docs/agent-architecture.md`,邻接仓库仅作只读证据,不运行 spec 测试或修改其配置。
|
|
33
|
+
|
|
34
|
+
## 2. 验证与审查
|
|
35
|
+
|
|
36
|
+
执行 `docs/agent-workflows.md#delivery-gate`,具体命令取 `docs/development.md#development`。
|
|
37
|
+
代码、契约、依赖或构建行为变化先聚焦再完整测试;聚焦用例不能代替完整 `pytest`。修复需补充的用例来自契约、
|
|
38
|
+
独立示例或可验证属性,不复刻实现,不增加 Provider 逐路由 conformance。
|
|
39
|
+
|
|
40
|
+
真实 smoke 按 `docs/agent-workflows.md#provider-validation` 确认目标、ignore 和授权。授权覆盖隔离服务的登录、
|
|
41
|
+
Session 恢复、刷新和查询时,完整测试显式加载 SDK 自己的配置:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
uv run --frozen --env-file .env.test pytest -ra
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
否则运行不加载 Provider 连接变量的完整测试,明确报告 smoke 的 skip 及原因;用户要求的实时验证缺失时不能声称
|
|
48
|
+
已经完成。区分 MockTransport 的 SDK 映射证据与真实服务证据,禁止将凭证或敏感 payload 打印或提交。
|
|
49
|
+
|
|
50
|
+
对实质代码差异使用系统 `review-agent` skill,委派一个全新、只读子代理(`fork_turns: "none"`)。根代理处理
|
|
51
|
+
可证实问题并验证受影响部分。复用仍覆盖当前差异的完整测试、静态检查和审查结果,记录命令、结果及其覆盖的
|
|
52
|
+
commit/diff;只改版本后补版本一致性检查,不重跑未受影响的全套。
|
|
53
|
+
|
|
54
|
+
## 3. 收敛并一次提交
|
|
55
|
+
|
|
56
|
+
按 `docs/development.md#versioning` 选择兼容的下个 SDK 版本;使用用户指定版本时先检查 tag 和 PyPI 是否占用。
|
|
57
|
+
SDK 版本与 spec 日历版本独立,不跳过未占用的预发布序号。`pyproject.toml` 是唯一版本来源,更新 `uv.lock` 的
|
|
58
|
+
本项目版本。仅改版本时保持其余 lock 内容,使用发布 workflow 固定的 uv 检查兼容性。
|
|
59
|
+
|
|
60
|
+
按需读配置/依赖章节;不因发布顺手升级依赖、Node 或 Actions。文档在提交前完成,仅维护受影响的当前态,不新建
|
|
61
|
+
CHANGELOG。正常发布由 CI 完成可复现构建、制品审计和安装后消费;只有打包故障或打包配置变化需要时才本地构建,
|
|
62
|
+
使用临时目录,不积累 `dist/`。
|
|
63
|
+
|
|
64
|
+
复核最终 diff、版本、Contract pin 和有效验证证据,将本次尚未提交的实现、测试、文档及版本号放入同一个
|
|
65
|
+
release commit。已有提交的历史仅在另有授权时改写。
|
|
66
|
+
|
|
67
|
+
## 4. 推送、发布并核验
|
|
68
|
+
|
|
69
|
+
执行 `docs/agent-workflows.md#release`:创建指向 release commit 的 annotated `v{version}` tag,按规定顺序推送
|
|
70
|
+
同一 branch/tag 到两个远端,并验证远端 refs。跟踪该 commit/tag 的发布 workflow,逐项确认测试、可复现构建、
|
|
71
|
+
制品审计、安装后消费和 PyPI 上传成功;tag 推送成功不等于发布完成。
|
|
72
|
+
|
|
73
|
+
使用可用的 GitHub 工具读取 jobs 和构建日志;能力不足时用 `gh`。从本次 CI 构建审计输出或下载的 CI 制品取得
|
|
74
|
+
wheel/sdist 的 SHA-256,不能仅用 PyPI 自报哈希证明 CI/PyPI 一致。在 SDK 锁定开发环境中运行本技能的只读核验器:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
uv run --frozen --no-sync python -B .agents/skills/matelab-sdk-release/scripts/verify_pypi.py \
|
|
78
|
+
--repo "$PWD" --ref RELEASE_COMMIT \
|
|
79
|
+
--sha256 'WHEEL_FILENAME=CI_SHA256' --sha256 'SDIST_FILENAME=CI_SHA256'
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
参数替换为这次发布的 commit、文件名和 CI 哈希。
|
|
83
|
+
纯核验使用现有环境;`--no-sync` 禁止自动同步虚拟环境,`-B` 禁止写入字节码,环境缺失时如实报告。
|
|
84
|
+
[核验器](scripts/verify_pypi.py) 在内存中下载制品,比对发布提交的源码、配置、版本、依赖和 README,不构建或保留
|
|
85
|
+
历史制品。PyPI 暂未可读时允许短暂重试,等待期间保持进度更新。
|
|
86
|
+
|
|
87
|
+
失败时定位具体阶段,优先重试失败环节;不通过删测试、跳检查或自动另选版本制造成功。改变已推送历史或处理失败
|
|
88
|
+
候选前核对现有授权,已发布版本不可覆盖。最后报告版本/链接、单次提交、两个远端 refs、CI/PyPI 核验和工作区状态;
|
|
89
|
+
明确剩余失败或未验证项。
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""Read-only comparison of a Matelab SDK release, CI hashes and PyPI artifacts."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import hashlib
|
|
5
|
+
import io
|
|
6
|
+
import json
|
|
7
|
+
import re
|
|
8
|
+
import subprocess
|
|
9
|
+
import tarfile
|
|
10
|
+
import tomllib
|
|
11
|
+
import zipfile
|
|
12
|
+
from email.message import Message
|
|
13
|
+
from email.parser import BytesParser
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from urllib.parse import quote, urlsplit
|
|
16
|
+
from urllib.request import urlopen
|
|
17
|
+
|
|
18
|
+
from packaging.requirements import Requirement
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def require(condition: bool, message: str) -> None:
|
|
22
|
+
if not condition:
|
|
23
|
+
raise ValueError(message)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def git_bytes(repo: Path, commit: str, path: str) -> bytes:
|
|
27
|
+
return subprocess.check_output(["git", "-C", str(repo), "show", f"{commit}:{path}"])
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def ci_hash(value: str) -> tuple[str, str]:
|
|
31
|
+
name, separator, digest = value.partition("=")
|
|
32
|
+
if not separator or not name or re.fullmatch(r"[a-fA-F0-9]{64}", digest) is None:
|
|
33
|
+
raise argparse.ArgumentTypeError("Expected FILENAME=64_HEX_SHA256 from this release's CI artifacts")
|
|
34
|
+
return name, digest.lower()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def metadata_matches(metadata: Message, project: dict, readme: str) -> None:
|
|
38
|
+
require(metadata["Version"] == project["version"], "Artifact version mismatch")
|
|
39
|
+
require(metadata["Requires-Python"] == project["requires-python"], "Artifact Python requirement mismatch")
|
|
40
|
+
expected = {Requirement(value) for value in project["dependencies"]}
|
|
41
|
+
actual = {Requirement(value) for value in metadata.get_all("Requires-Dist", [])}
|
|
42
|
+
require(actual == expected, "Artifact dependency mismatch")
|
|
43
|
+
require(metadata["License-Expression"] == project["license"], "Artifact license mismatch")
|
|
44
|
+
author = ", ".join(entry["name"] for entry in project["authors"])
|
|
45
|
+
require(metadata["Author"] == author, "Artifact author mismatch")
|
|
46
|
+
description = metadata.get_payload()
|
|
47
|
+
require(isinstance(description, str) and description.strip() == readme.strip(), "Artifact README mismatch")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def verify_wheel(payload: bytes, repo: Path, commit: str, project: dict, readme: str, tracked: set[str]) -> int:
|
|
51
|
+
package = {path.removeprefix("src/"): path for path in tracked if path.startswith("src/matelab/")}
|
|
52
|
+
metadata_dir = f"matelab_python_sdk-{project['version']}.dist-info"
|
|
53
|
+
with zipfile.ZipFile(io.BytesIO(payload)) as archive:
|
|
54
|
+
names = archive.namelist()
|
|
55
|
+
require(len(names) == len(set(names)), "Duplicate wheel members")
|
|
56
|
+
require({name for name in names if name.startswith("matelab/")} == set(package), "Wheel package file mismatch")
|
|
57
|
+
for name, source in package.items():
|
|
58
|
+
require(archive.read(name) == git_bytes(repo, commit, source), f"Wheel source mismatch: {source}")
|
|
59
|
+
metadata_matches(BytesParser().parsebytes(archive.read(f"{metadata_dir}/METADATA")), project, readme)
|
|
60
|
+
for license_file in project["license-files"]:
|
|
61
|
+
require(
|
|
62
|
+
archive.read(f"{metadata_dir}/licenses/{license_file}") == git_bytes(repo, commit, license_file),
|
|
63
|
+
f"Wheel license file mismatch: {license_file}",
|
|
64
|
+
)
|
|
65
|
+
return len(package)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def verify_sdist(payload: bytes, repo: Path, commit: str, project: dict, readme: str, tracked: set[str]) -> int:
|
|
69
|
+
prefix = f"matelab_python_sdk-{project['version']}/"
|
|
70
|
+
seen: set[str] = set()
|
|
71
|
+
with tarfile.open(fileobj=io.BytesIO(payload), mode="r:gz") as archive:
|
|
72
|
+
for member in archive.getmembers():
|
|
73
|
+
require(member.name.startswith(prefix) or member.name == prefix.rstrip("/"), "Unexpected sdist root")
|
|
74
|
+
if member.isdir():
|
|
75
|
+
continue
|
|
76
|
+
require(member.isfile(), "Unexpected sdist link or special member")
|
|
77
|
+
path = member.name.removeprefix(prefix)
|
|
78
|
+
require(path not in seen, f"Duplicate sdist member: {path}")
|
|
79
|
+
seen.add(path)
|
|
80
|
+
stream = archive.extractfile(member)
|
|
81
|
+
require(stream is not None, f"Unreadable sdist member: {path}")
|
|
82
|
+
contents = stream.read()
|
|
83
|
+
if path == "PKG-INFO":
|
|
84
|
+
metadata_matches(BytesParser().parsebytes(contents), project, readme)
|
|
85
|
+
else:
|
|
86
|
+
require(path in tracked, f"Untracked sdist member: {path}")
|
|
87
|
+
require(contents == git_bytes(repo, commit, path), f"Sdist source mismatch: {path}")
|
|
88
|
+
required = {path for path in tracked if path.startswith("src/matelab/")}
|
|
89
|
+
required.update({"PKG-INFO", "pyproject.toml", "uv.lock", project["readme"], *project["license-files"]})
|
|
90
|
+
required.update(path for path in tracked if path.startswith("contracts/"))
|
|
91
|
+
require(required <= seen, f"Missing sdist files: {sorted(required - seen)}")
|
|
92
|
+
return len(seen - {"PKG-INFO"})
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def main() -> None:
|
|
96
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
97
|
+
parser.add_argument("--repo", type=Path, default=Path.cwd())
|
|
98
|
+
parser.add_argument("--ref", required=True, help="Release commit or immutable release tag")
|
|
99
|
+
parser.add_argument(
|
|
100
|
+
"--sha256", action="append", type=ci_hash, required=True, help="FILENAME=CI_SHA256; repeat per file"
|
|
101
|
+
)
|
|
102
|
+
args = parser.parse_args()
|
|
103
|
+
repo = args.repo.resolve()
|
|
104
|
+
commit = subprocess.check_output(
|
|
105
|
+
["git", "-C", str(repo), "rev-parse", "--verify", "--end-of-options", f"{args.ref}^{{commit}}"], text=True
|
|
106
|
+
).strip()
|
|
107
|
+
project = tomllib.loads(git_bytes(repo, commit, "pyproject.toml").decode())["project"]
|
|
108
|
+
require(project["name"] == "matelab-python-sdk", "This verifier is for matelab-python-sdk")
|
|
109
|
+
expected_hashes = dict(args.sha256)
|
|
110
|
+
require(len(expected_hashes) == len(args.sha256), "Duplicate expected artifact names")
|
|
111
|
+
readme = git_bytes(repo, commit, project["readme"]).decode()
|
|
112
|
+
tracked = set(
|
|
113
|
+
subprocess.check_output(
|
|
114
|
+
["git", "-C", str(repo), "ls-tree", "-r", "--name-only", commit], text=True
|
|
115
|
+
).splitlines()
|
|
116
|
+
)
|
|
117
|
+
url = f"https://pypi.org/pypi/{quote(project['name'], safe='')}/{quote(project['version'], safe='')}/json"
|
|
118
|
+
with urlopen(url, timeout=30) as response:
|
|
119
|
+
published = json.load(response)
|
|
120
|
+
info = published["info"]
|
|
121
|
+
require(info["version"] == project["version"], "PyPI version mismatch")
|
|
122
|
+
require(info["requires_python"] == project["requires-python"], "PyPI Python requirement mismatch")
|
|
123
|
+
require(info["description"].strip() == readme.strip(), "PyPI README mismatch")
|
|
124
|
+
require(
|
|
125
|
+
{Requirement(value) for value in info["requires_dist"] or []}
|
|
126
|
+
== {Requirement(value) for value in project["dependencies"]},
|
|
127
|
+
"PyPI dependency mismatch",
|
|
128
|
+
)
|
|
129
|
+
entries = published["urls"]
|
|
130
|
+
require(len(entries) == len(expected_hashes), "PyPI/CI artifact count mismatch")
|
|
131
|
+
require({entry["filename"] for entry in entries} == set(expected_hashes), "PyPI/CI artifact names mismatch")
|
|
132
|
+
counts = {}
|
|
133
|
+
for entry in entries:
|
|
134
|
+
name = entry["filename"]
|
|
135
|
+
digest = expected_hashes[name]
|
|
136
|
+
require(entry["digests"]["sha256"] == digest, f"CI/PyPI hash mismatch: {name}")
|
|
137
|
+
location = urlsplit(entry["url"])
|
|
138
|
+
require(
|
|
139
|
+
location.scheme == "https" and location.hostname == "files.pythonhosted.org", "Unexpected artifact host"
|
|
140
|
+
)
|
|
141
|
+
with urlopen(entry["url"], timeout=30) as response:
|
|
142
|
+
payload = response.read()
|
|
143
|
+
require(hashlib.sha256(payload).hexdigest() == digest, f"Downloaded hash mismatch: {name}")
|
|
144
|
+
if entry["packagetype"] == "bdist_wheel":
|
|
145
|
+
counts["wheel_package_files"] = verify_wheel(payload, repo, commit, project, readme, tracked)
|
|
146
|
+
elif entry["packagetype"] == "sdist":
|
|
147
|
+
counts["sdist_source_files"] = verify_sdist(payload, repo, commit, project, readme, tracked)
|
|
148
|
+
else:
|
|
149
|
+
raise ValueError(f"Unexpected artifact type: {entry['packagetype']}")
|
|
150
|
+
require("wheel_package_files" in counts and "sdist_source_files" in counts, "Wheel and sdist are both required")
|
|
151
|
+
print(json.dumps({"version": project["version"], "commit": commit, "files": expected_hashes, **counts}, indent=2))
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
if __name__ == "__main__":
|
|
155
|
+
try:
|
|
156
|
+
main()
|
|
157
|
+
except (
|
|
158
|
+
OSError,
|
|
159
|
+
ValueError,
|
|
160
|
+
KeyError,
|
|
161
|
+
subprocess.CalledProcessError,
|
|
162
|
+
tarfile.TarError,
|
|
163
|
+
zipfile.BadZipFile,
|
|
164
|
+
) as error:
|
|
165
|
+
raise SystemExit(f"Release verification failed: {error}") from None
|
|
@@ -86,8 +86,8 @@ A notebook-scoped hierarchical container used to organize records. Records at th
|
|
|
86
86
|
_Avoid_: Subtype, record type, template
|
|
87
87
|
|
|
88
88
|
**Record import value**:
|
|
89
|
-
A template-shaped value composed of JSON-compatible scalars and containers, with a staged attachment
|
|
90
|
-
allowed wherever the template expects a file.
|
|
89
|
+
A template-shaped value composed of JSON-compatible scalars and containers, with a staged attachment or existing
|
|
90
|
+
cloud file allowed wherever the template expects a file.
|
|
91
91
|
_Avoid_: Arbitrary object, stored record module, Provider attachment reference
|
|
92
92
|
|
|
93
93
|
**Encrypted record modules**:
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
"schema_version": 1,
|
|
3
3
|
"contract_line": "integration-v1",
|
|
4
4
|
"release": {
|
|
5
|
-
"tag": "v2026.
|
|
6
|
-
"commit": "
|
|
5
|
+
"tag": "v2026.11.2",
|
|
6
|
+
"commit": "a986f73b0edc6bb67f2993a9ee1291aba31df5e5"
|
|
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": "4a485f0ab2b589136cdcccad77407d8b5768e6e8cbb8e3a7636d8b9a07649991"
|
|
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: "2026.
|
|
4
|
+
version: "2026.11.2"
|
|
5
5
|
description: |
|
|
6
6
|
Current PHP wire contract for generating SDKs against the selected
|
|
7
7
|
`matelab-server` Provider version.
|
|
@@ -10,15 +10,20 @@ info:
|
|
|
10
10
|
and attachment operations that resolve a data server support the
|
|
11
11
|
`localhost` branch; remote forwarding is excluded pending downstream
|
|
12
12
|
wire evidence and end-to-end verification (PVD-049).
|
|
13
|
+
Integration requests omit the optional browser `Version` header. If it is
|
|
14
|
+
supplied, PHP applies `intval` and compares it with the current minimum
|
|
15
|
+
20261029; a lower value returns HTTP 200 with string `code` and `errcode`
|
|
16
|
+
equal to `reload`. That browser refresh branch is outside the integer
|
|
17
|
+
response envelopes selected by this integration contract.
|
|
13
18
|
x-contract-line: integration-v1
|
|
14
19
|
x-provider-evidence:
|
|
15
20
|
repository: /home/tnzhu/projects/matelab-server
|
|
16
|
-
commit:
|
|
21
|
+
commit: f5a714e17cd76076efe7579db279936ac29b50a6
|
|
17
22
|
verificationRuntime:
|
|
18
|
-
baseUrl:
|
|
23
|
+
baseUrl: https://matelab.iphy.ac.cn/eln-test/api
|
|
19
24
|
container: matelab-test-server-1
|
|
20
25
|
coreSourceComparison: sha256-exact
|
|
21
|
-
coreSourceFileCount:
|
|
26
|
+
coreSourceFileCount: 153
|
|
22
27
|
dataServerScope: localhost
|
|
23
28
|
files:
|
|
24
29
|
- controller/Actions.php
|
|
@@ -48,6 +53,7 @@ info:
|
|
|
48
53
|
- lib/Route.php
|
|
49
54
|
- lib/HttpServer.php
|
|
50
55
|
- lib/WsServer.php
|
|
56
|
+
- start.php
|
|
51
57
|
- lib/MultiException.php
|
|
52
58
|
- config/Db.php
|
|
53
59
|
- config/Channel.php
|
|
@@ -56,9 +62,9 @@ info:
|
|
|
56
62
|
- sql/matelab.sql
|
|
57
63
|
- sql/recommend.sql
|
|
58
64
|
x-provider-audit:
|
|
59
|
-
currentCommit:
|
|
60
|
-
previousReleaseEvidenceCommit:
|
|
61
|
-
lastProviderVerifiedCommit:
|
|
65
|
+
currentCommit: f5a714e17cd76076efe7579db279936ac29b50a6
|
|
66
|
+
previousReleaseEvidenceCommit: 00f17b6ce0906bb78b0a3e9bb8c50abee90ca3d0
|
|
67
|
+
lastProviderVerifiedCommit: f5a714e17cd76076efe7579db279936ac29b50a6
|
|
62
68
|
contractStatus: current-provider-aligned
|
|
63
69
|
# Summary of changes adopted by current Supported Routes since the last
|
|
64
70
|
# released contract, including contract corrections. The Provider diff is
|
|
@@ -66,8 +72,7 @@ info:
|
|
|
66
72
|
# updates against that fixed release baseline; use [] if no relevant changes.
|
|
67
73
|
# Entries are not per-change verification results or a cumulative history.
|
|
68
74
|
incorporatedChanges:
|
|
69
|
-
- {operationId: stageTemporaryFile, change: filename-
|
|
70
|
-
- {operationId: uploadRecordAttachment, change: temporary-bytes-retain-sanitized-source-extension}
|
|
75
|
+
- {operationId: stageTemporaryFile, change: filename-independent-continuation-with-final-fragment-name-and-extensionless-abort}
|
|
71
76
|
x-consumer-evidence:
|
|
72
77
|
- repository: /home/tnzhu/projects/matelab-mcp
|
|
73
78
|
commit: 94a129b2df0ef0fbcf179c10550fb7c9a5dd2e6f
|
|
@@ -88,10 +93,11 @@ info:
|
|
|
88
93
|
identity must come from token issuance. ELN-group and notebook-list
|
|
89
94
|
calls still provide Consumer evidence for their canonical GET forms.
|
|
90
95
|
- repository: /home/tnzhu/projects/matelab-front
|
|
91
|
-
commit:
|
|
96
|
+
commit: f7a7b952d7981f169d5c2585aea5c94b418086a3
|
|
92
97
|
files:
|
|
93
98
|
- src/App.vue
|
|
94
99
|
- src/fn/index.js
|
|
100
|
+
- src/store/index.js
|
|
95
101
|
- src/components/fullview.vue
|
|
96
102
|
- src/components/userSelect.vue
|
|
97
103
|
- src/components/users.vue
|
|
@@ -107,7 +113,8 @@ info:
|
|
|
107
113
|
The current Front dev commit reads environment/menu from GET
|
|
108
114
|
`/actions/app` and obtains session identity from token issuance.
|
|
109
115
|
The shared upload client sends byte offsets to `/actions/bigupload`
|
|
110
|
-
and uses the acknowledged offset for the next fragment.
|
|
116
|
+
and uses the acknowledged offset for the next fragment. GET requests
|
|
117
|
+
also carry the browser version stored in `src/store/index.js`.
|
|
111
118
|
Existing record, comment, template, literature and cloud-drive calls
|
|
112
119
|
retain their Consumer use cases; no new routes are added by this audit.
|
|
113
120
|
x-record-attachment-staging:
|
|
@@ -1627,7 +1634,13 @@ paths:
|
|
|
1627
1634
|
Template-shaped file values supplied by the request must be strings in
|
|
1628
1635
|
the `#file{name}` or `#hash{filename.ext, hash}` form. The current
|
|
1629
1636
|
validator rejects even valid `#base64` input (PVD-042); inline base64
|
|
1630
|
-
is outside the callable contract.
|
|
1637
|
+
is outside the callable contract. Existing cloud-drive files can be
|
|
1638
|
+
referenced by `#hash{filename.ext, hash}` when the selected notebook
|
|
1639
|
+
owner's userid owns the personal-root file (`root_id=0`), or has a
|
|
1640
|
+
`files_permissions` row for its shared root. An existing cloud quote
|
|
1641
|
+
alone does not grant access. The reference creates a record attachment
|
|
1642
|
+
quote without consuming the cloud-drive file.
|
|
1643
|
+
In particular,
|
|
1631
1644
|
table/form file arrays cannot contain explicit JSON null values on the
|
|
1632
1645
|
current Provider. To create empty table file cells, omit the file column
|
|
1633
1646
|
and let the Provider complete it with canonical null cells.
|
|
@@ -1645,6 +1658,17 @@ paths:
|
|
|
1645
1658
|
template: Example Template
|
|
1646
1659
|
dataset:
|
|
1647
1660
|
- {uid: GENERATED-WIRE-UID, title: Imported Example, data: {Notes: Sanitized}, keyword: calibration}
|
|
1661
|
+
existing_cloud_attachment:
|
|
1662
|
+
summary: Reuse an authorized cloud-drive file without consuming it
|
|
1663
|
+
value:
|
|
1664
|
+
eln: Example Notebook
|
|
1665
|
+
template: Example Template
|
|
1666
|
+
dataset:
|
|
1667
|
+
- uid: REC-CLOUD-EXAMPLE-001
|
|
1668
|
+
title: Existing cloud attachment
|
|
1669
|
+
data:
|
|
1670
|
+
Attachments:
|
|
1671
|
+
File: ["#hash{evidence.zip, 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef}"]
|
|
1648
1672
|
preuploaded_attachment:
|
|
1649
1673
|
summary: Upload-event UID and future record UID are deliberately different
|
|
1650
1674
|
value:
|
|
@@ -1678,6 +1702,12 @@ paths:
|
|
|
1678
1702
|
value: {code: 0, msg: Data imported, errcode: 0, errmsg: Data imported, id: [9004], eln_id: 12}
|
|
1679
1703
|
error:
|
|
1680
1704
|
value: {code: 4, msg: Multiple errors, errcode: 4, errmsg: Multiple errors, errs: [Invalid imported field]}
|
|
1705
|
+
inaccessible_hash:
|
|
1706
|
+
value:
|
|
1707
|
+
code: 2
|
|
1708
|
+
msg: The referenced hash 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef does not exist
|
|
1709
|
+
errcode: 2
|
|
1710
|
+
errmsg: The referenced hash 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef does not exist
|
|
1681
1711
|
/eln_api/export:
|
|
1682
1712
|
post:
|
|
1683
1713
|
operationId: exportRecords
|
|
@@ -1772,7 +1802,12 @@ paths:
|
|
|
1772
1802
|
strings containing canonical `elnurl://h=...&s=...&f=...` references.
|
|
1773
1803
|
The Provider scans the incoming richtext value, resolves hashes against
|
|
1774
1804
|
caller-owned staging or authorized existing file quotes, and binds the
|
|
1775
|
-
resulting attachments while saving the record.
|
|
1805
|
+
resulting attachments while saving the record. Template-shaped
|
|
1806
|
+
`#hash{filename.ext, hash}` values also accept existing cloud-drive
|
|
1807
|
+
files: permission is checked against the selected notebook owner's
|
|
1808
|
+
userid, using personal-root ownership or a `files_permissions` row for
|
|
1809
|
+
the shared root. The cloud file is retained; an inaccessible hash is
|
|
1810
|
+
rejected before record content is written.
|
|
1776
1811
|
|
|
1777
1812
|
This operation does not modify record title or other record metadata.
|
|
1778
1813
|
`UpdateRecordRequest` rejects a top-level `title`; the Front uses the
|
|
@@ -1827,6 +1862,15 @@ paths:
|
|
|
1827
1862
|
examples:
|
|
1828
1863
|
update:
|
|
1829
1864
|
value: {eln: Example Notebook, uid: REC-EXAMPLE-001, modify: [{path: [Notes], data: Sanitized update}]}
|
|
1865
|
+
add_existing_cloud_attachment_field:
|
|
1866
|
+
value:
|
|
1867
|
+
eln: Example Notebook
|
|
1868
|
+
uid: REC-EXAMPLE-001
|
|
1869
|
+
add:
|
|
1870
|
+
- module: Attachments
|
|
1871
|
+
name: Cloud file
|
|
1872
|
+
type: file
|
|
1873
|
+
data: ["#hash{evidence.zip, 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef}"]
|
|
1830
1874
|
add_preuploaded_attachment_field:
|
|
1831
1875
|
value:
|
|
1832
1876
|
eln: Example Notebook
|
|
@@ -1930,6 +1974,12 @@ paths:
|
|
|
1930
1974
|
value: {code: 0, msg: Collaboration data update completed, errcode: 0, errmsg: Collaboration data update completed}
|
|
1931
1975
|
error:
|
|
1932
1976
|
value: {code: 4, msg: Multiple errors, errcode: 4, errmsg: Multiple errors, errs: [Invalid update path]}
|
|
1977
|
+
inaccessible_hash:
|
|
1978
|
+
value:
|
|
1979
|
+
code: 2
|
|
1980
|
+
msg: The referenced hash 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef does not exist
|
|
1981
|
+
errcode: 2
|
|
1982
|
+
errmsg: The referenced hash 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef does not exist
|
|
1933
1983
|
/eln_api/delete:
|
|
1934
1984
|
post:
|
|
1935
1985
|
operationId: deleteRecordsByUid
|
|
@@ -1970,11 +2020,11 @@ paths:
|
|
|
1970
2020
|
workflow therefore preserves the exact upload `eln`/`user` selector and
|
|
1971
2021
|
permits one occurrence in one target record. The operation has no
|
|
1972
2022
|
contracted abort, status, expiration, or idempotent retry. A hash
|
|
1973
|
-
mismatch in this one-fragment flow
|
|
1974
|
-
new bytes. Errors after import/update writing starts still
|
|
1975
|
-
whether the stage was consumed.
|
|
1976
|
-
filename's extension after removing
|
|
1977
|
-
the response still reports the original source filename.
|
|
2023
|
+
mismatch in this one-fragment flow occurs before row registration and
|
|
2024
|
+
removes the new bytes. Errors after import/update writing starts still
|
|
2025
|
+
do not prove whether the stage was consumed. On completion, bytes retain
|
|
2026
|
+
the multipart filename's extension after removing characters outside
|
|
2027
|
+
`[A-Za-z0-9]`; the response still reports the original source filename.
|
|
1978
2028
|
Raw identifiers identify the staging row;
|
|
1979
2029
|
their filesystem components are sanitized and can alias (PVD-051).
|
|
1980
2030
|
x-known-provider-issues: [PVD-014, PVD-051]
|
|
@@ -3685,18 +3735,18 @@ paths:
|
|
|
3685
3735
|
successful response's `offset`. Omitting it defaults to zero and
|
|
3686
3736
|
restarts incomplete bytes rather than appending. The final fragment
|
|
3687
3737
|
returns the combined SHA-256, size, source filename, temporary row id,
|
|
3688
|
-
and legacy preview query.
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
filename
|
|
3738
|
+
and legacy preview query. Incomplete bytes are selected by the same
|
|
3739
|
+
`uid` and `tempid`, independently of the multipart filename and extension.
|
|
3740
|
+
Both may change between fragments. Only after final hash validation does
|
|
3741
|
+
PHP append the final fragment's extension, stripped of characters outside
|
|
3742
|
+
`[A-Za-z0-9]` with case preserved, and register the completed row. The
|
|
3743
|
+
response `filename` is the final fragment's original source filename.
|
|
3694
3744
|
|
|
3695
3745
|
`abort=1` is the compensating cleanup operation. It requires the same
|
|
3696
3746
|
`uid` and `tempid` and omits `file`. If no completed row exists for
|
|
3697
|
-
those identifiers, it removes
|
|
3698
|
-
|
|
3699
|
-
it preserves the row and bytes without cleaning any
|
|
3747
|
+
those identifiers, it removes the extensionless incomplete file.
|
|
3748
|
+
It does not scan extension-suffixed orphan files. If a completed row
|
|
3749
|
+
exists, it preserves the row and bytes without cleaning any files. Both
|
|
3700
3750
|
states return the standard HTTP 200 code-2 error envelope ("upload
|
|
3701
3751
|
cancelled"); that response does not prove completed staging was deleted.
|
|
3702
3752
|
Complete uploads must be consumed by their intended finalizer; this
|
|
@@ -7440,9 +7490,9 @@ components:
|
|
|
7440
7490
|
type: string
|
|
7441
7491
|
format: binary
|
|
7442
7492
|
description: |
|
|
7443
|
-
Required for upload branches and omitted for abort.
|
|
7444
|
-
|
|
7445
|
-
|
|
7493
|
+
Required for upload branches and omitted for abort. Filename and
|
|
7494
|
+
extension may change between fragments in the same uid/tempid
|
|
7495
|
+
session. Completion uses the final fragment's source filename.
|
|
7446
7496
|
allOf:
|
|
7447
7497
|
- if:
|
|
7448
7498
|
required: [abort]
|