matelab-python-sdk 0.2.0a9__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.
Files changed (71) hide show
  1. matelab_python_sdk-0.2.0a10/.agents/skills/matelab-sdk-release/SKILL.md +89 -0
  2. matelab_python_sdk-0.2.0a10/.agents/skills/matelab-sdk-release/agents/openai.yaml +4 -0
  3. matelab_python_sdk-0.2.0a10/.agents/skills/matelab-sdk-release/scripts/verify_pypi.py +165 -0
  4. {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/PKG-INFO +1 -1
  5. {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/contracts/matelab-integration-v1.lock.json +3 -3
  6. {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/contracts/matelab-integration-v1.openapi.yaml +26 -26
  7. {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/docs/api.md +16 -14
  8. {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/docs/operation-coverage.yaml +1 -1
  9. {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/pyproject.toml +1 -1
  10. {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/src/matelab/_generated/models.py +4 -4
  11. {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/src/matelab/domains/uploads.py +2 -2
  12. {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/tests/test_uploads_and_literature.py +1 -1
  13. {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/uv.lock +1 -1
  14. {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/.env.example +0 -0
  15. {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/.github/workflows/release.yml +0 -0
  16. {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/.gitignore +0 -0
  17. {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/AGENTS.md +0 -0
  18. {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/CONTEXT.md +0 -0
  19. {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/LICENSE +0 -0
  20. {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/NOTICE +0 -0
  21. {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/README.md +0 -0
  22. {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/docs/agent-architecture.md +0 -0
  23. {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/docs/agent-workflows.md +0 -0
  24. {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/docs/development.md +0 -0
  25. {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/scripts/check_installed_package.py +0 -0
  26. {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/scripts/check_release.py +0 -0
  27. {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/scripts/generate_models.py +0 -0
  28. {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/src/matelab/__init__.py +0 -0
  29. {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/src/matelab/_generated/__init__.py +0 -0
  30. {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/src/matelab/_hashes.py +0 -0
  31. {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/src/matelab/_transport.py +0 -0
  32. {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/src/matelab/_wire_validation.py +0 -0
  33. {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/src/matelab/client.py +0 -0
  34. {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/src/matelab/domains/__init__.py +0 -0
  35. {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/src/matelab/domains/_attachment_content.py +0 -0
  36. {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/src/matelab/domains/cloud_drive.py +0 -0
  37. {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/src/matelab/domains/groups.py +0 -0
  38. {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/src/matelab/domains/literature.py +0 -0
  39. {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/src/matelab/domains/notebooks.py +0 -0
  40. {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/src/matelab/domains/records.py +0 -0
  41. {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/src/matelab/domains/templates.py +0 -0
  42. {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/src/matelab/domains/users.py +0 -0
  43. {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/src/matelab/errors.py +0 -0
  44. {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/src/matelab/py.typed +0 -0
  45. {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/src/matelab/session.py +0 -0
  46. {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/src/matelab/streaming.py +0 -0
  47. {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/tests/conftest.py +0 -0
  48. {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/tests/provider/test_provider_smoke.py +0 -0
  49. {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/tests/test_async_attachment_upload.py +0 -0
  50. {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/tests/test_client.py +0 -0
  51. {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/tests/test_cloud_drive.py +0 -0
  52. {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/tests/test_comments_and_downloads.py +0 -0
  53. {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/tests/test_consumer_smoke.py +0 -0
  54. {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/tests/test_errors.py +0 -0
  55. {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/tests/test_extended_records.py +0 -0
  56. {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/tests/test_generation.py +0 -0
  57. {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/tests/test_groups.py +0 -0
  58. {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/tests/test_notebook_lifecycle.py +0 -0
  59. {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/tests/test_notebooks.py +0 -0
  60. {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/tests/test_operation_coverage.py +0 -0
  61. {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/tests/test_record_lifecycle.py +0 -0
  62. {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/tests/test_record_mutations.py +0 -0
  63. {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/tests/test_records.py +0 -0
  64. {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/tests/test_release.py +0 -0
  65. {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/tests/test_streaming.py +0 -0
  66. {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/tests/test_template_lifecycle.py +0 -0
  67. {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/tests/test_templates.py +0 -0
  68. {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/tests/test_transport.py +0 -0
  69. {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/tests/test_users.py +0 -0
  70. {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/tests/typing/consumer.py +0 -0
  71. {matelab_python_sdk-0.2.0a9 → 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,4 @@
1
+ interface:
2
+ display_name: "Matelab SDK 完整发布"
3
+ short_description: "Matelab SDK 的 spec 同步、完整验证、单次提交与 PyPI 发布"
4
+ default_prompt: "使用 $matelab-sdk-release 完成 Matelab Python SDK 的 spec 增量同步、必要适配、完整测试和审查,将版本号与全部改动放进同一个提交,推送并发布到 PyPI。"
@@ -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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: matelab-python-sdk
3
- Version: 0.2.0a9
3
+ Version: 0.2.0a10
4
4
  Summary: Reusable async Python client for the Matelab Integration Contract
5
5
  Author: CMPDC, Institute of Physics, Chinese Academy of Sciences
6
6
  License-Expression: Apache-2.0
@@ -2,12 +2,12 @@
2
2
  "schema_version": 1,
3
3
  "contract_line": "integration-v1",
4
4
  "release": {
5
- "tag": "v2026.11.1",
6
- "commit": "402a158052995850e0b7fad5ed5399c8a2eed294"
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": "f9d3bd0baa4e5e70fe13870c21d9be70e40e2686831a0ffcc4632b39f83a3257"
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.11.1"
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.
@@ -18,12 +18,12 @@ info:
18
18
  x-contract-line: integration-v1
19
19
  x-provider-evidence:
20
20
  repository: /home/tnzhu/projects/matelab-server
21
- commit: 00f17b6ce0906bb78b0a3e9bb8c50abee90ca3d0
21
+ commit: f5a714e17cd76076efe7579db279936ac29b50a6
22
22
  verificationRuntime:
23
23
  baseUrl: https://matelab.iphy.ac.cn/eln-test/api
24
24
  container: matelab-test-server-1
25
25
  coreSourceComparison: sha256-exact
26
- coreSourceFileCount: 152
26
+ coreSourceFileCount: 153
27
27
  dataServerScope: localhost
28
28
  files:
29
29
  - controller/Actions.php
@@ -53,6 +53,7 @@ info:
53
53
  - lib/Route.php
54
54
  - lib/HttpServer.php
55
55
  - lib/WsServer.php
56
+ - start.php
56
57
  - lib/MultiException.php
57
58
  - config/Db.php
58
59
  - config/Channel.php
@@ -61,9 +62,9 @@ info:
61
62
  - sql/matelab.sql
62
63
  - sql/recommend.sql
63
64
  x-provider-audit:
64
- currentCommit: 00f17b6ce0906bb78b0a3e9bb8c50abee90ca3d0
65
- previousReleaseEvidenceCommit: 161c4560ae1cb6267d46841d6fe8adc8270ee1bc
66
- lastProviderVerifiedCommit: 00f17b6ce0906bb78b0a3e9bb8c50abee90ca3d0
65
+ currentCommit: f5a714e17cd76076efe7579db279936ac29b50a6
66
+ previousReleaseEvidenceCommit: 00f17b6ce0906bb78b0a3e9bb8c50abee90ca3d0
67
+ lastProviderVerifiedCommit: f5a714e17cd76076efe7579db279936ac29b50a6
67
68
  contractStatus: current-provider-aligned
68
69
  # Summary of changes adopted by current Supported Routes since the last
69
70
  # released contract, including contract corrections. The Provider diff is
@@ -71,8 +72,7 @@ info:
71
72
  # updates against that fixed release baseline; use [] if no relevant changes.
72
73
  # Entries are not per-change verification results or a cumulative history.
73
74
  incorporatedChanges:
74
- - {operationId: importRecordDataset, change: authorized-cloud-drive-hash-references-pass-permission-query}
75
- - {operationId: updateRecordData, change: authorized-cloud-drive-hash-references-pass-permission-query}
75
+ - {operationId: stageTemporaryFile, change: filename-independent-continuation-with-final-fragment-name-and-extensionless-abort}
76
76
  x-consumer-evidence:
77
77
  - repository: /home/tnzhu/projects/matelab-mcp
78
78
  commit: 94a129b2df0ef0fbcf179c10550fb7c9a5dd2e6f
@@ -93,7 +93,7 @@ info:
93
93
  identity must come from token issuance. ELN-group and notebook-list
94
94
  calls still provide Consumer evidence for their canonical GET forms.
95
95
  - repository: /home/tnzhu/projects/matelab-front
96
- commit: 55bc8d95d9d4ef02841621f0b793d273cea66137
96
+ commit: f7a7b952d7981f169d5c2585aea5c94b418086a3
97
97
  files:
98
98
  - src/App.vue
99
99
  - src/fn/index.js
@@ -2020,11 +2020,11 @@ paths:
2020
2020
  workflow therefore preserves the exact upload `eln`/`user` selector and
2021
2021
  permits one occurrence in one target record. The operation has no
2022
2022
  contracted abort, status, expiration, or idempotent retry. A hash
2023
- mismatch in this one-fragment flow rolls back the temporary row and
2024
- new bytes. Errors after import/update writing starts still do not prove
2025
- whether the stage was consumed. Temporary bytes retain the multipart
2026
- filename's extension after removing non-ASCII-alphanumeric characters;
2027
- 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.
2028
2028
  Raw identifiers identify the staging row;
2029
2029
  their filesystem components are sanitized and can alias (PVD-051).
2030
2030
  x-known-provider-issues: [PVD-014, PVD-051]
@@ -3735,18 +3735,18 @@ paths:
3735
3735
  successful response's `offset`. Omitting it defaults to zero and
3736
3736
  restarts incomplete bytes rather than appending. The final fragment
3737
3737
  returns the combined SHA-256, size, source filename, temporary row id,
3738
- and legacy preview query. The multipart filename's final extension,
3739
- stripped of characters outside `[A-Za-z0-9]` with case preserved,
3740
- also selects the temporary bytes. Repeat that extension on every
3741
- fragment; changing it cannot continue the previous bytes and returns
3742
- code 2 when the requested offset has no matching file. A different
3743
- filename stem with the same sanitized extension can continue them.
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.
3744
3744
 
3745
3745
  `abort=1` is the compensating cleanup operation. It requires the same
3746
3746
  `uid` and `tempid` and omits `file`. If no completed row exists for
3747
- those identifiers, it removes incomplete bytes for all extension
3748
- variants, including the extensionless file. If a completed row exists,
3749
- it preserves the row and bytes without cleaning any variants. Both
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
3750
3750
  states return the standard HTTP 200 code-2 error envelope ("upload
3751
3751
  cancelled"); that response does not prove completed staging was deleted.
3752
3752
  Complete uploads must be consumed by their intended finalizer; this
@@ -7490,9 +7490,9 @@ components:
7490
7490
  type: string
7491
7491
  format: binary
7492
7492
  description: |
7493
- Required for upload branches and omitted for abort. The multipart
7494
- filename's sanitized final extension selects the temporary bytes;
7495
- keep that extension unchanged throughout continuation.
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.
7496
7496
  allOf:
7497
7497
  - if:
7498
7498
  required: [abort]
@@ -5,15 +5,15 @@ See the [README](../README.md) for installation and quick start and [CONTEXT.md]
5
5
 
6
6
  ## Contract
7
7
 
8
- The SDK is pinned to the immutable `matelab-spec v2026.11.1` Contract Release. The sole pin,
8
+ The SDK is pinned to the immutable `matelab-spec v2026.11.2` Contract Release. The sole pin,
9
9
  [contracts/matelab-integration-v1.lock.json](../contracts/matelab-integration-v1.lock.json), records peeled commit
10
- `402a158052995850e0b7fad5ed5399c8a2eed294`, source `openapi/matelab-integration-v1.openapi.yaml`, the local snapshot path,
11
- and SHA-256 `f9d3bd0baa4e5e70fe13870c21d9be70e40e2686831a0ffcc4632b39f83a3257`. The annotated tag object is
12
- `18dbfd99f36ee7fc8cd620a251c7973d86e8dedd`.
10
+ `a986f73b0edc6bb67f2993a9ee1291aba31df5e5`, source `openapi/matelab-integration-v1.openapi.yaml`, the local snapshot path,
11
+ and SHA-256 `4a485f0ab2b589136cdcccad77407d8b5768e6e8cbb8e3a7636d8b9a07649991`. The annotated tag object is
12
+ `dc7a09dccf4187100cd05241757fc3f500107bdd`.
13
13
 
14
- The Contract uses Provider source evidence from `00f17b6ce0906bb78b0a3e9bb8c50abee90ca3d0`; v2026.11.1 validation ran
14
+ The Contract uses Provider source evidence from `f5a714e17cd76076efe7579db279936ac29b50a6`; v2026.11.2 validation ran
15
15
  against an isolated test instance and does not establish production behavior. The Front Consumer evidence
16
- `55bc8d95d9d4ef02841621f0b793d273cea66137` informs cross-Consumer capability review but does not define the SDK interface.
16
+ `f7a7b952d7981f169d5c2585aea5c94b418086a3` informs cross-Consumer capability review but does not define the SDK interface.
17
17
 
18
18
  This snapshot covers the Workerman runtime with the `localhost` data-server branch for ELN records and attachments;
19
19
  remote data-server forwarding is outside its verified callable scope (PVD-049).
@@ -723,25 +723,27 @@ assert isinstance(staged, StagedFile)
723
723
  The SDK derives `fragment_size` and computes the complete SHA-256 for a single `bytes` fragment. File-like content
724
724
  must declare its fragment size. For multiple fragments, pass the returned `StagedUploadSession` into the next call and
725
725
  supply `complete_sha256` on the final call. `next_offset` is the Provider-acknowledged byte offset, checked against the
726
- declared fragment size and sent with the next fragment. Repeat the filename's final extension across fragments:
727
- the Provider removes characters outside `[A-Za-z0-9]` and preserves case, so `paper.P-d_f` and `renamed.Pdf` select
728
- the same bytes. Changing that sanitized extension cannot resume the upload and returns code 2 when no matching
729
- file exists at the requested offset. The SDK sends each filename unchanged, checks the returned filename and total
726
+ declared fragment size and sent with the next fragment. Incomplete bytes use the same binding and upload-session
727
+ identities independently of filename or extension, so both may change between fragments. After final hash validation,
728
+ the Provider appends the final fragment's extension, removing characters outside `[A-Za-z0-9]` and preserving case;
729
+ its response retains that fragment's original filename. The SDK sends each filename unchanged, checks the returned filename and total
730
730
  size, and compares hashes when a local expected hash is available. A final result contains the Provider hash, temporary row
731
731
  identity and direct `binding`; it no longer embeds resumable session state. The temporary Provider URL and upload-session
732
732
  identity needed by internal binding and cleanup remain hidden. Callers do not need to
733
733
  splice either into content. The result does not claim that a later literature/cloud operation checks the uploader or
734
734
  consumes the file exactly once. `uploads.abort()` accepts either an active `StagedUploadSession` or a completed
735
735
  `StagedFile` and returns `None` for the Provider's code-2 cancellation signal. Without a completed staging row, it
736
- removes all incomplete extension variants, including extensionless bytes. A completed row prevents any cleanup for
737
- that identity; completed staging must be consumed by its intended finalizer (PVD-052).
736
+ removes only the extensionless incomplete file, without scanning extension-suffixed orphan files. A completed row
737
+ prevents any cleanup for that identity; completed staging must be consumed by its intended finalizer (PVD-052).
738
738
  Binding UIDs allow up to 100 characters; upload-session IDs allow up to 45 (PVD-024). Use fresh identities: different
739
739
  raw identifiers can alias after Provider filename sanitization (PVD-051). Workerman upload errors supply no usable
740
740
  resume offset (PVD-050). After a failed non-final fragment, cancel incomplete bytes and restart with fresh identities.
741
741
  An ambiguous final response can leave completed staging that `abort()` cannot delete.
742
742
  Staging mutations are never automatically retried.
743
- After upgrading from the previous Provider's extensionless storage, restart old incomplete uploads from offset zero
744
- or cancel them and use fresh session identities.
743
+ When upgrading from the previous Provider's extension-suffixed partial storage, restart with fresh session identities:
744
+ old incomplete files cannot resume through the new path and the new abort does not remove them. Existing completed
745
+ staging keeps its registered finalizer path. Hash-mismatch cleanup does not guarantee byte preservation after later
746
+ rename or row-registration failures; those fault paths have no verification evidence.
745
747
 
746
748
  Literature identities distinguish the personal library from shared libraries:
747
749
 
@@ -3,7 +3,7 @@
3
3
  schema_version: 1
4
4
  contract:
5
5
  line: integration-v1
6
- release: v2026.11.1
6
+ release: v2026.11.2
7
7
  operation_count: 71
8
8
 
9
9
  operations:
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "matelab-python-sdk"
7
- version = "0.2.0a9"
7
+ version = "0.2.0a10"
8
8
  description = "Reusable async Python client for the Matelab Integration Contract"
9
9
  requires-python = ">=3.11"
10
10
  keywords = ["matelab", "electronic-lab-notebook", "eln", "asyncio", "sdk"]
@@ -11,7 +11,7 @@ from pydantic import ConfigDict, Field, RootModel, SecretStr, StrictBool, Strict
11
11
 
12
12
  from matelab._wire_validation import WireModel
13
13
 
14
- OPENAPI_INFO_VERSION = "2026.11.1"
14
+ OPENAPI_INFO_VERSION = "2026.11.2"
15
15
 
16
16
 
17
17
  class ResponseEnvelope(WireModel):
@@ -2759,9 +2759,9 @@ class StageTemporaryFileRequest(WireModel):
2759
2759
  """
2760
2760
  file: bytes | None = None
2761
2761
  """
2762
- Required for upload branches and omitted for abort. The multipart
2763
- filename's sanitized final extension selects the temporary bytes;
2764
- keep that extension unchanged throughout continuation.
2762
+ Required for upload branches and omitted for abort. Filename and
2763
+ extension may change between fragments in the same uid/tempid
2764
+ session. Completion uses the final fragment's source filename.
2765
2765
 
2766
2766
  """
2767
2767
 
@@ -79,7 +79,7 @@ class Uploads:
79
79
  complete_sha256: str | None = None,
80
80
  content_type: str = "application/octet-stream",
81
81
  ) -> StagedUploadSession | StagedFile:
82
- """Stage bytes; preserve the sanitized, case-sensitive filename extension when resuming."""
82
+ """Stage bytes; continuation filenames may change, and completion uses the final filename."""
83
83
  if session is None:
84
84
  session = StagedUploadSession(
85
85
  binding=UploadBindingRef.new(), upload_session_id=f"tmp-{secrets.token_urlsafe(24)}"
@@ -163,7 +163,7 @@ class Uploads:
163
163
  )
164
164
 
165
165
  async def abort(self, upload: StagedUploadSession | StagedFile) -> None:
166
- """Cancel incomplete extension variants; completed staging identities remain untouched (PVD-052)."""
166
+ """Cancel extensionless incomplete bytes; completed staging identities remain untouched (PVD-052)."""
167
167
  if isinstance(upload, StagedFile):
168
168
  # The completed file keeps this Provider identity private except for same-module cleanup.
169
169
  upload_session_id = upload._upload_session_id # pyright: ignore[reportPrivateUsage]
@@ -209,7 +209,7 @@ async def test_stage_single_bytes_creates_session_and_computes_hash(
209
209
 
210
210
  @pytest.mark.asyncio
211
211
  @pytest.mark.parametrize(
212
- ("first_filename", "final_filename"), [("paper.pdf", "paper.pdf"), ("paper.P-d_f", "renamed.Pdf")]
212
+ ("first_filename", "final_filename"), [("paper.pdf", "paper.pdf"), ("paper.txt", "renamed.P-d_f")]
213
213
  )
214
214
  async def test_stage_fragments_sends_acknowledged_offset_and_maps_completion_and_abort(
215
215
  provider_session: Session,
@@ -433,7 +433,7 @@ wheels = [
433
433
 
434
434
  [[package]]
435
435
  name = "matelab-python-sdk"
436
- version = "0.2.0a9"
436
+ version = "0.2.0a10"
437
437
  source = { editable = "." }
438
438
  dependencies = [
439
439
  { name = "httpx2" },