Metadata-Version: 2.5
Name: involve-benchling
Version: 0.1.0a2
Summary: Caching, name resolution, dataframes, read-only clients and testing utilities for the Benchling V3 Python SDK.
Project-URL: Homepage, https://github.com/involvedata/involve-benchling
Project-URL: Repository, https://github.com/involvedata/involve-benchling.git
Project-URL: Issues, https://github.com/involvedata/involve-benchling/issues
Project-URL: Changelog, https://github.com/involvedata/involve-benchling/blob/main/docs/CHANGELOG.md
Author: Involve Data
License-Expression: Apache-2.0
License-File: LICENSE
Keywords: api,benchling,biology,biotech,laboratory,lims,sdk
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Science/Research
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: benchling-v3-sdk-python>=15.0.0
Requires-Dist: httpx>=0.27
Requires-Dist: pydantic>=2.0
Provides-Extra: dev
Requires-Dist: mypy>=1.11; extra == 'dev'
Requires-Dist: pandas>=2.0; extra == 'dev'
Requires-Dist: pre-commit>=3.0; extra == 'dev'
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Provides-Extra: pandas
Requires-Dist: pandas>=2.0; extra == 'pandas'
Provides-Extra: testing
Requires-Dist: pytest>=8.0; extra == 'testing'
Description-Content-Type: text/markdown

# involve-benchling

**The toolkit the official Benchling SDK doesn't give you.**

A thin, composable layer over [`benchling-v3-sdk-python`](https://pypi.org/project/benchling-v3-sdk-python/)
— it never hides the SDK. You keep Benchling's typed models, pagination,
and full API surface; this library adds what's missing around them.

> **Status: alpha.** The API may change before 1.0. See the [changelog](https://github.com/involvedata/involve-benchling/blob/main/docs/CHANGELOG.md).

involve-benchling is an independent project. It is not affiliated with or endorsed by
Benchling, Inc.

## Why

The Benchling V3 SDK is a complete API client. Teams that build on it tend to
rebuild the same pieces around it:

| Pillar | Module | What it gives you |
|---|---|---|
| **Caching** | `involve_benchling.cache` | Tiered (memory + disk, shared across processes), pickle-free JSON serialisation of SDK models, per-category TTLs. Reference data (schemas, dropdowns, registries) goes from ~500ms to <1ms. |
| **Resolution** | `involve_benchling.resolve` | Generic `get_by_name` / `find_one` over *any* V3 resource, with typed `NotFoundError` / `AmbiguousError` instead of silent first-match. |
| **DataFrames** | `involve_benchling.dataframes` | Explicit `to_dataframe()` that expands V3 `schemaFields` into one typed, named column per field. Optional extra. |
| **Read-only clients** | `involve_benchling.core.readonly` | A client that cannot change tenant data, enforced in the HTTP transport below every SDK method. |
| **Testing** | `involve_benchling.testing` | pytest plugin: env-based config/client fixtures and a `benchling_tracker` that cleans up every resource a test created — even on failure. |

## Install

```bash
pip install involve-benchling              # core (the SDK and its own dependencies)
pip install "involve-benchling[pandas]"    # + dataframe support
```

## Quick start

```python
from involve_benchling import BenchlingConfig, connect, resolve

config = BenchlingConfig.from_env()   # BENCHLING_BASE_URL, BENCHLING_API_KEY, ...
benchling = connect(config)           # a plain benchling_v3_sdk.Benchling

# The SDK is right there — use it directly:
for seq in benchling.dna_sequence.list(page_size=50):
    print(seq.id, seq.name)

# Resolution with typed errors, over any resource:
plasmid = resolve.get_by_name(benchling.custom_entity, "pEX-001")
schema_id = resolve.get_id_by_name(benchling.entity_schema, "Plasmid")

# Caching reference data:
from involve_benchling.cache import BenchlingCache

cache = BenchlingCache.from_policy(config.cache, namespace=config.base_url)
schemas = cache.get_or_fetch(
    "schemas:entity",
    category="schema",
    fetch=lambda: list(benchling.entity_schema.list()),
)

# Analysis-ready dataframes:
from involve_benchling.dataframes import field_names, to_dataframe

df = to_dataframe(
    benchling.custom_entity.list(schema_eq=schema_id),
    field_names=field_names(benchling.custom_entity_schema, schema_id),
)
```

### Read-only clients

```python
config = BenchlingConfig.from_env(read_only=True)   # or BENCHLING_READ_ONLY=1
benchling = connect(config)

benchling.folder.list()                     # fine
benchling.folder.create(name="x", parent_id="lib_1")
# ReadOnlyViolationError: Read-only client blocked POST .../api/v3/folder/items
```

The check sits in the `httpx` transport, below every SDK method. A
read-only client sends only `GET`, `HEAD` and `OPTIONS`, plus `POST` to the
OAuth token exchange and `/api/v3/search`, which read without changing data.
Raw-response calls and methods added in later SDK releases are covered without
a list of write verbs to maintain.

### Cache storage

The disk tier defaults to `.benchling-cache/` in the working directory. It
creates the directory and its files readable only by the current user, and
writes a `.gitignore` into it so the cache is never committed. Cached
responses can hold sensitive data, so point `CachePolicy.directory` somewhere
protected.

The cache is keyed by `namespace` (usually the tenant URL), not by
credential. Don't share one cache directory between credentials with
different access, or a narrower credential can read what a broader one
cached.

### Testing your own Benchling integrations

```python
import pytest

@pytest.mark.integration
def test_my_workflow(benchling, benchling_tracker):
    entity = benchling.custom_entity.create(...)
    benchling_tracker.track(
        f"custom_entity {entity.id}",
        lambda: benchling.custom_entity.update(
            entity.id, archived=True, archive_reason="Made in error"
        ),
    )
    # ... test things; cleanup happens even if assertions fail
```

## Design principles

1. **Pass through SDK models.** No parallel dataclass universe; functions
   accept and return `benchling_v3_sdk` types.
2. **Generic over bespoke.** One `get_by_name` for 100+ resources, not
   one service class per resource.
3. **Explicit APIs.** No magic kwargs, no hidden state. Strictly typed
   (`py.typed`, mypy `--strict`).
4. **Everything optional is an extra.** Core depends on the SDK and
   nothing else.
5. **No pickle.** Cached values are JSON documents; a shared cache
   directory is never a code-execution vector.

## Roadmap

- `config` — configuration as code: export, plan and apply tenant configuration from
  YAML state files. Design: [`docs/design/config-as-code.md`](https://github.com/involvedata/involve-benchling/blob/main/docs/design/config-as-code.md).
- `tools.discovery` — tenant environment discovery: schemas, dropdowns,
  relationships; ERD and documentation generation.
- `tools.entry_search` — entry search with automatic user/project
  resolution.
- Cache invalidation hooks for write operations.
- Async parity helpers for `AsyncBenchling`.

## Development

```bash
python -m venv venv && source venv/bin/activate
make install   # pip install -e ".[dev]"
make lint type test
```

Integration tests need a tenant. Copy `.test.env.template` to `.test.env`,
fill it in, then run `pytest -m integration`. See [CONTRIBUTING.md](https://github.com/involvedata/involve-benchling/blob/main/CONTRIBUTING.md)
for the full workflow.

## License

Apache-2.0 © Involve Data
