matelab-python-sdk 0.1.0a12__tar.gz → 0.1.0a13__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 (63) hide show
  1. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a13}/CHANGELOG.md +8 -0
  2. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a13}/PKG-INFO +3 -3
  3. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a13}/README.md +2 -2
  4. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a13}/pyproject.toml +1 -1
  5. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a13}/src/matelab/_transport.py +8 -11
  6. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a13}/src/matelab/_wire_validation.py +6 -10
  7. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a13}/src/matelab/client.py +9 -7
  8. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a13}/src/matelab/domains/cloud_drive.py +1 -3
  9. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a13}/src/matelab/domains/literature.py +1 -3
  10. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a13}/src/matelab/domains/records.py +2 -2
  11. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a13}/src/matelab/session.py +4 -2
  12. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a13}/src/matelab/streaming.py +6 -8
  13. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a13}/tests/test_client.py +5 -4
  14. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a13}/tests/test_streaming.py +2 -2
  15. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a13}/uv.lock +1 -1
  16. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a13}/.env.example +0 -0
  17. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a13}/.github/workflows/release.yml +0 -0
  18. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a13}/.gitignore +0 -0
  19. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a13}/AGENTS.md +0 -0
  20. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a13}/LICENSE +0 -0
  21. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a13}/NOTICE +0 -0
  22. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a13}/contracts/matelab-integration-v1.lock.json +0 -0
  23. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a13}/contracts/matelab-integration-v1.openapi.yaml +0 -0
  24. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a13}/docs/operation-coverage.yaml +0 -0
  25. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a13}/scripts/check_installed_package.py +0 -0
  26. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a13}/scripts/check_release.py +0 -0
  27. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a13}/scripts/generate_models.py +0 -0
  28. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a13}/src/matelab/__init__.py +0 -0
  29. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a13}/src/matelab/_generated/__init__.py +0 -0
  30. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a13}/src/matelab/_generated/models.py +0 -0
  31. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a13}/src/matelab/domains/__init__.py +0 -0
  32. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a13}/src/matelab/domains/_attachment_content.py +0 -0
  33. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a13}/src/matelab/domains/groups.py +0 -0
  34. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a13}/src/matelab/domains/notebooks.py +0 -0
  35. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a13}/src/matelab/domains/templates.py +0 -0
  36. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a13}/src/matelab/domains/uploads.py +0 -0
  37. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a13}/src/matelab/domains/users.py +0 -0
  38. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a13}/src/matelab/errors.py +0 -0
  39. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a13}/src/matelab/py.typed +0 -0
  40. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a13}/tests/conftest.py +0 -0
  41. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a13}/tests/provider/test_provider_smoke.py +0 -0
  42. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a13}/tests/test_async_attachment_upload.py +0 -0
  43. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a13}/tests/test_cloud_drive.py +0 -0
  44. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a13}/tests/test_comments_and_downloads.py +0 -0
  45. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a13}/tests/test_consumer_smoke.py +0 -0
  46. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a13}/tests/test_errors.py +0 -0
  47. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a13}/tests/test_extended_records.py +0 -0
  48. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a13}/tests/test_generation.py +0 -0
  49. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a13}/tests/test_groups.py +0 -0
  50. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a13}/tests/test_notebook_lifecycle.py +0 -0
  51. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a13}/tests/test_notebooks.py +0 -0
  52. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a13}/tests/test_operation_coverage.py +0 -0
  53. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a13}/tests/test_record_lifecycle.py +0 -0
  54. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a13}/tests/test_record_mutations.py +0 -0
  55. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a13}/tests/test_records.py +0 -0
  56. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a13}/tests/test_release.py +0 -0
  57. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a13}/tests/test_template_lifecycle.py +0 -0
  58. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a13}/tests/test_templates.py +0 -0
  59. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a13}/tests/test_transport.py +0 -0
  60. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a13}/tests/test_uploads_and_literature.py +0 -0
  61. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a13}/tests/test_users.py +0 -0
  62. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a13}/tests/typing/consumer.py +0 -0
  63. {matelab_python_sdk-0.1.0a12 → matelab_python_sdk-0.1.0a13}/tests/typing/pyproject.toml +0 -0
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.0a13 - 2026-08-08
4
+
5
+ - Breaking: `Identity.username` now removes surrounding whitespace and rejects values that become empty.
6
+ - Keeps `ByteRange` typed through the transport adapter and closes the underlying HTTPX2 response when download reads
7
+ or iteration finish or fail, while retaining explicit `DownloadStream.aclose()` for deferred framework consumers.
8
+ - Consolidates Provider session validation through the public `Session` models and simplifies private wire-rule and
9
+ token-refresh validation without changing the pinned Integration Contract.
10
+
3
11
  ## 0.1.0a12 - 2026-08-07
4
12
 
5
13
  - 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.0a13
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.0a13`. `[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.0a13-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
@@ -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.0a13`. `[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.0a13-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
@@ -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.0a13"
8
8
  description = "Reusable async Python client for the Matelab Integration Contract"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -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(
@@ -1,4 +1,6 @@
1
- from pydantic import BaseModel, ConfigDict, Field
1
+ from typing import Annotated
2
+
3
+ from pydantic import BaseModel, ConfigDict, Field, StringConstraints
2
4
 
3
5
 
4
6
  class Token(BaseModel):
@@ -16,7 +18,7 @@ class Identity(BaseModel):
16
18
  model_config = ConfigDict(frozen=True, extra="forbid")
17
19
 
18
20
  userid: int = Field(gt=0)
19
- username: str = Field(min_length=1)
21
+ username: Annotated[str, StringConstraints(strip_whitespace=True, min_length=1)]
20
22
  email: str | None
21
23
 
22
24
 
@@ -1,5 +1,5 @@
1
1
  from collections.abc import AsyncIterator
2
- from contextlib import suppress
2
+ from contextlib import aclosing
3
3
  from dataclasses import dataclass
4
4
  from typing import Self
5
5
 
@@ -75,19 +75,17 @@ class DownloadStream:
75
75
 
76
76
  async def aiter_bytes(self, chunk_size: int | None = None) -> AsyncIterator[bytes]:
77
77
  try:
78
- async for chunk in self._response.aiter_bytes(chunk_size):
79
- yield chunk
78
+ async with aclosing(self._response) as response:
79
+ async for chunk in response.aiter_bytes(chunk_size):
80
+ yield chunk
80
81
  except RequestError:
81
- with suppress(MatelabTransportError):
82
- await self.aclose()
83
82
  raise MatelabTransportError("Could not read data from the Matelab Provider.") from None
84
83
 
85
84
  async def read(self) -> bytes:
86
85
  try:
87
- return await self._response.aread()
86
+ async with aclosing(self._response) as response:
87
+ return await response.aread()
88
88
  except RequestError:
89
- with suppress(MatelabTransportError):
90
- await self.aclose()
91
89
  raise MatelabTransportError("Could not read data from the Matelab Provider.") from None
92
90
 
93
91
  async def aclose(self) -> None:
@@ -169,7 +169,7 @@ async def test_authenticate_rejects_empty_provider_username(username: str | None
169
169
 
170
170
  async with AsyncClient(base_url="https://matelab.example/api", transport=MockTransport(handler)) as http_client:
171
171
  client = AsyncMatelab("https://ignored.example", session=existing_session, http_client=http_client)
172
- with pytest.raises(MatelabProtocolError, match="empty username"):
172
+ with pytest.raises(MatelabProtocolError, match="invalid session data"):
173
173
  _ = await client.authenticate("alice@example.org", "password")
174
174
 
175
175
  assert client.session is existing_session
@@ -297,12 +297,13 @@ def test_session_models_validate_values_and_extra_fields() -> None:
297
297
  _ = Token(value="access", expires_at_ms=0)
298
298
  with pytest.raises(ValidationError, match="greater_than"):
299
299
  _ = Identity(userid=0, username="User", email=None)
300
- with pytest.raises(ValidationError, match="string_too_short"):
301
- _ = Identity(userid=1, username="", email=None)
300
+ for username in ("", " "):
301
+ with pytest.raises(ValidationError, match="string_too_short"):
302
+ _ = Identity(userid=1, username=username, email=None)
302
303
  with pytest.raises(ValidationError, match="extra_forbidden"):
303
304
  _ = Session.model_validate({**_session().model_dump(), "schema_version": 1})
304
305
  identity = Identity(userid=1, username=" Example User ", email=None)
305
- assert identity.username == " Example User "
306
+ assert identity.username == "Example User"
306
307
  assert Identity.model_json_schema()["properties"]["username"]["minLength"] == 1
307
308
 
308
309
 
@@ -97,7 +97,7 @@ async def test_stream_returns_lazy_binary_response_and_closes_on_context_exit(pr
97
97
 
98
98
  async with AsyncClient(base_url="https://matelab.example/api", transport=MockTransport(handler)) as http_client:
99
99
  transport = SessionTransport(http_client=http_client, session=provider_session)
100
- stream = await transport.stream(_DOWNLOAD, payload={"h": _HASH}, range_header="bytes=0-")
100
+ stream = await transport.stream(_DOWNLOAD, payload={"h": _HASH}, byte_range=ByteRange.from_start(0))
101
101
  assert stream.status_code == 200
102
102
  assert stream.content_type == "application/octet-stream"
103
103
  assert stream.content_length == 16
@@ -175,7 +175,7 @@ async def test_stream_validates_and_exposes_partial_response_metadata(provider_s
175
175
 
176
176
  async with AsyncClient(base_url="https://matelab.example/api", transport=MockTransport(handler)) as http_client:
177
177
  stream = await SessionTransport(http_client=http_client, session=provider_session).stream(
178
- _DOWNLOAD, payload={"h": _HASH}, range_header="bytes=1-4"
178
+ _DOWNLOAD, payload={"h": _HASH}, byte_range=ByteRange.between(1, 4)
179
179
  )
180
180
  async with stream:
181
181
  assert await stream.read() == b"part"
@@ -352,7 +352,7 @@ wheels = [
352
352
 
353
353
  [[package]]
354
354
  name = "matelab-python-sdk"
355
- version = "0.1.0a12"
355
+ version = "0.1.0a13"
356
356
  source = { editable = "." }
357
357
  dependencies = [
358
358
  { name = "httpx2" },