matelab-python-sdk 0.1.0a7__tar.gz → 0.1.0a8__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 (61) hide show
  1. {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a8}/AGENTS.md +2 -0
  2. {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a8}/CHANGELOG.md +5 -0
  3. {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a8}/PKG-INFO +23 -19
  4. {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a8}/README.md +22 -18
  5. {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a8}/pyproject.toml +1 -1
  6. {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a8}/scripts/check_installed_package.py +3 -0
  7. {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a8}/src/matelab/__init__.py +2 -0
  8. {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a8}/src/matelab/errors.py +22 -0
  9. {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a8}/tests/test_client.py +14 -7
  10. matelab_python_sdk-0.1.0a8/tests/test_errors.py +65 -0
  11. {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a8}/tests/typing/consumer.py +8 -0
  12. {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a8}/uv.lock +1 -1
  13. {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a8}/.env.example +0 -0
  14. {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a8}/.github/workflows/release.yml +0 -0
  15. {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a8}/.gitignore +0 -0
  16. {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a8}/LICENSE +0 -0
  17. {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a8}/NOTICE +0 -0
  18. {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a8}/contracts/matelab-integration-v1.lock.json +0 -0
  19. {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a8}/contracts/matelab-integration-v1.openapi.yaml +0 -0
  20. {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a8}/docs/operation-coverage.yaml +0 -0
  21. {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a8}/scripts/check_release.py +0 -0
  22. {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a8}/scripts/generate_models.py +0 -0
  23. {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a8}/src/matelab/_generated/__init__.py +0 -0
  24. {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a8}/src/matelab/_generated/models.py +0 -0
  25. {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a8}/src/matelab/_transport.py +0 -0
  26. {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a8}/src/matelab/_wire_validation.py +0 -0
  27. {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a8}/src/matelab/client.py +0 -0
  28. {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a8}/src/matelab/domains/__init__.py +0 -0
  29. {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a8}/src/matelab/domains/cloud_drive.py +0 -0
  30. {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a8}/src/matelab/domains/groups.py +0 -0
  31. {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a8}/src/matelab/domains/literature.py +0 -0
  32. {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a8}/src/matelab/domains/notebooks.py +0 -0
  33. {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a8}/src/matelab/domains/records.py +0 -0
  34. {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a8}/src/matelab/domains/templates.py +0 -0
  35. {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a8}/src/matelab/domains/uploads.py +0 -0
  36. {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a8}/src/matelab/domains/users.py +0 -0
  37. {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a8}/src/matelab/py.typed +0 -0
  38. {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a8}/src/matelab/session.py +0 -0
  39. {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a8}/src/matelab/streaming.py +0 -0
  40. {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a8}/tests/conftest.py +0 -0
  41. {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a8}/tests/provider/test_provider_smoke.py +0 -0
  42. {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a8}/tests/test_cloud_drive.py +0 -0
  43. {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a8}/tests/test_comments_and_downloads.py +0 -0
  44. {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a8}/tests/test_consumer_smoke.py +0 -0
  45. {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a8}/tests/test_extended_records.py +0 -0
  46. {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a8}/tests/test_generation.py +0 -0
  47. {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a8}/tests/test_groups.py +0 -0
  48. {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a8}/tests/test_notebook_lifecycle.py +0 -0
  49. {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a8}/tests/test_notebooks.py +0 -0
  50. {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a8}/tests/test_operation_coverage.py +0 -0
  51. {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a8}/tests/test_record_lifecycle.py +0 -0
  52. {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a8}/tests/test_record_mutations.py +0 -0
  53. {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a8}/tests/test_records.py +0 -0
  54. {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a8}/tests/test_release.py +0 -0
  55. {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a8}/tests/test_streaming.py +0 -0
  56. {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a8}/tests/test_template_lifecycle.py +0 -0
  57. {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a8}/tests/test_templates.py +0 -0
  58. {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a8}/tests/test_transport.py +0 -0
  59. {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a8}/tests/test_uploads_and_literature.py +0 -0
  60. {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a8}/tests/test_users.py +0 -0
  61. {matelab_python_sdk-0.1.0a7 → matelab_python_sdk-0.1.0a8}/tests/typing/pyproject.toml +0 -0
@@ -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`;Provider kind 和 HTTP
66
+ status 只用于诊断,下游不得重新解释 Provider 数字 code 才能决定恢复类别。
65
67
  - password、token、SSO code/key、临时附件凭证、原始文件内容和敏感 payload 不得进入日志、异常或提交内容。
66
68
 
67
69
  ## 测试与质量门槛
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.0a8 - 2026-08-04
4
+
5
+ - Adds a unified `MatelabErrorCategory` and conservative `retryable` flag to every public SDK error, centralizing
6
+ Provider error semantics so integrations no longer reinterpret `MatelabProviderErrorKind`.
7
+
3
8
  ## 0.1.0a7 - 2026-08-04
4
9
 
5
10
  - 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.0a7
3
+ Version: 0.1.0a8
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.0a7`. `[project].version` in `pyproject.toml` is the sole SDK version source;
32
+ The current alpha is `0.1.0a8`. `[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.0a7-py3-none-any.whl
61
+ python -m pip install dist/release/matelab_python_sdk-0.1.0a8-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,29 @@ explicitly when needed, for example `AsyncMatelab("https://custom.example/api")`
85
85
 
86
86
  ### Error handling
87
87
 
88
- Catch `MatelabError` for one application-level fallback, or a specific subclass when recovery differs:
89
-
90
- | Error | Meaning and normal response |
91
- |---|---|
92
- | `MatelabUsageError` | The call cannot be represented safely; correct its arguments. |
93
- | `MatelabAuthenticationError` | The Session is missing, expired, invalid, or rejected; refresh or authenticate again as appropriate. |
94
- | `MatelabProviderError` | The Provider returned a non-authentication error; inspect its stable `kind` and do not assume a mutation was applied. |
95
- | `MatelabTransportError` | The HTTP exchange failed; `status_code` is present for HTTP failures, and a mutation outcome may be unknown. |
96
- | `MatelabProtocolError` | The Provider response does not match the pinned Contract; treat it as Provider drift or an SDK defect. A mutation may already have been applied. |
97
-
98
- `MatelabProviderError.kind` is a `MatelabProviderErrorKind`: `REJECTED` covers input, permission, and business
99
- rejections; `SERVER` identifies Provider server failures; `VALIDATION` identifies multi-error validation failures; and
100
- `UNKNOWN` safely represents all other non-success, non-authentication Provider codes. Authentication codes are hidden
101
- behind `MatelabAuthenticationError`; raw Provider codes are not part of the public error interface.
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 by `category`, with `kind` available only for diagnosis. |
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 `REJECTED` to `BUSINESS`, `VALIDATION` to `VALIDATION`, and `SERVER` or
101
+ `UNKNOWN` to `UPSTREAM`. `MatelabProviderError.kind` remains a `MatelabProviderErrorKind` diagnostic, but integrations
102
+ do not need it for recovery routing. Authentication codes are hidden behind `MatelabAuthenticationError`; raw Provider
103
+ codes are not part of the public error interface.
102
104
  Codes `0` and `10` remain operation-specific successes selected by the pinned Contract; an operation that receives a
103
105
  success code it does not allow raises `MatelabProtocolError` rather than assigning an error kind.
104
106
  `MatelabTransportError.status_code` remains available for HTTP failures. Provider response bodies and caller inputs are
105
- never attached to exceptions. Never automatically retry a mutation solely because it raised a transport or protocol
106
- error.
107
+ never attached to exceptions. `retryable` means the identical SDK call is safe to replay without further
108
+ interpretation. It is conservatively `False` for every current SDK error because the Contract does not guarantee
109
+ mutation idempotency or outcome; an integration may add a narrower operation-specific retry policy only when it owns
110
+ that evidence.
107
111
 
108
112
  ### Session ownership
109
113
 
@@ -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.0a7`. `[project].version` in `pyproject.toml` is the sole SDK version source;
5
+ The current alpha is `0.1.0a8`. `[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.0a7-py3-none-any.whl
34
+ python -m pip install dist/release/matelab_python_sdk-0.1.0a8-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,29 @@ explicitly when needed, for example `AsyncMatelab("https://custom.example/api")`
58
58
 
59
59
  ### Error handling
60
60
 
61
- Catch `MatelabError` for one application-level fallback, or a specific subclass when recovery differs:
62
-
63
- | Error | Meaning and normal response |
64
- |---|---|
65
- | `MatelabUsageError` | The call cannot be represented safely; correct its arguments. |
66
- | `MatelabAuthenticationError` | The Session is missing, expired, invalid, or rejected; refresh or authenticate again as appropriate. |
67
- | `MatelabProviderError` | The Provider returned a non-authentication error; inspect its stable `kind` and do not assume a mutation was applied. |
68
- | `MatelabTransportError` | The HTTP exchange failed; `status_code` is present for HTTP failures, and a mutation outcome may be unknown. |
69
- | `MatelabProtocolError` | The Provider response does not match the pinned Contract; treat it as Provider drift or an SDK defect. A mutation may already have been applied. |
70
-
71
- `MatelabProviderError.kind` is a `MatelabProviderErrorKind`: `REJECTED` covers input, permission, and business
72
- rejections; `SERVER` identifies Provider server failures; `VALIDATION` identifies multi-error validation failures; and
73
- `UNKNOWN` safely represents all other non-success, non-authentication Provider codes. Authentication codes are hidden
74
- behind `MatelabAuthenticationError`; raw Provider codes are not part of the public error interface.
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 by `category`, with `kind` available only for diagnosis. |
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 `REJECTED` to `BUSINESS`, `VALIDATION` to `VALIDATION`, and `SERVER` or
74
+ `UNKNOWN` to `UPSTREAM`. `MatelabProviderError.kind` remains a `MatelabProviderErrorKind` diagnostic, but integrations
75
+ do not need it for recovery routing. Authentication codes are hidden behind `MatelabAuthenticationError`; raw Provider
76
+ codes are not part of the public error interface.
75
77
  Codes `0` and `10` remain operation-specific successes selected by the pinned Contract; an operation that receives a
76
78
  success code it does not allow raises `MatelabProtocolError` rather than assigning an error kind.
77
79
  `MatelabTransportError.status_code` remains available for HTTP failures. Provider response bodies and caller inputs are
78
- never attached to exceptions. Never automatically retry a mutation solely because it raised a transport or protocol
79
- error.
80
+ never attached to exceptions. `retryable` means the identical SDK call is safe to replay without further
81
+ interpretation. It is conservatively `False` for every current SDK error because the Contract does not guarantee
82
+ mutation idempotency or outcome; an integration may add a narrower operation-specific retry policy only when it owns
83
+ that evidence.
80
84
 
81
85
  ### Session ownership
82
86
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "matelab-python-sdk"
7
- version = "0.1.0a7"
7
+ version = "0.1.0a8"
8
8
  description = "Reusable async Python client for the Matelab Integration Contract"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -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,6 +115,7 @@ 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
121
  MatelabProviderErrorKind,
@@ -165,6 +166,7 @@ __all__ = [
165
166
  "LiteratureStoredMetadata",
166
167
  "MatelabAuthenticationError",
167
168
  "MatelabError",
169
+ "MatelabErrorCategory",
168
170
  "MatelabProtocolError",
169
171
  "MatelabProviderError",
170
172
  "MatelabProviderErrorKind",
@@ -1,9 +1,21 @@
1
1
  from enum import StrEnum
2
2
 
3
3
 
4
+ class MatelabErrorCategory(StrEnum):
5
+ """Stable integration-level recovery category for an SDK error."""
6
+
7
+ AUTHENTICATION = "authentication"
8
+ BUSINESS = "business"
9
+ VALIDATION = "validation"
10
+ UPSTREAM = "upstream"
11
+
12
+
4
13
  class MatelabError(Exception):
5
14
  """Base class for stable Matelab SDK errors."""
6
15
 
16
+ category: MatelabErrorCategory = MatelabErrorCategory.UPSTREAM
17
+ retryable: bool = False
18
+
7
19
 
8
20
  class MatelabProviderErrorKind(StrEnum):
9
21
  """Stable interpretation of a Provider error envelope."""
@@ -20,15 +32,25 @@ class MatelabProviderError(MatelabError):
20
32
  def __init__(self, message: str, *, kind: MatelabProviderErrorKind) -> None:
21
33
  super().__init__(message)
22
34
  self.kind = kind
35
+ self.category = {
36
+ MatelabProviderErrorKind.REJECTED: MatelabErrorCategory.BUSINESS,
37
+ MatelabProviderErrorKind.SERVER: MatelabErrorCategory.UPSTREAM,
38
+ MatelabProviderErrorKind.VALIDATION: MatelabErrorCategory.VALIDATION,
39
+ MatelabProviderErrorKind.UNKNOWN: MatelabErrorCategory.UPSTREAM,
40
+ }[kind]
23
41
 
24
42
 
25
43
  class MatelabAuthenticationError(MatelabError):
26
44
  """The Matelab session cannot authenticate Provider requests."""
27
45
 
46
+ category = MatelabErrorCategory.AUTHENTICATION
47
+
28
48
 
29
49
  class MatelabUsageError(MatelabError):
30
50
  """The requested SDK operation cannot be represented safely."""
31
51
 
52
+ category = MatelabErrorCategory.VALIDATION
53
+
32
54
 
33
55
  class MatelabTransportError(MatelabError):
34
56
  """The HTTP exchange with the Provider failed."""
@@ -10,6 +10,7 @@ from matelab import (
10
10
  Identity,
11
11
  MatelabAuthenticationError,
12
12
  MatelabError,
13
+ MatelabErrorCategory,
13
14
  MatelabProtocolError,
14
15
  MatelabProviderError,
15
16
  MatelabProviderErrorKind,
@@ -365,17 +366,19 @@ def test_session_models_reject_invalid_values_and_extra_fields() -> None:
365
366
 
366
367
 
367
368
  @pytest.mark.parametrize(
368
- ("code", "kind"),
369
+ ("code", "kind", "category"),
369
370
  [
370
- (2, MatelabProviderErrorKind.REJECTED),
371
- (3, MatelabProviderErrorKind.SERVER),
372
- (4, MatelabProviderErrorKind.VALIDATION),
373
- (11, MatelabProviderErrorKind.UNKNOWN),
374
- (99, MatelabProviderErrorKind.UNKNOWN),
371
+ (2, MatelabProviderErrorKind.REJECTED, MatelabErrorCategory.BUSINESS),
372
+ (3, MatelabProviderErrorKind.SERVER, MatelabErrorCategory.UPSTREAM),
373
+ (4, MatelabProviderErrorKind.VALIDATION, MatelabErrorCategory.VALIDATION),
374
+ (11, MatelabProviderErrorKind.UNKNOWN, MatelabErrorCategory.UPSTREAM),
375
+ (99, MatelabProviderErrorKind.UNKNOWN, MatelabErrorCategory.UPSTREAM),
375
376
  ],
376
377
  )
377
378
  @pytest.mark.asyncio
378
- async def test_provider_error_codes_are_hidden_behind_stable_kinds(code: int, kind: MatelabProviderErrorKind) -> None:
379
+ async def test_provider_error_codes_are_hidden_behind_stable_semantics(
380
+ code: int, kind: MatelabProviderErrorKind, category: MatelabErrorCategory
381
+ ) -> None:
379
382
  async def handler(_: Request) -> Response:
380
383
  return Response(200, json={"code": code, "msg": "rejected", "errcode": code, "errmsg": "rejected"})
381
384
 
@@ -386,6 +389,8 @@ async def test_provider_error_codes_are_hidden_behind_stable_kinds(code: int, ki
386
389
 
387
390
  assert isinstance(caught.value, MatelabError)
388
391
  assert caught.value.kind is kind
392
+ assert caught.value.category is category
393
+ assert caught.value.retryable is False
389
394
  assert not hasattr(caught.value, "code")
390
395
 
391
396
 
@@ -400,6 +405,8 @@ async def test_provider_authentication_codes_are_hidden_behind_authentication_er
400
405
  with pytest.raises(MatelabAuthenticationError) as caught:
401
406
  _ = await client.authenticate("alice@example.org", "bad-password")
402
407
 
408
+ assert caught.value.category is MatelabErrorCategory.AUTHENTICATION
409
+ assert caught.value.retryable is False
403
410
  assert not hasattr(caught.value, "code")
404
411
 
405
412
 
@@ -0,0 +1,65 @@
1
+ import pytest
2
+
3
+ from matelab import (
4
+ MatelabAuthenticationError,
5
+ MatelabError,
6
+ MatelabErrorCategory,
7
+ MatelabProtocolError,
8
+ MatelabProviderError,
9
+ MatelabProviderErrorKind,
10
+ MatelabTransportError,
11
+ MatelabUsageError,
12
+ )
13
+
14
+
15
+ def test_error_category_values_are_stable_for_integrations() -> None:
16
+ assert {category.name: category.value for category in MatelabErrorCategory} == {
17
+ "AUTHENTICATION": "authentication",
18
+ "BUSINESS": "business",
19
+ "VALIDATION": "validation",
20
+ "UPSTREAM": "upstream",
21
+ }
22
+
23
+
24
+ @pytest.mark.parametrize(
25
+ ("error", "category"),
26
+ [
27
+ pytest.param(MatelabError("failure"), MatelabErrorCategory.UPSTREAM, id="base"),
28
+ pytest.param(MatelabAuthenticationError("failure"), MatelabErrorCategory.AUTHENTICATION, id="authentication"),
29
+ pytest.param(MatelabUsageError("failure"), MatelabErrorCategory.VALIDATION, id="usage"),
30
+ pytest.param(MatelabTransportError("failure"), MatelabErrorCategory.UPSTREAM, id="transport"),
31
+ pytest.param(MatelabProtocolError("failure"), MatelabErrorCategory.UPSTREAM, id="protocol"),
32
+ ],
33
+ )
34
+ def test_public_errors_expose_integration_category_and_conservative_retryability(
35
+ error: MatelabError, category: MatelabErrorCategory
36
+ ) -> None:
37
+ assert error.category is category
38
+ assert error.retryable is False
39
+
40
+
41
+ @pytest.mark.parametrize(
42
+ ("kind", "category"),
43
+ [
44
+ (MatelabProviderErrorKind.REJECTED, MatelabErrorCategory.BUSINESS),
45
+ (MatelabProviderErrorKind.SERVER, MatelabErrorCategory.UPSTREAM),
46
+ (MatelabProviderErrorKind.VALIDATION, MatelabErrorCategory.VALIDATION),
47
+ (MatelabProviderErrorKind.UNKNOWN, MatelabErrorCategory.UPSTREAM),
48
+ ],
49
+ )
50
+ def test_provider_error_kind_is_diagnostic_and_category_is_integration_semantics(
51
+ kind: MatelabProviderErrorKind, category: MatelabErrorCategory
52
+ ) -> None:
53
+ error = MatelabProviderError("failure", kind=kind)
54
+
55
+ assert error.kind is kind
56
+ assert error.category is category
57
+ assert error.retryable is False
58
+
59
+
60
+ def test_transport_error_preserves_http_status_without_changing_integration_semantics() -> None:
61
+ error = MatelabTransportError("failure", status_code=503)
62
+
63
+ assert error.status_code == 503
64
+ assert error.category is MatelabErrorCategory.UPSTREAM
65
+ assert error.retryable is False
@@ -1,5 +1,7 @@
1
1
  from matelab import (
2
2
  AsyncMatelab,
3
+ MatelabError,
4
+ MatelabErrorCategory,
3
5
  NotebookCollection,
4
6
  NotebookRef,
5
7
  RecordAttachmentChange,
@@ -21,6 +23,12 @@ from matelab import (
21
23
  )
22
24
 
23
25
 
26
+ def consume_error(error: MatelabError) -> tuple[MatelabErrorCategory, bool]:
27
+ category: MatelabErrorCategory = error.category
28
+ retryable: bool = error.retryable
29
+ return category, retryable
30
+
31
+
24
32
  async def consume_public_types(client: AsyncMatelab) -> tuple[Session | None, NotebookCollection]:
25
33
  notebooks: NotebookCollection = await client.notebooks.list()
26
34
  session: Session | None = client.session
@@ -352,7 +352,7 @@ wheels = [
352
352
 
353
353
  [[package]]
354
354
  name = "matelab-python-sdk"
355
- version = "0.1.0a7"
355
+ version = "0.1.0a8"
356
356
  source = { editable = "." }
357
357
  dependencies = [
358
358
  { name = "httpx2" },