matelab-python-sdk 0.2.0a1__tar.gz → 0.2.0a3__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.0a3/.env.example +7 -0
  2. {matelab_python_sdk-0.2.0a1 → matelab_python_sdk-0.2.0a3}/.github/workflows/release.yml +2 -2
  3. {matelab_python_sdk-0.2.0a1 → matelab_python_sdk-0.2.0a3}/AGENTS.md +14 -9
  4. {matelab_python_sdk-0.2.0a1 → matelab_python_sdk-0.2.0a3}/CHANGELOG.md +23 -0
  5. matelab_python_sdk-0.2.0a3/PKG-INFO +26 -0
  6. {matelab_python_sdk-0.2.0a1 → matelab_python_sdk-0.2.0a3}/README.md +30 -34
  7. {matelab_python_sdk-0.2.0a1 → matelab_python_sdk-0.2.0a3}/docs/agent-architecture.md +2 -1
  8. matelab_python_sdk-0.2.0a3/docs/agent-workflows.md +129 -0
  9. {matelab_python_sdk-0.2.0a1 → matelab_python_sdk-0.2.0a3}/pyproject.toml +21 -22
  10. {matelab_python_sdk-0.2.0a1 → matelab_python_sdk-0.2.0a3}/scripts/check_release.py +6 -9
  11. {matelab_python_sdk-0.2.0a1 → matelab_python_sdk-0.2.0a3}/src/matelab/_generated/models.py +1 -1
  12. {matelab_python_sdk-0.2.0a1 → matelab_python_sdk-0.2.0a3}/src/matelab/_transport.py +7 -4
  13. {matelab_python_sdk-0.2.0a1 → matelab_python_sdk-0.2.0a3}/src/matelab/domains/records.py +9 -10
  14. {matelab_python_sdk-0.2.0a1 → matelab_python_sdk-0.2.0a3}/tests/test_async_attachment_upload.py +1 -18
  15. {matelab_python_sdk-0.2.0a1 → matelab_python_sdk-0.2.0a3}/tests/test_client.py +4 -1
  16. {matelab_python_sdk-0.2.0a1 → matelab_python_sdk-0.2.0a3}/tests/test_generation.py +5 -12
  17. {matelab_python_sdk-0.2.0a1 → matelab_python_sdk-0.2.0a3}/tests/test_operation_coverage.py +0 -2
  18. {matelab_python_sdk-0.2.0a1 → matelab_python_sdk-0.2.0a3}/tests/test_record_lifecycle.py +0 -14
  19. {matelab_python_sdk-0.2.0a1 → matelab_python_sdk-0.2.0a3}/tests/test_record_mutations.py +90 -0
  20. {matelab_python_sdk-0.2.0a1 → matelab_python_sdk-0.2.0a3}/tests/test_release.py +34 -42
  21. {matelab_python_sdk-0.2.0a1 → matelab_python_sdk-0.2.0a3}/tests/test_streaming.py +93 -8
  22. {matelab_python_sdk-0.2.0a1 → matelab_python_sdk-0.2.0a3}/tests/test_uploads_and_literature.py +45 -0
  23. matelab_python_sdk-0.2.0a3/uv.lock +940 -0
  24. matelab_python_sdk-0.2.0a1/.env.example +0 -14
  25. matelab_python_sdk-0.2.0a1/PKG-INFO +0 -993
  26. matelab_python_sdk-0.2.0a1/docs/agent-workflows.md +0 -79
  27. matelab_python_sdk-0.2.0a1/uv.lock +0 -921
  28. {matelab_python_sdk-0.2.0a1 → matelab_python_sdk-0.2.0a3}/.gitignore +0 -0
  29. {matelab_python_sdk-0.2.0a1 → matelab_python_sdk-0.2.0a3}/CONTEXT.md +0 -0
  30. {matelab_python_sdk-0.2.0a1 → matelab_python_sdk-0.2.0a3}/LICENSE +0 -0
  31. {matelab_python_sdk-0.2.0a1 → matelab_python_sdk-0.2.0a3}/NOTICE +0 -0
  32. {matelab_python_sdk-0.2.0a1 → matelab_python_sdk-0.2.0a3}/contracts/matelab-integration-v1.lock.json +0 -0
  33. {matelab_python_sdk-0.2.0a1 → matelab_python_sdk-0.2.0a3}/contracts/matelab-integration-v1.openapi.yaml +0 -0
  34. {matelab_python_sdk-0.2.0a1 → matelab_python_sdk-0.2.0a3}/docs/operation-coverage.yaml +0 -0
  35. {matelab_python_sdk-0.2.0a1 → matelab_python_sdk-0.2.0a3}/scripts/check_installed_package.py +0 -0
  36. {matelab_python_sdk-0.2.0a1 → matelab_python_sdk-0.2.0a3}/scripts/generate_models.py +0 -0
  37. {matelab_python_sdk-0.2.0a1 → matelab_python_sdk-0.2.0a3}/src/matelab/__init__.py +0 -0
  38. {matelab_python_sdk-0.2.0a1 → matelab_python_sdk-0.2.0a3}/src/matelab/_generated/__init__.py +0 -0
  39. {matelab_python_sdk-0.2.0a1 → matelab_python_sdk-0.2.0a3}/src/matelab/_hashes.py +0 -0
  40. {matelab_python_sdk-0.2.0a1 → matelab_python_sdk-0.2.0a3}/src/matelab/_wire_validation.py +0 -0
  41. {matelab_python_sdk-0.2.0a1 → matelab_python_sdk-0.2.0a3}/src/matelab/client.py +0 -0
  42. {matelab_python_sdk-0.2.0a1 → matelab_python_sdk-0.2.0a3}/src/matelab/domains/__init__.py +0 -0
  43. {matelab_python_sdk-0.2.0a1 → matelab_python_sdk-0.2.0a3}/src/matelab/domains/_attachment_content.py +0 -0
  44. {matelab_python_sdk-0.2.0a1 → matelab_python_sdk-0.2.0a3}/src/matelab/domains/cloud_drive.py +0 -0
  45. {matelab_python_sdk-0.2.0a1 → matelab_python_sdk-0.2.0a3}/src/matelab/domains/groups.py +0 -0
  46. {matelab_python_sdk-0.2.0a1 → matelab_python_sdk-0.2.0a3}/src/matelab/domains/literature.py +0 -0
  47. {matelab_python_sdk-0.2.0a1 → matelab_python_sdk-0.2.0a3}/src/matelab/domains/notebooks.py +0 -0
  48. {matelab_python_sdk-0.2.0a1 → matelab_python_sdk-0.2.0a3}/src/matelab/domains/templates.py +0 -0
  49. {matelab_python_sdk-0.2.0a1 → matelab_python_sdk-0.2.0a3}/src/matelab/domains/uploads.py +0 -0
  50. {matelab_python_sdk-0.2.0a1 → matelab_python_sdk-0.2.0a3}/src/matelab/domains/users.py +0 -0
  51. {matelab_python_sdk-0.2.0a1 → matelab_python_sdk-0.2.0a3}/src/matelab/errors.py +0 -0
  52. {matelab_python_sdk-0.2.0a1 → matelab_python_sdk-0.2.0a3}/src/matelab/py.typed +0 -0
  53. {matelab_python_sdk-0.2.0a1 → matelab_python_sdk-0.2.0a3}/src/matelab/session.py +0 -0
  54. {matelab_python_sdk-0.2.0a1 → matelab_python_sdk-0.2.0a3}/src/matelab/streaming.py +0 -0
  55. {matelab_python_sdk-0.2.0a1 → matelab_python_sdk-0.2.0a3}/tests/conftest.py +0 -0
  56. {matelab_python_sdk-0.2.0a1 → matelab_python_sdk-0.2.0a3}/tests/provider/test_provider_smoke.py +0 -0
  57. {matelab_python_sdk-0.2.0a1 → matelab_python_sdk-0.2.0a3}/tests/test_cloud_drive.py +0 -0
  58. {matelab_python_sdk-0.2.0a1 → matelab_python_sdk-0.2.0a3}/tests/test_comments_and_downloads.py +0 -0
  59. {matelab_python_sdk-0.2.0a1 → matelab_python_sdk-0.2.0a3}/tests/test_consumer_smoke.py +0 -0
  60. {matelab_python_sdk-0.2.0a1 → matelab_python_sdk-0.2.0a3}/tests/test_errors.py +0 -0
  61. {matelab_python_sdk-0.2.0a1 → matelab_python_sdk-0.2.0a3}/tests/test_extended_records.py +0 -0
  62. {matelab_python_sdk-0.2.0a1 → matelab_python_sdk-0.2.0a3}/tests/test_groups.py +0 -0
  63. {matelab_python_sdk-0.2.0a1 → matelab_python_sdk-0.2.0a3}/tests/test_notebook_lifecycle.py +0 -0
  64. {matelab_python_sdk-0.2.0a1 → matelab_python_sdk-0.2.0a3}/tests/test_notebooks.py +0 -0
  65. {matelab_python_sdk-0.2.0a1 → matelab_python_sdk-0.2.0a3}/tests/test_records.py +0 -0
  66. {matelab_python_sdk-0.2.0a1 → matelab_python_sdk-0.2.0a3}/tests/test_template_lifecycle.py +0 -0
  67. {matelab_python_sdk-0.2.0a1 → matelab_python_sdk-0.2.0a3}/tests/test_templates.py +0 -0
  68. {matelab_python_sdk-0.2.0a1 → matelab_python_sdk-0.2.0a3}/tests/test_transport.py +0 -0
  69. {matelab_python_sdk-0.2.0a1 → matelab_python_sdk-0.2.0a3}/tests/test_users.py +0 -0
  70. {matelab_python_sdk-0.2.0a1 → matelab_python_sdk-0.2.0a3}/tests/typing/consumer.py +0 -0
  71. {matelab_python_sdk-0.2.0a1 → matelab_python_sdk-0.2.0a3}/tests/typing/pyproject.toml +0 -0
@@ -0,0 +1,7 @@
1
+ # SDK Provider smoke targets the confirmed isolated test service.
2
+ # Configure this repository's .env.test independently of matelab-spec.
3
+ # Authentication and external-token refresh persist Provider token state.
4
+ # Keep these names aligned with matelab-spec Provider Verification.
5
+ MATELAB_PROVIDER_BASE_URL=
6
+ MATELAB_PROVIDER_USERNAME=
7
+ MATELAB_PROVIDER_PASSWORD=
@@ -84,7 +84,7 @@ jobs:
84
84
  uv pip install \
85
85
  --python "${RUNNER_TEMP}/matelab-consumer/bin/python" \
86
86
  dist/release/*.whl \
87
- "basedpyright==1.39.9" \
87
+ "basedpyright<2,>=1.40.1" \
88
88
  "nodejs-wheel-binaries==22.20.0"
89
89
  "${RUNNER_TEMP}/matelab-consumer/bin/python" scripts/check_installed_package.py
90
90
  "${RUNNER_TEMP}/matelab-consumer/bin/basedpyright" \
@@ -116,4 +116,4 @@ jobs:
116
116
  name: python-package-distributions
117
117
  path: dist
118
118
  - name: Publish distributions
119
- uses: pypa/gh-action-pypi-publish@ba38be9e461d3875417946c167d0b5f3d385a247 # v1.14.1
119
+ uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
@@ -5,11 +5,12 @@
5
5
  ## 维护顺序
6
6
 
7
7
  1. **定界**:先读 `git status` 和适用权威;完成标准是任务范围、用户现有改动和所需外部权限均已识别。
8
- 2. **查证**:沿 public intent → wire operation → Provider evidence 核对;完成标准是每个受影响 operation 的
9
- request、response、security、examples、成功码及 PVD/PCG 均有结论,Contract 缺口已明确报告。
10
- 3. **收敛**:只修改任务必需的层;完成标准是 public、adapter、generated、tests、coverage 和 docs 的影响均已
11
- 记账,未加入 Provider 推测、Consumer 私有概念或无真实第二实现的 seam。
12
- 4. **验证**:先跑聚焦测试,再跑适用的完整门槛;完成标准是必需命令全部退出为零,skip 单独说明。
8
+ 2. **查证**:涉及 SDK 行为或 Contract 时,沿 public intent → wire operation → Provider evidence 核对受影响
9
+ operation 的 request、response、security、examples、成功码及 PVD/PCG,明确报告 Contract 缺口。文档、排版和
10
+ 工具配置修改只核对相关权威。
11
+ 3. **收敛**:只修改任务必需的层,检查 public、adapter、generated、tests、coverage 和 docs 中受影响的内容。
12
+ 4. **验证**:按变更类型选择 [delivery gate](docs/agent-workflows.md#delivery-gate),先运行相关检查;代码变更先跑
13
+ 聚焦测试,再跑完整门槛。完成标准是必需命令全部退出为零,skip 和 deselected 分别说明。
13
14
  5. **交付**:复核最终 diff、状态和验证证据;完成标准是结果、限制和未执行项都可由用户独立核对。
14
15
 
15
16
  ## 分支指针
@@ -20,11 +21,13 @@
20
21
  [`docs/agent-architecture.md#transport-session-and-errors`](docs/agent-architecture.md#transport-session-and-errors)。
21
22
  - **Contract adoption**:同步 spec tag、snapshot、lock 或生成模型时执行
22
23
  [`docs/agent-workflows.md#contract-adoption`](docs/agent-workflows.md#contract-adoption)。
23
- - **Delivery gate**:代码、Contract、依赖或构建元数据变化后执行
24
+ - **Configuration and dependencies**:修改 `pyproject.toml`、依赖范围、lock 或对应 CI 配置时读
25
+ [`docs/agent-workflows.md#configuration-and-dependencies`](docs/agent-workflows.md#configuration-and-dependencies)。
26
+ - **Delivery gate**:交付前按变更类型执行
24
27
  [`docs/agent-workflows.md#delivery-gate`](docs/agent-workflows.md#delivery-gate)。
25
28
  - **Provider validation**:任何实时 Provider smoke、读取或 mutation 前执行
26
29
  [`docs/agent-workflows.md#provider-validation`](docs/agent-workflows.md#provider-validation)。
27
- - **Release**:版本、制品、commit、tag、push 或 publish 任务执行
30
+ - **Release**:准备版本或发布制品时执行;普通 commit/push 只应用其中的授权与 diff 审查要求
28
31
  [`docs/agent-workflows.md#release`](docs/agent-workflows.md#release)。
29
32
 
30
33
  ## 权威
@@ -48,5 +51,7 @@
48
51
  仓库只作只读证据来源。
49
52
  - `/home/tnzhu/projects/matelab-spec` 定义 Wire Contract、兼容策略及 Provider 问题;`matelab-server` 是 Provider
50
53
  实现,`matelab-mcp` 只提供 Consumer evidence,均不能反向定义 SDK interface。
51
- - commit、push、tag、publish、生产访问和真实 Provider mutation 需要本轮明确授权;授权只覆盖用户指定的目标与
52
- 副作用。
54
+ - SDK 与 spec 可以使用同一个 Docker 测试服务,各自维护环境配置和测试职责;修改 SDK 不授权修改邻接仓库或
55
+ 运行 spec 测试。
56
+ - commit、push、tag、publish、生产访问和真实 Provider mutation 需要用户对目标及副作用的明确授权。沿用会话中
57
+ 已有的有效授权;只在目标、副作用超出授权或必要信息缺失时再询问。
@@ -3,6 +3,29 @@
3
3
  This file records release-to-release changes and migration impact. It is not the authority for the current interface;
4
4
  see [README.md](README.md) for current behavior and [AGENTS.md](AGENTS.md) for maintenance policy.
5
5
 
6
+ ## 0.2.0a3 - 2026-10-02
7
+
8
+ - Updates the pinned PyPI publishing action to v1.14.2, whose Twine 7 supports the Core Metadata 2.5 emitted by
9
+ Hatchling 1.32.4. Publishes the changes recorded under `0.2.0a2`; that candidate passed SDK and artifact checks but
10
+ was not uploaded to PyPI because the previous publisher rejected its metadata version.
11
+
12
+ ## 0.2.0a2 - 2026-10-02
13
+
14
+ - Raises runtime and build dependency lower bounds to validated stable releases and uses compatible
15
+ development tool ranges, retaining `nodejs-wheel-binaries==22.20.0` as the sole system-specific exact pin. Refreshes
16
+ the lock against PyPI and aligns the installed-wheel CI checker with the declared Basedpyright range.
17
+ - Release artifact auditing compares dependency metadata with `pyproject.toml`, and release tests verify version
18
+ constraints and CI consistency without freezing dependency update values.
19
+ - Sets the distribution author to CMPDC, Institute of Physics, Chinese Academy of Sciences.
20
+ - Documents that SDK smoke can use the same isolated test service as spec while each repository keeps its own test
21
+ configuration; limits the SDK environment template to its smoke connection settings. Clarifies maintenance
22
+ validation by change type, keeps the default gate offline, and records the project and Basedpyright field order.
23
+ - Fixes rich-text and comment attachment markers whose names share a prefix, preserving exact attachment bindings
24
+ regardless of mapping order.
25
+ - Keeps download rejection errors within the public SDK error boundary when response cleanup fails.
26
+ - Consolidates duplicate attachment tests and removes documentation, formatting, and current operation-count
27
+ assertions. Strengthens SDK stream lifecycle and upload metadata validation coverage with controlled offline fixtures.
28
+
6
29
  ## 0.2.0a1 - 2026-10-02
7
30
 
8
31
  - Establishes the `0.x` versioning rule: incompatible public changes advance the SDK minor version; alpha/beta/RC
@@ -0,0 +1,26 @@
1
+ Metadata-Version: 2.5
2
+ Name: matelab-python-sdk
3
+ Version: 0.2.0a3
4
+ Summary: Reusable async Python client for the Matelab Integration Contract
5
+ Author: CMPDC, Institute of Physics, Chinese Academy of Sciences
6
+ License-Expression: Apache-2.0
7
+ License-File: LICENSE
8
+ License-File: NOTICE
9
+ Keywords: asyncio,electronic-lab-notebook,eln,matelab,sdk
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Framework :: AsyncIO
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Topic :: Scientific/Engineering
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.11
23
+ Requires-Dist: httpx2<3,>=2.13.1
24
+ Requires-Dist: jsonschema<5,>=4.26.0
25
+ Requires-Dist: pydantic<3,>=2.13.5
26
+ Requires-Dist: typing-extensions<5,>=4.16.0
@@ -7,7 +7,7 @@ This README describes the current SDK interface, integration semantics, examples
7
7
  [AGENTS.md](AGENTS.md) for durable maintenance rules, and `docs/operation-coverage.yaml` for the machine-readable
8
8
  operation inventory.
9
9
 
10
- The current alpha is `0.2.0a1`. `[project].version` in `pyproject.toml` is the sole SDK version source;
10
+ The current alpha is `0.2.0a3`. `[project].version` in `pyproject.toml` is the sole SDK version source;
11
11
  `uv.lock` only mirrors that source.
12
12
 
13
13
  The SDK is pinned to the immutable `matelab-spec v2026.10.1` Contract Release. The sole pin,
@@ -38,7 +38,7 @@ To test the same artifact a downstream Consumer will install, build and install
38
38
 
39
39
  ```bash
40
40
  uv build --no-build-isolation --out-dir dist/release
41
- python -m pip install dist/release/matelab_python_sdk-0.2.0a1-py3-none-any.whl
41
+ python -m pip install dist/release/matelab_python_sdk-0.2.0a3-py3-none-any.whl
42
42
  ```
43
43
 
44
44
  Do not infer Provider compatibility from the SDK version alone. A release is also bound to the Contract
@@ -57,7 +57,7 @@ forwarding is outside its verified callable scope (PVD-049).
57
57
  ## Versioning
58
58
 
59
59
  SDK versions express compatibility of the public Python interface and behavior. Python prerelease spelling follows
60
- PEP 440: `0.2.0a1` is the first alpha candidate for `0.2.0`. During `0.x` development, incompatible changes advance
60
+ PEP 440: `0.2.0a3` is the third alpha candidate for `0.2.0`. During `0.x` development, incompatible changes advance
61
61
  the minor version. From `1.0.0`, compatible fixes advance patch, compatible additions advance minor, and incompatible
62
62
  changes advance major. Successive candidates for the same target version use `aN`, `bN`, or `rcN`.
63
63
 
@@ -872,16 +872,20 @@ aliases, and unrelated fields do not enter exceptions.
872
872
  ## Development
873
873
 
874
874
  ```bash
875
- uv sync
876
- uv run python scripts/generate_models.py
875
+ uv sync --frozen
877
876
  uv run python scripts/generate_models.py --check
878
877
  uv run ruff check .
879
878
  uv run ruff format --check .
880
879
  uv run basedpyright
881
- uv run pytest
882
- uv build
880
+ uv run pytest -m 'not provider'
881
+ uv build --no-build-isolation
883
882
  ```
884
883
 
884
+ This gate checks the SDK with offline fixtures and excludes the real Provider smoke. Run focused tests first when
885
+ changing code; documentation and configuration-only edits use the relevant checks described in
886
+ [the maintenance workflows](docs/agent-workflows.md#delivery-gate). Contract or generator changes regenerate the
887
+ private models with `uv run python scripts/generate_models.py` before running the gate.
888
+
885
889
  The generator first verifies the contract lock, OpenAPI release metadata, and snapshot digest. It then
886
890
  creates a temporary OpenAPI 3.1 generation projection, resolves references without network access, and
887
891
  generates private component, operation-response, and parameter models. Generator emission uses its built-in formatter;
@@ -890,23 +894,19 @@ projection flattens pure
890
894
  object inheritance and preserves constraints the model generator cannot express as self-contained JSON Schema
891
895
  2020-12 metadata; the checked-in release snapshot remains unchanged. `--check` performs the same validation and
892
896
  deterministic generation without writing the checked-in models. `WireModel` applies that metadata with the standard
893
- `jsonschema` Draft 2020-12 validator; the SDK does not maintain a second hand-written schema interpreter. The current
894
- lock resolves
895
- `datamodel-code-generator 0.71.0` and
896
- `hatchling 1.31.0`. Published metadata requires `httpx2>=2.9.1,<3`, `jsonschema>=4.26,<5`,
897
- `pydantic>=2.13.4,<3`, and `typing-extensions>=4.14.1,<5`; the build backend requires
898
- `hatchling>=1.27,<2`. These lower bounds are verified
899
- against the complete test suite on the supported Python boundary versions rather than inferred from
900
- `uv.lock`. The exact toolchain remains locked for development and release builds. Basedpyright and its
901
- Node wheel retain the compatible exact pair `basedpyright==1.39.9` and
902
- `nodejs-wheel-binaries==22.20.0`.
897
+ `jsonschema` Draft 2020-12 validator; the SDK does not maintain a second hand-written schema interpreter.
898
+
899
+ `pyproject.toml` declares runtime, build, and development compatibility ranges; `uv.lock` fixes the exact environment
900
+ for development and release builds. Runtime and build lower bounds are verified against the complete test suite on
901
+ the supported Python boundary versions. The sole exact development requirement is `nodejs-wheel-binaries`, retained
902
+ for system compatibility. The `0.x` generator and Ruff ranges allow patch updates within their current minor versions.
903
903
 
904
904
  ## Opt-in Provider consumer smoke
905
905
 
906
906
  `tests/provider/test_provider_smoke.py` exercises the consumer flow through only the public SDK interface. Its base
907
907
  scenario covers credential authentication, persistence of the complete returned Session, restoration through a new
908
- `AsyncMatelab` instance, refresh with identity preservation, and notebook discovery. It is not Provider Verification
909
- and is skipped by default.
908
+ `AsyncMatelab` instance, refresh with identity preservation, and notebook discovery. The development gate excludes it;
909
+ a direct pytest run skips it if any of its three connection variables are missing.
910
910
 
911
911
  Raw Provider conformance remains the responsibility of `matelab-spec`, which sends direct HTTP requests and validates
912
912
  the unmodified responses. The SDK does not repeat its route-by-route, cross-account, sharing, or attachment-isolation
@@ -918,31 +918,27 @@ Provider smoke is restricted to the confirmed isolated test service. Authenticat
918
918
  token state. This side effect is inherent to the tested Provider operations; it cannot be disabled by a test setting.
919
919
  Explicitly loading `.env.test` and selecting the `provider` marker is the opt-in for this flow.
920
920
 
921
- Copy `.env.example` to the git-ignored local `.env.test`, then fill in the shared Provider connection settings:
921
+ SDK smoke can use the same isolated `matelab-test` service as `matelab-spec`. Each repository keeps its own test
922
+ configuration: the SDK uses `.env.test`, and spec keeps its existing `.env`.
923
+
924
+ Copy this repository's `.env.example` to the git-ignored local `.env.test`, then fill in the SDK connection settings:
922
925
 
923
926
  - `MATELAB_PROVIDER_BASE_URL`
924
927
  - `MATELAB_PROVIDER_USERNAME`
925
928
  - `MATELAB_PROVIDER_PASSWORD`
926
929
 
927
- These names intentionally match `matelab-spec` Provider Verification. The isolated target may copy them from the spec
928
- `.env` into this repository's `.env.test`. Refreshing the restored Session must preserve its authenticated identity.
929
- With the environment prepared:
930
+ These names intentionally match `matelab-spec` Provider Verification. Refreshing the restored Session must preserve
931
+ its authenticated identity. With the SDK environment prepared, run the public-interface smoke:
930
932
 
931
933
  ```bash
932
934
  uv run --env-file .env.test pytest -m provider tests/provider/test_provider_smoke.py
933
935
  ```
934
936
 
935
- This command runs only the SDK public-interface smoke; it is not the 71-operation Provider Verification. To reuse the
936
- same `.env.test` for the complete Contract suite, also populate the optional share user, secondary account, record
937
- staging opt-in, and Chat SSO settings documented in `.env.example`, then run from sibling checkouts:
938
-
939
- ```bash
940
- cd ../matelab-spec
941
- uv run --env-file ../matelab-python-sdk/.env.test pytest
942
- ```
943
-
944
- The files are never loaded implicitly, so normal test runs remain safely skipped. Do not use either flow against
945
- production, and never commit Provider credentials.
937
+ This command runs only the SDK public-interface smoke; Provider Verification is maintained in
938
+ `matelab-spec` with its own configuration and run instructions.
939
+ SDK tests do not load `.env.test` implicitly; without Provider environment variables, the real smoke remains skipped.
940
+ Offline SDK tests continue to use `MockTransport`. Do not use the smoke flow against production, and never commit
941
+ Provider credentials.
946
942
 
947
943
  ## Reproducible release build
948
944
 
@@ -12,7 +12,8 @@
12
12
  identity、affected rows、排序、分页、原子性、CAS、幂等性和持久化结果。
13
13
  - MCP typed refs、capabilities、fingerprints、FastMCP/Redis context 与 Agent 展示模型留在 Consumer。只有真实
14
14
  第二种实现才能引入 seam。
15
- - 支持 Python 3.11+;源码不使用 `from __future__ import annotations`,前向引用优先靠定义顺序解决。
15
+ - 源码语法遵循 `pyproject.toml` 中 `requires-python` 的最低版本;不使用 `from __future__ import annotations`,
16
+ 前向引用优先靠定义顺序解决。
16
17
 
17
18
  ## Transport, Session, and errors
18
19
 
@@ -0,0 +1,129 @@
1
+ # Agent Maintenance Workflows
2
+
3
+ 本文件只在对应分支触发时加载。机器值始终从仓库读取;这里记录顺序、授权边界和完成标准。
4
+
5
+ ## Contract adoption
6
+
7
+ 1. 读取当前工作区、Contract lock 和远端 release refs。只选择新的 annotated tag;移动分支和改写后的旧
8
+ snapshot 不能成为 SDK pin。记录 tag object、peeled commit、OpenAPI path 与 SHA-256。完成标准:四者固定且
9
+ 互相一致。
10
+ 2. 比较当前 snapshot 与 release commit。逐项核对 operation request、response、security、examples、成功码、
11
+ Provider evidence 及 PVD/PCG。完成标准:每个 schema/operation 差异都分类为 generated-only、adapter、public
12
+ interface、test、coverage 或 docs 影响。
13
+ 3. request/response 均由生成 wire model 校验,adapter 只映射 public Python 名称与 wire 字段。required nullable
14
+ 使用无默认值的 `T | None`;request 拒绝未知字段,response 接受新增字段。若专用 schema、顶层 request 或
15
+ Provider evidence 相互矛盾,保留当前安全边界并报告缺口。
16
+ 4. 从 release commit 复制 OpenAPI snapshot;同一变更更新 lock 的 tag、commit、snapshot path 和 SHA-256,运行
17
+ 生成脚本并审查完整 generated diff。`_generated` 只由生成脚本修改;projection 只补偿生成器表达能力并保留
18
+ inherited required、`if/then`、`oneOf`、`anyOf` 和 `not`。完成标准:snapshot hash 与 lock 相等,生成差异
19
+ 全部可由 OpenAPI 差异解释,`--check` 确定、非写入且能发现 stale output。
20
+ 5. 同步 runtime、测试、operation coverage、README、CHANGELOG、`pyproject.toml` 与 `uv.lock` 中受影响的当前态。
21
+ 完成标准:每个 Contract 差异都有实现或明确 containment,历史 changelog 未被改写。
22
+ 6. 执行 [delivery gate](#delivery-gate)。
23
+
24
+ ## Configuration and dependencies
25
+
26
+ 配置值以 `pyproject.toml` 为准,`uv.lock` 固定本仓库的解析环境,不反向定义发布依赖范围。
27
+
28
+ - `[project]` 字段依次为 `name`、`version`、`description`、`requires-python`、`keywords`、`authors`、
29
+ `dependencies`,其他字段放在其后。
30
+ - `[tool.basedpyright]` 字段依次为 `pythonVersion`、`typeCheckingMode`、`include`、`exclude`、
31
+ `reportUnannotatedClassAttribute`、`reportIncompatibleUnannotatedOverride`,其他设置放在其后。沿用 SDK 的目标
32
+ Python 版本与检查路径,不照搬其他项目的值。
33
+ - runtime、build 和 dev 依赖声明兼容范围;`nodejs-wheel-binaries` 保留系统兼容性所需的精确固定。更新范围时
34
+ 同步 lock 和安装后消费检查使用的 CI requirement。下限须有实际验证;上限排除未经验证的下一 major,
35
+ generator 和 Ruff 的 `0.x` 范围排除下一 minor。
36
+ - 构建后端范围和 wheel package 显式配置;metadata 与制品检查从这些声明推导预期值。
37
+
38
+ 完成标准:配置可解析、范围与 lock 一致、受影响 CI 使用同一约束;字段重排不改变配置含义。
39
+
40
+ ## Delivery gate
41
+
42
+ 按本次变更选择验证范围:
43
+
44
+ - 纯文档修改检查相关事实、本地路径和 Markdown 引用;示例或接口说明变化要对照当前 public interface。
45
+ - 字段重排或显式写出既有默认值,检查语法与有效配置一致性。工具配置的有效设置变化运行对应工具。
46
+ - SDK 代码、测试行为、Contract、依赖范围或影响制品的构建元数据变化,先跑聚焦测试,再完成下列完整门槛。
47
+ - 发布准备在完整门槛之后继续执行 [release](#release) 的制品检查。
48
+
49
+ 已有验证仍对应当前变更且无新失败或疑点时可以复用。文档整理不触发重复的完整测试、构建或实时 Provider 调用。
50
+
51
+ ### SDK test scope
52
+
53
+ SDK 测试覆盖 public interface 到 wire adapter 的映射、认证与 Session、错误与重试、stream/上传生命周期及
54
+ 分发制品。Provider 的逐路由原始 HTTP conformance 由 `matelab-spec` 负责,不在 SDK 重复实现或附带重跑。
55
+
56
+ 新增或修改 operation 覆盖 method/path/encoding、认证、成功映射及适用的空结果、
57
+ required nullable 和 additive response fields;operation coverage 与 pinned OpenAPI exact match,implemented
58
+ operation 同时具备 public interface、wire adapter 和测试。删除或收紧 public interface 时同步移除过时测试,
59
+ 不保留无承诺的墓碑 alias。生成测试覆盖 metadata/checksum、schema、required/nullable、extra policy、确定性和
60
+ stale detection。
61
+
62
+ 优先使用 pinned sanitized examples 或小型受控 fixture 验证稳定行为。operation 数量从 pinned Contract 推导,
63
+ 不固定当前数量;当前服务地址和依赖版本值不作为回归断言,除非它们本身就是明确的稳定契约。
64
+ 配置值变化只验证语法、约束及相关行为,不另加冻结新值或重复实现逻辑的测试。
65
+
66
+ Ruff/Basedpyright 诊断通过修复代码或精确 ignore 解决,不缩小检查范围;Basedpyright 目标为 0 errors / 0 warnings。
67
+
68
+ ### Full gate
69
+
70
+ 在上面的完整门槛条件成立时运行:
71
+
72
+ ```bash
73
+ uv sync --frozen
74
+ uv run python scripts/generate_models.py --check
75
+ uv run ruff check .
76
+ uv run ruff format --check .
77
+ uv run basedpyright
78
+ uv run pytest -m 'not provider'
79
+ uv build --no-build-isolation
80
+ ```
81
+
82
+ `not provider` 显式排除实时 smoke,但保留离线 smoke 配置测试。确需实时验证时单独执行
83
+ [Provider validation](#provider-validation),不通过加载凭证扩展完整门槛的副作用。
84
+
85
+ 检查因权限失败或无进展时,先查看输出和自己启动的进程,区分普通失败与文件、socket、网络或子进程限制。
86
+ 有沙箱阻塞证据后停止自己启动的停滞测试,通过工具审批机制申请最小范围重试;保留配置,不改权限、凭证或测试
87
+ 行为来绕过限制。超时本身不是沙箱拒绝的证据;无法升级或被拒绝时报告阻塞。
88
+
89
+ 完成标准:必需检查退出为零;skip 按测试名称和原因报告,不能计为通过;deselected 说明筛选范围;最终
90
+ `git diff --check` 无错误。本次新增修改限于任务范围,原有工作区改动按归属保留。
91
+
92
+ ## Provider validation
93
+
94
+ 1. 默认使用 pinned examples、`MockTransport` 和 SDK adapter tests。SDK 不自动读取 `.env` 或 `.env.test`;
95
+ smoke 从进程环境读取三个连接变量,缺少任一变量就 skip。默认门槛显式排除 `provider`;只有任务确需实时
96
+ 验证且会话授权覆盖目标及副作用时才启用。
97
+ 2. 执行前确认目标是隔离服务、`.env.test` 处于 Git ignore、命令显式加载该文件,并列出 authentication、refresh、
98
+ mutation 或带写痕迹读取的副作用。生产目标或 mutation 必须有对应的明确授权;已有授权不因多轮对话失效。
99
+ 3. SDK public smoke 使用 README 的 `Opt-in Provider consumer smoke` 命令;route-by-route conformance 由
100
+ `matelab-spec` Provider Verification 负责。可以共用 Docker 中的隔离服务,但 SDK `.env.test` 与 spec 的环境
101
+ 配置分别维护,不跨仓库传入 SDK 配置,也不因运行 SDK smoke 附带运行 spec 测试。两者的结果分开报告。
102
+ 4. mutation 场景在写前记录 fixture,在写后回读,并尽力清理。完成标准:目标、Provider commit、命令、通过数、
103
+ skip/failure 和清理结果均可追溯;凭证及敏感 payload 未进入输出或提交。
104
+
105
+ ## Release
106
+
107
+ 版本规划和本地构建可以在请求范围内完成;commit、push、tag、publish 只执行会话已明确授权的步骤。普通
108
+ commit/push 审查并提交或推送已授权的 diff,不自动改版本、建 tag 或发布。发布准备完成后若缺少发布授权,
109
+ 交付版本、diff、验证证据和制品结果,授权是执行外部动作前的最后一步。
110
+
111
+ 以下步骤适用于实际发布;只准备版本或制品时止于所需步骤:
112
+
113
+ 1. 读取 `[project].version`,确认待发布版本未发布、changelog 已追加且 Contract pin 完整。SDK 采用 SemVer 的
114
+ 兼容规则,Python 版本拼写遵循 PEP 440;公开接口
115
+ 删除、重命名、类型收紧或不兼容行为变化按 breaking change 处理。`0.x` 阶段 breaking change 递增 minor;
116
+ `1.0.0` 起兼容修复递增 patch、兼容新增递增 minor、breaking change 递增 major。同一候选版本的预发布使用
117
+ `aN`、`bN`、`rcN` 并在阶段内递增序号;改变目标版本后重新从 1 开始。spec 的年月版本与 SDK 独立,通过
118
+ Contract lock 关联,不按 spec 版本号机械递增 SDK。完成标准:版本、文档和 tag 名 `v{version}` 一致。
119
+ 2. 在完整 delivery gate 通过且 commit 已授权后创建单一 release commit。完成标准:commit 仅包含已审查、
120
+ 已授权的 diff;隔离无关用户改动,不把清理用户工作区作为发布前提。
121
+ 3. 以 release commit 的 committer timestamp 设置 `SOURCE_DATE_EPOCH`,从锁定环境构建两次并逐字节比较 wheel
122
+ 与 sdist;运行 `scripts/check_release.py`,再从 wheel 验证公开导入和类型消费。完成标准:制品可复现,
123
+ Apache-2.0 `LICENSE`/`NOTICE`、PEP 639 METADATA、`py.typed` 与公开边界审计通过,`_generated` 未公开,
124
+ checksum 已记录。未提交的本地构建标明对应工作区状态,不作为正式发布制品。
125
+ 4. 创建指向 release commit 的 annotated `v{version}` tag。先推送 `origin` 的 main/tag,再推送 GitHub mirror 的
126
+ main/tag;只有与项目版本完全匹配的 tag 触发 Trusted Publishing workflow,第三方 Action 固定 immutable
127
+ commit SHA。
128
+ 5. 跟踪 CI 的 Python 边界测试、生成检查、可复现构建、安装后消费和 PyPI publish。完成标准:两个远端 main/tag
129
+ 都指向 release commit,workflow success,PyPI 对应版本可读取;原有无关工作区改动保留。
@@ -1,17 +1,24 @@
1
1
  [build-system]
2
- requires = ["hatchling>=1.27,<2"]
2
+ requires = ["hatchling>=1.32.4,<2"]
3
3
  build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "matelab-python-sdk"
7
- version = "0.2.0a1"
7
+ version = "0.2.0a3"
8
8
  description = "Reusable async Python client for the Matelab Integration Contract"
9
- readme = "README.md"
10
9
  requires-python = ">=3.11"
10
+ keywords = ["matelab", "electronic-lab-notebook", "eln", "asyncio", "sdk"]
11
+ authors = [
12
+ { name = "CMPDC, Institute of Physics, Chinese Academy of Sciences" },
13
+ ]
14
+ dependencies = [
15
+ "httpx2>=2.13.1,<3",
16
+ "jsonschema>=4.26.0,<5",
17
+ "pydantic>=2.13.5,<3",
18
+ "typing-extensions>=4.16.0,<5",
19
+ ]
11
20
  license = "Apache-2.0"
12
21
  license-files = ["LICENSE", "NOTICE"]
13
- authors = [{ name = "朱天念", email = "zhutiannian@gmail.com" }]
14
- keywords = ["matelab", "electronic-lab-notebook", "eln", "asyncio", "sdk"]
15
22
  classifiers = [
16
23
  "Development Status :: 3 - Alpha",
17
24
  "Framework :: AsyncIO",
@@ -26,28 +33,19 @@ classifiers = [
26
33
  "Topic :: Software Development :: Libraries :: Python Modules",
27
34
  "Typing :: Typed",
28
35
  ]
29
- dependencies = [
30
- "httpx2>=2.9.1,<3",
31
- "jsonschema>=4.26,<5",
32
- "pydantic>=2.13.4,<3",
33
- "typing-extensions>=4.14.1,<5",
34
- ]
35
36
 
36
37
  [dependency-groups]
37
38
  dev = [
38
- "basedpyright==1.39.9",
39
+ "basedpyright>=1.40.1,<2",
39
40
  "nodejs-wheel-binaries==22.20.0",
40
- "datamodel-code-generator",
41
- "hatchling>=1.27,<2",
42
- "pytest",
43
- "pytest-asyncio",
44
- "pyyaml",
45
- "ruff",
41
+ "datamodel-code-generator>=0.83.0,<0.84",
42
+ "hatchling>=1.32.4,<2",
43
+ "pytest>=9.1.1,<10",
44
+ "pytest-asyncio>=1.4.0,<2",
45
+ "pyyaml>=6.0.3,<7",
46
+ "ruff>=0.16.10,<0.17",
46
47
  ]
47
48
 
48
- [tool.uv]
49
- index-url = "https://repo.huaweicloud.com/repository/pypi/simple"
50
-
51
49
  [tool.ruff]
52
50
  line-length = 120
53
51
  lint.extend-select = ["I"]
@@ -55,9 +53,10 @@ lint.isort.split-on-trailing-comma = false
55
53
  format.skip-magic-trailing-comma = true
56
54
 
57
55
  [tool.basedpyright]
56
+ pythonVersion = "3.11"
57
+ typeCheckingMode = "recommended"
58
58
  include = ["src", "tests", "scripts"]
59
59
  exclude = ["src/matelab/_generated"]
60
- pythonVersion = "3.11"
61
60
  reportUnannotatedClassAttribute = "none"
62
61
  reportIncompatibleUnannotatedOverride = "error"
63
62
 
@@ -11,20 +11,17 @@ from pathlib import Path, PurePosixPath
11
11
  from typing import cast
12
12
  from zipfile import ZipFile
13
13
 
14
+ from packaging.requirements import Requirement
15
+
14
16
  _ROOT = Path(__file__).resolve().parents[1]
15
17
  _PROJECT = cast(dict[str, object], tomllib.loads((_ROOT / "pyproject.toml").read_text())["project"])
16
18
  _PROJECT_NAME = cast(str, _PROJECT["name"])
17
19
  _VERSION = cast(str, _PROJECT["version"])
18
20
  _DIST_INFO = f"{_PROJECT_NAME.replace('-', '_')}-{_VERSION}.dist-info"
19
- _EXPECTED_REQUIRES_DIST = {
20
- "httpx2<3,>=2.9.1",
21
- "jsonschema<5,>=4.26",
22
- "pydantic<3,>=2.13.4",
23
- "typing-extensions<5,>=4.14.1",
24
- }
21
+ _EXPECTED_REQUIRES_DIST = {str(Requirement(value)) for value in cast(list[str], _PROJECT["dependencies"])}
25
22
  _EXPECTED_LICENSE_FILES = set(cast(list[str], _PROJECT["license-files"]))
26
23
  _AUTHOR = cast(list[dict[str, str]], _PROJECT["authors"])[0]
27
- _EXPECTED_AUTHOR_EMAIL = f"{_AUTHOR['name']} <{_AUTHOR['email']}>"
24
+ _EXPECTED_AUTHOR = _AUTHOR["name"]
28
25
  _ALLOWED_ENV_EXAMPLE = ".env.example"
29
26
  _FORBIDDEN_PARTS = {
30
27
  ".env",
@@ -98,8 +95,8 @@ def _check_metadata(artifact: Path, metadata_bytes: bytes) -> None:
98
95
  raise ValueError(f"{artifact.name} contains unexpected License-Expression metadata")
99
96
  if set(metadata.get_all("License-File", [])) != _EXPECTED_LICENSE_FILES:
100
97
  raise ValueError(f"{artifact.name} contains unexpected License-File metadata")
101
- if metadata["Author-email"] != _EXPECTED_AUTHOR_EMAIL:
102
- raise ValueError(f"{artifact.name} contains unexpected Author-email metadata")
98
+ if metadata["Author"] != _EXPECTED_AUTHOR or metadata["Author-email"] is not None:
99
+ raise ValueError(f"{artifact.name} contains unexpected author metadata")
103
100
 
104
101
 
105
102
  def _audit_wheel(path: Path) -> None:
@@ -2,7 +2,7 @@
2
2
  #
3
3
  # @generated by datamodel-codegen:
4
4
  # filename: openapi.yaml
5
- # version: 0.71.0
5
+ # version: 0.83.0
6
6
 
7
7
  from enum import Enum, StrEnum
8
8
  from typing import Annotated, Any, Literal
@@ -80,7 +80,7 @@ def _provider_error(code: int) -> MatelabAuthenticationError | MatelabProviderEr
80
80
 
81
81
 
82
82
  def _header(response: Response, name: str) -> str:
83
- return cast(str, response.headers.get(name, ""))
83
+ return response.headers.get(name, "")
84
84
 
85
85
 
86
86
  def _parse_content_length(response: Response) -> int | None:
@@ -293,7 +293,8 @@ class SessionTransport:
293
293
  raise MatelabTransportError("Could not exchange data with the Matelab Provider.") from None
294
294
 
295
295
  if response.status_code not in {200, 206}:
296
- await response.aclose()
296
+ with suppress(RequestError):
297
+ await response.aclose()
297
298
  raise MatelabTransportError(
298
299
  f"Matelab returned HTTP {response.status_code}.", status_code=response.status_code
299
300
  )
@@ -321,7 +322,8 @@ class SessionTransport:
321
322
  continue
322
323
  raise _provider_error(code)
323
324
  if operation.media_types is not None and media_type not in operation.media_types:
324
- await response.aclose()
325
+ with suppress(RequestError):
326
+ await response.aclose()
325
327
  raise MatelabProtocolError("Matelab returned an unexpected download media type.")
326
328
 
327
329
  content_length = _parse_content_length(response)
@@ -331,6 +333,7 @@ class SessionTransport:
331
333
  or not _CONTENT_RANGE_PATTERN.fullmatch(_header(response, "Content-Range"))
332
334
  or not _header(response, "Content-Disposition")
333
335
  ):
334
- await response.aclose()
336
+ with suppress(RequestError):
337
+ await response.aclose()
335
338
  raise MatelabProtocolError("Matelab returned invalid partial-download metadata.")
336
339
  return DownloadStream(response, content_length=content_length)
@@ -976,10 +976,9 @@ def _encode_record_rich_text(update: RecordRichTextUpdate) -> str:
976
976
  markers = set(_ATTACHMENT_MARKER_PATTERN.findall(update.html))
977
977
  if markers != set(update.attachments):
978
978
  raise MatelabUsageError("Rich-text attachment names must exactly match matelab-attachment:<name> markers.")
979
- encoded = update.html
980
- for name, attachment in update.attachments.items():
981
- encoded = encoded.replace(f"matelab-attachment:{name}", _encode_record_attachment_reference(attachment))
982
- return encoded
979
+ return _ATTACHMENT_MARKER_PATTERN.sub(
980
+ lambda match: _encode_record_attachment_reference(update.attachments[match.group(1)]), update.html
981
+ )
983
982
 
984
983
 
985
984
  def _validate_upload_filename(filename: str) -> None:
@@ -1001,15 +1000,15 @@ def _bind_comment_attachments(
1001
1000
 
1002
1001
  first = next(iter(selected_attachments.values()))
1003
1002
  binding_uid = first._binding_uid # pyright: ignore[reportPrivateUsage]
1004
- for name, attachment in selected_attachments.items():
1003
+ for attachment in selected_attachments.values():
1005
1004
  attachment_binding_uid = attachment._binding_uid # pyright: ignore[reportPrivateUsage]
1006
1005
  if attachment.source != source or attachment_binding_uid != binding_uid:
1007
1006
  raise MatelabUsageError("Comment attachments must belong to the same record and upload batch.")
1008
- body = body.replace(
1009
- f"matelab-attachment:{name}",
1010
- attachment._temporary_url, # pyright: ignore[reportPrivateUsage]
1011
- )
1012
- return body, binding_uid
1007
+ bound_body = _ATTACHMENT_MARKER_PATTERN.sub(
1008
+ lambda match: selected_attachments[match.group(1)]._temporary_url, # pyright: ignore[reportPrivateUsage]
1009
+ body,
1010
+ )
1011
+ return bound_body, binding_uid
1013
1012
 
1014
1013
 
1015
1014
  def _notebook_selector(notebook: NotebookRef) -> str | dict[str, JsonValue]:
@@ -44,24 +44,6 @@ def _upload_response(
44
44
  )
45
45
 
46
46
 
47
- @pytest.mark.asyncio
48
- async def test_upload_attachment_computes_missing_async_checksum(
49
- provider_session: Session, provider_envelope: Callable[..., dict[str, object]]
50
- ) -> None:
51
- async def content():
52
- yield _CONTENT
53
-
54
- async def handler(request: Request) -> Response:
55
- assert _SHA256.encode() in request.content
56
- return _upload_response(provider_envelope, request)
57
-
58
- async with AsyncClient(base_url="https://matelab.example/api", transport=MockTransport(handler)) as http_client:
59
- client = AsyncMatelab("https://ignored.example", session=provider_session, http_client=http_client)
60
- staged = await client.records.upload_attachment(_NOTEBOOK, filename="alpha.txt", content=content())
61
-
62
- assert staged.binding.uid.startswith("upl-")
63
-
64
-
65
47
  @pytest.mark.asyncio
66
48
  async def test_record_attachment_upload_uses_shared_async_content_handling(
67
49
  provider_session: Session, provider_envelope: Callable[..., dict[str, object]]
@@ -80,6 +62,7 @@ async def test_record_attachment_upload_uses_shared_async_content_handling(
80
62
 
81
63
  assert staged.filename == "alpha.txt"
82
64
  assert staged.sha256 == _SHA256
65
+ assert staged.binding.uid.startswith("upl-")
83
66
 
84
67
 
85
68
  @pytest.mark.asyncio