matelab-python-sdk 0.1.0a12__tar.gz → 0.1.0a14__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 (65) hide show
  1. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a14}/CHANGELOG.md +14 -0
  2. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a14}/PKG-INFO +49 -3
  3. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a14}/README.md +48 -2
  4. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a14}/pyproject.toml +1 -1
  5. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a14}/src/matelab/__init__.py +49 -1
  6. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a14}/src/matelab/_transport.py +8 -11
  7. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a14}/src/matelab/_wire_validation.py +6 -10
  8. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a14}/src/matelab/client.py +9 -7
  9. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a14}/src/matelab/domains/cloud_drive.py +1 -3
  10. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a14}/src/matelab/domains/literature.py +1 -3
  11. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a14}/src/matelab/domains/records.py +2 -2
  12. matelab_python_sdk-0.1.0a14/src/matelab/domains/template_documents.py +527 -0
  13. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a14}/src/matelab/domains/templates.py +26 -12
  14. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a14}/src/matelab/session.py +4 -2
  15. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a14}/src/matelab/streaming.py +6 -8
  16. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a14}/tests/test_client.py +5 -4
  17. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a14}/tests/test_streaming.py +2 -2
  18. matelab_python_sdk-0.1.0a14/tests/test_template_documents.py +393 -0
  19. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a14}/tests/typing/consumer.py +27 -0
  20. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a14}/uv.lock +1 -1
  21. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a14}/.env.example +0 -0
  22. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a14}/.github/workflows/release.yml +0 -0
  23. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a14}/.gitignore +0 -0
  24. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a14}/AGENTS.md +0 -0
  25. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a14}/LICENSE +0 -0
  26. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a14}/NOTICE +0 -0
  27. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a14}/contracts/matelab-integration-v1.lock.json +0 -0
  28. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a14}/contracts/matelab-integration-v1.openapi.yaml +0 -0
  29. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a14}/docs/operation-coverage.yaml +0 -0
  30. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a14}/scripts/check_installed_package.py +0 -0
  31. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a14}/scripts/check_release.py +0 -0
  32. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a14}/scripts/generate_models.py +0 -0
  33. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a14}/src/matelab/_generated/__init__.py +0 -0
  34. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a14}/src/matelab/_generated/models.py +0 -0
  35. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a14}/src/matelab/domains/__init__.py +0 -0
  36. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a14}/src/matelab/domains/_attachment_content.py +0 -0
  37. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a14}/src/matelab/domains/groups.py +0 -0
  38. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a14}/src/matelab/domains/notebooks.py +0 -0
  39. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a14}/src/matelab/domains/uploads.py +0 -0
  40. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a14}/src/matelab/domains/users.py +0 -0
  41. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a14}/src/matelab/errors.py +0 -0
  42. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a14}/src/matelab/py.typed +0 -0
  43. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a14}/tests/conftest.py +0 -0
  44. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a14}/tests/provider/test_provider_smoke.py +0 -0
  45. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a14}/tests/test_async_attachment_upload.py +0 -0
  46. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a14}/tests/test_cloud_drive.py +0 -0
  47. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a14}/tests/test_comments_and_downloads.py +0 -0
  48. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a14}/tests/test_consumer_smoke.py +0 -0
  49. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a14}/tests/test_errors.py +0 -0
  50. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a14}/tests/test_extended_records.py +0 -0
  51. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a14}/tests/test_generation.py +0 -0
  52. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a14}/tests/test_groups.py +0 -0
  53. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a14}/tests/test_notebook_lifecycle.py +0 -0
  54. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a14}/tests/test_notebooks.py +0 -0
  55. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a14}/tests/test_operation_coverage.py +0 -0
  56. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a14}/tests/test_record_lifecycle.py +0 -0
  57. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a14}/tests/test_record_mutations.py +0 -0
  58. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a14}/tests/test_records.py +0 -0
  59. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a14}/tests/test_release.py +0 -0
  60. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a14}/tests/test_template_lifecycle.py +0 -0
  61. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a14}/tests/test_templates.py +0 -0
  62. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a14}/tests/test_transport.py +0 -0
  63. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a14}/tests/test_uploads_and_literature.py +0 -0
  64. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a14}/tests/test_users.py +0 -0
  65. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a14}/tests/typing/pyproject.toml +0 -0
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.0a14 - 2026-08-10
4
+
5
+ - Adds immutable semantic template modules, editable `TemplateDocument` values, ordered name-based patch intents,
6
+ stable content fingerprints, and `Templates.read_document()`, `save_document()`, and `patch_content()` while
7
+ retaining the low-level `TemplateModule`, `read()`, and `save_content()` interface.
8
+
9
+ ## 0.1.0a13 - 2026-08-08
10
+
11
+ - Breaking: `Identity.username` now removes surrounding whitespace and rejects values that become empty.
12
+ - Keeps `ByteRange` typed through the transport adapter and closes the underlying HTTPX2 response when download reads
13
+ or iteration finish or fail, while retaining explicit `DownloadStream.aclose()` for deferred framework consumers.
14
+ - Consolidates Provider session validation through the public `Session` models and simplifies private wire-rule and
15
+ token-refresh validation without changing the pinned Integration Contract.
16
+
3
17
  ## 0.1.0a12 - 2026-08-07
4
18
 
5
19
  - Breaking: removes `MatelabAttachmentValidationError`; local attachment size and checksum mismatches now use the
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: matelab-python-sdk
3
- Version: 0.1.0a12
3
+ Version: 0.1.0a14
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
@@ -30,7 +30,7 @@ Description-Content-Type: text/markdown
30
30
 
31
31
  Reusable async Python client for the Matelab Integration Contract.
32
32
 
33
- The current alpha is `0.1.0a12`. `[project].version` in `pyproject.toml` is the sole SDK version source;
33
+ The current alpha is `0.1.0a14`. `[project].version` in `pyproject.toml` is the sole SDK version source;
34
34
  `uv.lock` only mirrors that source.
35
35
 
36
36
  The SDK is pinned to the immutable `matelab-spec v0.4.0` Contract Release. The sole release pin is
@@ -59,7 +59,7 @@ To test the same artifact a downstream Consumer will install, build and install
59
59
 
60
60
  ```bash
61
61
  uv build --no-build-isolation --out-dir dist/release
62
- python -m pip install dist/release/matelab_python_sdk-0.1.0a12-py3-none-any.whl
62
+ python -m pip install dist/release/matelab_python_sdk-0.1.0a14-py3-none-any.whl
63
63
  ```
64
64
 
65
65
  Do not infer Provider compatibility from the SDK version alone. A release is also bound to the Contract
@@ -450,6 +450,52 @@ market = await client.templates.search_market("calibration", page=1, page_size=2
450
450
  modules = await client.templates.read(templates.owned[0].ref)
451
451
  ```
452
452
 
453
+ Semantic template documents hide canonical `uid`, `width`, `data`, `rows`, `folders`, and option encoding:
454
+
455
+ ```python
456
+ from matelab import (
457
+ TemplateDocument,
458
+ TemplateFieldAdd,
459
+ TemplateFormField,
460
+ TemplateFormModule,
461
+ TemplatePatch,
462
+ TemplateRichTextModule,
463
+ )
464
+
465
+ document = TemplateDocument.create(
466
+ (
467
+ TemplateFormModule(
468
+ name="Basic",
469
+ fields=(TemplateFormField(name="Temperature", field_type="number", required=True, unit="K"),),
470
+ ),
471
+ TemplateRichTextModule(name="Notes", content="<p>Instructions</p>"),
472
+ )
473
+ )
474
+ template = await client.templates.create(title="Measurement")
475
+ await client.templates.save_document(template, document)
476
+
477
+ observed = await client.templates.read_document(template)
478
+ await client.templates.patch_content(
479
+ template,
480
+ TemplatePatch(
481
+ changes=(
482
+ TemplateFieldAdd(
483
+ module="Basic",
484
+ field=TemplateFormField(name="Operator", field_type="text", required=True),
485
+ ),
486
+ )
487
+ ),
488
+ expected_fingerprint=observed.fingerprint,
489
+ )
490
+ updated = await client.templates.read_document(template)
491
+ ```
492
+
493
+ `TemplateDocument` is immutable at its interface, preserves unknown canonical properties, keeps existing UIDs stable,
494
+ and converts losslessly back to `TemplateModule`. Patches are ordered name-based intents and reject missing, duplicate,
495
+ or incompatible targets before saving. The fingerprint preflight detects a stale prior read, but is client-side only:
496
+ the current Provider has no atomic template patch or CAS (PCG-007). `patch_content()` returns `None`; explicitly
497
+ re-read before using a new fingerprint because the mutation response contains no saved representation.
498
+
453
499
  The market result reports the Provider `total_count`, the requested and effective page sizes, and a `has_more`
454
500
  value derived from the total; it does not claim a stable order or continuation token. Canonical
455
501
  modules are mapped to public `TemplateModule` values and retain additive module attributes. Template reads remain
@@ -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.0a12`. `[project].version` in `pyproject.toml` is the sole SDK version source;
5
+ The current alpha is `0.1.0a14`. `[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.4.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.0a12-py3-none-any.whl
34
+ python -m pip install dist/release/matelab_python_sdk-0.1.0a14-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
@@ -422,6 +422,52 @@ market = await client.templates.search_market("calibration", page=1, page_size=2
422
422
  modules = await client.templates.read(templates.owned[0].ref)
423
423
  ```
424
424
 
425
+ Semantic template documents hide canonical `uid`, `width`, `data`, `rows`, `folders`, and option encoding:
426
+
427
+ ```python
428
+ from matelab import (
429
+ TemplateDocument,
430
+ TemplateFieldAdd,
431
+ TemplateFormField,
432
+ TemplateFormModule,
433
+ TemplatePatch,
434
+ TemplateRichTextModule,
435
+ )
436
+
437
+ document = TemplateDocument.create(
438
+ (
439
+ TemplateFormModule(
440
+ name="Basic",
441
+ fields=(TemplateFormField(name="Temperature", field_type="number", required=True, unit="K"),),
442
+ ),
443
+ TemplateRichTextModule(name="Notes", content="<p>Instructions</p>"),
444
+ )
445
+ )
446
+ template = await client.templates.create(title="Measurement")
447
+ await client.templates.save_document(template, document)
448
+
449
+ observed = await client.templates.read_document(template)
450
+ await client.templates.patch_content(
451
+ template,
452
+ TemplatePatch(
453
+ changes=(
454
+ TemplateFieldAdd(
455
+ module="Basic",
456
+ field=TemplateFormField(name="Operator", field_type="text", required=True),
457
+ ),
458
+ )
459
+ ),
460
+ expected_fingerprint=observed.fingerprint,
461
+ )
462
+ updated = await client.templates.read_document(template)
463
+ ```
464
+
465
+ `TemplateDocument` is immutable at its interface, preserves unknown canonical properties, keeps existing UIDs stable,
466
+ and converts losslessly back to `TemplateModule`. Patches are ordered name-based intents and reject missing, duplicate,
467
+ or incompatible targets before saving. The fingerprint preflight detects a stale prior read, but is client-side only:
468
+ the current Provider has no atomic template patch or CAS (PCG-007). `patch_content()` returns `None`; explicitly
469
+ re-read before using a new fingerprint because the mutation response contains no saved representation.
470
+
425
471
  The market result reports the Provider `total_count`, the requested and effective page sizes, and a `has_more`
426
472
  value derived from the total; it does not claim a stable order or continuation token. Canonical
427
473
  modules are mapped to public `TemplateModule` values and retain additive module attributes. Template reads remain
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "matelab-python-sdk"
7
- version = "0.1.0a12"
7
+ version = "0.1.0a14"
8
8
  description = "Reusable async Python client for the Matelab Integration Contract"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -99,13 +99,38 @@ from matelab.domains.records import (
99
99
  StagedRecordAttachment,
100
100
  StagedRecordCommentAttachment,
101
101
  )
102
+ from matelab.domains.template_documents import (
103
+ TemplateChange,
104
+ TemplateCodeLanguage,
105
+ TemplateCodeModule,
106
+ TemplateCodeStyle,
107
+ TemplateDocument,
108
+ TemplateFieldAdd,
109
+ TemplateFieldRemove,
110
+ TemplateFieldRename,
111
+ TemplateFieldUpdate,
112
+ TemplateFilesModule,
113
+ TemplateFormField,
114
+ TemplateFormFieldType,
115
+ TemplateFormModule,
116
+ TemplateModule,
117
+ TemplateModuleAdd,
118
+ TemplateModuleContentUpdate,
119
+ TemplateModuleDefinition,
120
+ TemplateModuleRemove,
121
+ TemplateModuleRename,
122
+ TemplatePatch,
123
+ TemplateRichTextModule,
124
+ TemplateTableColumn,
125
+ TemplateTableColumnType,
126
+ TemplateTableModule,
127
+ )
102
128
  from matelab.domains.templates import (
103
129
  TemplateCollection,
104
130
  TemplateLibraryEntryRef,
105
131
  TemplateLibraryRelationKind,
106
132
  TemplateMarketEntry,
107
133
  TemplateMarketPage,
108
- TemplateModule,
109
134
  TemplateRef,
110
135
  TemplateSource,
111
136
  TemplateSummary,
@@ -235,15 +260,38 @@ __all__ = [
235
260
  "StagedRecordAttachment",
236
261
  "StagedRecordCommentAttachment",
237
262
  "StagedUploadSession",
263
+ "TemplateChange",
264
+ "TemplateCodeLanguage",
265
+ "TemplateCodeModule",
266
+ "TemplateCodeStyle",
238
267
  "TemplateCollection",
268
+ "TemplateDocument",
269
+ "TemplateFieldAdd",
270
+ "TemplateFieldRemove",
271
+ "TemplateFieldRename",
272
+ "TemplateFieldUpdate",
273
+ "TemplateFilesModule",
274
+ "TemplateFormField",
275
+ "TemplateFormFieldType",
276
+ "TemplateFormModule",
239
277
  "TemplateLibraryEntryRef",
240
278
  "TemplateLibraryRelationKind",
241
279
  "TemplateMarketEntry",
242
280
  "TemplateMarketPage",
243
281
  "TemplateModule",
282
+ "TemplateModuleAdd",
283
+ "TemplateModuleContentUpdate",
284
+ "TemplateModuleDefinition",
285
+ "TemplateModuleRemove",
286
+ "TemplateModuleRename",
287
+ "TemplatePatch",
244
288
  "TemplateRef",
289
+ "TemplateRichTextModule",
245
290
  "TemplateSource",
246
291
  "TemplateSummary",
292
+ "TemplateTableColumn",
293
+ "TemplateTableColumnType",
294
+ "TemplateTableModule",
247
295
  "Token",
248
296
  "UploadBindingRef",
249
297
  "UserRef",
@@ -12,7 +12,6 @@ from pydantic import BaseModel, JsonValue, ValidationError
12
12
  from typing_extensions import override
13
13
 
14
14
  from matelab._generated import models as wire_models
15
- from matelab._wire_validation import validate_request
16
15
  from matelab.errors import (
17
16
  MatelabAuthenticationError,
18
17
  MatelabErrorCategory,
@@ -22,7 +21,7 @@ from matelab.errors import (
22
21
  MatelabUsageError,
23
22
  )
24
23
  from matelab.session import Session, Token
25
- from matelab.streaming import DownloadStream
24
+ from matelab.streaming import ByteRange, DownloadStream
26
25
 
27
26
  WireResponse = TypeVar("WireResponse", bound=BaseModel)
28
27
  _ACCESS_EXPIRY_SKEW_MS = 30_000
@@ -152,12 +151,10 @@ class SessionTransport:
152
151
  return session
153
152
  if session.refresh.expires_at_ms <= time.time_ns() // 1_000_000:
154
153
  raise MatelabAuthenticationError("Matelab refresh token expired.")
155
- request = validate_request(
156
- wire_models.TokenRefreshRequest,
157
- {"refresh_token": session.refresh.value},
158
- "The Matelab refresh token is invalid.",
159
- error_type=MatelabAuthenticationError,
160
- )
154
+ try:
155
+ request = wire_models.TokenRefreshRequest.model_validate({"refresh_token": session.refresh.value})
156
+ except ValidationError:
157
+ raise MatelabAuthenticationError("The Matelab refresh token is invalid.") from None
161
158
  response = await self.request(_REFRESH_TOKENS, payload=request)
162
159
  self.session = Session(
163
160
  access=Token(value=response.access.token, expires_at_ms=response.access.expiredAt),
@@ -266,15 +263,15 @@ class SessionTransport:
266
263
  raise MatelabProtocolError("Matelab response does not match the Integration Contract.") from None
267
264
 
268
265
  async def stream(
269
- self, operation: StreamOperation, *, payload: _Payload, range_header: str | None = None
266
+ self, operation: StreamOperation, *, payload: _Payload, byte_range: ByteRange | None = None
270
267
  ) -> DownloadStream:
271
268
  encoded_payload = _payload_mapping(payload)
272
269
  retry_access_expired = True
273
270
  while True:
274
271
  access_session = await self.session_for_request()
275
272
  headers = {"Authorization": f"Bearer {access_session.access.value}"}
276
- if range_header is not None:
277
- headers["Range"] = range_header
273
+ if byte_range is not None:
274
+ headers["Range"] = byte_range.to_header()
278
275
 
279
276
  request = self._http.build_request(
280
277
  operation.method,
@@ -8,7 +8,7 @@ from jsonschema.protocols import Validator
8
8
  from pydantic import BaseModel, JsonValue, ValidationError, model_validator
9
9
  from pydantic_core import PydanticCustomError
10
10
 
11
- from matelab.errors import MatelabAuthenticationError, MatelabUsageError
11
+ from matelab.errors import MatelabUsageError
12
12
 
13
13
  _RUNTIME_RULES_KEY = "matelab-runtime-rules"
14
14
  JsonObject = dict[str, JsonValue]
@@ -20,23 +20,19 @@ def validate_request(
20
20
  model: type[ModelT],
21
21
  data: Mapping[str, WireValue],
22
22
  message: str,
23
- *,
24
- error_type: type[MatelabUsageError] | type[MatelabAuthenticationError] = MatelabUsageError,
25
23
  ) -> ModelT:
26
24
  """Validate caller input without exposing it in the public exception."""
27
25
  try:
28
26
  return model.model_validate(data)
29
27
  except ValidationError:
30
- raise error_type(message) from None
28
+ raise MatelabUsageError(message) from None
31
29
 
32
30
 
33
31
  def encode_json_field(value: JsonValue, *, ensure_ascii: bool = True) -> str:
34
32
  return json.dumps(value, ensure_ascii=ensure_ascii, separators=(",", ":"))
35
33
 
36
34
 
37
- def _rules(extra: JsonObject | None) -> JsonObject | None:
38
- if extra is None:
39
- return None
35
+ def _rules(extra: JsonObject) -> JsonObject | None:
40
36
  rules = extra.get(_RUNTIME_RULES_KEY)
41
37
  if rules is None:
42
38
  return None
@@ -64,7 +60,7 @@ class WireModel(BaseModel):
64
60
  data = cast(Mapping[str, WireValue], value)
65
61
 
66
62
  model_extra = cls.model_config.get("json_schema_extra")
67
- model_rules = _rules(model_extra if isinstance(model_extra, dict) else None)
63
+ model_rules = _rules(model_extra) if isinstance(model_extra, dict) else None
68
64
  if model_rules is not None:
69
65
  _validate_schema(data, model_rules)
70
66
 
@@ -73,12 +69,12 @@ class WireModel(BaseModel):
73
69
  if key not in data:
74
70
  continue
75
71
  extra = field.json_schema_extra
76
- json_extra = extra if isinstance(extra, dict) else None
72
+ json_extra = extra if isinstance(extra, dict) else {}
77
73
  field_rules = _rules(json_extra)
78
74
  raw_value = data[key]
79
75
  if field_rules is not None:
80
76
  _validate_schema(raw_value, field_rules)
81
- content_schema = json_extra.get("contentSchema") if json_extra is not None else None
77
+ content_schema = json_extra.get("contentSchema")
82
78
  if isinstance(content_schema, dict):
83
79
  if raw_value in (None, "") and not field.is_required():
84
80
  continue
@@ -1,6 +1,7 @@
1
1
  from typing import Self
2
2
 
3
3
  from httpx2 import AsyncClient
4
+ from pydantic import ValidationError
4
5
 
5
6
  from matelab._generated import models as wire_models
6
7
  from matelab._transport import Encoding, Operation, SessionTransport
@@ -39,17 +40,18 @@ _CHAT_SSO = Operation(
39
40
  def _session_from_wire(
40
41
  source: wire_models.TokenLoginResponse | wire_models.ChatSsoCodeExchangeResponse,
41
42
  ) -> Session:
42
- if source.username is None or not source.username.strip():
43
- raise MatelabProtocolError("Matelab returned an identity with an empty username.")
44
43
  if isinstance(source, wire_models.TokenLoginResponse):
45
44
  access, refresh = source.access, source.refresh
46
45
  else:
47
46
  access, refresh = source.token.access, source.token.refresh
48
- return Session(
49
- access=Token(value=access.token, expires_at_ms=access.expiredAt),
50
- refresh=Token(value=refresh.token, expires_at_ms=refresh.expiredAt),
51
- identity=Identity(userid=source.userid, username=source.username, email=source.email),
52
- )
47
+ try:
48
+ return Session(
49
+ access=Token(value=access.token, expires_at_ms=access.expiredAt),
50
+ refresh=Token(value=refresh.token, expires_at_ms=refresh.expiredAt),
51
+ identity=Identity.model_validate(source, from_attributes=True),
52
+ )
53
+ except ValidationError:
54
+ raise MatelabProtocolError("Matelab returned invalid session data.") from None
53
55
 
54
56
 
55
57
  class AsyncMatelab:
@@ -332,6 +332,4 @@ class CloudDrive:
332
332
  {"id": file.cloud_file_id, "thumb": int(thumbnail), "c": int(preview)},
333
333
  "Cloud-drive file identity does not satisfy the Integration Contract.",
334
334
  )
335
- return await self._transport.stream(
336
- _DOWNLOAD_FILE, payload=request, range_header=byte_range.to_header() if byte_range is not None else None
337
- )
335
+ return await self._transport.stream(_DOWNLOAD_FILE, payload=request, byte_range=byte_range)
@@ -718,9 +718,7 @@ class Literature:
718
718
  },
719
719
  "Literature PDF identity does not satisfy the Integration Contract.",
720
720
  )
721
- return await self._transport.stream(
722
- _DOWNLOAD_PDF, payload=request, range_header=byte_range.to_header() if byte_range is not None else None
723
- )
721
+ return await self._transport.stream(_DOWNLOAD_PDF, payload=request, byte_range=byte_range)
724
722
 
725
723
  async def create(self, metadata: LiteratureMetadata, *, staged_pdf: StagedFile | None = None) -> None:
726
724
  """Create a literature item; the Provider acknowledgement contains no new item identity."""
@@ -1921,7 +1921,7 @@ class Records:
1921
1921
  return await self._transport.stream(
1922
1922
  _DOWNLOAD_RECORD_ATTACHMENT,
1923
1923
  payload=request,
1924
- range_header=byte_range.to_header() if byte_range is not None else None,
1924
+ byte_range=byte_range,
1925
1925
  )
1926
1926
 
1927
1927
  async def download_comment_attachment(
@@ -1947,7 +1947,7 @@ class Records:
1947
1947
  return await self._transport.stream(
1948
1948
  _DOWNLOAD_COMMENT_ATTACHMENT,
1949
1949
  payload=request,
1950
- range_header=byte_range.to_header() if byte_range is not None else None,
1950
+ byte_range=byte_range,
1951
1951
  )
1952
1952
 
1953
1953
  async def read(