matelab-python-sdk 0.2.0a6__tar.gz → 0.2.0a8__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. {matelab_python_sdk-0.2.0a6 → matelab_python_sdk-0.2.0a8}/.github/workflows/release.yml +1 -1
  2. {matelab_python_sdk-0.2.0a6 → matelab_python_sdk-0.2.0a8}/PKG-INFO +3 -3
  3. {matelab_python_sdk-0.2.0a6 → matelab_python_sdk-0.2.0a8}/contracts/matelab-integration-v1.lock.json +3 -3
  4. {matelab_python_sdk-0.2.0a6 → matelab_python_sdk-0.2.0a8}/contracts/matelab-integration-v1.openapi.yaml +26 -34
  5. {matelab_python_sdk-0.2.0a6 → matelab_python_sdk-0.2.0a8}/docs/api.md +18 -11
  6. {matelab_python_sdk-0.2.0a6 → matelab_python_sdk-0.2.0a8}/docs/operation-coverage.yaml +1 -1
  7. {matelab_python_sdk-0.2.0a6 → matelab_python_sdk-0.2.0a8}/pyproject.toml +11 -10
  8. {matelab_python_sdk-0.2.0a6 → matelab_python_sdk-0.2.0a8}/src/matelab/_generated/models.py +6 -3
  9. {matelab_python_sdk-0.2.0a6 → matelab_python_sdk-0.2.0a8}/src/matelab/domains/uploads.py +2 -1
  10. {matelab_python_sdk-0.2.0a6 → matelab_python_sdk-0.2.0a8}/tests/test_uploads_and_literature.py +13 -5
  11. matelab_python_sdk-0.2.0a8/uv.lock +1054 -0
  12. matelab_python_sdk-0.2.0a6/uv.lock +0 -940
  13. {matelab_python_sdk-0.2.0a6 → matelab_python_sdk-0.2.0a8}/.env.example +0 -0
  14. {matelab_python_sdk-0.2.0a6 → matelab_python_sdk-0.2.0a8}/.gitignore +0 -0
  15. {matelab_python_sdk-0.2.0a6 → matelab_python_sdk-0.2.0a8}/AGENTS.md +0 -0
  16. {matelab_python_sdk-0.2.0a6 → matelab_python_sdk-0.2.0a8}/CONTEXT.md +0 -0
  17. {matelab_python_sdk-0.2.0a6 → matelab_python_sdk-0.2.0a8}/LICENSE +0 -0
  18. {matelab_python_sdk-0.2.0a6 → matelab_python_sdk-0.2.0a8}/NOTICE +0 -0
  19. {matelab_python_sdk-0.2.0a6 → matelab_python_sdk-0.2.0a8}/README.md +0 -0
  20. {matelab_python_sdk-0.2.0a6 → matelab_python_sdk-0.2.0a8}/docs/agent-architecture.md +0 -0
  21. {matelab_python_sdk-0.2.0a6 → matelab_python_sdk-0.2.0a8}/docs/agent-workflows.md +0 -0
  22. {matelab_python_sdk-0.2.0a6 → matelab_python_sdk-0.2.0a8}/docs/development.md +0 -0
  23. {matelab_python_sdk-0.2.0a6 → matelab_python_sdk-0.2.0a8}/scripts/check_installed_package.py +0 -0
  24. {matelab_python_sdk-0.2.0a6 → matelab_python_sdk-0.2.0a8}/scripts/check_release.py +0 -0
  25. {matelab_python_sdk-0.2.0a6 → matelab_python_sdk-0.2.0a8}/scripts/generate_models.py +0 -0
  26. {matelab_python_sdk-0.2.0a6 → matelab_python_sdk-0.2.0a8}/src/matelab/__init__.py +0 -0
  27. {matelab_python_sdk-0.2.0a6 → matelab_python_sdk-0.2.0a8}/src/matelab/_generated/__init__.py +0 -0
  28. {matelab_python_sdk-0.2.0a6 → matelab_python_sdk-0.2.0a8}/src/matelab/_hashes.py +0 -0
  29. {matelab_python_sdk-0.2.0a6 → matelab_python_sdk-0.2.0a8}/src/matelab/_transport.py +0 -0
  30. {matelab_python_sdk-0.2.0a6 → matelab_python_sdk-0.2.0a8}/src/matelab/_wire_validation.py +0 -0
  31. {matelab_python_sdk-0.2.0a6 → matelab_python_sdk-0.2.0a8}/src/matelab/client.py +0 -0
  32. {matelab_python_sdk-0.2.0a6 → matelab_python_sdk-0.2.0a8}/src/matelab/domains/__init__.py +0 -0
  33. {matelab_python_sdk-0.2.0a6 → matelab_python_sdk-0.2.0a8}/src/matelab/domains/_attachment_content.py +0 -0
  34. {matelab_python_sdk-0.2.0a6 → matelab_python_sdk-0.2.0a8}/src/matelab/domains/cloud_drive.py +0 -0
  35. {matelab_python_sdk-0.2.0a6 → matelab_python_sdk-0.2.0a8}/src/matelab/domains/groups.py +0 -0
  36. {matelab_python_sdk-0.2.0a6 → matelab_python_sdk-0.2.0a8}/src/matelab/domains/literature.py +0 -0
  37. {matelab_python_sdk-0.2.0a6 → matelab_python_sdk-0.2.0a8}/src/matelab/domains/notebooks.py +0 -0
  38. {matelab_python_sdk-0.2.0a6 → matelab_python_sdk-0.2.0a8}/src/matelab/domains/records.py +0 -0
  39. {matelab_python_sdk-0.2.0a6 → matelab_python_sdk-0.2.0a8}/src/matelab/domains/templates.py +0 -0
  40. {matelab_python_sdk-0.2.0a6 → matelab_python_sdk-0.2.0a8}/src/matelab/domains/users.py +0 -0
  41. {matelab_python_sdk-0.2.0a6 → matelab_python_sdk-0.2.0a8}/src/matelab/errors.py +0 -0
  42. {matelab_python_sdk-0.2.0a6 → matelab_python_sdk-0.2.0a8}/src/matelab/py.typed +0 -0
  43. {matelab_python_sdk-0.2.0a6 → matelab_python_sdk-0.2.0a8}/src/matelab/session.py +0 -0
  44. {matelab_python_sdk-0.2.0a6 → matelab_python_sdk-0.2.0a8}/src/matelab/streaming.py +0 -0
  45. {matelab_python_sdk-0.2.0a6 → matelab_python_sdk-0.2.0a8}/tests/conftest.py +0 -0
  46. {matelab_python_sdk-0.2.0a6 → matelab_python_sdk-0.2.0a8}/tests/provider/test_provider_smoke.py +0 -0
  47. {matelab_python_sdk-0.2.0a6 → matelab_python_sdk-0.2.0a8}/tests/test_async_attachment_upload.py +0 -0
  48. {matelab_python_sdk-0.2.0a6 → matelab_python_sdk-0.2.0a8}/tests/test_client.py +0 -0
  49. {matelab_python_sdk-0.2.0a6 → matelab_python_sdk-0.2.0a8}/tests/test_cloud_drive.py +0 -0
  50. {matelab_python_sdk-0.2.0a6 → matelab_python_sdk-0.2.0a8}/tests/test_comments_and_downloads.py +0 -0
  51. {matelab_python_sdk-0.2.0a6 → matelab_python_sdk-0.2.0a8}/tests/test_consumer_smoke.py +0 -0
  52. {matelab_python_sdk-0.2.0a6 → matelab_python_sdk-0.2.0a8}/tests/test_errors.py +0 -0
  53. {matelab_python_sdk-0.2.0a6 → matelab_python_sdk-0.2.0a8}/tests/test_extended_records.py +0 -0
  54. {matelab_python_sdk-0.2.0a6 → matelab_python_sdk-0.2.0a8}/tests/test_generation.py +0 -0
  55. {matelab_python_sdk-0.2.0a6 → matelab_python_sdk-0.2.0a8}/tests/test_groups.py +0 -0
  56. {matelab_python_sdk-0.2.0a6 → matelab_python_sdk-0.2.0a8}/tests/test_notebook_lifecycle.py +0 -0
  57. {matelab_python_sdk-0.2.0a6 → matelab_python_sdk-0.2.0a8}/tests/test_notebooks.py +0 -0
  58. {matelab_python_sdk-0.2.0a6 → matelab_python_sdk-0.2.0a8}/tests/test_operation_coverage.py +0 -0
  59. {matelab_python_sdk-0.2.0a6 → matelab_python_sdk-0.2.0a8}/tests/test_record_lifecycle.py +0 -0
  60. {matelab_python_sdk-0.2.0a6 → matelab_python_sdk-0.2.0a8}/tests/test_record_mutations.py +0 -0
  61. {matelab_python_sdk-0.2.0a6 → matelab_python_sdk-0.2.0a8}/tests/test_records.py +0 -0
  62. {matelab_python_sdk-0.2.0a6 → matelab_python_sdk-0.2.0a8}/tests/test_release.py +0 -0
  63. {matelab_python_sdk-0.2.0a6 → matelab_python_sdk-0.2.0a8}/tests/test_streaming.py +0 -0
  64. {matelab_python_sdk-0.2.0a6 → matelab_python_sdk-0.2.0a8}/tests/test_template_lifecycle.py +0 -0
  65. {matelab_python_sdk-0.2.0a6 → matelab_python_sdk-0.2.0a8}/tests/test_templates.py +0 -0
  66. {matelab_python_sdk-0.2.0a6 → matelab_python_sdk-0.2.0a8}/tests/test_transport.py +0 -0
  67. {matelab_python_sdk-0.2.0a6 → matelab_python_sdk-0.2.0a8}/tests/test_users.py +0 -0
  68. {matelab_python_sdk-0.2.0a6 → matelab_python_sdk-0.2.0a8}/tests/typing/consumer.py +0 -0
  69. {matelab_python_sdk-0.2.0a6 → matelab_python_sdk-0.2.0a8}/tests/typing/pyproject.toml +0 -0
@@ -84,7 +84,7 @@ jobs:
84
84
  uv pip install \
85
85
  --python "${RUNNER_TEMP}/matelab-consumer/bin/python" \
86
86
  dist/release/*.whl \
87
- "basedpyright>=1.40.1" \
87
+ "basedpyright>=1.40.2" \
88
88
  "nodejs-wheel-binaries==22.20.0"
89
89
  "${RUNNER_TEMP}/matelab-consumer/bin/python" scripts/check_installed_package.py
90
90
  "${RUNNER_TEMP}/matelab-consumer/bin/basedpyright" \
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: matelab-python-sdk
3
- Version: 0.2.0a6
3
+ Version: 0.2.0a8
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
@@ -20,9 +20,9 @@ Classifier: Topic :: Scientific/Engineering
20
20
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
21
  Classifier: Typing :: Typed
22
22
  Requires-Python: >=3.11
23
- Requires-Dist: httpx2>=2.13.1
23
+ Requires-Dist: httpx2>=2.16.0
24
24
  Requires-Dist: jsonschema>=4.26.0
25
- Requires-Dist: pydantic>=2.13.5
25
+ Requires-Dist: pydantic>=2.14.1
26
26
  Requires-Dist: typing-extensions>=4.16.0
27
27
  Description-Content-Type: text/markdown
28
28
 
@@ -2,12 +2,12 @@
2
2
  "schema_version": 1,
3
3
  "contract_line": "integration-v1",
4
4
  "release": {
5
- "tag": "v2026.10.3",
6
- "commit": "c668cff75b7fcedbee048a8f1c8792162bf4eb77"
5
+ "tag": "v2026.10.4",
6
+ "commit": "c422f4f7425ade81eb5a65d9448ca4ea8bcacc79"
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": "757017be241f1014f3b5966c2a0f89a4ef578a700ebd626c51d02a8ff92a6b2a"
11
+ "sha256": "1bc28a61647b7b8f38b86c8ea0085415616857f967c997031794a88deb2e377c"
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.3"
4
+ version: "2026.10.4"
5
5
  description: |
6
6
  Current PHP wire contract for generating SDKs against the selected
7
7
  `matelab-server` Provider version.
@@ -13,7 +13,7 @@ info:
13
13
  x-contract-line: integration-v1
14
14
  x-provider-evidence:
15
15
  repository: /home/tnzhu/projects/matelab-server
16
- commit: b70f590571b860f043365c9f2d6f95818d595862
16
+ commit: 161c4560ae1cb6267d46841d6fe8adc8270ee1bc
17
17
  verificationRuntime:
18
18
  baseUrl: http://127.0.0.1:18080/api
19
19
  container: matelab-test-server-1
@@ -56,9 +56,9 @@ info:
56
56
  - sql/matelab.sql
57
57
  - sql/recommend.sql
58
58
  x-provider-audit:
59
- currentCommit: b70f590571b860f043365c9f2d6f95818d595862
60
- previousReleaseEvidenceCommit: 3f39d47b7ad43bed618b029a7d9cc51fab852eb6
61
- lastProviderVerifiedCommit: b70f590571b860f043365c9f2d6f95818d595862
59
+ currentCommit: 161c4560ae1cb6267d46841d6fe8adc8270ee1bc
60
+ previousReleaseEvidenceCommit: b70f590571b860f043365c9f2d6f95818d595862
61
+ lastProviderVerifiedCommit: 161c4560ae1cb6267d46841d6fe8adc8270ee1bc
62
62
  contractStatus: current-provider-aligned
63
63
  # Summary of changes adopted by current Supported Routes since the last
64
64
  # released contract, including contract corrections. The Provider diff is
@@ -66,29 +66,8 @@ info:
66
66
  # updates against that fixed release baseline; use [] if no relevant changes.
67
67
  # Entries are not per-change verification results or a cumulative history.
68
68
  incorporatedChanges:
69
- - {operationId: stageTemporaryFile, change: byte-offset-continuation-and-45-character-session-limit}
70
- - {operationId: stageTemporaryFile, change: abort-only-cleans-incomplete-bytes-and-workerman-errors-have-no-resume-anchor}
71
- - {operationId: uploadRecordAttachment, change: single-fragment-hash-failure-rollback}
72
- - {operationId: searchUsers, change: utf8-byte-minimum-without-a-keyword-length-cap-or-whitespace-filter}
73
- - {operationId: listNotebookRecords, change: owner-id-or-email-selector-and-caller-defaults}
74
- - {operationId: createBlankRecord, change: owner-id-or-email-selector-and-php-zero-title-rejection}
75
- - {operationId: importRecordDataset, change: owner-and-template-owner-selectors-json-list-data-and-php-zero-string-rejection}
76
- - {operationId: updateRecordData, change: owner-selector-numeric-extraction-selectors-and-module-specific-field-validation}
77
- - {operationId: deleteRecordsByUid, change: owner-id-or-email-selector-and-caller-defaults}
78
- - {operationId: saveNotebookMetadata, change: numeric-string-and-number-extraction-selectors}
79
- - {operationId: searchRecordData, change: numeric-string-and-number-extraction-selectors}
80
- - {operationId: addNotebookShare, change: provider-deduplicated-recipient-lists}
81
- - {operationId: shareMultipleTemplatesWithUsers, change: list-limits-apply-after-provider-deduplication}
82
- - {operationId: listLiteratureItems, change: duplicate-source-rating-and-keyword-filters-are-accepted}
83
- - {operationId: shareLiteratureItems, change: provider-deduplicated-ids-and-optional-empty-recipient-list}
84
- - {operationId: moveCloudDriveFiles, change: provider-deduplicated-file-ids}
85
- - {operationId: deleteCloudDriveFiles, change: provider-deduplicated-file-ids}
86
- - {operationId: listDeletedRecordPage, change: unpaged-zero-page-size-omits-count-and-page}
87
- - {operationId: extractLiteratureMetadata, change: nonempty-doi-takes-precedence-over-staged-file-id}
88
- - {operationId: saveTemplateContent, change: php-name-validation-and-json-body-checks-apply-only-on-write}
89
- - {operationId: listTemplates, change: stored-keywords-accept-nonnull-json-values}
90
- - {operationId: readTemplateContent, change: stored-module-names-and-language-are-returned-without-template-write-validation}
91
- - {operationId: readRecord, change: record-update-names-and-stored-module-language-use-raw-response-types}
69
+ - {operationId: stageTemporaryFile, change: filename-extension-selects-continuation-bytes-and-abort-cleans-incomplete-extension-variants}
70
+ - {operationId: uploadRecordAttachment, change: temporary-bytes-retain-sanitized-source-extension}
92
71
  x-consumer-evidence:
93
72
  - repository: /home/tnzhu/projects/matelab-mcp
94
73
  commit: 94a129b2df0ef0fbcf179c10550fb7c9a5dd2e6f
@@ -109,7 +88,7 @@ info:
109
88
  identity must come from token issuance. ELN-group and notebook-list
110
89
  calls still provide Consumer evidence for their canonical GET forms.
111
90
  - repository: /home/tnzhu/projects/matelab-front
112
- commit: c6a2fef86ef215256556d340b964f56dded7d3ce
91
+ commit: 89332767c2f20de210baed1e1e42df56e7071ad6
113
92
  files:
114
93
  - src/App.vue
115
94
  - src/fn/index.js
@@ -1993,7 +1972,10 @@ paths:
1993
1972
  contracted abort, status, expiration, or idempotent retry. A hash
1994
1973
  mismatch in this one-fragment flow rolls back the temporary row and
1995
1974
  new bytes. Errors after import/update writing starts still do not prove
1996
- whether the stage was consumed. Raw identifiers identify the staging row;
1975
+ whether the stage was consumed. Temporary bytes retain the multipart
1976
+ filename's extension after removing non-ASCII-alphanumeric characters;
1977
+ the response still reports the original source filename.
1978
+ Raw identifiers identify the staging row;
1997
1979
  their filesystem components are sanitized and can alias (PVD-051).
1998
1980
  x-known-provider-issues: [PVD-014, PVD-051]
1999
1981
  security: [{bearerAuth: []}]
@@ -3703,11 +3685,18 @@ paths:
3703
3685
  successful response's `offset`. Omitting it defaults to zero and
3704
3686
  restarts incomplete bytes rather than appending. The final fragment
3705
3687
  returns the combined SHA-256, size, source filename, temporary row id,
3706
- and legacy preview query.
3688
+ and legacy preview query. The multipart filename's final extension,
3689
+ stripped of characters outside `[A-Za-z0-9]` with case preserved,
3690
+ also selects the temporary bytes. Repeat that extension on every
3691
+ fragment; changing it cannot continue the previous bytes and returns
3692
+ code 2 when the requested offset has no matching file. A different
3693
+ filename stem with the same sanitized extension can continue them.
3707
3694
 
3708
3695
  `abort=1` is the compensating cleanup operation. It requires the same
3709
- `uid` and `tempid`, omits `file`, and removes incomplete bytes only.
3710
- It preserves an already completed staging row and its bytes. Both
3696
+ `uid` and `tempid` and omits `file`. If no completed row exists for
3697
+ those identifiers, it removes incomplete bytes for all extension
3698
+ variants, including the extensionless file. If a completed row exists,
3699
+ it preserves the row and bytes without cleaning any variants. Both
3711
3700
  states return the standard HTTP 200 code-2 error envelope ("upload
3712
3701
  cancelled"); that response does not prove completed staging was deleted.
3713
3702
  Complete uploads must be consumed by their intended finalizer; this
@@ -7450,7 +7439,10 @@ components:
7450
7439
  file:
7451
7440
  type: string
7452
7441
  format: binary
7453
- description: Required for upload branches and omitted for abort.
7442
+ description: |
7443
+ Required for upload branches and omitted for abort. The multipart
7444
+ filename's sanitized final extension selects the temporary bytes;
7445
+ keep that extension unchanged throughout continuation.
7454
7446
  allOf:
7455
7447
  - if:
7456
7448
  required: [abort]
@@ -5,15 +5,15 @@ 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.3` Contract Release. The sole pin,
8
+ The SDK is pinned to the immutable `matelab-spec v2026.10.4` Contract Release. The sole pin,
9
9
  [contracts/matelab-integration-v1.lock.json](../contracts/matelab-integration-v1.lock.json), records peeled commit
10
- `c668cff75b7fcedbee048a8f1c8792162bf4eb77`, source `openapi/matelab-integration-v1.openapi.yaml`, the local snapshot path,
11
- and SHA-256 `757017be241f1014f3b5966c2a0f89a4ef578a700ebd626c51d02a8ff92a6b2a`. The annotated tag object is
12
- `b1d3ff039c576032f69ec7116347af1ba441ba5f`.
10
+ `c422f4f7425ade81eb5a65d9448ca4ea8bcacc79`, source `openapi/matelab-integration-v1.openapi.yaml`, the local snapshot path,
11
+ and SHA-256 `1bc28a61647b7b8f38b86c8ea0085415616857f967c997031794a88deb2e377c`. The annotated tag object is
12
+ `72c52e2267598f9bde820f629bc62d5982f7081c`.
13
13
 
14
- The Contract uses Provider source evidence from `b70f590571b860f043365c9f2d6f95818d595862`; v2026.10.3 validation ran
14
+ The Contract uses Provider source evidence from `161c4560ae1cb6267d46841d6fe8adc8270ee1bc`; v2026.10.4 validation ran
15
15
  against an isolated test instance and does not establish production behavior. The Front Consumer evidence
16
- `c6a2fef86ef215256556d340b964f56dded7d3ce` informs cross-Consumer capability review but does not define the SDK interface.
16
+ `89332767c2f20de210baed1e1e42df56e7071ad6` informs cross-Consumer capability review but does not define the SDK interface.
17
17
 
18
18
  This snapshot covers the Workerman runtime with the `localhost` data-server branch for ELN records and attachments;
19
19
  remote data-server forwarding is outside its verified callable scope (PVD-049).
@@ -198,7 +198,8 @@ objects are outside the interface and are also rejected at runtime before a Prov
198
198
  checksum, or multipart content type. The SDK calculates the SHA-256 itself; streams are fully consumed into a
199
199
  `SpooledTemporaryFile`, rewound, and only then sent to the Provider. Temporary-file write, seek, read, and close
200
200
  operations run outside the event-loop thread, including after the spool rolls to disk. The Contract fixes attachment
201
- parts to `application/octet-stream`.
201
+ parts to `application/octet-stream`. Temporary record-attachment bytes retain the source filename's final extension,
202
+ stripped to ASCII letters and digits with case preserved; the response retains the original source filename.
202
203
 
203
204
  An exception raised by an async source is propagated unchanged after the SDK closes its temporary file. The SDK reads
204
205
  streams through end-of-file and imposes no upload-size policy; callers remain responsible for limits such as an HTTP
@@ -692,19 +693,25 @@ assert isinstance(staged, StagedFile)
692
693
  The SDK derives `fragment_size` and computes the complete SHA-256 for a single `bytes` fragment. File-like content
693
694
  must declare its fragment size. For multiple fragments, pass the returned `StagedUploadSession` into the next call and
694
695
  supply `complete_sha256` on the final call. `next_offset` is the Provider-acknowledged byte offset, checked against the
695
- declared fragment size and sent with the next fragment. The SDK verifies the returned filename and total size, and
696
- compares hashes when a local expected hash is available. A final result contains the Provider hash, temporary row
696
+ declared fragment size and sent with the next fragment. Repeat the filename's final extension across fragments:
697
+ the Provider removes characters outside `[A-Za-z0-9]` and preserves case, so `paper.P-d_f` and `renamed.Pdf` select
698
+ the same bytes. Changing that sanitized extension cannot resume the upload and returns code 2 when no matching
699
+ file exists at the requested offset. The SDK sends each filename unchanged, checks the returned filename and total
700
+ size, and compares hashes when a local expected hash is available. A final result contains the Provider hash, temporary row
697
701
  identity and direct `binding`; it no longer embeds resumable session state. The temporary Provider URL and upload-session
698
702
  identity needed by internal binding and cleanup remain hidden. Callers do not need to
699
703
  splice either into content. The result does not claim that a later literature/cloud operation checks the uploader or
700
704
  consumes the file exactly once. `uploads.abort()` accepts either an active `StagedUploadSession` or a completed
701
- `StagedFile` and returns `None` for the Provider's code-2 cancellation signal. It removes incomplete bytes only;
702
- completed staging rows and bytes remain and must be consumed by their intended finalizer (PVD-052).
705
+ `StagedFile` and returns `None` for the Provider's code-2 cancellation signal. Without a completed staging row, it
706
+ removes all incomplete extension variants, including extensionless bytes. A completed row prevents any cleanup for
707
+ that identity; completed staging must be consumed by its intended finalizer (PVD-052).
703
708
  Binding UIDs allow up to 100 characters; upload-session IDs allow up to 45 (PVD-024). Use fresh identities: different
704
709
  raw identifiers can alias after Provider filename sanitization (PVD-051). Workerman upload errors supply no usable
705
710
  resume offset (PVD-050). After a failed non-final fragment, cancel incomplete bytes and restart with fresh identities.
706
711
  An ambiguous final response can leave completed staging that `abort()` cannot delete.
707
712
  Staging mutations are never automatically retried.
713
+ After upgrading from the previous Provider's extensionless storage, restart old incomplete uploads from offset zero
714
+ or cancel them and use fresh session identities.
708
715
 
709
716
  Literature identities distinguish the personal library from shared libraries:
710
717
 
@@ -3,7 +3,7 @@
3
3
  schema_version: 1
4
4
  contract:
5
5
  line: integration-v1
6
- release: v2026.10.3
6
+ release: v2026.10.4
7
7
  operation_count: 71
8
8
 
9
9
  operations:
@@ -1,10 +1,10 @@
1
1
  [build-system]
2
- requires = ["hatchling>=1.32.4"]
2
+ requires = ["hatchling>=1.33.0"]
3
3
  build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "matelab-python-sdk"
7
- version = "0.2.0a6"
7
+ version = "0.2.0a8"
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"]
@@ -12,9 +12,9 @@ authors = [
12
12
  { name = "CMPDC, Institute of Physics, Chinese Academy of Sciences" },
13
13
  ]
14
14
  dependencies = [
15
- "httpx2>=2.13.1",
15
+ "httpx2>=2.16.0",
16
16
  "jsonschema>=4.26.0",
17
- "pydantic>=2.13.5",
17
+ "pydantic>=2.14.1",
18
18
  "typing-extensions>=4.16.0",
19
19
  ]
20
20
  readme = "README.md"
@@ -37,14 +37,14 @@ classifiers = [
37
37
 
38
38
  [dependency-groups]
39
39
  dev = [
40
- "basedpyright>=1.40.1",
40
+ "basedpyright>=1.40.2",
41
41
  "nodejs-wheel-binaries==22.20.0",
42
- "datamodel-code-generator>=0.83.0",
43
- "hatchling>=1.32.4",
44
- "pytest>=9.1.1",
45
- "pytest-asyncio>=1.4.0",
42
+ "datamodel-code-generator>=0.87.0",
43
+ "hatchling>=1.33.0",
44
+ "pytest>=9.2.0",
45
+ "pytest-asyncio>=1.4.1",
46
46
  "pyyaml>=6.0.3",
47
- "ruff>=0.16.10",
47
+ "ruff>=0.17.3",
48
48
  ]
49
49
 
50
50
  [tool.ruff]
@@ -65,6 +65,7 @@ reportIncompatibleUnannotatedOverride = "error"
65
65
  markers = [
66
66
  "provider: opt-in smoke tests against an explicitly acknowledged real Provider target",
67
67
  ]
68
+ asyncio_default_fixture_loop_scope = "function"
68
69
 
69
70
  [tool.hatch.build.targets.wheel]
70
71
  packages = ["src/matelab"]
@@ -2,7 +2,7 @@
2
2
  #
3
3
  # @generated by datamodel-codegen:
4
4
  # filename: openapi.yaml
5
- # version: 0.83.0
5
+ # version: 0.87.0
6
6
 
7
7
  from enum import StrEnum
8
8
  from typing import Annotated, Any, Literal
@@ -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.3"
14
+ OPENAPI_INFO_VERSION = "2026.10.4"
15
15
 
16
16
 
17
17
  class ResponseEnvelope(WireModel):
@@ -2759,7 +2759,10 @@ class StageTemporaryFileRequest(WireModel):
2759
2759
  """
2760
2760
  file: bytes | None = None
2761
2761
  """
2762
- Required for upload branches and omitted for abort.
2762
+ Required for upload branches and omitted for abort. The multipart
2763
+ filename's sanitized final extension selects the temporary bytes;
2764
+ keep that extension unchanged throughout continuation.
2765
+
2763
2766
  """
2764
2767
 
2765
2768
 
@@ -79,6 +79,7 @@ class Uploads:
79
79
  complete_sha256: str | None = None,
80
80
  content_type: str = "application/octet-stream",
81
81
  ) -> StagedUploadSession | StagedFile:
82
+ """Stage bytes; preserve the sanitized, case-sensitive filename extension when resuming."""
82
83
  if session is None:
83
84
  session = StagedUploadSession(
84
85
  binding=UploadBindingRef.new(), upload_session_id=f"tmp-{secrets.token_urlsafe(24)}"
@@ -162,7 +163,7 @@ class Uploads:
162
163
  )
163
164
 
164
165
  async def abort(self, upload: StagedUploadSession | StagedFile) -> None:
165
- """Cancel incomplete bytes; completed files remain staged (PVD-052)."""
166
+ """Cancel incomplete extension variants; completed staging identities remain untouched (PVD-052)."""
166
167
  if isinstance(upload, StagedFile):
167
168
  # The completed file keeps this Provider identity private except for same-module cleanup.
168
169
  upload_session_id = upload._upload_session_id # pyright: ignore[reportPrivateUsage]
@@ -208,8 +208,14 @@ async def test_stage_single_bytes_creates_session_and_computes_hash(
208
208
 
209
209
 
210
210
  @pytest.mark.asyncio
211
+ @pytest.mark.parametrize(
212
+ ("first_filename", "final_filename"), [("paper.pdf", "paper.pdf"), ("paper.P-d_f", "renamed.Pdf")]
213
+ )
211
214
  async def test_stage_fragments_sends_acknowledged_offset_and_maps_completion_and_abort(
212
- provider_session: Session, provider_envelope: Callable[..., dict[str, object]]
215
+ provider_session: Session,
216
+ provider_envelope: Callable[..., dict[str, object]],
217
+ first_filename: str,
218
+ final_filename: str,
213
219
  ) -> None:
214
220
  upload_session = StagedUploadSession(
215
221
  binding=UploadBindingRef(uid="lit-binding-123"), upload_session_id="lit-session-123"
@@ -225,11 +231,13 @@ async def test_stage_fragments_sends_acknowledged_offset_and_maps_completion_and
225
231
  assert _multipart_value(request, "uid") == "lit-binding-123"
226
232
  assert _multipart_value(request, "tempid") == "lit-session-123"
227
233
  if request_count == 1:
234
+ assert f'filename="{first_filename}"'.encode() in request.content
228
235
  assert _multipart_value(request, "offset") == "0"
229
236
  assert _multipart_value(request, "last") == "0"
230
237
  assert _multipart_value(request, "file") == _CHUNK_1.decode()
231
238
  return Response(200, json=provider_envelope(hash=_HASH_1, tempid="lit-session-123", offset=len(_CHUNK_1)))
232
239
  if request_count == 2:
240
+ assert f'filename="{final_filename}"'.encode() in request.content
233
241
  assert _multipart_value(request, "offset") == str(len(_CHUNK_1))
234
242
  assert _multipart_value(request, "last") == "1"
235
243
  assert _multipart_value(request, "hash") == _HASH_FULL
@@ -239,9 +247,9 @@ async def test_stage_fragments_sends_acknowledged_offset_and_maps_completion_and
239
247
  json=provider_envelope(
240
248
  id=412,
241
249
  path="url_eln/temp",
242
- query=f"h={_HASH_FULL}&id=412&s={len(_FULL_CONTENT)}&f=paper.pdf",
250
+ query=f"h={_HASH_FULL}&id=412&s={len(_FULL_CONTENT)}&f={final_filename}",
243
251
  name="lit-session-123",
244
- filename="paper.pdf",
252
+ filename=final_filename,
245
253
  size=len(_FULL_CONTENT),
246
254
  hash=_HASH_FULL,
247
255
  future_field=True,
@@ -254,12 +262,12 @@ async def test_stage_fragments_sends_acknowledged_offset_and_maps_completion_and
254
262
  async with AsyncClient(base_url="https://matelab.example/api", transport=MockTransport(handler)) as http_client:
255
263
  client = AsyncMatelab("https://ignored.example", session=provider_session, http_client=http_client)
256
264
  progress = await client.uploads.stage(
257
- _CHUNK_1, filename="paper.pdf", fragment_size=len(_CHUNK_1), session=upload_session, final=False
265
+ _CHUNK_1, filename=first_filename, fragment_size=len(_CHUNK_1), session=upload_session, final=False
258
266
  )
259
267
  assert isinstance(progress, StagedUploadSession)
260
268
  completed = await client.uploads.stage(
261
269
  _CHUNK_2,
262
- filename="paper.pdf",
270
+ filename=final_filename,
263
271
  fragment_size=len(_CHUNK_2),
264
272
  session=progress,
265
273
  final=True,