This release is a pre-release and may not be stable for production use.
Biocompile Python SDK
A small, dependable client for the Biocompile API
(v1). It handles the token exchange, refresh, retries and error mapping, and
returns the API's JSON as plain dicts. Python 3.10+, one dependency (httpx).
pip install --pre biocompile # pre-release while the API is in alpha
Quickstart
Personal access token
Create a personal access token (bcp_...) in Biocompile under your account
settings, choosing the scopes the script needs.
from biocompile import Client
with Client(
base_url="https://app.biocompile.com",
personal_access_token="bcp_...",
organization_id="org_...", # optional; required for Knowledge Hub file calls
scopes=["projects:read", "runs:read"], # optional; narrows the token further
) as client:
for org in client.organizations.iter():
print(org["id"], org["name"])
for run in client.runs.iter("org_..."):
print(run["slug"], run["status"])
Service account
A service account authenticates with its user id and a key (bcs_...). It is
always bound to one organization.
client = Client(client_id="<service account id>", client_secret="bcs_...")
client.files.upload("results/table.tsv", absolute_path="/projects/alpha/table.tsv")
From the environment
client = Client.from_env()
reads BIOCOMPILE_BASE_URL (default https://app.biocompile.com),
BIOCOMPILE_PAT, or BIOCOMPILE_CLIENT_ID + BIOCOMPILE_CLIENT_SECRET, and
BIOCOMPILE_ORGANIZATION_ID. Keyword arguments override the environment.
How authentication works
The client never sends your long-lived credential to the API. On the first call
it exchanges it at POST /api/v1/oauth/token for a bearer token that lives
about ten minutes, caches that token, and exchanges again about a minute before
it expires. If the API still answers 401 (say, the token was revoked
server-side), the client exchanges once more and retries the call once; a
credential_revoked answer is raised straight away. Pass token= instead to
use an access token you obtained yourself; it is never refreshed.
Credentials are held in a wrapper that prints as ***, and neither the client's
repr nor any error message contains them.
Resources
Organization-scoped methods take the organization id first; pass None to use
the client's organization_id.
| Area | Methods | Scope |
|---|---|---|
client.organizations |
list, iter, get |
projects:read |
client.projects |
list, iter, get |
projects:read |
create(org, name, ...), update(org, project_id, ...), archive(org, project_id) |
projects:write |
|
client.contributions |
list(org, project_id, status=...), iter, get |
entities:read |
accept, withdraw, request_revision(org, project_id, contribution_id, instructions) |
contributions:review |
|
client.entities |
list(org, entity_type, **filters), iter |
entities:read |
client.observations, client.hypotheses |
list, iter, get(org, slug) |
entities:read |
client.executables |
list, iter, catalog, get |
runs:read |
invoke(org, executable_id, payload), create_plan(...) |
runs:execute |
|
client.plans |
refresh(org, plan_id), invoke(org, plan_id, payload) |
runs:execute |
approve_spending(org, plan_id, binding), and direct_user_intent=, network_access=, network_acknowledged= on the invoke calls |
runs:execute + spending:approve |
|
client.runs |
list, iter, get, logs, iter_logs, download_artifact(org, run, artifact, dest) |
runs:read |
cancel |
runs:execute |
|
client.files |
list, iter, get, download(file_id, dest) |
files:read |
upload(path, ...), and the steps init_upload, upload_chunk, complete_upload, upload_status, cancel_upload |
files:write |
|
create_folder(name, path=...), move(file_id, new_path), rename(file_id, name), update(file_id, description=..., tags=...), delete(file_id) |
files:write |
|
client.workbenches |
list_action_types, get_action_type, get, list_changes |
entities:read |
open(org, project_slug), stage(org, workbench, intents, expected_revision_id=...), submit(...) |
entities:write |
|
client.conversations |
create, list, iter, get (transcript), send, get_turn, send_and_wait |
agent:converse |
create(..., full_auto=True) |
agent:converse + spending:approve |
|
client.webhooks |
list, create, get, update, delete, rotate_secret, test, list_deliveries, iter_deliveries, redeliver |
webhooks:manage |
iter* helpers page through a list for you, following pagination.next_cursor
where the API offers a cursor and skip/limit (or page) otherwise. Filters
are passed as keyword arguments named as in the API (status=, project_slug=...).
Knowledge Hub file calls act in the organization the token is pinned to: a
service account always is, and a PAT is when the client has organization_id.
Uploads and downloads
status = client.files.upload(
"reads.fastq",
absolute_path="/projects/alpha/reads.fastq",
on_conflict="rename",
progress=lambda sent, total: print(f"{sent}/{total}"),
)
print(status["file_id"])
client.files.download(status["file_id"], "copy.fastq")
client.runs.download_artifact("org_...", "run_abc", "art_1", "outputs/") # server filename
upload uses the resumable protocol: it sends each chunk with its SHA-256,
skips chunks the server already holds, completes the upload and (unless
wait=False) polls until processing finishes. An interrupted upload resumes
with client.files.upload(path, upload_id="resumable_..."); a terminal failure
raises UploadFailed with the server's status on .upload. Downloads stream to
a .part file and are renamed into place only when complete; dest may be a
path, a directory, or a binary file object.
Changing a project: workbenches
Writes are staged in a workbench and submitted as a contribution that a person reviews in Biocompile; nothing changes in the project until they accept it (see Propose changes through the API).
workbench = client.workbenches.open("org_...", "rp_a7x3", title="Nightly import")
staged = client.workbenches.stage(
"org_...",
workbench["workbench_id"],
[{"operation": "create", "collection": "hypotheses",
"payload": {"statement": "Protein X activates pathway Y."},
"rationale": "Seen in three independent screens."}],
expected_revision_id=workbench["revision_id"],
idempotency_key="import-2026-10-27-batch-1", # makes a retry safe
)
result = client.workbenches.submit(
"org_...", workbench["workbench_id"],
expected_revision_id=staged["revision_id"],
title="Nightly import", summary="Three hypotheses from the screening run.",
)
print(result["review_url"])
Reviewing contributions and approving spending
A contribution waits for a person to review it. A personal access token (or an
application acting for you) with contributions:review decides as you would in
the app; service accounts can read contributions but not decide
(see Review contributions through the API).
for contribution in client.contributions.iter("org_...", "rp_a7x3", status="awaiting_approval"):
result = client.contributions.accept("org_...", "rp_a7x3", contribution["contribution_id"],
reason="Checked against the screen.")
if result["status"] == "conflicted": # nothing was applied
client.contributions.request_revision("org_...", "rp_a7x3", result["contribution_id"],
"Rebase on the current project.")
With spending:approve a token approves the credit spending of its own plan,
for the plan's current quote only:
plan = client.executables.create_plan("org_...", "exe_...", input={"samples": "/projects/alpha/samples.csv"})
approval = plan["approval"]
client.plans.approve_spending("org_...", plan["plan_id"], approval.get("spending_binding") or approval["binding"])
run = client.plans.invoke("org_...", plan["plan_id"], idempotency_key="screen-2026-10-30")
Grant spending:approve and contributions:review only to credentials that
should make those decisions without anyone opening the app
(see Scopes).
Conversations with the agent
conversation = client.conversations.create("org_...", title="Weekly scan", project_slug="rp_cardio01")
turn = client.conversations.send_and_wait(
"org_...", conversation["conversation_id"],
"Summarize this week's new papers on the target.",
idempotency_key="weekly-scan-2026-10-27",
timeout=900,
)
print(turn["status"]) # completed, blocked, failed or cancelled
for event in turn["events"]:
for part in event["parts"]:
if part["type"] == "text":
print(event["author"], part["text"])
send_and_wait sends the message, then polls get_turn with after= and
returns the final turn with all its events. A turn in requires_action waits
for a person in the app, so polling continues through it unless
return_on_requires_action=True. After timeout seconds it raises
TurnTimeout (the last state is on .turn).
create(..., full_auto=True) (needs spending:approve, and an organization
that allows Auto Mode) lets the agent answer its own checkpoints, spending
included, under your Auto Mode policy; the conversation's autonomy_mode is
then auto. Contributions, project creation and open questions still stop.
Webhooks
client.webhooks.create(org, url, events) returns the signing secret once;
store it. Verify each delivery against the raw body before parsing it:
from biocompile import WebhookVerificationError
from biocompile.webhooks import verify_signature
def receive(headers, raw_body: bytes) -> int:
try:
event = verify_signature(SECRET, headers, raw_body) # tolerance_seconds=300
except WebhookVerificationError:
return 400
queue_for_processing(headers["webhook-id"], event) # dedupe on webhook-id; answer fast
return 204
It implements Standard Webhooks: an
HMAC-SHA256 over webhook-id.webhook-timestamp.body with the base64 key after
whsec_, compared in constant time against every v1, entry in
webhook-signature, and refuses timestamps more than tolerance_seconds
from now.
Errors
Every error derives from biocompile.BiocompileError. HTTP errors are
ApiError subclasses carrying .status, .code (the API's stable problem
code, e.g. insufficient_scope), .detail, .problem (the full RFC 9457
document) and .retry_after:
| Class | When |
|---|---|
AuthenticationError |
401 (credential_revoked, expired token), or the token endpoint rejected the credential (invalid_grant, invalid_client) |
PermissionDenied |
403: insufficient_scope (see .missing_scopes), organization_mismatch, not_public; also invalid_scope / invalid_target from the token endpoint |
NotFound |
404 |
RateLimited |
429 after retries ran out (.retry_after) |
ServiceUnavailable |
503, e.g. revocation_unavailable, after retries ran out |
ApiError |
any other non-2xx status |
TransportError |
no HTTP response at all (DNS, connection, timeout) |
TurnTimeout |
conversations.send_and_wait ran out of time (.turn) |
WebhookVerificationError |
webhooks.verify_signature refused a delivery |
from biocompile import PermissionDenied
try:
client.runs.cancel("org_...", "run_abc")
except PermissionDenied as exc:
if exc.code == "insufficient_scope":
print("token is missing:", ", ".join(exc.missing_scopes))
Retries and rate limits
429and503are retried up tomax_retriestimes (default 3), waiting for the server'sRetry-After(or a short jittered backoff when it sends none). ARetry-Afterlonger thanmax_retry_wait(default 60 s) is raised at once instead of blocking the script.- Non-idempotent
POSTs are only retried when the server refused them before doing anything: any429, or a503the API's admission step raised (revocation_unavailable,rate_limit_unavailable). APOSTthat carries an idempotency key the server deduplicates on (idempotency_key=onworkbenches.stageandconversations.send, or aclient_turn_id) is retried like aGET. Use theidempotency_keyfield on invocations if you add retries of your own. - Connection failures (nothing reached the server) are retried the same way.
client.rate_limitholds theRateLimit-Limit/-Remaining/-Resetvalues of the most recent response that carried them.
Documentation and license
Guides and the full API reference: https://www.biocompile.com/docs/api-overview/.
Licensed under the Apache License 2.0 (see LICENSE).
Metadata
Release files for biocompile 0.1.0a1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| biocompile-0.1.0a1.tar.gz | 29.3 kB | Without provenance | Details for biocompile-0.1.0a1.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| biocompile-0.1.0a1-py3-none-any.whl | Python 3 | none | any | Details for biocompile-0.1.0a1-py3-none-any.whl |
Total release size: 62.6 kB
Release files / biocompile-0.1.0a1.tar.gz
| Download URL | biocompile-0.1.0a1.tar.gz |
|---|---|
| Size | 29.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
d79526fe53176e1e411614d83a18db64a60dc1180c8f3d5ed91a34ab241ebf6d
|
|
BLAKE2b-256 checksum How to use checksums |
68ef1677e3f0582ebad0eeccd00eff5192a6166aab9a0c400040790eb0e35288
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.13 {"installer":{"name":"uv","version":"0.12.13","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / biocompile-0.1.0a1-py3-none-any.whl
| Download URL | biocompile-0.1.0a1-py3-none-any.whl |
|---|---|
| Size | 33.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
c60afbdd2ff7eb6e9f090fa22884009f669ace6c1305b298aab1c6eea3f0747e
|
|
BLAKE2b-256 checksum How to use checksums |
b830c4290ac1d17bd1dd030c6d974f8229025d969c82c1717fced945d0da7c7b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.13 {"installer":{"name":"uv","version":"0.12.13","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Security for biocompile 0.1.0a1
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.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.