Skip to content

LabguruPython SDK 2.0 relaunch (token transport + resource layer)#24

Open
jgross wants to merge 20 commits into
masterfrom
feat/sdk-relaunch-2.0
Open

LabguruPython SDK 2.0 relaunch (token transport + resource layer)#24
jgross wants to merge 20 commits into
masterfrom
feat/sdk-relaunch-2.0

Conversation

@jgross

@jgross jgross commented Jun 16, 2026

Copy link
Copy Markdown
Member

Summary

Ground-up rewrite replacing the pre-2.0 requests/email-password client with a modern, namespaced SDK (sync-first). Implements Phases 0–5 of the LabguruPython SDK Relaunch plan.

  • TransportLabguruClient(base_url, token) over sync httpx; token as query param; LabguruAPIError on HTTP ≥400; {"success": True} on 204/empty. New LabguruError/LabguruAPIError.
  • Resource layerBaseResource CRUD + 11 namespaces on the Labguru facade: protocols, experiments, sections, elements, members (read-only), tags, search (read-only global_search), biocollections (for_collection/find_by_external_uuid), stocks, storages, projects. create/update wrap inner fields under the API's item key and forward arbitrary fields (external_uuid, custom1..N, tags).
  • Auth change — email/password removed; Labguru(url=, token=). Legacy kwargs raise LabguruError.
  • Packagingpyproject.toml (hatchling, Python ≥3.9); legacy modules deleted; builds to labguru-2.0.0 sdist+wheel.
  • Docs — rewritten README, MIGRATION.md (incl. unverified-assumptions ledger), CHANGELOG.md, 2.0-accurate CLAUDE.md, token-API examples/example.py.
  • CIci.yml (ruff + pytest on 3.9/3.11/3.13) and release.yml (PyPI trusted-publishing on v* tag).

Verification boundary

Spec-complete and mock-tested: the respx tests assert request shaping (URL/params/body), not behavior against a live Labguru instance. The item wrapping, endpoint paths, the global_search term param, and the members/search read-only assumptions are best-guess defaults — see the unverified-assumptions ledger in MIGRATION.md. Treat as ready to validate, not production-ready.

Out of scope (follow-on)

  • Actual PyPI publish / v2.0.0 tag; confirming the labguru PyPI name + pypi trusted-publisher are owned by BioData.
  • Phase 6: repoint labguru-mcp/lg-copier, archive PyLabguru, triage open issues.
  • Live-API verification; porting the 1.x element helpers (get_elements_by_type, get_data, update_stock_amount, add_step, add_attachment) — lab.elements is generic CRUD for now.

Test plan

  • pytest — 21 passing (transport + resource respx tests)
  • ruff check labguru tests examples — clean
  • python -m build — sdist + wheel build
  • import labguru; labguru.__version__ == "2.0.0"
  • Validate against a live Labguru instance (clear the MIGRATION.md ledger)

🤖 Generated with Claude Code

jgross and others added 20 commits June 15, 2026 23:41
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The pre-2.0 __init__ eagerly imported requests-based modules, which breaks
importing any new labguru.* submodule under the httpx-only dev env. Emptied
until the facade is wired in Phase 2.2.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mirrors f909b67 — the prep commit neutralized labguru/__init__.py but
missed tests/__init__.py which still imported labguru.core → labguru.api
→ requests, breaking pytest collection under the httpx-only venv.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Introduce labguru/exceptions.py with LabguruError (base) and
LabguruAPIError (carries status_code and a readable message string).
Add tests/test_client.py with TDD test for Task 1.1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Introduce labguru/client.py with a synchronous httpx-based HTTP client.
Token is injected as a query parameter on every request (Labguru auth
convention). 4xx/5xx raises LabguruAPIError; 204/empty body returns
{"success": True}. Includes get_with_filters (Kendo filter params) and
post_multipart helpers. All four transport tests pass under respx mocks.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Consolidate all imports at top of tests/test_client.py (fix E402, F811)
- Extract httpx.Response locals to fix E501; remove inline `import json`
- Add full type annotations to LabguruClient._request keyword params
- Extend LabguruClient docstring noting per-request httpx.Client tradeoff
- Add unit test for build_filter_params Kendo-style output (int value stringified)
- Add respx integration test for get_with_filters token + filter query params

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add labguru/resources/__init__.py (empty) and labguru/resources/base.py
with BaseResource providing list/get/create/update/delete methods over
/api/v1/<resource_name>. Subclasses set resource_name; the client result
is returned directly. Test-driven: wrote failing test first, then
implemented.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ags, stocks, storages, projects)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…auth

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rch read-only, item-wrap docstrings (review)

- biocollections.py: add missing update() override with item_key wrap; add empty-string guard in __init__
- facade.py: hoist legacy login/password rejection before the falsy url/token check (catches both ordering bugs and empty-string inputs)
- search.py: rewrite as read-only resource using _path(); block create/update/delete with LabguruError
- protocols/experiments/sections/elements/tags/stocks/storages/projects: add item-wrap docstring to create and update overrides
- tests/test_resources.py: add 4 new tests covering all the above (12 total, all pass)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Deletes the pre-2.0 requests/email-password modules and their tests/cassettes
(superseded by client.py + resources/). Rewrites README quickstart to the token
API, adds MIGRATION.md (with an unverified-assumptions ledger), CHANGELOG.md, a
token-scrubbing tests/conftest.py, and a 2.0-accurate CLAUDE.md. Removes the
Opentrons example, which relied on 1.x element helpers not yet ported.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…flow

Release workflow publishes via PyPI trusted publishing and only triggers on a
pushed v* tag. Not wired up yet: the PyPI 'labguru' project/trusted-publisher and
the actual tag push are intentionally left to a human (see Phase 5/6 follow-ups).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…cope (review)

Adds respx tests for ProtocolsResource.tags(id) and StoragesResource.boxes(id)
path building, and asserts the experiments-create item-wrap. Rewords README/CLAUDE
so 'search' isn't claimed to be global_search-only (it inherits list/get); notes
get_with_filters as the low-level Kendo-filter escape hatch.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reviewed every facade resource against the published API spec
(my.labguru.com/api-docs/v1/swagger.yaml, OpenAPI 3.0.0, retrieved 2026-06-16).

Affirmative corrections:
- members: /api/v1/members -> /api/v1/admin/members (GET index only; was 404)
- search.global_search: add required 'size' param (default 20); was missing
- tags: model as create + delete only (no list/show/update endpoints exist)
- remove protocols.tags(id) and storages.boxes(id) -- neither endpoint exists
  (boxes is a top-level /api/v1/boxes resource)
- flag biocollections.find_by_external_uuid as UNVERIFIED ('external_uuid'
  appears nowhere in the spec)

Verified correct and left unchanged: token-as-query-param, no .json suffix,
item-key write wrapping, global_search 'term' param, core CRUD paths.

Rewrites the MIGRATION.md ledger from assumptions to spec-verified/disproven and
adds a resource support matrix. Headline finding: the cross-instance re-discovery
capability (external_uuid + tag search + protocol tags) has no documented API
support -- a design issue for lg-copier, captured in the ledger.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds datasets, documents, notes, papers, reports, sops, workflows, boxes,
instruments, units, requests, measurements, visualizations, webhooks, attachments,
and comments; plus generic/custom collections via
biocollections.for_generic_collection (restores 1.x generic-inventory parity).

Each resource is shaped to the verbs the OpenAPI spec actually supports, not blind
CRUD:
- read-only: workflows
- create + delete only: visualizations (like tags)
- create only (custom body): measurements -> {input_name, experiment_id, item}
- multipart upload: attachments.create(files, data)
- standard item-wrapped CRUD for the rest; per-verb gaps (no DELETE, etc.) raise
  LabguruAPIError(404) and are recorded in the support matrix.

Updates the MIGRATION.md support matrix (now 27 rows), README namespace overview,
CHANGELOG, and CLAUDE.md. 28 tests, ruff clean, package builds.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@noaazerrad

Copy link
Copy Markdown

in many resources it is mentioned that delete action doesnt exits - which is not the case. is that on purpose or should i add the delete action in all missing places?

class BiocollectionsResource(BaseResource):
item_key = "item"

def __init__(self, client, collection: str = "plasmids"):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there a reason for the hardcoded "plasmid" here?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll fix it in the next release - no good reason.



class DocumentsResource(BaseResource):
"""Documents. list/get/create/update; the API has no delete endpoint."""

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jgross i was able to use the delete action here - is that ok? or do we want to prevent for users to do it? there is a destroy action for this controller

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

delete should be prevented by permssions not at the SDK level - keep it.

"""fields: inner payload, wrapped as {item_key: fields} before sending."""
return self.client.put(self._path(f"/{resource_id}"), {self.item_key: fields})

def find_by_external_uuid(self, external_uuid: str):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we want to keep this? i'm not even sure this field is even populated

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it should (I think) this is the back-reference to UUIDs (not sure when it is populated)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants