matelab-python-sdk 0.1.0a7__tar.gz → 0.1.0a9__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a9}/.github/workflows/release.yml +2 -0
- {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a9}/AGENTS.md +2 -0
- {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a9}/CHANGELOG.md +13 -0
- {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a9}/PKG-INFO +25 -20
- {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a9}/README.md +24 -19
- {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a9}/pyproject.toml +1 -1
- {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a9}/scripts/check_installed_package.py +3 -0
- {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a9}/src/matelab/__init__.py +2 -2
- {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a9}/src/matelab/_transport.py +7 -7
- {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a9}/src/matelab/errors.py +17 -10
- {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a9}/tests/test_client.py +16 -11
- {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a9}/tests/test_cloud_drive.py +2 -2
- matelab_python_sdk-0.1.0a9/tests/test_errors.py +67 -0
- {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a9}/tests/test_records.py +2 -2
- {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a9}/tests/test_release.py +9 -0
- {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a9}/tests/test_streaming.py +2 -2
- {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a9}/tests/test_transport.py +7 -5
- {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a9}/tests/test_users.py +2 -2
- {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a9}/tests/typing/consumer.py +13 -0
- {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a9}/uv.lock +1 -1
- {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a9}/.env.example +0 -0
- {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a9}/.gitignore +0 -0
- {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a9}/LICENSE +0 -0
- {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a9}/NOTICE +0 -0
- {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a9}/contracts/matelab-integration-v1.lock.json +0 -0
- {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a9}/contracts/matelab-integration-v1.openapi.yaml +0 -0
- {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a9}/docs/operation-coverage.yaml +0 -0
- {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a9}/scripts/check_release.py +0 -0
- {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a9}/scripts/generate_models.py +0 -0
- {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a9}/src/matelab/_generated/__init__.py +0 -0
- {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a9}/src/matelab/_generated/models.py +0 -0
- {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a9}/src/matelab/_wire_validation.py +0 -0
- {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a9}/src/matelab/client.py +0 -0
- {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a9}/src/matelab/domains/__init__.py +0 -0
- {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a9}/src/matelab/domains/cloud_drive.py +0 -0
- {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a9}/src/matelab/domains/groups.py +0 -0
- {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a9}/src/matelab/domains/literature.py +0 -0
- {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a9}/src/matelab/domains/notebooks.py +0 -0
- {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a9}/src/matelab/domains/records.py +0 -0
- {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a9}/src/matelab/domains/templates.py +0 -0
- {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a9}/src/matelab/domains/uploads.py +0 -0
- {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a9}/src/matelab/domains/users.py +0 -0
- {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a9}/src/matelab/py.typed +0 -0
- {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a9}/src/matelab/session.py +0 -0
- {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a9}/src/matelab/streaming.py +0 -0
- {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a9}/tests/conftest.py +0 -0
- {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a9}/tests/provider/test_provider_smoke.py +0 -0
- {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a9}/tests/test_comments_and_downloads.py +0 -0
- {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a9}/tests/test_consumer_smoke.py +0 -0
- {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a9}/tests/test_extended_records.py +0 -0
- {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a9}/tests/test_generation.py +0 -0
- {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a9}/tests/test_groups.py +0 -0
- {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a9}/tests/test_notebook_lifecycle.py +0 -0
- {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a9}/tests/test_notebooks.py +0 -0
- {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a9}/tests/test_operation_coverage.py +0 -0
- {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a9}/tests/test_record_lifecycle.py +0 -0
- {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a9}/tests/test_record_mutations.py +0 -0
- {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a9}/tests/test_template_lifecycle.py +0 -0
- {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a9}/tests/test_templates.py +0 -0
- {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a9}/tests/test_uploads_and_literature.py +0 -0
- {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a9}/tests/typing/pyproject.toml +0 -0
|
@@ -23,6 +23,7 @@ jobs:
|
|
|
23
23
|
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
|
24
24
|
with:
|
|
25
25
|
version: "0.11.32"
|
|
26
|
+
python-version: ${{ matrix.python-version }}
|
|
26
27
|
- name: Sync locked environment
|
|
27
28
|
run: uv sync --frozen --python "${{ matrix.python-version }}"
|
|
28
29
|
- name: Check generated wire models
|
|
@@ -53,6 +54,7 @@ jobs:
|
|
|
53
54
|
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
|
54
55
|
with:
|
|
55
56
|
version: "0.11.32"
|
|
57
|
+
python-version: "3.14"
|
|
56
58
|
- name: Verify tag and configure timestamp
|
|
57
59
|
shell: bash
|
|
58
60
|
run: |
|
|
@@ -62,6 +62,8 @@
|
|
|
62
62
|
- mutation 默认不自动重试。multipart 或 stream 只有在确认可重放时才能重试;download stream 不自动重放,
|
|
63
63
|
必须支持显式关闭。
|
|
64
64
|
- HTTP/transport、Provider business/authentication、Wire Contract/schema 和 client usage 错误必须稳定区分。
|
|
65
|
+
- 每个公开 `MatelabError` 必须直接提供稳定的集成级 `category` 和保守的 `retryable`;HTTP status 只用于诊断,
|
|
66
|
+
下游不得重新解释 Provider 数字 code 才能决定恢复类别。
|
|
65
67
|
- password、token、SSO code/key、临时附件凭证、原始文件内容和敏感 payload 不得进入日志、异常或提交内容。
|
|
66
68
|
|
|
67
69
|
## 测试与质量门槛
|
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.0a9 - 2026-08-04
|
|
4
|
+
|
|
5
|
+
- Breaking: removes `MatelabProviderErrorKind` and `MatelabProviderError.kind`; Provider wire codes now map directly
|
|
6
|
+
to the final `MatelabErrorCategory`, which integrations consume through `MatelabError.category`. This public API
|
|
7
|
+
removal requires a subsequent SemVer-appropriate release before distribution. Operation-specific code `0`/`10`
|
|
8
|
+
success handling and the bounded code `5` authentication refresh remain unchanged.
|
|
9
|
+
- Adds a conservative `retryable` flag to every public SDK error.
|
|
10
|
+
|
|
11
|
+
## 0.1.0a8 - 2026-08-04
|
|
12
|
+
|
|
13
|
+
- Adds a unified `MatelabErrorCategory` and conservative `retryable` flag to every public SDK error, centralizing
|
|
14
|
+
Provider error semantics so integrations no longer reinterpret `MatelabProviderErrorKind`.
|
|
15
|
+
|
|
3
16
|
## 0.1.0a7 - 2026-08-04
|
|
4
17
|
|
|
5
18
|
- Replaces the hand-maintained wire-validation rule tables with deterministic generated JSON Schema 2020-12 rules.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: matelab-python-sdk
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.0a9
|
|
4
4
|
Summary: Reusable async Python client for the Matelab Integration Contract
|
|
5
5
|
Author-email: 朱天念 <zhutiannian@gmail.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -29,7 +29,7 @@ Description-Content-Type: text/markdown
|
|
|
29
29
|
|
|
30
30
|
Reusable async Python client for the Matelab Integration Contract.
|
|
31
31
|
|
|
32
|
-
The current alpha is `0.1.
|
|
32
|
+
The current alpha is `0.1.0a9`. `[project].version` in `pyproject.toml` is the sole SDK version source;
|
|
33
33
|
`uv.lock` only mirrors that source.
|
|
34
34
|
|
|
35
35
|
The SDK is pinned to the immutable `matelab-spec v0.3.0` Contract Release. The sole release pin is
|
|
@@ -58,7 +58,7 @@ To test the same artifact a downstream Consumer will install, build and install
|
|
|
58
58
|
|
|
59
59
|
```bash
|
|
60
60
|
uv build --no-build-isolation --out-dir dist/release
|
|
61
|
-
python -m pip install dist/release/matelab_python_sdk-0.1.
|
|
61
|
+
python -m pip install dist/release/matelab_python_sdk-0.1.0a9-py3-none-any.whl
|
|
62
62
|
```
|
|
63
63
|
|
|
64
64
|
Do not infer Provider compatibility from the SDK version alone. A release is also bound to the Contract
|
|
@@ -85,25 +85,30 @@ explicitly when needed, for example `AsyncMatelab("https://custom.example/api")`
|
|
|
85
85
|
|
|
86
86
|
### Error handling
|
|
87
87
|
|
|
88
|
-
Catch `MatelabError`
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
|
93
|
-
|
|
94
|
-
| `
|
|
95
|
-
| `
|
|
96
|
-
| `
|
|
97
|
-
|
|
98
|
-
`
|
|
99
|
-
|
|
100
|
-
`
|
|
101
|
-
|
|
88
|
+
Catch `MatelabError` once at an integration seam. Every instance exposes a stable `category: MatelabErrorCategory` and
|
|
89
|
+
`retryable: bool`, so an integration can copy the message, category, and retry flag without inspecting subclasses or
|
|
90
|
+
Provider details:
|
|
91
|
+
|
|
92
|
+
| Error | Category | Retryable | Meaning and normal response |
|
|
93
|
+
|---|---|---:|---|
|
|
94
|
+
| `MatelabAuthenticationError` | `AUTHENTICATION` | No | The Session or credentials cannot authenticate; obtain valid authentication before making a new call. |
|
|
95
|
+
| `MatelabUsageError` | `VALIDATION` | No | The call cannot be represented safely; correct its arguments. |
|
|
96
|
+
| `MatelabProviderError` | `BUSINESS`, `VALIDATION`, or `UPSTREAM` | No | The Provider rejected the request; route directly by `category`. |
|
|
97
|
+
| `MatelabTransportError` | `UPSTREAM` | No | The HTTP exchange failed; `status_code` is present for HTTP failures, and a mutation outcome may be unknown. |
|
|
98
|
+
| `MatelabProtocolError` | `UPSTREAM` | No | The response violates the pinned Contract; treat it as Provider drift or an SDK defect. |
|
|
99
|
+
|
|
100
|
+
For `MatelabProviderError`, the SDK maps Provider wire code `2` to `BUSINESS`, `4` to `VALIDATION`, and `3` or an
|
|
101
|
+
unknown code to `UPSTREAM`. Authentication codes are hidden behind `MatelabAuthenticationError`; raw Provider codes
|
|
102
|
+
are not part of the public error interface. Code `5` triggers at most one refresh and replay only for authenticated
|
|
103
|
+
operations that explicitly enable `retry_on_access_expired`; otherwise codes `1` and `5` raise
|
|
104
|
+
`MatelabAuthenticationError` directly.
|
|
102
105
|
Codes `0` and `10` remain operation-specific successes selected by the pinned Contract; an operation that receives a
|
|
103
|
-
success code it does not allow raises `MatelabProtocolError` rather than assigning an error
|
|
106
|
+
success code it does not allow raises `MatelabProtocolError` rather than assigning an error category.
|
|
104
107
|
`MatelabTransportError.status_code` remains available for HTTP failures. Provider response bodies and caller inputs are
|
|
105
|
-
never attached to exceptions.
|
|
106
|
-
error
|
|
108
|
+
never attached to exceptions. `retryable` means the identical SDK call is safe to replay without further
|
|
109
|
+
interpretation. It is conservatively `False` for every current SDK error because the Contract does not guarantee
|
|
110
|
+
mutation idempotency or outcome; an integration may add a narrower operation-specific retry policy only when it owns
|
|
111
|
+
that evidence.
|
|
107
112
|
|
|
108
113
|
### Session ownership
|
|
109
114
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Reusable async Python client for the Matelab Integration Contract.
|
|
4
4
|
|
|
5
|
-
The current alpha is `0.1.
|
|
5
|
+
The current alpha is `0.1.0a9`. `[project].version` in `pyproject.toml` is the sole SDK version source;
|
|
6
6
|
`uv.lock` only mirrors that source.
|
|
7
7
|
|
|
8
8
|
The SDK is pinned to the immutable `matelab-spec v0.3.0` Contract Release. The sole release pin is
|
|
@@ -31,7 +31,7 @@ To test the same artifact a downstream Consumer will install, build and install
|
|
|
31
31
|
|
|
32
32
|
```bash
|
|
33
33
|
uv build --no-build-isolation --out-dir dist/release
|
|
34
|
-
python -m pip install dist/release/matelab_python_sdk-0.1.
|
|
34
|
+
python -m pip install dist/release/matelab_python_sdk-0.1.0a9-py3-none-any.whl
|
|
35
35
|
```
|
|
36
36
|
|
|
37
37
|
Do not infer Provider compatibility from the SDK version alone. A release is also bound to the Contract
|
|
@@ -58,25 +58,30 @@ explicitly when needed, for example `AsyncMatelab("https://custom.example/api")`
|
|
|
58
58
|
|
|
59
59
|
### Error handling
|
|
60
60
|
|
|
61
|
-
Catch `MatelabError`
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
|
66
|
-
|
|
67
|
-
| `
|
|
68
|
-
| `
|
|
69
|
-
| `
|
|
70
|
-
|
|
71
|
-
`
|
|
72
|
-
|
|
73
|
-
`
|
|
74
|
-
|
|
61
|
+
Catch `MatelabError` once at an integration seam. Every instance exposes a stable `category: MatelabErrorCategory` and
|
|
62
|
+
`retryable: bool`, so an integration can copy the message, category, and retry flag without inspecting subclasses or
|
|
63
|
+
Provider details:
|
|
64
|
+
|
|
65
|
+
| Error | Category | Retryable | Meaning and normal response |
|
|
66
|
+
|---|---|---:|---|
|
|
67
|
+
| `MatelabAuthenticationError` | `AUTHENTICATION` | No | The Session or credentials cannot authenticate; obtain valid authentication before making a new call. |
|
|
68
|
+
| `MatelabUsageError` | `VALIDATION` | No | The call cannot be represented safely; correct its arguments. |
|
|
69
|
+
| `MatelabProviderError` | `BUSINESS`, `VALIDATION`, or `UPSTREAM` | No | The Provider rejected the request; route directly by `category`. |
|
|
70
|
+
| `MatelabTransportError` | `UPSTREAM` | No | The HTTP exchange failed; `status_code` is present for HTTP failures, and a mutation outcome may be unknown. |
|
|
71
|
+
| `MatelabProtocolError` | `UPSTREAM` | No | The response violates the pinned Contract; treat it as Provider drift or an SDK defect. |
|
|
72
|
+
|
|
73
|
+
For `MatelabProviderError`, the SDK maps Provider wire code `2` to `BUSINESS`, `4` to `VALIDATION`, and `3` or an
|
|
74
|
+
unknown code to `UPSTREAM`. Authentication codes are hidden behind `MatelabAuthenticationError`; raw Provider codes
|
|
75
|
+
are not part of the public error interface. Code `5` triggers at most one refresh and replay only for authenticated
|
|
76
|
+
operations that explicitly enable `retry_on_access_expired`; otherwise codes `1` and `5` raise
|
|
77
|
+
`MatelabAuthenticationError` directly.
|
|
75
78
|
Codes `0` and `10` remain operation-specific successes selected by the pinned Contract; an operation that receives a
|
|
76
|
-
success code it does not allow raises `MatelabProtocolError` rather than assigning an error
|
|
79
|
+
success code it does not allow raises `MatelabProtocolError` rather than assigning an error category.
|
|
77
80
|
`MatelabTransportError.status_code` remains available for HTTP failures. Provider response bodies and caller inputs are
|
|
78
|
-
never attached to exceptions.
|
|
79
|
-
error
|
|
81
|
+
never attached to exceptions. `retryable` means the identical SDK call is safe to replay without further
|
|
82
|
+
interpretation. It is conservatively `False` for every current SDK error because the Contract does not guarantee
|
|
83
|
+
mutation idempotency or outcome; an integration may add a narrower operation-specific retry policy only when it owns
|
|
84
|
+
that evidence.
|
|
80
85
|
|
|
81
86
|
### Session ownership
|
|
82
87
|
|
{matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a9}/scripts/check_installed_package.py
RENAMED
|
@@ -17,6 +17,9 @@ def main() -> None:
|
|
|
17
17
|
raise RuntimeError(f"Public symbol {name} leaks a generated wire type.")
|
|
18
18
|
if not files(matelab).joinpath("py.typed").is_file():
|
|
19
19
|
raise RuntimeError("The installed wheel does not contain matelab/py.typed.")
|
|
20
|
+
error = matelab.MatelabUsageError("invalid input")
|
|
21
|
+
if error.category is not matelab.MatelabErrorCategory.VALIDATION or error.retryable:
|
|
22
|
+
raise RuntimeError("The installed package does not expose stable integration error semantics.")
|
|
20
23
|
|
|
21
24
|
|
|
22
25
|
if __name__ == "__main__":
|
|
@@ -115,9 +115,9 @@ from matelab.domains.users import UserRef, UserSummary
|
|
|
115
115
|
from matelab.errors import (
|
|
116
116
|
MatelabAuthenticationError,
|
|
117
117
|
MatelabError,
|
|
118
|
+
MatelabErrorCategory,
|
|
118
119
|
MatelabProtocolError,
|
|
119
120
|
MatelabProviderError,
|
|
120
|
-
MatelabProviderErrorKind,
|
|
121
121
|
MatelabTransportError,
|
|
122
122
|
MatelabUsageError,
|
|
123
123
|
)
|
|
@@ -165,9 +165,9 @@ __all__ = [
|
|
|
165
165
|
"LiteratureStoredMetadata",
|
|
166
166
|
"MatelabAuthenticationError",
|
|
167
167
|
"MatelabError",
|
|
168
|
+
"MatelabErrorCategory",
|
|
168
169
|
"MatelabProtocolError",
|
|
169
170
|
"MatelabProviderError",
|
|
170
|
-
"MatelabProviderErrorKind",
|
|
171
171
|
"MatelabTransportError",
|
|
172
172
|
"MatelabUsageError",
|
|
173
173
|
"NotebookCollection",
|
|
@@ -14,9 +14,9 @@ from matelab._generated import models as wire_models
|
|
|
14
14
|
from matelab._wire_validation import validate_request
|
|
15
15
|
from matelab.errors import (
|
|
16
16
|
MatelabAuthenticationError,
|
|
17
|
+
MatelabErrorCategory,
|
|
17
18
|
MatelabProtocolError,
|
|
18
19
|
MatelabProviderError,
|
|
19
|
-
MatelabProviderErrorKind,
|
|
20
20
|
MatelabTransportError,
|
|
21
21
|
MatelabUsageError,
|
|
22
22
|
)
|
|
@@ -42,12 +42,12 @@ def _provider_error(code: int, *, provider_message: str) -> MatelabAuthenticatio
|
|
|
42
42
|
return MatelabAuthenticationError("Matelab authentication failed.")
|
|
43
43
|
if code == 5:
|
|
44
44
|
return MatelabAuthenticationError("Access token expired.")
|
|
45
|
-
|
|
46
|
-
2:
|
|
47
|
-
3:
|
|
48
|
-
4:
|
|
49
|
-
}.get(code,
|
|
50
|
-
return MatelabProviderError(provider_message,
|
|
45
|
+
category = {
|
|
46
|
+
2: MatelabErrorCategory.BUSINESS,
|
|
47
|
+
3: MatelabErrorCategory.UPSTREAM,
|
|
48
|
+
4: MatelabErrorCategory.VALIDATION,
|
|
49
|
+
}.get(code, MatelabErrorCategory.UPSTREAM)
|
|
50
|
+
return MatelabProviderError(provider_message, category=category)
|
|
51
51
|
|
|
52
52
|
|
|
53
53
|
class Encoding(StrEnum):
|
|
@@ -1,34 +1,41 @@
|
|
|
1
1
|
from enum import StrEnum
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
class
|
|
5
|
-
"""
|
|
4
|
+
class MatelabErrorCategory(StrEnum):
|
|
5
|
+
"""Stable integration-level recovery category for an SDK error."""
|
|
6
6
|
|
|
7
|
+
AUTHENTICATION = "authentication"
|
|
8
|
+
BUSINESS = "business"
|
|
9
|
+
VALIDATION = "validation"
|
|
10
|
+
UPSTREAM = "upstream"
|
|
7
11
|
|
|
8
|
-
class MatelabProviderErrorKind(StrEnum):
|
|
9
|
-
"""Stable interpretation of a Provider error envelope."""
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
class MatelabError(Exception):
|
|
14
|
+
"""Base class for stable Matelab SDK errors."""
|
|
15
|
+
|
|
16
|
+
category: MatelabErrorCategory = MatelabErrorCategory.UPSTREAM
|
|
17
|
+
retryable: bool = False
|
|
15
18
|
|
|
16
19
|
|
|
17
20
|
class MatelabProviderError(MatelabError):
|
|
18
21
|
"""The Provider returned a non-authentication error envelope."""
|
|
19
22
|
|
|
20
|
-
def __init__(self, message: str, *,
|
|
23
|
+
def __init__(self, message: str, *, category: MatelabErrorCategory) -> None:
|
|
21
24
|
super().__init__(message)
|
|
22
|
-
self.
|
|
25
|
+
self.category = category
|
|
23
26
|
|
|
24
27
|
|
|
25
28
|
class MatelabAuthenticationError(MatelabError):
|
|
26
29
|
"""The Matelab session cannot authenticate Provider requests."""
|
|
27
30
|
|
|
31
|
+
category = MatelabErrorCategory.AUTHENTICATION
|
|
32
|
+
|
|
28
33
|
|
|
29
34
|
class MatelabUsageError(MatelabError):
|
|
30
35
|
"""The requested SDK operation cannot be represented safely."""
|
|
31
36
|
|
|
37
|
+
category = MatelabErrorCategory.VALIDATION
|
|
38
|
+
|
|
32
39
|
|
|
33
40
|
class MatelabTransportError(MatelabError):
|
|
34
41
|
"""The HTTP exchange with the Provider failed."""
|
|
@@ -10,9 +10,9 @@ from matelab import (
|
|
|
10
10
|
Identity,
|
|
11
11
|
MatelabAuthenticationError,
|
|
12
12
|
MatelabError,
|
|
13
|
+
MatelabErrorCategory,
|
|
13
14
|
MatelabProtocolError,
|
|
14
15
|
MatelabProviderError,
|
|
15
|
-
MatelabProviderErrorKind,
|
|
16
16
|
MatelabUsageError,
|
|
17
17
|
Session,
|
|
18
18
|
Token,
|
|
@@ -132,7 +132,7 @@ async def test_authenticate_does_not_expose_provider_error_body() -> None:
|
|
|
132
132
|
rendered = str(caught.value)
|
|
133
133
|
assert password not in rendered
|
|
134
134
|
assert rendered == "Matelab request failed."
|
|
135
|
-
assert caught.value.
|
|
135
|
+
assert caught.value.category is MatelabErrorCategory.BUSINESS
|
|
136
136
|
assert not hasattr(caught.value, "code")
|
|
137
137
|
assert not hasattr(caught.value, "payload")
|
|
138
138
|
|
|
@@ -226,7 +226,7 @@ async def test_exchange_chat_sso_code_does_not_expose_provider_error_body() -> N
|
|
|
226
226
|
assert code not in rendered
|
|
227
227
|
assert key not in rendered
|
|
228
228
|
assert rendered == "Matelab request failed."
|
|
229
|
-
assert caught.value.
|
|
229
|
+
assert caught.value.category is MatelabErrorCategory.BUSINESS
|
|
230
230
|
assert not hasattr(caught.value, "payload")
|
|
231
231
|
|
|
232
232
|
|
|
@@ -365,17 +365,19 @@ def test_session_models_reject_invalid_values_and_extra_fields() -> None:
|
|
|
365
365
|
|
|
366
366
|
|
|
367
367
|
@pytest.mark.parametrize(
|
|
368
|
-
("code", "
|
|
368
|
+
("code", "category"),
|
|
369
369
|
[
|
|
370
|
-
(2,
|
|
371
|
-
(3,
|
|
372
|
-
(4,
|
|
373
|
-
(11,
|
|
374
|
-
(99,
|
|
370
|
+
(2, MatelabErrorCategory.BUSINESS),
|
|
371
|
+
(3, MatelabErrorCategory.UPSTREAM),
|
|
372
|
+
(4, MatelabErrorCategory.VALIDATION),
|
|
373
|
+
(11, MatelabErrorCategory.UPSTREAM),
|
|
374
|
+
(99, MatelabErrorCategory.UPSTREAM),
|
|
375
375
|
],
|
|
376
376
|
)
|
|
377
377
|
@pytest.mark.asyncio
|
|
378
|
-
async def
|
|
378
|
+
async def test_provider_error_codes_are_hidden_behind_stable_semantics(
|
|
379
|
+
code: int, category: MatelabErrorCategory
|
|
380
|
+
) -> None:
|
|
379
381
|
async def handler(_: Request) -> Response:
|
|
380
382
|
return Response(200, json={"code": code, "msg": "rejected", "errcode": code, "errmsg": "rejected"})
|
|
381
383
|
|
|
@@ -385,7 +387,8 @@ async def test_provider_error_codes_are_hidden_behind_stable_kinds(code: int, ki
|
|
|
385
387
|
_ = await client.authenticate("alice@example.org", "bad-password")
|
|
386
388
|
|
|
387
389
|
assert isinstance(caught.value, MatelabError)
|
|
388
|
-
assert caught.value.
|
|
390
|
+
assert caught.value.category is category
|
|
391
|
+
assert caught.value.retryable is False
|
|
389
392
|
assert not hasattr(caught.value, "code")
|
|
390
393
|
|
|
391
394
|
|
|
@@ -400,6 +403,8 @@ async def test_provider_authentication_codes_are_hidden_behind_authentication_er
|
|
|
400
403
|
with pytest.raises(MatelabAuthenticationError) as caught:
|
|
401
404
|
_ = await client.authenticate("alice@example.org", "bad-password")
|
|
402
405
|
|
|
406
|
+
assert caught.value.category is MatelabErrorCategory.AUTHENTICATION
|
|
407
|
+
assert caught.value.retryable is False
|
|
403
408
|
assert not hasattr(caught.value, "code")
|
|
404
409
|
|
|
405
410
|
|
|
@@ -12,9 +12,9 @@ from matelab import (
|
|
|
12
12
|
CloudFile,
|
|
13
13
|
CloudFileRef,
|
|
14
14
|
CloudFolderRef,
|
|
15
|
+
MatelabErrorCategory,
|
|
15
16
|
MatelabProtocolError,
|
|
16
17
|
MatelabProviderError,
|
|
17
|
-
MatelabProviderErrorKind,
|
|
18
18
|
MatelabUsageError,
|
|
19
19
|
Session,
|
|
20
20
|
StagedFile,
|
|
@@ -430,4 +430,4 @@ async def test_download_cloud_file_uses_identity_thumbnail_preview_and_range_str
|
|
|
430
430
|
CloudFileRef(cloud_file_id=501), thumbnail=True, preview=False, byte_range=ByteRange.between(1, 3)
|
|
431
431
|
)
|
|
432
432
|
|
|
433
|
-
assert caught.value.
|
|
433
|
+
assert caught.value.category is MatelabErrorCategory.BUSINESS
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
import matelab
|
|
4
|
+
from matelab import (
|
|
5
|
+
MatelabAuthenticationError,
|
|
6
|
+
MatelabError,
|
|
7
|
+
MatelabErrorCategory,
|
|
8
|
+
MatelabProtocolError,
|
|
9
|
+
MatelabProviderError,
|
|
10
|
+
MatelabTransportError,
|
|
11
|
+
MatelabUsageError,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_provider_error_kind_is_not_part_of_public_interface() -> None:
|
|
16
|
+
assert "MatelabProviderErrorKind" not in matelab.__all__
|
|
17
|
+
assert not hasattr(matelab, "MatelabProviderErrorKind")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_error_category_values_are_stable_for_integrations() -> None:
|
|
21
|
+
assert {category.name: category.value for category in MatelabErrorCategory} == {
|
|
22
|
+
"AUTHENTICATION": "authentication",
|
|
23
|
+
"BUSINESS": "business",
|
|
24
|
+
"VALIDATION": "validation",
|
|
25
|
+
"UPSTREAM": "upstream",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@pytest.mark.parametrize(
|
|
30
|
+
("error", "category"),
|
|
31
|
+
[
|
|
32
|
+
pytest.param(MatelabError("failure"), MatelabErrorCategory.UPSTREAM, id="base"),
|
|
33
|
+
pytest.param(MatelabAuthenticationError("failure"), MatelabErrorCategory.AUTHENTICATION, id="authentication"),
|
|
34
|
+
pytest.param(MatelabUsageError("failure"), MatelabErrorCategory.VALIDATION, id="usage"),
|
|
35
|
+
pytest.param(MatelabTransportError("failure"), MatelabErrorCategory.UPSTREAM, id="transport"),
|
|
36
|
+
pytest.param(MatelabProtocolError("failure"), MatelabErrorCategory.UPSTREAM, id="protocol"),
|
|
37
|
+
],
|
|
38
|
+
)
|
|
39
|
+
def test_public_errors_expose_integration_category_and_conservative_retryability(
|
|
40
|
+
error: MatelabError, category: MatelabErrorCategory
|
|
41
|
+
) -> None:
|
|
42
|
+
assert error.category is category
|
|
43
|
+
assert error.retryable is False
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@pytest.mark.parametrize(
|
|
47
|
+
"category",
|
|
48
|
+
[
|
|
49
|
+
MatelabErrorCategory.BUSINESS,
|
|
50
|
+
MatelabErrorCategory.UPSTREAM,
|
|
51
|
+
MatelabErrorCategory.VALIDATION,
|
|
52
|
+
],
|
|
53
|
+
)
|
|
54
|
+
def test_provider_error_exposes_final_integration_category(category: MatelabErrorCategory) -> None:
|
|
55
|
+
error = MatelabProviderError("failure", category=category)
|
|
56
|
+
|
|
57
|
+
assert error.category is category
|
|
58
|
+
assert error.retryable is False
|
|
59
|
+
assert not hasattr(error, "kind")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_transport_error_preserves_http_status_without_changing_integration_semantics() -> None:
|
|
63
|
+
error = MatelabTransportError("failure", status_code=503)
|
|
64
|
+
|
|
65
|
+
assert error.status_code == 503
|
|
66
|
+
assert error.category is MatelabErrorCategory.UPSTREAM
|
|
67
|
+
assert error.retryable is False
|
|
@@ -7,9 +7,9 @@ from httpx2 import AsyncClient, MockTransport, Request, Response
|
|
|
7
7
|
from matelab import (
|
|
8
8
|
AsyncMatelab,
|
|
9
9
|
Identity,
|
|
10
|
+
MatelabErrorCategory,
|
|
10
11
|
MatelabProtocolError,
|
|
11
12
|
MatelabProviderError,
|
|
12
|
-
MatelabProviderErrorKind,
|
|
13
13
|
MatelabUsageError,
|
|
14
14
|
NotebookRef,
|
|
15
15
|
RecordLocator,
|
|
@@ -184,7 +184,7 @@ async def test_record_domain_classifies_provider_business_errors() -> None:
|
|
|
184
184
|
with pytest.raises(MatelabProviderError, match="Matelab request failed") as caught:
|
|
185
185
|
_ = await client.records.list(notebook=notebook)
|
|
186
186
|
|
|
187
|
-
assert caught.value.
|
|
187
|
+
assert caught.value.category is MatelabErrorCategory.BUSINESS
|
|
188
188
|
|
|
189
189
|
|
|
190
190
|
@pytest.mark.asyncio
|
|
@@ -101,6 +101,15 @@ def test_release_version_has_one_runtime_source() -> None:
|
|
|
101
101
|
assert all("__version__" not in path.read_text() for path in (_ROOT / "src" / "matelab").rglob("*.py"))
|
|
102
102
|
|
|
103
103
|
|
|
104
|
+
def test_release_workflow_uses_python_specific_uv_cache_keys() -> None:
|
|
105
|
+
workflow = (_ROOT / ".github" / "workflows" / "release.yml").read_text()
|
|
106
|
+
test_job = workflow.partition(" test:")[2].partition("\n build:")[0]
|
|
107
|
+
build_job = workflow.partition(" build:")[2].partition("\n publish:")[0]
|
|
108
|
+
|
|
109
|
+
assert " python-version: ${{ matrix.python-version }}" in test_job
|
|
110
|
+
assert ' python-version: "3.14"' in build_job
|
|
111
|
+
|
|
112
|
+
|
|
104
113
|
def test_lockfile_mirrors_project_version() -> None:
|
|
105
114
|
pyproject = tomllib.loads((_ROOT / "pyproject.toml").read_text())
|
|
106
115
|
project = cast(dict[str, object], pyproject["project"])
|
|
@@ -6,9 +6,9 @@ from httpx2 import AsyncByteStream, AsyncClient, MockTransport, ReadError, Reque
|
|
|
6
6
|
from matelab import (
|
|
7
7
|
ByteRange,
|
|
8
8
|
MatelabAuthenticationError,
|
|
9
|
+
MatelabErrorCategory,
|
|
9
10
|
MatelabProtocolError,
|
|
10
11
|
MatelabProviderError,
|
|
11
|
-
MatelabProviderErrorKind,
|
|
12
12
|
MatelabTransportError,
|
|
13
13
|
MatelabUsageError,
|
|
14
14
|
Session,
|
|
@@ -257,7 +257,7 @@ async def test_stream_classifies_json_error_without_exposing_response_body(provi
|
|
|
257
257
|
assert _HASH not in rendered
|
|
258
258
|
assert access_token not in rendered
|
|
259
259
|
assert rendered == "Matelab download failed."
|
|
260
|
-
assert caught.value.
|
|
260
|
+
assert caught.value.category is MatelabErrorCategory.BUSINESS
|
|
261
261
|
assert not hasattr(caught.value, "code")
|
|
262
262
|
assert not hasattr(caught.value, "payload")
|
|
263
263
|
|
|
@@ -11,9 +11,9 @@ from matelab._generated.models import BasicSuccessResponse, RecordListResponse,
|
|
|
11
11
|
from matelab._transport import Encoding, Operation, SessionTransport
|
|
12
12
|
from matelab.errors import (
|
|
13
13
|
MatelabAuthenticationError,
|
|
14
|
+
MatelabErrorCategory,
|
|
14
15
|
MatelabProtocolError,
|
|
15
16
|
MatelabProviderError,
|
|
16
|
-
MatelabProviderErrorKind,
|
|
17
17
|
MatelabTransportError,
|
|
18
18
|
MatelabUsageError,
|
|
19
19
|
)
|
|
@@ -39,8 +39,9 @@ def _session(
|
|
|
39
39
|
)
|
|
40
40
|
|
|
41
41
|
|
|
42
|
+
@pytest.mark.parametrize("code", [0, 10])
|
|
42
43
|
@pytest.mark.asyncio
|
|
43
|
-
async def
|
|
44
|
+
async def test_transport_accepts_each_success_code_only_when_declared_for_operation(code: int) -> None:
|
|
44
45
|
async def handler(request: Request) -> Response:
|
|
45
46
|
assert request.method == "POST"
|
|
46
47
|
assert request.url.path == "/api/eln_api/items"
|
|
@@ -48,7 +49,8 @@ async def test_transport_uses_operation_encoding_bearer_and_success_codes() -> N
|
|
|
48
49
|
assert request.headers["Content-Type"] == "application/json"
|
|
49
50
|
assert json.loads(request.content) == {"eln": "Example Notebook"}
|
|
50
51
|
return Response(
|
|
51
|
-
200,
|
|
52
|
+
200,
|
|
53
|
+
json={"code": code, "msg": "", "errcode": code, "errmsg": "", "items": [], "subtypes": [], "trash_num": 0},
|
|
52
54
|
)
|
|
53
55
|
|
|
54
56
|
operation = Operation(
|
|
@@ -56,7 +58,7 @@ async def test_transport_uses_operation_encoding_bearer_and_success_codes() -> N
|
|
|
56
58
|
path="/eln_api/items",
|
|
57
59
|
encoding=Encoding.JSON,
|
|
58
60
|
response_model=RecordListResponse,
|
|
59
|
-
success_codes=frozenset({
|
|
61
|
+
success_codes=frozenset({code}),
|
|
60
62
|
)
|
|
61
63
|
async with AsyncClient(base_url="https://matelab.example/api", transport=MockTransport(handler)) as http_client:
|
|
62
64
|
transport = SessionTransport(http_client=http_client, session=_session())
|
|
@@ -104,7 +106,7 @@ async def test_transport_classifies_provider_business_errors() -> None:
|
|
|
104
106
|
with pytest.raises(MatelabProviderError, match="Matelab request failed") as caught:
|
|
105
107
|
_ = await transport.request(operation, payload={"eln": ""})
|
|
106
108
|
|
|
107
|
-
assert caught.value.
|
|
109
|
+
assert caught.value.category is MatelabErrorCategory.BUSINESS
|
|
108
110
|
assert not hasattr(caught.value, "code")
|
|
109
111
|
assert not hasattr(caught.value, "payload")
|
|
110
112
|
|
|
@@ -5,9 +5,9 @@ from httpx2 import AsyncClient, MockTransport, Request, Response
|
|
|
5
5
|
|
|
6
6
|
from matelab import (
|
|
7
7
|
AsyncMatelab,
|
|
8
|
+
MatelabErrorCategory,
|
|
8
9
|
MatelabProtocolError,
|
|
9
10
|
MatelabProviderError,
|
|
10
|
-
MatelabProviderErrorKind,
|
|
11
11
|
MatelabTransportError,
|
|
12
12
|
MatelabUsageError,
|
|
13
13
|
Session,
|
|
@@ -112,7 +112,7 @@ async def test_search_users_classifies_provider_business_errors(provider_session
|
|
|
112
112
|
with pytest.raises(MatelabProviderError, match="Matelab request failed") as caught:
|
|
113
113
|
_ = await client.users.search("Example")
|
|
114
114
|
|
|
115
|
-
assert caught.value.
|
|
115
|
+
assert caught.value.category is MatelabErrorCategory.BUSINESS
|
|
116
116
|
|
|
117
117
|
|
|
118
118
|
@pytest.mark.asyncio
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
from matelab import (
|
|
2
2
|
AsyncMatelab,
|
|
3
|
+
MatelabError,
|
|
4
|
+
MatelabErrorCategory,
|
|
5
|
+
MatelabProviderError,
|
|
3
6
|
NotebookCollection,
|
|
4
7
|
NotebookRef,
|
|
5
8
|
RecordAttachmentChange,
|
|
@@ -21,6 +24,16 @@ from matelab import (
|
|
|
21
24
|
)
|
|
22
25
|
|
|
23
26
|
|
|
27
|
+
def consume_error(error: MatelabError) -> tuple[MatelabErrorCategory, bool]:
|
|
28
|
+
category: MatelabErrorCategory = error.category
|
|
29
|
+
retryable: bool = error.retryable
|
|
30
|
+
return category, retryable
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def construct_provider_error(category: MatelabErrorCategory) -> MatelabProviderError:
|
|
34
|
+
return MatelabProviderError("failure", category=category)
|
|
35
|
+
|
|
36
|
+
|
|
24
37
|
async def consume_public_types(client: AsyncMatelab) -> tuple[Session | None, NotebookCollection]:
|
|
25
38
|
notebooks: NotebookCollection = await client.notebooks.list()
|
|
26
39
|
session: Session | None = client.session
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a9}/contracts/matelab-integration-v1.lock.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a9}/src/matelab/_generated/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a9}/src/matelab/domains/cloud_drive.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
|
{matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a9}/tests/provider/test_provider_smoke.py
RENAMED
|
File without changes
|
{matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a9}/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
|
{matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a9}/tests/test_uploads_and_literature.py
RENAMED
|
File without changes
|
|
File without changes
|