matelab-python-sdk 0.1.0a22__tar.gz → 0.1.0a23__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 (67) hide show
  1. {matelab_python_sdk-0.1.0a22 → matelab_python_sdk-0.1.0a23}/CHANGELOG.md +6 -0
  2. {matelab_python_sdk-0.1.0a22 → matelab_python_sdk-0.1.0a23}/PKG-INFO +9 -3
  3. {matelab_python_sdk-0.1.0a22 → matelab_python_sdk-0.1.0a23}/README.md +8 -2
  4. {matelab_python_sdk-0.1.0a22 → matelab_python_sdk-0.1.0a23}/pyproject.toml +1 -1
  5. {matelab_python_sdk-0.1.0a22 → matelab_python_sdk-0.1.0a23}/src/matelab/domains/records.py +54 -3
  6. {matelab_python_sdk-0.1.0a22 → matelab_python_sdk-0.1.0a23}/tests/test_records.py +71 -0
  7. {matelab_python_sdk-0.1.0a22 → matelab_python_sdk-0.1.0a23}/uv.lock +1 -1
  8. {matelab_python_sdk-0.1.0a22 → matelab_python_sdk-0.1.0a23}/.env.example +0 -0
  9. {matelab_python_sdk-0.1.0a22 → matelab_python_sdk-0.1.0a23}/.github/workflows/release.yml +0 -0
  10. {matelab_python_sdk-0.1.0a22 → matelab_python_sdk-0.1.0a23}/.gitignore +0 -0
  11. {matelab_python_sdk-0.1.0a22 → matelab_python_sdk-0.1.0a23}/AGENTS.md +0 -0
  12. {matelab_python_sdk-0.1.0a22 → matelab_python_sdk-0.1.0a23}/CONTEXT.md +0 -0
  13. {matelab_python_sdk-0.1.0a22 → matelab_python_sdk-0.1.0a23}/LICENSE +0 -0
  14. {matelab_python_sdk-0.1.0a22 → matelab_python_sdk-0.1.0a23}/NOTICE +0 -0
  15. {matelab_python_sdk-0.1.0a22 → matelab_python_sdk-0.1.0a23}/contracts/matelab-integration-v1.lock.json +0 -0
  16. {matelab_python_sdk-0.1.0a22 → matelab_python_sdk-0.1.0a23}/contracts/matelab-integration-v1.openapi.yaml +0 -0
  17. {matelab_python_sdk-0.1.0a22 → matelab_python_sdk-0.1.0a23}/docs/agent-architecture.md +0 -0
  18. {matelab_python_sdk-0.1.0a22 → matelab_python_sdk-0.1.0a23}/docs/agent-workflows.md +0 -0
  19. {matelab_python_sdk-0.1.0a22 → matelab_python_sdk-0.1.0a23}/docs/operation-coverage.yaml +0 -0
  20. {matelab_python_sdk-0.1.0a22 → matelab_python_sdk-0.1.0a23}/scripts/check_installed_package.py +0 -0
  21. {matelab_python_sdk-0.1.0a22 → matelab_python_sdk-0.1.0a23}/scripts/check_release.py +0 -0
  22. {matelab_python_sdk-0.1.0a22 → matelab_python_sdk-0.1.0a23}/scripts/generate_models.py +0 -0
  23. {matelab_python_sdk-0.1.0a22 → matelab_python_sdk-0.1.0a23}/src/matelab/__init__.py +0 -0
  24. {matelab_python_sdk-0.1.0a22 → matelab_python_sdk-0.1.0a23}/src/matelab/_generated/__init__.py +0 -0
  25. {matelab_python_sdk-0.1.0a22 → matelab_python_sdk-0.1.0a23}/src/matelab/_generated/models.py +0 -0
  26. {matelab_python_sdk-0.1.0a22 → matelab_python_sdk-0.1.0a23}/src/matelab/_hashes.py +0 -0
  27. {matelab_python_sdk-0.1.0a22 → matelab_python_sdk-0.1.0a23}/src/matelab/_transport.py +0 -0
  28. {matelab_python_sdk-0.1.0a22 → matelab_python_sdk-0.1.0a23}/src/matelab/_wire_validation.py +0 -0
  29. {matelab_python_sdk-0.1.0a22 → matelab_python_sdk-0.1.0a23}/src/matelab/client.py +0 -0
  30. {matelab_python_sdk-0.1.0a22 → matelab_python_sdk-0.1.0a23}/src/matelab/domains/__init__.py +0 -0
  31. {matelab_python_sdk-0.1.0a22 → matelab_python_sdk-0.1.0a23}/src/matelab/domains/_attachment_content.py +0 -0
  32. {matelab_python_sdk-0.1.0a22 → matelab_python_sdk-0.1.0a23}/src/matelab/domains/cloud_drive.py +0 -0
  33. {matelab_python_sdk-0.1.0a22 → matelab_python_sdk-0.1.0a23}/src/matelab/domains/groups.py +0 -0
  34. {matelab_python_sdk-0.1.0a22 → matelab_python_sdk-0.1.0a23}/src/matelab/domains/literature.py +0 -0
  35. {matelab_python_sdk-0.1.0a22 → matelab_python_sdk-0.1.0a23}/src/matelab/domains/notebooks.py +0 -0
  36. {matelab_python_sdk-0.1.0a22 → matelab_python_sdk-0.1.0a23}/src/matelab/domains/templates.py +0 -0
  37. {matelab_python_sdk-0.1.0a22 → matelab_python_sdk-0.1.0a23}/src/matelab/domains/uploads.py +0 -0
  38. {matelab_python_sdk-0.1.0a22 → matelab_python_sdk-0.1.0a23}/src/matelab/domains/users.py +0 -0
  39. {matelab_python_sdk-0.1.0a22 → matelab_python_sdk-0.1.0a23}/src/matelab/errors.py +0 -0
  40. {matelab_python_sdk-0.1.0a22 → matelab_python_sdk-0.1.0a23}/src/matelab/py.typed +0 -0
  41. {matelab_python_sdk-0.1.0a22 → matelab_python_sdk-0.1.0a23}/src/matelab/session.py +0 -0
  42. {matelab_python_sdk-0.1.0a22 → matelab_python_sdk-0.1.0a23}/src/matelab/streaming.py +0 -0
  43. {matelab_python_sdk-0.1.0a22 → matelab_python_sdk-0.1.0a23}/tests/conftest.py +0 -0
  44. {matelab_python_sdk-0.1.0a22 → matelab_python_sdk-0.1.0a23}/tests/provider/test_provider_smoke.py +0 -0
  45. {matelab_python_sdk-0.1.0a22 → matelab_python_sdk-0.1.0a23}/tests/test_async_attachment_upload.py +0 -0
  46. {matelab_python_sdk-0.1.0a22 → matelab_python_sdk-0.1.0a23}/tests/test_client.py +0 -0
  47. {matelab_python_sdk-0.1.0a22 → matelab_python_sdk-0.1.0a23}/tests/test_cloud_drive.py +0 -0
  48. {matelab_python_sdk-0.1.0a22 → matelab_python_sdk-0.1.0a23}/tests/test_comments_and_downloads.py +0 -0
  49. {matelab_python_sdk-0.1.0a22 → matelab_python_sdk-0.1.0a23}/tests/test_consumer_smoke.py +0 -0
  50. {matelab_python_sdk-0.1.0a22 → matelab_python_sdk-0.1.0a23}/tests/test_errors.py +0 -0
  51. {matelab_python_sdk-0.1.0a22 → matelab_python_sdk-0.1.0a23}/tests/test_extended_records.py +0 -0
  52. {matelab_python_sdk-0.1.0a22 → matelab_python_sdk-0.1.0a23}/tests/test_generation.py +0 -0
  53. {matelab_python_sdk-0.1.0a22 → matelab_python_sdk-0.1.0a23}/tests/test_groups.py +0 -0
  54. {matelab_python_sdk-0.1.0a22 → matelab_python_sdk-0.1.0a23}/tests/test_notebook_lifecycle.py +0 -0
  55. {matelab_python_sdk-0.1.0a22 → matelab_python_sdk-0.1.0a23}/tests/test_notebooks.py +0 -0
  56. {matelab_python_sdk-0.1.0a22 → matelab_python_sdk-0.1.0a23}/tests/test_operation_coverage.py +0 -0
  57. {matelab_python_sdk-0.1.0a22 → matelab_python_sdk-0.1.0a23}/tests/test_record_lifecycle.py +0 -0
  58. {matelab_python_sdk-0.1.0a22 → matelab_python_sdk-0.1.0a23}/tests/test_record_mutations.py +0 -0
  59. {matelab_python_sdk-0.1.0a22 → matelab_python_sdk-0.1.0a23}/tests/test_release.py +0 -0
  60. {matelab_python_sdk-0.1.0a22 → matelab_python_sdk-0.1.0a23}/tests/test_streaming.py +0 -0
  61. {matelab_python_sdk-0.1.0a22 → matelab_python_sdk-0.1.0a23}/tests/test_template_lifecycle.py +0 -0
  62. {matelab_python_sdk-0.1.0a22 → matelab_python_sdk-0.1.0a23}/tests/test_templates.py +0 -0
  63. {matelab_python_sdk-0.1.0a22 → matelab_python_sdk-0.1.0a23}/tests/test_transport.py +0 -0
  64. {matelab_python_sdk-0.1.0a22 → matelab_python_sdk-0.1.0a23}/tests/test_uploads_and_literature.py +0 -0
  65. {matelab_python_sdk-0.1.0a22 → matelab_python_sdk-0.1.0a23}/tests/test_users.py +0 -0
  66. {matelab_python_sdk-0.1.0a22 → matelab_python_sdk-0.1.0a23}/tests/typing/consumer.py +0 -0
  67. {matelab_python_sdk-0.1.0a22 → matelab_python_sdk-0.1.0a23}/tests/typing/pyproject.toml +0 -0
@@ -3,6 +3,12 @@
3
3
  This file records release-to-release changes and migration impact. It is not the authority for the current interface;
4
4
  see [README.md](README.md) for current behavior and [AGENTS.md](AGENTS.md) for maintenance policy.
5
5
 
6
+ ## 0.1.0a23 - 2026-09-04
7
+
8
+ - Temporarily accepts `readRecord` table file attachments that contain valid `filename`, `hash`, and `size` fields but
9
+ omit the Contract-required `timestamp`. The SDK preserves the omission instead of synthesizing a timestamp; explicit
10
+ null timestamps and other malformed attachment values remain rejected.
11
+
6
12
  ## 0.1.0a22 - 2026-09-03
7
13
 
8
14
  - Adopts immutable `matelab-spec v0.4.5` at commit `6afac7ff151680130e6e4bc6d95782589b97f4e7` and OpenAPI
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: matelab-python-sdk
3
- Version: 0.1.0a22
3
+ Version: 0.1.0a23
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
@@ -35,7 +35,7 @@ This README describes the current SDK interface, integration semantics, examples
35
35
  [AGENTS.md](AGENTS.md) for durable maintenance rules, and `docs/operation-coverage.yaml` for the machine-readable
36
36
  operation inventory.
37
37
 
38
- The current alpha is `0.1.0a22`. `[project].version` in `pyproject.toml` is the sole SDK version source;
38
+ The current alpha is `0.1.0a23`. `[project].version` in `pyproject.toml` is the sole SDK version source;
39
39
  `uv.lock` only mirrors that source.
40
40
 
41
41
  The SDK is pinned to the immutable `matelab-spec v0.4.5` Contract Release. The sole release pin is
@@ -64,7 +64,7 @@ To test the same artifact a downstream Consumer will install, build and install
64
64
 
65
65
  ```bash
66
66
  uv build --no-build-isolation --out-dir dist/release
67
- python -m pip install dist/release/matelab_python_sdk-0.1.0a22-py3-none-any.whl
67
+ python -m pip install dist/release/matelab_python_sdk-0.1.0a23-py3-none-any.whl
68
68
  ```
69
69
 
70
70
  Do not infer Provider compatibility from the SDK version alone. A release is also bound to the Contract
@@ -354,6 +354,12 @@ not accept a caption. Record reads likewise accept only one canonical attachment
354
354
  an attachment array violates the pinned response contract. Both replacement forms require the replacement hash to
355
355
  differ from the current occurrence.
356
356
 
357
+ As a temporary Provider-compatibility exception, `records.read()` also accepts a table file attachment object whose
358
+ otherwise valid `filename`, `hash`, and `size` are present but whose Contract-required `timestamp` is omitted. The SDK
359
+ preserves that omission in `Record.modules` and does not invent a timestamp; null timestamps and all other malformed
360
+ attachment shapes remain response errors. This exception can be removed after the Provider canonicalizes current and
361
+ historical record content.
362
+
357
363
  All record attachment intents reject handles from another notebook or authenticated uploader, duplicate use within one
358
364
  finalization request, and raw Provider attachment references. Row/index-based operations use an immediate SDK read and
359
365
  must not be called while a concurrent editor is known to be active. The Provider offers no expected hash or revision,
@@ -7,7 +7,7 @@ This README describes the current SDK interface, integration semantics, examples
7
7
  [AGENTS.md](AGENTS.md) for durable maintenance rules, and `docs/operation-coverage.yaml` for the machine-readable
8
8
  operation inventory.
9
9
 
10
- The current alpha is `0.1.0a22`. `[project].version` in `pyproject.toml` is the sole SDK version source;
10
+ The current alpha is `0.1.0a23`. `[project].version` in `pyproject.toml` is the sole SDK version source;
11
11
  `uv.lock` only mirrors that source.
12
12
 
13
13
  The SDK is pinned to the immutable `matelab-spec v0.4.5` Contract Release. The sole release pin is
@@ -36,7 +36,7 @@ To test the same artifact a downstream Consumer will install, build and install
36
36
 
37
37
  ```bash
38
38
  uv build --no-build-isolation --out-dir dist/release
39
- python -m pip install dist/release/matelab_python_sdk-0.1.0a22-py3-none-any.whl
39
+ python -m pip install dist/release/matelab_python_sdk-0.1.0a23-py3-none-any.whl
40
40
  ```
41
41
 
42
42
  Do not infer Provider compatibility from the SDK version alone. A release is also bound to the Contract
@@ -326,6 +326,12 @@ not accept a caption. Record reads likewise accept only one canonical attachment
326
326
  an attachment array violates the pinned response contract. Both replacement forms require the replacement hash to
327
327
  differ from the current occurrence.
328
328
 
329
+ As a temporary Provider-compatibility exception, `records.read()` also accepts a table file attachment object whose
330
+ otherwise valid `filename`, `hash`, and `size` are present but whose Contract-required `timestamp` is omitted. The SDK
331
+ preserves that omission in `Record.modules` and does not invent a timestamp; null timestamps and all other malformed
332
+ attachment shapes remain response errors. This exception can be removed after the Provider canonicalizes current and
333
+ historical record content.
334
+
329
335
  All record attachment intents reject handles from another notebook or authenticated uploader, duplicate use within one
330
336
  finalization request, and raw Provider attachment references. Row/index-based operations use an immediate SDK read and
331
337
  must not be called while a concurrent editor is known to be active. The Provider offers no expected hash or revision,
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "matelab-python-sdk"
7
- version = "0.1.0a22"
7
+ version = "0.1.0a23"
8
8
  description = "Reusable async Python client for the Matelab Integration Contract"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -1,13 +1,14 @@
1
1
  import re
2
2
  import secrets
3
3
  from collections.abc import AsyncIterable, Iterable, Iterator, Mapping, Sequence
4
+ from copy import deepcopy
4
5
  from dataclasses import dataclass, field
5
6
  from html.parser import HTMLParser
6
7
  from types import MappingProxyType
7
- from typing import IO, Literal, TypeAlias, cast
8
+ from typing import IO, Literal, Self, TypeAlias, cast
8
9
  from urllib.parse import unquote
9
10
 
10
- from pydantic import BaseModel, JsonValue
11
+ from pydantic import BaseModel, JsonValue, model_validator
11
12
  from typing_extensions import override
12
13
 
13
14
  from matelab._generated import models as wire_models
@@ -21,6 +22,56 @@ from matelab.domains.users import UserRef
21
22
  from matelab.errors import MatelabProtocolError, MatelabUsageError
22
23
  from matelab.streaming import ByteRange, DownloadStream
23
24
 
25
+ _MISSING_TIMESTAMP_MARKER = "__matelab_missing_record_attachment_timestamp__"
26
+
27
+
28
+ def _table_file_attachments(modules: JsonValue) -> Iterator[dict[str, JsonValue]]:
29
+ if not isinstance(modules, list):
30
+ return
31
+ for module in modules:
32
+ if not isinstance(module, dict) or module.get("type") != "table":
33
+ continue
34
+ columns = module.get("data")
35
+ if not isinstance(columns, list):
36
+ continue
37
+ for column in columns:
38
+ if not isinstance(column, dict) or column.get("type") != "file":
39
+ continue
40
+ values = column.get("data")
41
+ if not isinstance(values, list):
42
+ continue
43
+ for value in values:
44
+ if isinstance(value, dict):
45
+ yield value
46
+
47
+
48
+ class _ReadRecordResponse(wire_models.ItemViewResponse):
49
+ """Contain the Provider's temporary omission of stored attachment timestamps."""
50
+
51
+ @model_validator(mode="before")
52
+ @classmethod
53
+ def _allow_missing_attachment_timestamp(cls, value: object) -> object:
54
+ if not isinstance(value, dict):
55
+ return value
56
+ payload = deepcopy(cast(dict[str, JsonValue], value))
57
+ modules = payload.get("modules")
58
+ if modules is None:
59
+ return payload
60
+ for attachment in _table_file_attachments(modules):
61
+ if "timestamp" not in attachment and _MISSING_TIMESTAMP_MARKER not in attachment:
62
+ attachment["timestamp"] = 1
63
+ attachment[_MISSING_TIMESTAMP_MARKER] = True
64
+ return payload
65
+
66
+ @model_validator(mode="after")
67
+ def _restore_missing_attachment_timestamp(self) -> Self:
68
+ modules: JsonValue = [{"type": module.type, "data": cast(JsonValue, module.data)} for module in self.modules]
69
+ for attachment in _table_file_attachments(modules):
70
+ if attachment.pop(_MISSING_TIMESTAMP_MARKER, None) is True:
71
+ del attachment["timestamp"]
72
+ return self
73
+
74
+
24
75
  _LIST_RECORDS = Operation(
25
76
  method="POST",
26
77
  path="/eln_api/items",
@@ -68,7 +119,7 @@ _READ_RECORD = Operation(
68
119
  method="GET",
69
120
  path="/eln_items/item_view",
70
121
  encoding=Encoding.QUERY,
71
- response_model=wire_models.ItemViewResponse,
122
+ response_model=_ReadRecordResponse,
72
123
  success_codes=frozenset({0}),
73
124
  retry_on_access_expired=True,
74
125
  )
@@ -303,6 +303,77 @@ async def test_read_current_record_maps_required_nullable_and_open_modules() ->
303
303
  assert "74034dae" not in repr(record.attachments[0])
304
304
 
305
305
 
306
+ @pytest.mark.asyncio
307
+ async def test_read_tolerates_provider_table_attachment_without_timestamp() -> None:
308
+ payload = _item_view_payload(uid="REC-EXAMPLE-001")
309
+ attachment = {
310
+ "hash": "d6a8f491a743ee3c10b42b4519d7545d7f1d79a7e98ed2ef9c03c0c1d75a1a91",
311
+ "filename": "evidence.pdf",
312
+ "size": 34,
313
+ }
314
+ payload["modules"] = [
315
+ {
316
+ "type": "table",
317
+ "name": "Data sheet",
318
+ "rows": ["row-1"],
319
+ "data": [{"name": "File", "type": "file", "data": [attachment]}],
320
+ }
321
+ ]
322
+
323
+ async def handler(_: Request) -> Response:
324
+ return Response(200, json=payload)
325
+
326
+ notebook = NotebookRef(notebook_id=23, title="Shared Example", owner_userid=202, scope="shared")
327
+ source = RecordLocator(notebook=notebook, record=RecordRef(record_id=9001, record_uid="REC-EXAMPLE-001"))
328
+ async with AsyncClient(base_url="https://matelab.example/api", transport=MockTransport(handler)) as http_client:
329
+ client = AsyncMatelab("https://ignored.example", session=_session(), http_client=http_client)
330
+ record = await client.records.read(source)
331
+
332
+ assert record.modules[0] == {
333
+ "name": "Data sheet",
334
+ "type": "table",
335
+ "data": [{"name": "File", "type": "file", "data": [attachment]}],
336
+ "language": None,
337
+ "style": None,
338
+ "rows": ["row-1"],
339
+ }
340
+ assert [(item.filename, item.size) for item in record.attachments] == [("evidence.pdf", 34)]
341
+
342
+
343
+ @pytest.mark.parametrize("invalid_change", ["filename", "hash", "size", "null_timestamp"])
344
+ @pytest.mark.asyncio
345
+ async def test_read_timestamp_compatibility_does_not_relax_other_attachment_requirements(invalid_change: str) -> None:
346
+ payload = _item_view_payload(uid="REC-EXAMPLE-001")
347
+ attachment: dict[str, object] = {
348
+ "hash": "d6a8f491a743ee3c10b42b4519d7545d7f1d79a7e98ed2ef9c03c0c1d75a1a91",
349
+ "filename": "evidence.pdf",
350
+ "size": 34,
351
+ "timestamp": 1_767_946_200_000,
352
+ }
353
+ if invalid_change == "null_timestamp":
354
+ attachment["timestamp"] = None
355
+ else:
356
+ del attachment[invalid_change]
357
+ payload["modules"] = [
358
+ {
359
+ "type": "table",
360
+ "name": "Data sheet",
361
+ "rows": ["row-1"],
362
+ "data": [{"name": "File", "type": "file", "data": [attachment]}],
363
+ }
364
+ ]
365
+
366
+ async def handler(_: Request) -> Response:
367
+ return Response(200, json=payload)
368
+
369
+ notebook = NotebookRef(notebook_id=23, title="Shared Example", owner_userid=202, scope="shared")
370
+ source = RecordLocator(notebook=notebook, record=RecordRef(record_id=9001, record_uid="REC-EXAMPLE-001"))
371
+ async with AsyncClient(base_url="https://matelab.example/api", transport=MockTransport(handler)) as http_client:
372
+ client = AsyncMatelab("https://ignored.example", session=_session(), http_client=http_client)
373
+ with pytest.raises(MatelabProtocolError, match="does not match the Integration Contract"):
374
+ _ = await client.records.read(source)
375
+
376
+
306
377
  @pytest.mark.asyncio
307
378
  async def test_read_rejects_historical_table_attachment_array_at_public_boundary() -> None:
308
379
  payload = _item_view_payload(uid="REC-EXAMPLE-001")
@@ -352,7 +352,7 @@ wheels = [
352
352
 
353
353
  [[package]]
354
354
  name = "matelab-python-sdk"
355
- version = "0.1.0a22"
355
+ version = "0.1.0a23"
356
356
  source = { editable = "." }
357
357
  dependencies = [
358
358
  { name = "httpx2" },