matelab-python-sdk 0.2.0a4__tar.gz → 0.2.0a5__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 (68) hide show
  1. {matelab_python_sdk-0.2.0a4 → matelab_python_sdk-0.2.0a5}/CONTEXT.md +6 -1
  2. {matelab_python_sdk-0.2.0a4 → matelab_python_sdk-0.2.0a5}/PKG-INFO +1 -1
  3. {matelab_python_sdk-0.2.0a4 → matelab_python_sdk-0.2.0a5}/contracts/matelab-integration-v1.lock.json +3 -3
  4. {matelab_python_sdk-0.2.0a4 → matelab_python_sdk-0.2.0a5}/contracts/matelab-integration-v1.openapi.yaml +95 -39
  5. {matelab_python_sdk-0.2.0a4 → matelab_python_sdk-0.2.0a5}/docs/api.md +25 -6
  6. {matelab_python_sdk-0.2.0a4 → matelab_python_sdk-0.2.0a5}/docs/operation-coverage.yaml +2 -2
  7. {matelab_python_sdk-0.2.0a4 → matelab_python_sdk-0.2.0a5}/pyproject.toml +1 -1
  8. {matelab_python_sdk-0.2.0a4 → matelab_python_sdk-0.2.0a5}/src/matelab/__init__.py +2 -0
  9. {matelab_python_sdk-0.2.0a4 → matelab_python_sdk-0.2.0a5}/src/matelab/_generated/models.py +16 -1
  10. {matelab_python_sdk-0.2.0a4 → matelab_python_sdk-0.2.0a5}/src/matelab/domains/records.py +51 -10
  11. {matelab_python_sdk-0.2.0a4 → matelab_python_sdk-0.2.0a5}/tests/test_records.py +105 -0
  12. {matelab_python_sdk-0.2.0a4 → matelab_python_sdk-0.2.0a5}/tests/typing/consumer.py +8 -0
  13. {matelab_python_sdk-0.2.0a4 → matelab_python_sdk-0.2.0a5}/uv.lock +1 -1
  14. {matelab_python_sdk-0.2.0a4 → matelab_python_sdk-0.2.0a5}/.env.example +0 -0
  15. {matelab_python_sdk-0.2.0a4 → matelab_python_sdk-0.2.0a5}/.github/workflows/release.yml +0 -0
  16. {matelab_python_sdk-0.2.0a4 → matelab_python_sdk-0.2.0a5}/.gitignore +0 -0
  17. {matelab_python_sdk-0.2.0a4 → matelab_python_sdk-0.2.0a5}/AGENTS.md +0 -0
  18. {matelab_python_sdk-0.2.0a4 → matelab_python_sdk-0.2.0a5}/LICENSE +0 -0
  19. {matelab_python_sdk-0.2.0a4 → matelab_python_sdk-0.2.0a5}/NOTICE +0 -0
  20. {matelab_python_sdk-0.2.0a4 → matelab_python_sdk-0.2.0a5}/README.md +0 -0
  21. {matelab_python_sdk-0.2.0a4 → matelab_python_sdk-0.2.0a5}/docs/agent-architecture.md +0 -0
  22. {matelab_python_sdk-0.2.0a4 → matelab_python_sdk-0.2.0a5}/docs/agent-workflows.md +0 -0
  23. {matelab_python_sdk-0.2.0a4 → matelab_python_sdk-0.2.0a5}/docs/development.md +0 -0
  24. {matelab_python_sdk-0.2.0a4 → matelab_python_sdk-0.2.0a5}/scripts/check_installed_package.py +0 -0
  25. {matelab_python_sdk-0.2.0a4 → matelab_python_sdk-0.2.0a5}/scripts/check_release.py +0 -0
  26. {matelab_python_sdk-0.2.0a4 → matelab_python_sdk-0.2.0a5}/scripts/generate_models.py +0 -0
  27. {matelab_python_sdk-0.2.0a4 → matelab_python_sdk-0.2.0a5}/src/matelab/_generated/__init__.py +0 -0
  28. {matelab_python_sdk-0.2.0a4 → matelab_python_sdk-0.2.0a5}/src/matelab/_hashes.py +0 -0
  29. {matelab_python_sdk-0.2.0a4 → matelab_python_sdk-0.2.0a5}/src/matelab/_transport.py +0 -0
  30. {matelab_python_sdk-0.2.0a4 → matelab_python_sdk-0.2.0a5}/src/matelab/_wire_validation.py +0 -0
  31. {matelab_python_sdk-0.2.0a4 → matelab_python_sdk-0.2.0a5}/src/matelab/client.py +0 -0
  32. {matelab_python_sdk-0.2.0a4 → matelab_python_sdk-0.2.0a5}/src/matelab/domains/__init__.py +0 -0
  33. {matelab_python_sdk-0.2.0a4 → matelab_python_sdk-0.2.0a5}/src/matelab/domains/_attachment_content.py +0 -0
  34. {matelab_python_sdk-0.2.0a4 → matelab_python_sdk-0.2.0a5}/src/matelab/domains/cloud_drive.py +0 -0
  35. {matelab_python_sdk-0.2.0a4 → matelab_python_sdk-0.2.0a5}/src/matelab/domains/groups.py +0 -0
  36. {matelab_python_sdk-0.2.0a4 → matelab_python_sdk-0.2.0a5}/src/matelab/domains/literature.py +0 -0
  37. {matelab_python_sdk-0.2.0a4 → matelab_python_sdk-0.2.0a5}/src/matelab/domains/notebooks.py +0 -0
  38. {matelab_python_sdk-0.2.0a4 → matelab_python_sdk-0.2.0a5}/src/matelab/domains/templates.py +0 -0
  39. {matelab_python_sdk-0.2.0a4 → matelab_python_sdk-0.2.0a5}/src/matelab/domains/uploads.py +0 -0
  40. {matelab_python_sdk-0.2.0a4 → matelab_python_sdk-0.2.0a5}/src/matelab/domains/users.py +0 -0
  41. {matelab_python_sdk-0.2.0a4 → matelab_python_sdk-0.2.0a5}/src/matelab/errors.py +0 -0
  42. {matelab_python_sdk-0.2.0a4 → matelab_python_sdk-0.2.0a5}/src/matelab/py.typed +0 -0
  43. {matelab_python_sdk-0.2.0a4 → matelab_python_sdk-0.2.0a5}/src/matelab/session.py +0 -0
  44. {matelab_python_sdk-0.2.0a4 → matelab_python_sdk-0.2.0a5}/src/matelab/streaming.py +0 -0
  45. {matelab_python_sdk-0.2.0a4 → matelab_python_sdk-0.2.0a5}/tests/conftest.py +0 -0
  46. {matelab_python_sdk-0.2.0a4 → matelab_python_sdk-0.2.0a5}/tests/provider/test_provider_smoke.py +0 -0
  47. {matelab_python_sdk-0.2.0a4 → matelab_python_sdk-0.2.0a5}/tests/test_async_attachment_upload.py +0 -0
  48. {matelab_python_sdk-0.2.0a4 → matelab_python_sdk-0.2.0a5}/tests/test_client.py +0 -0
  49. {matelab_python_sdk-0.2.0a4 → matelab_python_sdk-0.2.0a5}/tests/test_cloud_drive.py +0 -0
  50. {matelab_python_sdk-0.2.0a4 → matelab_python_sdk-0.2.0a5}/tests/test_comments_and_downloads.py +0 -0
  51. {matelab_python_sdk-0.2.0a4 → matelab_python_sdk-0.2.0a5}/tests/test_consumer_smoke.py +0 -0
  52. {matelab_python_sdk-0.2.0a4 → matelab_python_sdk-0.2.0a5}/tests/test_errors.py +0 -0
  53. {matelab_python_sdk-0.2.0a4 → matelab_python_sdk-0.2.0a5}/tests/test_extended_records.py +0 -0
  54. {matelab_python_sdk-0.2.0a4 → matelab_python_sdk-0.2.0a5}/tests/test_generation.py +0 -0
  55. {matelab_python_sdk-0.2.0a4 → matelab_python_sdk-0.2.0a5}/tests/test_groups.py +0 -0
  56. {matelab_python_sdk-0.2.0a4 → matelab_python_sdk-0.2.0a5}/tests/test_notebook_lifecycle.py +0 -0
  57. {matelab_python_sdk-0.2.0a4 → matelab_python_sdk-0.2.0a5}/tests/test_notebooks.py +0 -0
  58. {matelab_python_sdk-0.2.0a4 → matelab_python_sdk-0.2.0a5}/tests/test_operation_coverage.py +0 -0
  59. {matelab_python_sdk-0.2.0a4 → matelab_python_sdk-0.2.0a5}/tests/test_record_lifecycle.py +0 -0
  60. {matelab_python_sdk-0.2.0a4 → matelab_python_sdk-0.2.0a5}/tests/test_record_mutations.py +0 -0
  61. {matelab_python_sdk-0.2.0a4 → matelab_python_sdk-0.2.0a5}/tests/test_release.py +0 -0
  62. {matelab_python_sdk-0.2.0a4 → matelab_python_sdk-0.2.0a5}/tests/test_streaming.py +0 -0
  63. {matelab_python_sdk-0.2.0a4 → matelab_python_sdk-0.2.0a5}/tests/test_template_lifecycle.py +0 -0
  64. {matelab_python_sdk-0.2.0a4 → matelab_python_sdk-0.2.0a5}/tests/test_templates.py +0 -0
  65. {matelab_python_sdk-0.2.0a4 → matelab_python_sdk-0.2.0a5}/tests/test_transport.py +0 -0
  66. {matelab_python_sdk-0.2.0a4 → matelab_python_sdk-0.2.0a5}/tests/test_uploads_and_literature.py +0 -0
  67. {matelab_python_sdk-0.2.0a4 → matelab_python_sdk-0.2.0a5}/tests/test_users.py +0 -0
  68. {matelab_python_sdk-0.2.0a4 → matelab_python_sdk-0.2.0a5}/tests/typing/pyproject.toml +0 -0
@@ -63,9 +63,14 @@ _Avoid_: Record locator, public record ref, deleted record ref
63
63
 
64
64
  **Record locator**:
65
65
  A record ref paired with the owned or shared notebook in which it was observed. It is the complete source identity
66
- used by record reads and record-scoped operations.
66
+ used by owned/shared record reads and record-scoped operations.
67
67
  _Avoid_: Record ref, notebook ID
68
68
 
69
+ **Public record ref**:
70
+ A public association's row ID, paired source notebook and record IDs, and nullable association UID metadata.
71
+ The source ID pair locates its current body; it does not grant notebook-wide access.
72
+ _Avoid_: Record locator, public notebook ID, association ID as record ID
73
+
69
74
  **Record version ref**:
70
75
  A historical version ID bound to its record locator. It identifies observed historical content, not a separate active
71
76
  record.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: matelab-python-sdk
3
- Version: 0.2.0a4
3
+ Version: 0.2.0a5
4
4
  Summary: Reusable async Python client for the Matelab Integration Contract
5
5
  Author: CMPDC, Institute of Physics, Chinese Academy of Sciences
6
6
  License-Expression: Apache-2.0
@@ -2,12 +2,12 @@
2
2
  "schema_version": 1,
3
3
  "contract_line": "integration-v1",
4
4
  "release": {
5
- "tag": "v2026.10.1",
6
- "commit": "9941e2d623a1c2633adb459b823d78d4e62d3802"
5
+ "tag": "v2026.10.2",
6
+ "commit": "1183d6833d88a6904598357321ef2d30cf944339"
7
7
  },
8
8
  "openapi": {
9
9
  "source_path": "openapi/matelab-integration-v1.openapi.yaml",
10
10
  "snapshot": "matelab-integration-v1.openapi.yaml",
11
- "sha256": "a07997128c1df17a03724c5af8e11b45a34166f9d6e15558e66e0a301f2e0813"
11
+ "sha256": "4806fc2e4b4cb4cf94464be6f1b5f574f5a44745bf7cbc03d8644a13e57f8ec9"
12
12
  }
13
13
  }
@@ -1,7 +1,7 @@
1
1
  openapi: 3.1.0
2
2
  info:
3
3
  title: Matelab Integration Contract
4
- version: "2026.10.1"
4
+ version: "2026.10.2"
5
5
  description: |
6
6
  Current PHP wire contract for generating SDKs against the selected
7
7
  `matelab-server` Provider version.
@@ -26,6 +26,7 @@ info:
26
26
  - controller/Eln.php
27
27
  - controller/Eln_api.php
28
28
  - controller/Eln_items.php
29
+ - controller/Eln_public.php
29
30
  - controller/Eln_template.php
30
31
  - controller/Eln_url.php
31
32
  - controller/Files.php
@@ -36,6 +37,7 @@ info:
36
37
  - model/FileQuoteModel.php
37
38
  - model/FilesModel.php
38
39
  - traits/ElnTrait.php
40
+ - traits/ElnPublicTrait.php
39
41
  - traits/RecommendTrait.php
40
42
  - service/ElnApiImport.php
41
43
  - service/ElnApiUpdate.php
@@ -53,7 +55,7 @@ info:
53
55
  - sql/recommend.sql
54
56
  x-provider-audit:
55
57
  currentCommit: 3f39d47b7ad43bed618b029a7d9cc51fab852eb6
56
- previousReleaseEvidenceCommit: acb0e12f2d62a8959ab9e9bddf38e34a67c19b48
58
+ previousReleaseEvidenceCommit: 3f39d47b7ad43bed618b029a7d9cc51fab852eb6
57
59
  lastProviderVerifiedCommit: 3f39d47b7ad43bed618b029a7d9cc51fab852eb6
58
60
  contractStatus: current-provider-aligned
59
61
  # Summary of changes adopted by current Supported Routes since the last
@@ -62,36 +64,8 @@ info:
62
64
  # updates against that fixed release baseline; use [] if no relevant changes.
63
65
  # Entries are not per-change verification results or a cumulative history.
64
66
  incorporatedChanges:
65
- - {operationId: readApplicationBootstrap, change: array-valued-query-token-rejected-as-missing-authentication}
66
- - {operationId: updateRecordData, change: collaboration-source-restricted-to-same-notebook-record-web-peer}
67
- - {operationId: readRecord, change: encrypted-modules-and-unresolved-user-fields-modeled}
68
- - {operationId: listRecordComments, change: unresolved-author-descriptors-modeled}
69
- - {operationId: listRecordRelations, change: unresolved-comment-author-descriptors-modeled}
70
- - {operationId: updateRecordData, change: staged-files-folder-append-wired-into-request-and-base64-excluded}
71
- - {operationId: importRecordDataset, change: unreachable-base64-file-input-excluded}
72
- - {operationId: searchRecordData, change: fixed-result-field-aliases-reserved-and-extraction-path-constrained}
73
- - {operationId: saveNotebookMetadata, change: decoded-list-display-requests-reject-null-uid-and-invalid-path}
74
- - {operationId: updateRecordData, change: quote-extraction-path-constrained-and-collaboration-code-0-documented}
75
- - {operationId: listNotebooks, change: stored-types-return-non-null-decoded-json}
76
- - {operationId: extractLiteratureMetadata, change: nested-errors-and-opaque-array-results-modeled}
77
- - {operationId: listCloudDriveFiles, change: unreachable-null-timestamp-success-removed}
78
- - {operationId: downloadRecordAttachment, change: zero-start-open-range-4097-byte-cap-documented}
79
- - {operationId: downloadRecordCommentAttachment, change: zero-start-open-range-4097-byte-cap-documented}
80
- - {operationId: downloadLiteraturePdf, change: zero-start-open-range-4097-byte-cap-documented}
81
- - {operationId: downloadCloudDriveFile, change: zero-start-open-range-4097-byte-cap-documented}
82
- - {operationId: listCurrentUserElnGroups, change: dangling-group-id-null-and-first-group-error-modeled}
83
- - {operationId: downloadRecordCommentAttachment, change: zero-byte-attachment-download-supported}
84
- - {operationId: listNotebookRecords, change: local-data-server-success-code-zero-internal-forwarding-code-excluded}
85
- - {operationId: exportRecords, change: local-data-server-success-code-zero-internal-forwarding-code-excluded}
86
- - {operationId: searchRecordData, change: local-data-server-success-code-zero-internal-forwarding-code-excluded}
87
- - {operationId: listDeletedRecordPage, change: local-data-server-success-code-zero-internal-forwarding-code-excluded}
88
- - {operationId: copyRecord, change: local-data-server-success-code-zero-internal-forwarding-code-excluded}
89
- - {operationId: addRecordRelation, change: local-data-server-success-code-zero-internal-forwarding-code-excluded}
90
- - {operationId: deleteRecordRelation, change: local-data-server-success-code-zero-internal-forwarding-code-excluded}
91
- - {operationId: saveRecordComment, change: local-data-server-success-code-zero-internal-forwarding-code-excluded}
92
- - {operationId: deleteRecordComment, change: local-data-server-success-code-zero-internal-forwarding-code-excluded}
93
- - {operationId: downloadRecordAttachment, change: remote-data-server-download-excluded-pending-proxy-verification}
94
- - {operationId: downloadRecordCommentAttachment, change: remote-data-server-download-excluded-pending-proxy-verification}
67
+ - {operationId: readRecord, change: public-only-body-access-and-source-record-identity-documented}
68
+ - {operationId: listPublicNotebookRecords, change: source-record-identifiers-distinguished-from-public-association-id}
95
69
  x-consumer-evidence:
96
70
  - repository: /home/tnzhu/projects/matelab-mcp
97
71
  commit: 94a129b2df0ef0fbcf179c10550fb7c9a5dd2e6f
@@ -2054,6 +2028,17 @@ paths:
2054
2028
  operationId: listPublicNotebookRecords
2055
2029
  tags: [Records]
2056
2030
  summary: List records in a visible public notebook
2031
+ description: |
2032
+ Authentication is required even for a globally visible public directory.
2033
+ `eln` is the public directory's name. Visibility comes from a global
2034
+ directory (`eln_public.userid=0`) or the caller's directory membership;
2035
+ owning or sharing the source notebook is not required.
2036
+ To read an item's current body, call `readRecord` with query
2037
+ `eln=items[n].eln_id&id=items[n].item_id&version=0`. The directory's own
2038
+ id and each returned association `id` are different identities and must
2039
+ not be substituted. `uid` is nullable association metadata, not a body
2040
+ selector. An association can survive a missing source notebook or record;
2041
+ listing it does not guarantee that its body can still be read.
2057
2042
  security: [{bearerAuth: []}]
2058
2043
  requestBody:
2059
2044
  required: true
@@ -2092,6 +2077,8 @@ paths:
2092
2077
  - {id: 52, item_id: 9002, eln_id: 12, uid: null, title: null, comm: null, locked: false, eln_name: null, userid: null, username: null, email: null, owner: false}
2093
2078
  error:
2094
2079
  value: {code: 2, msg: Public notebook not found, errcode: 2, errmsg: Public notebook not found}
2080
+ authenticationError:
2081
+ value: {code: 1, msg: Authentication required, errcode: 1, errmsg: Authentication required}
2095
2082
  /eln_items/items:
2096
2083
  get:
2097
2084
  operationId: listNotebookRecordPage
@@ -2676,19 +2663,37 @@ paths:
2676
2663
  operation uses query parameters. Reading records writes an audit-log entry;
2677
2664
  that incidental access logging does not turn the resource read into a
2678
2665
  mutation operation.
2666
+ Public-record bodies use this same operation and `ItemViewResponse`.
2667
+ After `listPublicNotebookRecords`, pass that item's `eln_id` as `eln` and
2668
+ `item_id` as `id`; keep them paired and use `version=0` for current content.
2669
+ Do not substitute the public directory id, the public association's `id`,
2670
+ or its nullable `uid`.
2671
+ Authentication is still required. For a caller with no source-notebook
2672
+ owner/share grant, an exact `(eln_id, item_id)` association in a global
2673
+ public directory or one they belong to grants record-level read access.
2674
+ This does not grant ownership or access to other records in that notebook.
2675
+ Global public access is read-only; a member directory can separately
2676
+ grant editing through the association's `editable` setting.
2677
+ `modules` contains the canonical body or the existing encrypted descriptor;
2678
+ this endpoint does not decrypt it. The required `uid` field can be null
2679
+ for historical content and is not needed to construct the body request.
2680
+ Missing/invalid authentication returns business code 1; denied access or
2681
+ an invalid current-record identity returns code 2, both over HTTP 200.
2682
+ Historical reads must use a version from this record's own `modify_log`;
2683
+ the Provider does not enforce historical-version ownership (PVD-005).
2679
2684
  security:
2680
2685
  - bearerAuth: []
2681
2686
  parameters:
2682
2687
  - name: eln
2683
2688
  in: query
2684
2689
  required: true
2685
- description: Notebook database identifier.
2690
+ description: Source notebook database identifier; use PublicRecordItem.eln_id for a public record, not the public directory id.
2686
2691
  schema: {type: integer, minimum: 1}
2687
2692
  example: 12
2688
2693
  - name: id
2689
2694
  in: query
2690
2695
  required: true
2691
- description: Record database identifier.
2696
+ description: Source record database identifier; use PublicRecordItem.item_id for a public record, not its association id or UID.
2692
2697
  schema: {type: integer, minimum: 1}
2693
2698
  example: 9001
2694
2699
  - name: version
@@ -2766,6 +2771,38 @@ paths:
2766
2771
  template_show: ""
2767
2772
  template_text: Calibration Template
2768
2773
  template_intro: ""
2774
+ public:
2775
+ summary: Current body for a public-only reader without source-notebook sharing
2776
+ value:
2777
+ code: 0
2778
+ msg: ""
2779
+ errcode: 0
2780
+ errmsg: ""
2781
+ title: Calibration Run
2782
+ keywords: [calibration]
2783
+ modules:
2784
+ - {type: richtext, name: Notes, uid: module-example, data: Sanitized content}
2785
+ template_id: 8
2786
+ datetime_modify: 1767946200000
2787
+ username: Example User
2788
+ modify_log: []
2789
+ uid: REC-EXAMPLE-001
2790
+ current_version: 0
2791
+ item_id: 9001
2792
+ datetime_locked: 0
2793
+ locked: false
2794
+ signs: []
2795
+ signed: false
2796
+ owner: false
2797
+ eln_text: Example Notebook
2798
+ eln_server: localhost
2799
+ eln_id: 12
2800
+ editable: false
2801
+ keyword_eln: [calibration]
2802
+ pdf_enable: true
2803
+ template_show: ""
2804
+ template_text: Calibration Template
2805
+ template_intro: ""
2769
2806
  nullable:
2770
2807
  summary: Historical record with nullable UID, user, template, and keywords
2771
2808
  value:
@@ -2902,6 +2939,11 @@ paths:
2902
2939
  msg: No operation permission
2903
2940
  errcode: 2
2904
2941
  errmsg: No operation permission
2942
+ authenticationError:
2943
+ value: {code: 1, msg: Authentication required, errcode: 1, errmsg: Authentication required}
2944
+ identityError:
2945
+ summary: Source notebook and record identifiers do not identify the same current record
2946
+ value: {code: 2, msg: Illegal operation, errcode: 2, errmsg: Illegal operation}
2905
2947
  /eln_url/eln:
2906
2948
  get:
2907
2949
  operationId: downloadRecordAttachment
@@ -5677,7 +5719,10 @@ components:
5677
5719
  type: object
5678
5720
  required: [eln]
5679
5721
  properties:
5680
- eln: {type: string, minLength: 1}
5722
+ eln:
5723
+ type: string
5724
+ minLength: 1
5725
+ description: Public directory name, not the source notebook name or numeric id.
5681
5726
  additionalProperties: false
5682
5727
  CreateRecordRequest:
5683
5728
  type: object
@@ -6214,10 +6259,21 @@ components:
6214
6259
  type: object
6215
6260
  required: [id, item_id, eln_id, uid, title, comm, locked, eln_name, userid, username, email, owner]
6216
6261
  properties:
6217
- id: {type: integer, minimum: 1}
6218
- item_id: {type: integer, minimum: 1}
6219
- eln_id: {type: integer, minimum: 1}
6220
- uid: {type: [string, "null"]}
6262
+ id:
6263
+ type: integer
6264
+ minimum: 1
6265
+ description: Public association row identifier; not the record id accepted by readRecord.
6266
+ item_id:
6267
+ type: integer
6268
+ minimum: 1
6269
+ description: Source record identifier; pass as readRecord query id together with this row's eln_id.
6270
+ eln_id:
6271
+ type: integer
6272
+ minimum: 1
6273
+ description: Source notebook identifier; pass as readRecord query eln, not the public directory id.
6274
+ uid:
6275
+ type: [string, "null"]
6276
+ description: Nullable UID copied into the association; not required for the numeric readRecord selector.
6221
6277
  title: {type: [string, "null"]}
6222
6278
  comm: {type: [string, "null"]}
6223
6279
  locked: {type: boolean}
@@ -5,13 +5,13 @@ See the [README](../README.md) for installation and quick start and [CONTEXT.md]
5
5
 
6
6
  ## Contract
7
7
 
8
- The SDK is pinned to the immutable `matelab-spec v2026.10.1` Contract Release. The sole pin,
8
+ The SDK is pinned to the immutable `matelab-spec v2026.10.2` Contract Release. The sole pin,
9
9
  [contracts/matelab-integration-v1.lock.json](../contracts/matelab-integration-v1.lock.json), records peeled commit
10
- `9941e2d623a1c2633adb459b823d78d4e62d3802`, source `openapi/matelab-integration-v1.openapi.yaml`, the local snapshot path,
11
- and SHA-256 `a07997128c1df17a03724c5af8e11b45a34166f9d6e15558e66e0a301f2e0813`. The annotated tag object is
12
- `ae8098e1b5178b69706bddd8b70c1b4cfe3e09ba`.
10
+ `1183d6833d88a6904598357321ef2d30cf944339`, source `openapi/matelab-integration-v1.openapi.yaml`, the local snapshot path,
11
+ and SHA-256 `4806fc2e4b4cb4cf94464be6f1b5f574f5a44745bf7cbc03d8644a13e57f8ec9`. The annotated tag object is
12
+ `4ea93c32637a8bcbea51cf6c6fb2da4c316a7134`.
13
13
 
14
- The Contract uses Provider source evidence from `3f39d47b7ad43bed618b029a7d9cc51fab852eb6`; v2026.10.1 validation ran
14
+ The Contract uses Provider source evidence from `3f39d47b7ad43bed618b029a7d9cc51fab852eb6`; v2026.10.2 validation ran
15
15
  against an isolated test instance and does not establish production behavior. The Front Consumer evidence
16
16
  `0c985f5b6da8e8ef4627ca7065cea85e408ae981` informs cross-Consumer capability review but does not define the SDK interface.
17
17
 
@@ -65,7 +65,7 @@ callable Contract and raises `MatelabProtocolError` rather than assigning an err
65
65
  `MatelabTransportError.status_code` remains available for HTTP failures. Provider response bodies, top-level `msg`,
66
66
  legacy `errmsg`, opaque `errs`, uncontracted debug fields, and request payloads are not attached to exceptions because
67
67
  Provider-authored diagnostics may contain stack traces, credentials, or submitted values.
68
- The pinned v2026.10.1 Contract also declares optional code-4 `errs`, but PVD-029 and PCG-010 record opaque array/object
68
+ The pinned Contract also declares optional code-4 `errs`, but PVD-029 and PCG-010 record opaque array/object
69
69
  containers and insufficient evidence for requiredness, cardinality, or exclusion from other codes. The SDK
70
70
  therefore keeps those diagnostics wire-only and does not expose `errs` as stable public metadata. A typed detail
71
71
  interface remains blocked until a future Contract release can define one from stronger Provider evidence. `retryable`
@@ -556,6 +556,25 @@ Search and relation order remain unspecified, and no continuation token is inven
556
556
  directly. Remote data-server forwarding code `10` is rejected because only local data-server results are covered.
557
557
  `DeletedRecordPage` does not repeat its input notebook.
558
558
 
559
+ Read a public entry's current body directly from its ref:
560
+
561
+ ```python
562
+ notebooks = await client.notebooks.list()
563
+ entries = await client.records.list_public(notebooks.public[0].ref)
564
+ if entries:
565
+ record = await client.records.read_public(entries[0].ref)
566
+ modules = record.modules
567
+ ```
568
+
569
+ `read_public()` returns `PublicRecord`, with the input `PublicRecordRef` as `source` and the same canonical `modules`
570
+ and content metadata as a private read. It uses the paired source notebook and record IDs; the public association ID
571
+ and its nullable, potentially stale UID do not select or validate the body. Authentication is required, and the Provider
572
+ checks current access through a global directory or directory membership and writes a read audit entry (PVD-035).
573
+ This grants no notebook-wide
574
+ access. A listed association may outlive its source, and revoked access or a missing record raises a Provider error.
575
+ Encrypted content remains in `encrypted_modules`, with empty plaintext `modules`. Public reads expose current content;
576
+ historical-version and downloadable-attachment refs remain available through owned/shared record reads.
577
+
559
578
  Record creation keeps blank creation and structured import as separate capabilities:
560
579
 
561
580
  ```python
@@ -3,7 +3,7 @@
3
3
  schema_version: 1
4
4
  contract:
5
5
  line: integration-v1
6
- release: v2026.10.1
6
+ release: v2026.10.2
7
7
  operation_count: 71
8
8
 
9
9
  operations:
@@ -429,7 +429,7 @@ operations:
429
429
  domain: records
430
430
  state: implemented
431
431
  stability: stable
432
- public_interface: AsyncMatelab.records.read
432
+ public_interface: AsyncMatelab.records.read / AsyncMatelab.records.read_public
433
433
  known_issues: [PCG-005, PCG-012, PVD-035, PVD-005, PVD-044]
434
434
 
435
435
  - operation_id: downloadRecordAttachment
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "matelab-python-sdk"
7
- version = "0.2.0a4"
7
+ version = "0.2.0a5"
8
8
  description = "Reusable async Python client for the Matelab Integration Contract"
9
9
  requires-python = ">=3.11"
10
10
  keywords = ["matelab", "electronic-lab-notebook", "eln", "asyncio", "sdk"]
@@ -41,6 +41,7 @@ from matelab.domains.records import (
41
41
  DeletedRecordSummary,
42
42
  EncryptedRecordModules,
43
43
  ExportedRecord,
44
+ PublicRecord,
44
45
  PublicRecordRef,
45
46
  PublicRecordSummary,
46
47
  Record,
@@ -145,6 +146,7 @@ __all__ = [
145
146
  "NotebookShareRef",
146
147
  "NotebookShares",
147
148
  "NotebookSummary",
149
+ "PublicRecord",
148
150
  "PublicRecordRef",
149
151
  "PublicRecordSummary",
150
152
  "PublicationAuthorCandidate",
@@ -11,7 +11,7 @@ from pydantic import ConfigDict, Field, RootModel, SecretStr, StrictBool, Strict
11
11
 
12
12
  from matelab._wire_validation import WireModel
13
13
 
14
- OPENAPI_INFO_VERSION = "2026.10.1"
14
+ OPENAPI_INFO_VERSION = "2026.10.2"
15
15
 
16
16
 
17
17
  class ResponseEnvelope(WireModel):
@@ -1433,6 +1433,9 @@ class DeleteNotebookShareRequest(WireModel):
1433
1433
  class PublicRecordListRequest(WireModel):
1434
1434
  model_config = ConfigDict(extra="forbid")
1435
1435
  eln: Annotated[str, Field(min_length=1)]
1436
+ """
1437
+ Public directory name, not the source notebook name or numeric id.
1438
+ """
1436
1439
 
1437
1440
 
1438
1441
  class CreateRecordRequest(WireModel):
@@ -2047,9 +2050,21 @@ class DeleteRecordsRequest(WireModel):
2047
2050
 
2048
2051
  class PublicRecordItem(WireModel):
2049
2052
  id: Annotated[StrictInt, Field(ge=1)]
2053
+ """
2054
+ Public association row identifier; not the record id accepted by readRecord.
2055
+ """
2050
2056
  item_id: Annotated[StrictInt, Field(ge=1)]
2057
+ """
2058
+ Source record identifier; pass as readRecord query id together with this row's eln_id.
2059
+ """
2051
2060
  eln_id: Annotated[StrictInt, Field(ge=1)]
2061
+ """
2062
+ Source notebook identifier; pass as readRecord query eln, not the public directory id.
2063
+ """
2052
2064
  uid: Annotated[str | None, Field(...)]
2065
+ """
2066
+ Nullable UID copied into the association; not required for the numeric readRecord selector.
2067
+ """
2053
2068
  title: Annotated[str | None, Field(...)]
2054
2069
  comm: Annotated[str | None, Field(...)]
2055
2070
  locked: StrictBool
@@ -320,6 +320,8 @@ class RecordSearchMatch:
320
320
 
321
321
  @dataclass(frozen=True, slots=True)
322
322
  class PublicRecordRef:
323
+ """Public association metadata with the paired source IDs used for body reads."""
324
+
323
325
  public_entry_id: int
324
326
  source_notebook_id: int
325
327
  source_record_id: int
@@ -587,6 +589,20 @@ class EncryptedRecordModules:
587
589
  extra: Mapping[str, JsonValue] = field(default_factory=dict[str, JsonValue], repr=False)
588
590
 
589
591
 
592
+ @dataclass(frozen=True, slots=True)
593
+ class PublicRecord:
594
+ """Current content read through a public record association."""
595
+
596
+ source: PublicRecordRef
597
+ title: str | None
598
+ keywords: tuple[str, ...] | None
599
+ modules: tuple[dict[str, JsonValue], ...]
600
+ template_id: int | None
601
+ modified_at_ms: int
602
+ author_name: str | None
603
+ encrypted_modules: EncryptedRecordModules | None = None
604
+
605
+
590
606
  @dataclass(frozen=True, slots=True)
591
607
  class Record:
592
608
  """Current or historical record content with safe attachment references."""
@@ -732,6 +748,18 @@ def _contains_native_attachment_reference(value: JsonValue) -> bool:
732
748
  return False
733
749
 
734
750
 
751
+ def _record_modules(
752
+ response: wire_models.ItemViewResponse,
753
+ ) -> tuple[tuple[dict[str, JsonValue], ...], EncryptedRecordModules | None]:
754
+ if isinstance(response.modules, wire_models.EncryptedRecordModules):
755
+ return (), EncryptedRecordModules(
756
+ ciphertext=response.modules.data,
757
+ algorithm=response.modules.encrypt,
758
+ extra=MappingProxyType(cast(dict[str, JsonValue], response.modules.model_extra or {})),
759
+ )
760
+ return tuple(item.model_dump(mode="json") for item in response.modules), None
761
+
762
+
735
763
  def _record_attachment_refs(
736
764
  modules: Sequence[Mapping[str, JsonValue]], *, source: RecordLocator, version: RecordVersionRef | None
737
765
  ) -> tuple[RecordAttachmentRef, ...]:
@@ -1708,6 +1736,28 @@ class Records:
1708
1736
  )
1709
1737
  return await self._transport.stream(_DOWNLOAD_COMMENT_ATTACHMENT, payload=request, byte_range=byte_range)
1710
1738
 
1739
+ async def read_public(self, source: PublicRecordRef) -> PublicRecord:
1740
+ """Read current content using a public entry's source notebook and record IDs."""
1741
+ request = validate_request(
1742
+ wire_models.ReadRecordParametersQuery,
1743
+ {"eln": source.source_notebook_id, "id": source.source_record_id, "version": 0},
1744
+ "Public record source identity does not satisfy the Integration Contract.",
1745
+ )
1746
+ response = await self._transport.request(_READ_RECORD, payload=request)
1747
+ modules, encrypted_modules = _record_modules(response)
1748
+ return PublicRecord(
1749
+ source=source,
1750
+ title=response.title,
1751
+ keywords=tuple(response.keywords) if response.keywords is not None else None,
1752
+ modules=modules,
1753
+ template_id=response.template_id,
1754
+ modified_at_ms=response.datetime_modify,
1755
+ author_name=response.username.root
1756
+ if response.username is not None and isinstance(response.username.root, str)
1757
+ else None,
1758
+ encrypted_modules=encrypted_modules,
1759
+ )
1760
+
1711
1761
  async def read(self, source: RecordLocator | RecordVersionRef) -> Record:
1712
1762
  if isinstance(source, RecordVersionRef):
1713
1763
  version = source
@@ -1719,16 +1769,7 @@ class Records:
1719
1769
  record_source, version_id=version.version_id if version is not None else None
1720
1770
  )
1721
1771
 
1722
- encrypted_modules = None
1723
- if isinstance(response.modules, wire_models.EncryptedRecordModules):
1724
- encrypted_modules = EncryptedRecordModules(
1725
- ciphertext=response.modules.data,
1726
- algorithm=response.modules.encrypt,
1727
- extra=MappingProxyType(cast(dict[str, JsonValue], response.modules.model_extra or {})),
1728
- )
1729
- modules: tuple[dict[str, JsonValue], ...] = ()
1730
- else:
1731
- modules = tuple(item.model_dump(mode="json") for item in response.modules)
1772
+ modules, encrypted_modules = _record_modules(response)
1732
1773
  return Record(
1733
1774
  source=record_source,
1734
1775
  version=version,
@@ -7,9 +7,13 @@ from httpx2 import AsyncClient, MockTransport, Request, Response
7
7
  from matelab import (
8
8
  AsyncMatelab,
9
9
  Identity,
10
+ MatelabAuthenticationError,
11
+ MatelabError,
10
12
  MatelabProtocolError,
13
+ MatelabProviderError,
11
14
  MatelabUsageError,
12
15
  NotebookRef,
16
+ PublicRecordRef,
13
17
  RecordLocator,
14
18
  RecordRef,
15
19
  RecordValueUpdate,
@@ -127,6 +131,107 @@ async def test_list_records_omits_owned_defaults_and_accepts_empty_example() ->
127
131
  assert records.trash_count == 0
128
132
 
129
133
 
134
+ @pytest.mark.asyncio
135
+ @pytest.mark.parametrize("association_uid", [None, "outdated-association-uid"])
136
+ async def test_read_public_record_uses_source_ids_and_maps_current_body(
137
+ association_uid: str | None,
138
+ contract_response_example: Callable[[str, str], dict[str, object]],
139
+ provider_session: Session,
140
+ ) -> None:
141
+ payload = contract_response_example("readRecord", "public")
142
+ payload["future_response_field"] = True
143
+ calls: list[Request] = []
144
+
145
+ async def handler(request: Request) -> Response:
146
+ calls.append(request)
147
+ assert request.method == "GET"
148
+ assert request.url.path == "/api/eln_items/item_view"
149
+ assert dict(request.url.params) == {"eln": "12", "id": "9001", "version": "0"}
150
+ assert request.headers["Authorization"] == "Bearer access"
151
+ assert request.content == b""
152
+ return Response(200, json=payload)
153
+
154
+ source = PublicRecordRef(
155
+ public_entry_id=51, source_notebook_id=12, source_record_id=9001, record_uid=association_uid
156
+ )
157
+ async with AsyncClient(base_url="https://matelab.example/api", transport=MockTransport(handler)) as http_client:
158
+ client = AsyncMatelab("https://ignored.example", session=provider_session, http_client=http_client)
159
+ record = await client.records.read_public(source)
160
+
161
+ assert len(calls) == 1
162
+ assert record.source == source
163
+ assert record.title == "Calibration Run"
164
+ assert record.keywords == ("calibration",)
165
+ assert record.modules == (
166
+ {
167
+ "type": "richtext",
168
+ "name": "Notes",
169
+ "uid": "module-example",
170
+ "data": "Sanitized content",
171
+ "language": None,
172
+ "style": None,
173
+ },
174
+ )
175
+ assert record.template_id == 8
176
+ assert record.modified_at_ms == payload["datetime_modify"]
177
+ assert record.author_name == "Example User"
178
+ assert record.encrypted_modules is None
179
+
180
+
181
+ @pytest.mark.asyncio
182
+ async def test_read_public_record_preserves_encryption_and_nullable_metadata(
183
+ contract_response_example: Callable[[str, str], dict[str, object]], provider_session: Session
184
+ ) -> None:
185
+ payload = contract_response_example("readRecord", "public")
186
+ payload.update(
187
+ title=None,
188
+ keywords=None,
189
+ template_id=None,
190
+ username=None,
191
+ modules={"data": "opaque-ciphertext", "encrypt": "CBC", "future_field": {"version": 1}},
192
+ )
193
+
194
+ async def handler(_: Request) -> Response:
195
+ return Response(200, json=payload)
196
+
197
+ source = PublicRecordRef(public_entry_id=51, source_notebook_id=12, source_record_id=9001, record_uid=None)
198
+ async with AsyncClient(base_url="https://matelab.example/api", transport=MockTransport(handler)) as http_client:
199
+ client = AsyncMatelab("https://ignored.example", session=provider_session, http_client=http_client)
200
+ record = await client.records.read_public(source)
201
+
202
+ assert record.title is None
203
+ assert record.keywords is None
204
+ assert record.template_id is None
205
+ assert record.author_name is None
206
+ assert record.modules == ()
207
+ descriptor = record.encrypted_modules
208
+ assert descriptor is not None
209
+ assert descriptor.ciphertext == "opaque-ciphertext"
210
+ assert descriptor.algorithm == "CBC"
211
+ assert descriptor.extra == {"future_field": {"version": 1}}
212
+ assert "opaque-ciphertext" not in repr(record)
213
+
214
+
215
+ @pytest.mark.asyncio
216
+ @pytest.mark.parametrize(
217
+ ("example", "error_type"), [("error", MatelabProviderError), ("authenticationError", MatelabAuthenticationError)]
218
+ )
219
+ async def test_read_public_record_raises_provider_errors(
220
+ example: str,
221
+ error_type: type[MatelabError],
222
+ contract_response_example: Callable[[str, str], dict[str, object]],
223
+ provider_session: Session,
224
+ ) -> None:
225
+ async def handler(_: Request) -> Response:
226
+ return Response(200, json=contract_response_example("readRecord", example))
227
+
228
+ source = PublicRecordRef(public_entry_id=51, source_notebook_id=12, source_record_id=9001, record_uid=None)
229
+ async with AsyncClient(base_url="https://matelab.example/api", transport=MockTransport(handler)) as http_client:
230
+ client = AsyncMatelab("https://ignored.example", session=provider_session, http_client=http_client)
231
+ with pytest.raises(error_type):
232
+ _ = await client.records.read_public(source)
233
+
234
+
130
235
  @pytest.mark.asyncio
131
236
  async def test_read_current_record_maps_pinned_contract_success_example(
132
237
  contract_response_example: Callable[[str, str], dict[str, object]],
@@ -11,6 +11,8 @@ from matelab import (
11
11
  NotebookCollection,
12
12
  NotebookRef,
13
13
  PublicationMetadataCandidate,
14
+ PublicRecord,
15
+ PublicRecordSummary,
14
16
  Record,
15
17
  RecordAttachmentRef,
16
18
  RecordAttachmentReplacement,
@@ -94,6 +96,12 @@ async def consume_record(client: AsyncMatelab, source: RecordLocator) -> Record:
94
96
  return record
95
97
 
96
98
 
99
+ async def consume_public_record(client: AsyncMatelab, notebook: NotebookRef) -> PublicRecord | None:
100
+ entries: tuple[PublicRecordSummary, ...] = await client.records.list_public(notebook)
101
+ record: PublicRecord | None = await client.records.read_public(entries[0].ref) if entries else None
102
+ return record
103
+
104
+
97
105
  async def consume_record_attachment_intents(
98
106
  client: AsyncMatelab,
99
107
  source: RecordLocator,
@@ -362,7 +362,7 @@ wheels = [
362
362
 
363
363
  [[package]]
364
364
  name = "matelab-python-sdk"
365
- version = "0.2.0a4"
365
+ version = "0.2.0a5"
366
366
  source = { editable = "." }
367
367
  dependencies = [
368
368
  { name = "httpx2" },