Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

matelab-python-sdk

Reusable async Python client for the Matelab Integration Contract.

The current alpha is 0.1.0a6. [project].version in pyproject.toml is the sole SDK version source; uv.lock only mirrors that source.

The SDK is pinned to the immutable matelab-spec v0.3.0 Contract Release. The sole release pin is contracts/matelab-integration-v1.lock.json, which records the source tag, commit, OpenAPI path, local snapshot path, and SHA-256.

Installation

Python 3.11 or newer is required. Install the alpha from a package index with either:

uv add matelab-python-sdk
python -m pip install matelab-python-sdk

Development installs use the locked checkout:

uv sync --frozen

To test the same artifact a downstream Consumer will install, build and install the wheel:

uv build --no-build-isolation --out-dir dist/release
python -m pip install dist/release/matelab_python_sdk-0.1.0a6-py3-none-any.whl

Do not infer Provider compatibility from the SDK version alone. A release is also bound to the Contract tag, commit, and checksum recorded below.

Design

The public module is intentionally small:

from matelab import AsyncMatelab

async with AsyncMatelab() as client:
    session = await client.authenticate("user@example.org", "password")
    assert client.session is session
    notebooks = await client.notebooks.list()
    notebook = notebooks.owned[0].ref
    records = await client.records.list(notebook=notebook)
    record = await client.records.read(notebook=notebook, record=records.records[0].ref)

AsyncMatelab() uses https://matelab.iphy.ac.cn/api by default. Pass another Provider API root explicitly when needed, for example AsyncMatelab("https://custom.example/api").

Error handling

Catch MatelabError for one application-level fallback, or a specific subclass when recovery differs:

Error Meaning and normal response
MatelabUsageError The call cannot be represented safely; correct its arguments.
MatelabAuthenticationError The Session is missing, expired, invalid, or rejected; refresh or authenticate again as appropriate.
MatelabProviderError The Provider rejected a valid request with a business error; inspect code and do not assume a mutation was applied.
MatelabTransportError The HTTP exchange failed; status_code is present for HTTP failures, and a mutation outcome may be unknown.
MatelabProtocolError The Provider response does not match the pinned Contract; treat it as Provider drift or an SDK defect. A mutation may already have been applied.

code and status_code are the stable scalar diagnostics. Provider response bodies and caller inputs are never attached to exceptions. Never automatically retry a mutation solely because it raised a transport or protocol error.

Session ownership

Each AsyncMatelab instance owns at most one current, process-local Session. The SDK injects its bearer token, refreshes it under a per-instance async lock, performs bounded safe retries, and exposes every token rotation through client.session. If refresh succeeds but the subsequent business request fails, client.session still contains the refreshed token pair.

SDK responsibility Integrator responsibility
Bearer injection, expiry checks, refresh and bounded retry Redis/database/file persistence and encryption
Per-instance, in-process refresh serialization Cross-process locking and conflict handling
Contract validation of token and identity responses Mapping userid/session_id to a persisted Session
Latest immutable Session through client.session Revocation, cleanup, and saving after each call

Session, Token, and Identity are frozen Pydantic models and form the stable, normalized SDK Session contract. Their JSON fields are access, refresh, and identity; tokens contain value and expires_at_ms, while identity contains userid, username, and email. Provider envelope fields are not part of this model. An Identity requires a positive userid and non-empty username; its email may be None.

Token values are excluded from model representations but intentionally remain present in model_dump() and model_dump_json() so an integration can persist and restore the complete Session:

serialized = session.model_dump_json()
restored = Session.model_validate_json(serialized)
assert restored == session

The serialized result contains live credentials. Encrypt it at rest and never write it to logs or send it to an untrusted party. The SDK does not read tokens from environment variables and does not provide a session store.

Credential authentication installs the returned Session on the client:

async with AsyncMatelab() as client:
    session = await client.authenticate(username, password)
    assert client.session is session

Restore a previously validated Session by passing it to the constructor. Construction performs no network request:

persisted_session = await session_store.load(userid, session_id)

async with AsyncMatelab(session=persisted_session) as client:
    result = await handle_request(client)

Every Session contains a validated identity, and refreshing a restored Session preserves it. Integrations that construct a Session from an external assertion are responsible for validating that assertion and the identity-token association before passing the complete Session to the SDK; the SDK does not accept bare external token pairs.

A Web or MCP integration should create one client for one logical session, then save the latest Session in finally, including when a business call fails after refresh:

persisted_session = await session_store.load(userid, session_id)
client = AsyncMatelab(session=persisted_session, http_client=shared_http_client)

try:
    result = await handle_request(client)
finally:
    latest_session = client.session
    try:
        if latest_session is not None:
            await session_store.save(userid, session_id, latest_session)
    finally:
        await client.aclose()

If several processes can use the same persisted session, the integration must place its own distributed lock around load, use, and save. The SDK lock only coordinates refreshes inside one AsyncMatelab instance.

Different logical sessions require different clients. They may reuse the same externally managed HTTP connection pool, but must never share one global AsyncMatelab singleton:

alice_client = AsyncMatelab(session=alice_session, http_client=shared_http_client)
bob_client = AsyncMatelab(session=bob_session, http_client=shared_http_client)

An injected http_client must be an httpx2.AsyncClient. The independently distributed httpx.AsyncClient has similar methods but uses incompatible request, response, transport, and exception types.

src/matelab/_generated is a private wire layer. Applications should not depend on its file layout or generated class names. The distribution includes py.typed, so type checkers can consume the public annotations directly from an installed wheel.

The current public domain scope includes authentication, group/user discovery, template and notebook lifecycle operations, record discovery/lifecycle operations, comment reads, and streaming record or comment attachment downloads, resumable file staging, literature discovery/lifecycle workflows, and personal cloud-drive management.

Staging a record attachment before record creation

records.stage_attachment supports the Contract's pre-upload workflow without inventing a target record UID. The returned StagedNotebookAttachment is scoped by the SDK to the resolved authenticated user and the exact notebook selector used for upload:

import hashlib

from matelab import RecordImportItem

content = b"measurement data"
staged = await client.records.stage_attachment(
    notebook=notebook,
    filename="measurement.csv",
    content=content,
    size=len(content),
    sha256=hashlib.sha256(content).hexdigest(),
)
result = await client.records.import_dataset(
    notebook=notebook,
    template_title="Example Template",
    items=(
        RecordImportItem(
            record_uid="REC-IMPORT-001", title="Imported measurement", data={"Attachments": {"File": [staged]}}
        ),
    ),
)

The same staged handle may instead be consumed by one safe update finalizer. For example, add a new file field to an existing form module:

from matelab import RecordFormAttachmentFieldAddition

# Alternative to the import above; do not run both with the same staged handle.
result = await client.records.update(
    source,
    attachment_changes=(
        RecordFormAttachmentFieldAddition(module="Attachments", name="Measurement", attachment=staged),
    ),
)

Choose exactly one finalizer. A staged name may occur once in either a single-record import or one update operation; do not reuse it after transport starts, even when the Provider outcome is unknown. A locally rejected target or argument does not spend the handle. The SDK rejects raw Provider attachment references, cross-user or cross-notebook handles, unsafe combinations, duplicate use in one request, and a second finalization attempt through the same client.

The Provider supplies no staging status, abort, TTL, atomicity, idempotency, or retry guarantee. Integrations that persist handles must durably claim their own uploaded -> finalizing/indeterminate transition before calling a finalizer.

Attachment-bearing record update boundary

The v0.1.2 Contract adds dedicated, verified notebook-staged update shapes. They remain public intent objects; callers never construct Provider paths or attachment strings:

from matelab import RecordFilesAttachmentRootAppend, RecordTableFileCellSet, RecordTableRowAttachmentAppend

# Each example is a separate finalizer; never run several with the same handle.
await client.records.update(
    source,
    attachment_changes=(RecordTableFileCellSet(table="Measurements", column="Evidence", row=0, attachment=staged),),
)

await client.records.update(
    source,
    attachment_changes=(
        RecordTableRowAttachmentAppend(
            table="Measurements", file_column="Evidence", values={"Label": "Sample C"}, attachment=another_staged
        ),
    ),
)

await client.records.update(
    source,
    attachment_changes=(RecordFilesAttachmentRootAppend(module="Files", caption="Evidence", attachment=third_staged),),
)

RecordTableFileCellSet requires an existing file column and a cell whose immediate canonical value is exactly null. The row-append intent addresses the new row by the row count from the SDK's immediate read and supports one staged file column. Files append is root-only and requires a string caption.

Replacement starts with an occurrence returned by records.read(); applications must not fabricate a RecordAttachmentRef:

from matelab import RecordFilesAttachmentReplacement

record = await client.records.read(notebook=source.notebook, record=source.record)
existing = next(
    attachment
    for attachment in record.attachments
    if attachment.location is not None and attachment.location.kind == "files_module"
)
await client.records.update(
    source, attachment_changes=(RecordFilesAttachmentReplacement(existing=existing, replacement=staged, caption=None),)
)

For a files occurrence, caption=None preserves the observed string caption (an observed null caption normalizes to the required empty string). The update preserves the Provider uid and folder in the submitted content. Table replacement requires exactly one current attachment in the selected cell and uses RecordTableFileAttachmentReplacement. Both replacement forms require the new hash to differ.

Row/index-based finalizers use an immediate SDK read and must not be called while a concurrent editor is known to be active. The Provider offers no expected hash or revision. expected_content_sha256 is only a client-side prewrite check, not Provider compare-and-swap.

File-bearing structure changes do not consume staging:

from matelab import RecordFormFileFieldDeletion, RecordTableFileColumnAddition, RecordTableFileColumnDeletion

await client.records.update(
    source,
    file_structure_changes=(
        RecordTableFileColumnAddition(table="Measurements", name="Additional evidence"),
        RecordFormFileFieldDeletion(form="Attachments", name="Obsolete evidence"),
        RecordTableFileColumnDeletion(table="Measurements", name="Old evidence"),
    ),
)

The SDK emits the v0.1.2 dedicated operations: a new file column omits wire data; whole form-field/table-column deletion uses a strict two-segment target. It does not expose a three-segment delete as table-cell clearing.

Deleting a complete canonical module is also supported even when the current read observes attachments in it:

result = await client.records.update(
    source, module_deletions=("Raw files",), expected_content_sha256=record.content_sha256
)

This requests only a canonical record-content mutation. A successful response acknowledges the submitted mutation; it does not prove attachment-quote cleanup or byte deletion. Collaboration-pending is reported as "pending_browser_save", not persisted.

Notebook-staged rich-text binding remains unsupported. Provider Verification shows that #file{name} is stored as plain text and that a hash-based URI can fall back to an existing quote, so neither is a one-shot staged finalizer. Existing RecordRichTextUpdate remains legal only with record-scoped StagedRecordAttachment. Attachment-bearing multi-record import is also forbidden; callers must split it into single-record finalizers.

Implementation roadmap

docs/roadmap.md is the complete SDK-only execution plan. It assigns all 71 matelab-spec v0.3.0 operations to ordered work packages, defines the machine-readable coverage that must be added, records Provider-risk gates, and specifies the final completion checks.

The SDK tracks all 71 Contract operations and exposes 70 through public domain interfaces; one identity-bootstrap operation is intentionally unexposed. It deliberately excludes MCP migration, adjacent-repository changes, external publishing, and automatic mutation against a real Provider.

Machine-readable status lives in docs/operation-coverage.yaml. An exact-coverage test keeps its 71 operation IDs, methods, paths, work packages, and Provider issue references aligned with the pinned OpenAPI snapshot.

Domain Implemented Planned Current public surface
Authentication 3 0 authenticate, refresh, exchange_chat_sso_code; identity bootstrap intentionally unexposed
Groups and users 2 0 groups.list, users.search
Notebooks 6 0 notebooks.list/create/update/shares/share/update_share/unshare
Records 18 0 Discovery, reads, lifecycle, typed patch/attachments, relations, and downloads
Comments 5 0 Read, staged attachment upload, create/update/delete, and download
Templates 13 0 Discovery, content, lifecycle, sharing, groups, and marketplace
File staging 1 0 Resumable fragment staging and compensating abort request
Literature 13 0 Libraries, items, canonical metadata, comments, sharing, PDF lifecycle and streaming
Cloud drive 9 0 Personal root/folders/files, staged binding, metadata, move/delete and streaming
Total 70 0 One operation is intentionally unexposed

Stability and known capability limits

Coverage currently contains 14 stable, 56 experimental, and one not_applicable operation. The stable operation IDs are loginTokenSet, refreshTokenSet, exchangeChatSsoCode, shareMultipleTemplatesWithUsers, removeTemplateFromGroup, deleteNotebookShare, listNotebooks, listNotebookRecords, exportRecords, deleteRecordsByUid, copyRecord, readRecord, deletePersonalLiteratureItem, and readLiteratureCreateTemplate.

Every other implemented operation is explicitly experimental; the exact per-operation list and its PVD/PCG references live in docs/operation-coverage.yaml. resolveCurrentIdentity is intentionally unexposed because the SDK accepts only complete, integration-validated Sessions and does not bind bare external token pairs. There are no planned operations. Experimental support means the SDK validates and exposes the pinned Contract while preserving limitations such as unstable ordering/pagination, incomplete mutation acknowledgements, missing batch atomicity or idempotency, weak attachment ownership binding, and known Provider authorization gaps. It does not turn those limitations into SDK guarantees.

Chat iframe SSO consumes a one-time code and shared key. Both arguments are treated as secrets, the request is never automatically retried, and the returned token set is stored in the same in-memory Session shape as credential login:

session = await client.exchange_chat_sso_code(code="chat-sanitizedcode123", key="sanitized-shared-key")

Group and user discovery expose sharing identities without inventing Provider pagination:

groups = await client.groups.list()
targets = await client.users.search("Example Researcher", global_scope=False)

Ordering remains Provider-unspecified and is documented rather than repeated as a constant result field. Group members belong only to groups.members_for, not to every returned group. These two discovery interfaces are experimental because the Provider returns members for an unstable first group and user search is unpaged, unordered, and not field-minimized (PVD-006, PVD-029, PCG-011).

Notebook create/update and direct sharing use the Provider acknowledgement without an automatic follow-up read:

await client.notebooks.create(title="Example Notebook")
shares = await client.notebooks.shares(notebook)
await client.notebooks.share(notebook, [target.ref])
await client.notebooks.update_share(shares.shares[0].ref, write=True, create=True)

Create, update, share, permission update, and unshare return None because their Provider responses contain no new resource representation. Call list() or shares() explicitly when the application needs current state. A stored share mask of zero still has effective read access (PVD-010), and share-list order remains unspecified.

Template discovery keeps a template database identity separate from direct-share, market-acquisition, and group relation identities:

templates = await client.templates.list()
market = await client.templates.search_market("calibration", page=1, page_size=20)
modules = await client.templates.read(templates.owned[0].ref)

The market result reports the Provider total_count, the requested and effective page sizes, and a has_more value derived from the total; it does not claim a stable order or continuation token. Canonical modules are mapped to public TemplateModule values and retain additive module attributes. Template reads remain experimental because Provider discovery ordering/pagination and historical images compatibility are not fully stable (PCG-003, PCG-009, PVD-013, PVD-022).

Template writes remain separate operations: metadata, canonical modules, and usage HTML are not presented as one transaction. Metadata create/update returns the TemplateRef built from the Provider template ID. Other mutations return None because the Provider supplies no new identity or resource representation; callers can explicitly list or read when they need current state. Direct-share, market-acquisition, and group relation refs remain distinct. Marketplace revision and uploader-binding limitations are documented operation semantics rather than constant fields on every result (PVD-021, PVD-026). UploadBindingRef.new() creates the fresh hidden correlation value required by intro attachment binding.

Extended record reads stay behind the same records interface:

from matelab import RecordLocator

exported = await client.records.export([RecordLocator(notebook=notebook, record=record)])
matches = await client.records.search(notebooks=[notebook], extractions={"notes": ("Notes",)})
page = await client.records.page(notebook)
deleted = await client.records.recycle_bin(notebook)
relations = await client.records.relations(notebook=notebook, record=record)

records.page fixes the legacy request to page_size=0&default=1, preventing the known owner-preference writes described by PVD-039; its total is derived from the Provider's complete matching ID list. Public catalog records and deleted records use identities distinct from active RecordRef. Relation targets separately expose declared and resolved notebook IDs because the Provider may return dangling or incomplete identities. Search and relation order remain unspecified, and no continuation token is invented.

Record creation keeps blank creation and structured import as separate capabilities:

from matelab import RecordImportItem

await client.records.create_blank(notebook=notebook, title="Blank Record", record_uid="caller-generated-uid")
imported = await client.records.import_dataset(
    notebook=notebook,
    template_title="Example Template",
    items=[RecordImportItem(record_uid="import-uid", title="Imported", data={"Notes": "value"})],
)

Blank-record creation returns None because the Provider returns no record identity, even when the caller supplies a UID. Import validates the complete batch with generated wire models but cannot map returned database IDs to individual inputs or promise atomicity (PCG-008). Delete means moving records into the recycle bin, not permanent deletion. Delete and restore return None; record mutations are not automatically retried.

Record patching exposes a deliberately narrower capability than the raw Provider operation. Scalar/module changes cannot smuggle Provider-native attachment strings; staged attachments use separate form-removal, table-replacement, files append/replace/remove, and rich-text types. Unsafe form replacement and table-file removal are absent, while a files/images removal is rejected when the observed module contains the same hash more than once (PVD-014 through PVD-016). Record.content_sha256 can be supplied as a client-side precondition, documented as advisory read-before-write rather than Provider CAS. records.update() returns the acknowledgement classification "provider_reported_persisted", "pending_browser_save", or "provider_acknowledged_unclassified"; it does not issue a post-write read. Database, active-browser, and unclassified acknowledgements remain distinct, and mutation retries stay disabled.

Relation addition reads both endpoints and checks their resolved data server before writing; this reduces PVD-019 risk but is not an atomic Provider authorization guarantee. Relation deletion refuses an observed cross-notebook target-ID collision because the Provider ignores target notebook identity (PVD-020). Both mutations return None after acknowledgement.

Comment upload follows the Provider's literal one-request upload field, not the incompatible Front fragment protocol (PVD-037). Comment mutations return None after acknowledgement. Edit and delete first verify that the selected comment is currently observed and caller-owned, but do not perform a post-write read (PVD-004). Staged comment attachments have no Contract abort operation, and binding remains affected by PVD-026.

Attachment bytes are streamed and must be consumed or closed explicitly:

from matelab import ByteRange

comments = await client.records.comments(notebook=notebook, record=record)
attachment = comments[0].attachments[0]
async with await client.records.download_comment_attachment(attachment, byte_range=ByteRange.from_start(0)) as download:
    async for chunk in download:
        consume(chunk)

DownloadStream exposes status, content type, length, range, and disposition metadata without buffering the complete file. Streams are not automatically replayed. ByteRange deliberately rejects bytes=0-0 (PVD-002). Comment attachment refs preserve the notebook/record/comment context where they were observed, but they are not Provider authorization credentials: current Providers do not verify that association (PVD-038).

Cross-domain staging keeps resumable state and completed-file identity separate:

import hashlib

from matelab import StagedFile

pdf_bytes = b"sanitized PDF bytes"
staged = await client.uploads.stage(
    pdf_bytes,
    filename="example.pdf",
    fragment_size=len(pdf_bytes),
    complete_sha256=hashlib.sha256(pdf_bytes).hexdigest(),
)
assert isinstance(staged, StagedFile)

For multiple fragments, pass StagedFileFragment.session into the next call. next_offset is explicitly a caller-side total derived from declared fragment sizes; the Provider does not confirm an offset. A final result contains the Provider hash, size, temporary row identity and fresh hidden binding value, but does not claim that a later literature/cloud operation checks the uploader or consumes the file exactly once. uploads.abort exposes the Provider's legacy code-2 cancellation signal as a None-returning compensating cleanup that is not independently verified (PVD-028). Staging mutations are never automatically retried.

Literature identities distinguish the personal library, shared libraries and pending incoming copies:

from matelab import LiteratureMetadata

libraries = await client.literature.libraries()
page = await client.literature.list(libraries.personal.ref)
detail = await client.literature.read(page.items[0].ref)
schema = await client.literature.creation_schema()

if schema.metadata_extraction_available:
    candidates = await client.literature.extract_metadata(doi="10.0000/example")

await client.literature.create(LiteratureMetadata(title="Example import", doi="10.0000/example"), staged_pdf=staged)

Create returns None and never guesses the new item from list position because the Provider returns no ID. Canonical update reads the item first and refuses to drop source/hidden fields unless allow_source_metadata_loss=True is explicit (PVD-027). PDF replace/delete are separate acknowledged mutations and are not presented as atomic with metadata (PCG-010). Permanent personal deletion is named permanently_delete, returns None, and is non-recoverable. Sharing requires list-observed item summaries, user-search summaries and a valid caller identity, then returns None because the Provider supplies no per-recipient IDs (PVD-012, PVD-036).

Literature comments use one public save intent: detail is read first, an existing caller-owned comment is edited, and otherwise a comment is created. Multiple caller-owned comments are rejected as ambiguous (PVD-035). A staged attachment can replace one matelab-staged-file marker; raw temporary URLs are rejected. These checks contain common misuse but do not repair the Provider's cross-user UID lookup (PVD-026). Shared-library reads and writes remain experimental because the Provider permission JOIN is not scoped to the current user (PVD-011); successful SDK calls must not be treated as independent authorization proof. Literature PDF downloads reuse DownloadStream and the stable ByteRange subset.

The personal cloud-drive surface keeps root, folder, final file and temporary staging identities separate:

listing = await client.cloud_drive.list()
folder = await client.cloud_drive.create_folder(name="Example data")
await client.cloud_drive.bind_staged_file(staged, target=folder)

CloudDriveListing contains a typed file page, complete folder snapshot, quota usage and personal-root permissions rather than flattening them into one ambiguous collection. Folder browse results retain their location; filename searches are explicitly root-wide and return location=None because the Provider omits each match's folder ID. Ordering has no stable ID tie-breaker (PCG-003, PVD-013).

Folder create returns a CloudFolderRef built from the Provider ID. Other folder and file mutations return None after acknowledgement and do not automatically list the drive. Staged finalize accepts a completed StagedFile; the Provider binds by temporary row ID without checking its owner (PVD-031), and finalize atomicity/idempotency remain absent (PCG-012). Batch move and permanent delete do not claim Provider per-item results or atomicity. Permanent deletion is named permanently_delete_files and is non-recoverable. Cloud downloads resolve bytes from the final file identity and reuse DownloadStream, thumbnail/preview choices and the PVD-002-safe range subset. Cloud mutations are not automatically retried.

To run the implementation as a persistent Codex goal, start a task in this repository and use:

完整阅读并严格遵循 AGENTS.md、README.md 和 docs/roadmap.md。创建并持续执行一个 goal: 只修改当前仓库,按照 roadmap 从第一个未完成 work package 开始,完成 71-operation 精确覆盖和全部 SDK 领域 interface;每个 package 通过局部验证后自动继续,最终让 generation --check、Ruff、 Ruff format、Basedpyright、Pytest 和 package build 全部通过。不要修改相邻仓库,不执行生产 Provider mutation,不 commit、push、tag 或发布。

Owned/shared NotebookRef, public PublicNotebookRef, RecordRef, and RecordVersionRef keep Provider identifiers distinct. Historical reads first re-read the authorized current record and confirm that the requested version is still present in its modify_log; both reads write Provider audit entries.

Errors are separated into Provider business errors, authentication errors, HTTP/transport errors, Integration Contract response errors, and client-side usage errors. Provider response bodies and caller inputs do not enter exceptions.

Development

uv sync
uv run python scripts/generate_models.py
uv run python scripts/generate_models.py --check
uv run ruff check .
uv run ruff format --check .
uv run basedpyright
uv run pytest
uv build

The generator first verifies the contract lock, OpenAPI release metadata, and snapshot digest. It then creates a temporary OpenAPI 3.1 generation projection, resolves references without network access, and generates private component, operation-response, and parameter models. The projection only flattens pure object inheritance that the generator cannot otherwise preserve correctly; the checked-in release snapshot remains unchanged. --check performs the same validation and deterministic generation without writing the checked-in models. The current lock resolves datamodel-code-generator 0.71.0 and hatchling 1.31.0. Published metadata requires httpx2>=2.9.1,<3 and pydantic>=2.13.4,<3; the build backend requires hatchling>=1.27,<2. These lower bounds are verified against the complete test suite on the supported Python boundary versions rather than inferred from uv.lock. The exact toolchain remains locked for development and release builds. Basedpyright and its Node wheel retain the compatible exact pair basedpyright==1.39.9 and nodejs-wheel-binaries==22.20.0.

Opt-in Provider consumer smoke

tests/provider/test_provider_smoke.py exercises the consumer flow through only the public SDK interface. Its base scenario covers credential authentication, persistence of the complete returned Session, restoration through a new AsyncMatelab instance, refresh with identity preservation, and notebook discovery. It is not Provider Verification and is skipped by default.

Raw Provider conformance remains the responsibility of matelab-spec, which sends direct HTTP requests and validates the unmodified responses. The SDK does not repeat its route-by-route, cross-account, sharing, or attachment-isolation verification. Representative SDK adapter tests instead feed the pinned OpenAPI's sanitized response examples through MockTransport and assert the resulting public values; synthetic fixtures remain where SDK-specific encoding, error, retry, and compatibility boundaries require evidence beyond those examples.

Provider smoke is restricted to the confirmed isolated test service. Authentication and refresh persist Provider token state. This side effect is inherent to the tested Provider operations; it cannot be disabled by a test setting. Explicitly loading .env.test and selecting the provider marker is the opt-in for this flow.

Copy .env.example to the git-ignored local .env.test, then fill in the shared Provider connection settings:

  • MATELAB_PROVIDER_BASE_URL
  • MATELAB_PROVIDER_USERNAME
  • MATELAB_PROVIDER_PASSWORD

These names intentionally match matelab-spec Provider Verification. The isolated target may copy them from the spec .env into this repository's .env.test. Refreshing the restored Session must preserve its authenticated identity. With the environment prepared:

uv run --env-file .env.test pytest -m provider tests/provider/test_provider_smoke.py

This command runs only the SDK public-interface smoke; it is not the 71-operation Provider Verification. To reuse the same .env.test for the complete Contract suite, also populate the optional share user, secondary account, record staging opt-in, and Chat SSO settings documented in .env.example, then run from sibling checkouts:

cd ../matelab-spec
uv run --env-file ../matelab-python-sdk/.env.test pytest

The files are never loaded implicitly, so normal test runs remain safely skipped. Do not use either flow against production, and never commit Provider credentials.

Reproducible release build

Build from a clean release commit (or its tag) and set the archive timestamp to that commit's committer timestamp. pyproject.toml declares the supported Hatchling range, while uv.lock supplies the exact version used by the frozen, no-build-isolation release environment:

export SOURCE_DATE_EPOCH="$(git show -s --format=%ct HEAD)"
uv sync --frozen
uv run python scripts/generate_models.py --check
uv build --no-build-isolation --out-dir dist/release
uv run python scripts/check_release.py dist/release/*.whl dist/release/*.tar.gz
(cd dist/release && sha256sum *.whl *.tar.gz > SHA256SUMS)

Rebuilding the same commit with the same locked environment and SOURCE_DATE_EPOCH must produce byte-identical wheel and source distribution hashes. The release is bound to matelab-spec v0.3.0, commit 0b5612588708b4639a42c7983ee2f08c350994bf, and OpenAPI SHA-256 ebf5d446e3a5866d773cf8cfffe1db28f27015635fbb621bc091673e865ef70e.

Release files for matelab-python-sdk 0.1.0a6

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for matelab-python-sdk 0.1.0a6
File Size Uploaded
matelab_python_sdk-0.1.0a6.tar.gz 248.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for matelab-python-sdk 0.1.0a6
File Interpreter ABI Platform
matelab_python_sdk-0.1.0a6-py3-none-any.whl Python 3 none any Details

Total release size: 336.0 kB

Release files / matelab_python_sdk-0.1.0a6.tar.gz

Download URL matelab_python_sdk-0.1.0a6.tar.gz
Size 248.7 kB
Tags Source
SHA-256 checksum
How to use checksums
4d3486cd6620914939a3027d89175d8e087ecdc51619b2af20d6a66cedc80190
BLAKE2b-256 checksum
How to use checksums
705999b8e3e2899f25e0d3b622dc87573f56bebdea21932ae2039a31d36c07ef
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 1, 2026.

Transparency log

Release files / matelab_python_sdk-0.1.0a6-py3-none-any.whl

Download URL matelab_python_sdk-0.1.0a6-py3-none-any.whl
Size 87.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
711db7d080f9fc9a8dfe6dbefb3403dedb397d201bad5e0a9e1d8d45ede157c3
BLAKE2b-256 checksum
How to use checksums
989ebfff6a37f28552dc9465dea4d6e5cd21bcbbe5060ab71fd0912e6898a9ed
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 1, 2026.

Transparency log
Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page