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.
- 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.0a9 → matelab_python_sdk-0.2.0a10}/PKG-INFO +1 -1
- {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/contracts/matelab-integration-v1.lock.json +3 -3
- {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/contracts/matelab-integration-v1.openapi.yaml +26 -26
- {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/docs/api.md +16 -14
- {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/docs/operation-coverage.yaml +1 -1
- {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/pyproject.toml +1 -1
- {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/src/matelab/_generated/models.py +4 -4
- {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/src/matelab/domains/uploads.py +2 -2
- {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/tests/test_uploads_and_literature.py +1 -1
- {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/uv.lock +1 -1
- {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/.env.example +0 -0
- {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/.github/workflows/release.yml +0 -0
- {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/.gitignore +0 -0
- {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/AGENTS.md +0 -0
- {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/CONTEXT.md +0 -0
- {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/LICENSE +0 -0
- {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/NOTICE +0 -0
- {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/README.md +0 -0
- {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/docs/agent-architecture.md +0 -0
- {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/docs/agent-workflows.md +0 -0
- {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/docs/development.md +0 -0
- {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/scripts/check_installed_package.py +0 -0
- {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/scripts/check_release.py +0 -0
- {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/scripts/generate_models.py +0 -0
- {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/src/matelab/__init__.py +0 -0
- {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/src/matelab/_generated/__init__.py +0 -0
- {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/src/matelab/_hashes.py +0 -0
- {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/src/matelab/_transport.py +0 -0
- {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/src/matelab/_wire_validation.py +0 -0
- {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/src/matelab/client.py +0 -0
- {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/src/matelab/domains/__init__.py +0 -0
- {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/src/matelab/domains/_attachment_content.py +0 -0
- {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/src/matelab/domains/cloud_drive.py +0 -0
- {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/src/matelab/domains/groups.py +0 -0
- {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/src/matelab/domains/literature.py +0 -0
- {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/src/matelab/domains/notebooks.py +0 -0
- {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/src/matelab/domains/records.py +0 -0
- {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/src/matelab/domains/templates.py +0 -0
- {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/src/matelab/domains/users.py +0 -0
- {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/src/matelab/errors.py +0 -0
- {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/src/matelab/py.typed +0 -0
- {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/src/matelab/session.py +0 -0
- {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/src/matelab/streaming.py +0 -0
- {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/tests/conftest.py +0 -0
- {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/tests/provider/test_provider_smoke.py +0 -0
- {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/tests/test_async_attachment_upload.py +0 -0
- {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/tests/test_client.py +0 -0
- {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/tests/test_cloud_drive.py +0 -0
- {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/tests/test_comments_and_downloads.py +0 -0
- {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/tests/test_consumer_smoke.py +0 -0
- {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/tests/test_errors.py +0 -0
- {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/tests/test_extended_records.py +0 -0
- {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/tests/test_generation.py +0 -0
- {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/tests/test_groups.py +0 -0
- {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/tests/test_notebook_lifecycle.py +0 -0
- {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/tests/test_notebooks.py +0 -0
- {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/tests/test_operation_coverage.py +0 -0
- {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/tests/test_record_lifecycle.py +0 -0
- {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/tests/test_record_mutations.py +0 -0
- {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/tests/test_records.py +0 -0
- {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/tests/test_release.py +0 -0
- {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/tests/test_streaming.py +0 -0
- {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/tests/test_template_lifecycle.py +0 -0
- {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/tests/test_templates.py +0 -0
- {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/tests/test_transport.py +0 -0
- {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/tests/test_users.py +0 -0
- {matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/tests/typing/consumer.py +0 -0
- {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,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
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
"schema_version": 1,
|
|
3
3
|
"contract_line": "integration-v1",
|
|
4
4
|
"release": {
|
|
5
|
-
"tag": "v2026.11.
|
|
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.11.
|
|
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:
|
|
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:
|
|
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:
|
|
65
|
-
previousReleaseEvidenceCommit:
|
|
66
|
-
lastProviderVerifiedCommit:
|
|
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:
|
|
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:
|
|
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
|
|
2024
|
-
new bytes. Errors after import/update writing starts still
|
|
2025
|
-
whether the stage was consumed.
|
|
2026
|
-
filename's extension after removing
|
|
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.
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
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.
|
|
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
|
|
3748
|
-
|
|
3749
|
-
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
|
|
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.
|
|
7494
|
-
|
|
7495
|
-
|
|
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.
|
|
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
|
-
`
|
|
11
|
-
and SHA-256 `
|
|
12
|
-
`
|
|
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 `
|
|
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
|
-
`
|
|
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.
|
|
727
|
-
|
|
728
|
-
the
|
|
729
|
-
|
|
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
|
|
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
|
-
|
|
744
|
-
|
|
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
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "matelab-python-sdk"
|
|
7
|
-
version = "0.2.
|
|
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.
|
|
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.
|
|
2763
|
-
|
|
2764
|
-
|
|
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;
|
|
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
|
|
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]
|
{matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/tests/test_uploads_and_literature.py
RENAMED
|
@@ -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.
|
|
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,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/scripts/check_installed_package.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/src/matelab/_generated/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/src/matelab/domains/cloud_drive.py
RENAMED
|
File without changes
|
|
File without changes
|
{matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/src/matelab/domains/literature.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/tests/provider/test_provider_smoke.py
RENAMED
|
File without changes
|
{matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/tests/test_async_attachment_upload.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{matelab_python_sdk-0.2.0a9 → matelab_python_sdk-0.2.0a10}/tests/test_comments_and_downloads.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|