Metadata-Version: 2.4
Name: wittgen-b2sc
Version: 0.5.0
Summary: Thin Python client for the WittGen B2SC API (bulk RNA-seq -> single-cell type proportions)
Author: WittGen
License: Apache-2.0
Project-URL: Homepage, https://www.wittgenbio.com
Project-URL: Documentation, https://www.wittgenbio.com/api/v1/docs
Keywords: bioinformatics,rna-seq,single-cell,deconvolution,cell-type,wittgen,b2sc
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: requests>=2.25
Provides-Extra: pandas
Requires-Dist: pandas>=1.3; extra == "pandas"
Provides-Extra: dev
Requires-Dist: pandas>=1.3; extra == "dev"
Requires-Dist: build; extra == "dev"
Requires-Dist: twine; extra == "dev"
Dynamic: license-file

# wittgen-b2sc

Thin Python client for the **WittGen B2SC API** — submit a bulk RNA-seq job, poll it to
completion, and pull per-sample single-cell type proportions, gene scores and plots. Built to
the OpenAPI contract at `https://www.wittgenbio.com/api/v1/openapi.json`.

## Install

```bash
pip install wittgen-b2sc            # core
pip install "wittgen-b2sc[pandas]"  # + DataFrame support
```

Installing from git requires access to the private platform repo, so it only works for WittGen
staff and partners who have been granted it:

```bash
pip install "wittgen-b2sc[pandas] @ git+ssh://git@github.com/WittGen-Inc/wittgen-b2sc-platform.git#subdirectory=clients/wittgen-b2sc-python"
```

## Authenticate

Create a per-user API key in the WittGen dashboard (it is shown once — store it as a secret).

```python
from wittgen_b2sc import B2SCClient
client = B2SCClient(api_key="wgk_...")
```

## Quickstart

```python
# 1. Submit a job against a built-in reference dataset
job = client.submit_job("breast-tumour-2k", source="reference_dataset")
job_id = job["job_id"]

# 2. Wait for the GPU pipeline, then for the R analysis stage.
#    The reference dataset is a single example bulk and finishes quickly; a large
#    uploaded cohort is what takes hours (see the timeout note below).
client.wait_for_completion(job_id, wait_for_analysis=True)

# 3. Per-sample cell-type proportions as a tidy DataFrame
df = client.get_proportions(job_id, as_dataframe=True)   # columns: sample, cell_type, proportion
print(df.head())

# 4. Gene scores and plots from the R stage, as presigned downloads
for f in client.list_files(job_id, category="analysis"):
    print(f["name"], f["url"])
```

## Databricks quickstart

Run inside a Databricks notebook. Store the key in a **secret scope**, never inline.

```python
# Cell 1 — install
%pip install "wittgen-b2sc[pandas]"

# Cell 2 — client (key from a Databricks secret scope)
from wittgen_b2sc import B2SCClient
api_key = dbutils.secrets.get(scope="wittgen", key="b2sc_api_key")
client = B2SCClient(api_key=api_key)

# Cell 3 — submit + await + load as a Spark-ready pandas DataFrame
job = client.submit_job("breast-tumour-2k", source="reference_dataset")
client.wait_for_completion(job["job_id"])
pdf = client.get_proportions(job["job_id"], as_dataframe=True)
sdf = spark.createDataFrame(pdf)      # -> a Spark DataFrame you can join/aggregate/save to Delta
sdf.display()
```

## API surface

| Method | Description |
|---|---|
| `list_models()` | Available disease models — read `applies_to` before submitting your own data |
| `list_atlases()` | Pre-computed public cohorts, ready to inspect |
| `get_usage()` | Your plan and remaining trial quota |
| `upload_file(path, content_type="text/plain")` | Upload your own matrix → `input_file_key` |
| `submit_job(disease_model, source, input_file_key=None, geo_accession=None, geo_file=None)` | Submit a job → job record |
| `list_jobs(limit=None)` | Your own jobs, newest first (not atlases) |
| `get_job(job_id)` | Current status |
| `wait_for_completion(job_id, poll_interval=15, timeout=10800, wait_for_analysis=False)` | Poll until COMPLETED (or, with `wait_for_analysis=True`, until the R stage reaches R_COMPLETE) |
| `get_results_data(job_id)` | Aggregated summary (mean proportions across the cohort) + metadata |
| `get_proportions(job_id, fmt="long"\|"wide", as_dataframe=False)` | Per-sample proportions |
| `list_files(job_id, category=None)` | Every artifact with a presigned URL — gene scores, plots, matrices |

### Running a public GEO series

Give an accession and we resolve it to the gene-level raw count matrix in that series, fetch it
and run it:

```python
job = client.submit_job("breast-tumour-2k", source="geo", geo_accession="GSE149050")
```

A series carries whatever its submitter uploaded — GSE149050 publishes both raw counts and TPM, and
we take the counts. When several files are plausible the call returns `422` with the listing; retry
with `geo_file="..."` naming one. Pass `geo_file` up front to override our choice.

**The models need raw counts.** They apply their own CPM + log1p normalization, so a matrix that has
already been through that is the wrong input rather than a lesser one — and the quiet failure is the
dangerous one: TPM and FPKM are non-negative, so nothing errors and the run finishes with confident,
wrong proportions. A series that publishes only normalized values is therefore refused up front with
`422 GEO_NO_RAW_COUNTS`, naming the unit that blocked it. GSE81538 and GSE96058 are both this case.
The same check runs on uploads, where there is no filename to judge.

The series is cached by accession, so the second person to run a given GSE does not wait for it
again. Requires a provisioned plan: the data is public, the GPU run is not.

### Pre-computed atlases

Some well-known cohorts are already run, so you can look at real output without waiting:

```python
for a in client.list_atlases():
    print(a["title"], a["source_citation"], a["n_samples"])
    df = client.get_proportions(a["job_id"], as_dataframe=True)
```

They are read-only and owned by WittGen; everything else works on them like any job.

### Analysing your own data

`upload_file()` wraps the two-step presigned-S3 handshake — the PUT is signed over its headers,
so hand-rolling it is easy to get wrong:

```python
key = client.upload_file("my_cohort.tsv")
job = client.submit_job("breast-tumour-2k", source="user_upload", input_file_key=key)
```

The matrix must be `.tsv`/`.csv`/`.txt`, optionally gzipped, under 256 MB, genes × samples, with **HGNC symbols** in the
gene column. Duplicate symbols — the ordinary result of an Ensembl→HGNC mapping — are collapsed by
**summing** their counts, the standard resolution for one gene measured across several loci; the
run reports how many rows were collapsed. Pre-aggregate yourself if you want different semantics.
Uploading requires a provisioned plan; self-serve accounts run the bundled reference datasets
(`source="reference_dataset"`).

### Know what the model is for

`list_models()` carries `applies_to` and `not_applicable_to` for each model. Read them before
submitting your own matrix or a GEO series:

> **A deconvolution model given the wrong tissue does not fail.** It distributes proportions
> across the cell types it knows and returns a well-formed table that sums to 1.000. Feed blood to
> the breast-tumour model and you get plausible epithelial and stromal fractions for a sample that
> has none. Read `applies_to` and `not_applicable_to` on `list_models()` first.

`get_results_data()` returns `metadata.gene_coverage` with the engine's own verdict on how much of
the model's gene panel your matrix matched. The model refuses to run below its floor rather than
producing a number from a partial panel. That catches a gene-identifier or
species mismatch — it **cannot** tell you the tissue was right, because human liver matches HGNC
symbols just as well as human blood.

### Order of operations

`wait_for_completion` returns as soon as the job is **COMPLETED** — proportions are ready at that
point. The R analysis stage (gene scores, plots) is triggered server-side and reaches
`R_COMPLETE`; pass `wait_for_analysis=True` to block for it, then collect the outputs:

```python
client.wait_for_completion(job_id, wait_for_analysis=True)
for f in client.list_files(job_id, category="analysis"):
    print(f["name"], f["url"])
```

Not every model has an analysis stage. `t-all-2k` has no R pipeline, so its jobs report
`report_status: NOT_APPLICABLE` — a terminal value, not a pending one. `wait_for_analysis=True`
returns immediately for those rather than waiting for a stage that will never start; check
`report_type` on `list_models()` if you want to know in advance.

### What the model accepts

| Limit | Value |
|---|---|
| Samples per job | **64** |
| Gene rows | 100,000 — gene-level annotations (40-60k) are fine, transcript-level is not |
| File size | 256 MB as sent, 512 MB expanded |
| Gene panel overlap | **70% of the model's own panel**, on two floors: matched by symbol AND carrying signal (1,400 of 2,000; 3,500 of 5,000). `n_genes` on `list_models()` |

A matrix that misses the overlap floor is refused rather than run on partial input, and
`get_results_data()` reports where a finished run landed under `metadata.gene_coverage`.

### One job takes at most 64 samples

The model refuses a matrix with more than 64 columns:

> `Bulk input exceeds the 64-sample limit.`

Split a larger cohort into batches of 64 or fewer and submit one job per batch — the per-sample
proportion tables concatenate directly, since every batch returns the same cell-type columns in
the same order. The 1,231-sample TCGA-BRCA atlas was produced exactly this way, as 20 batches.

The default timeout is **3 hours**. Runtime scales with sample count, not with the model: a
single-sample reference run finishes in minutes, and a full 64-sample batch stays well inside it.
The `breast-tumour-2k` R analysis stage adds time on top, since it runs subcluster differential
expression across all 15 cell types. A `B2SCTimeout` does not
cancel the job — it carries `.elapsed` and `.last_status`, and you can keep polling `get_job()`.

### How long results are kept

A job and its outputs are retained for **365 days** from submission, whichever way the input was
supplied. After that the job and its files are deleted and `get_job()` returns 404, so download
anything you need to keep — `list_files()` gives presigned URLs for every artifact.

Atlases are permanent. They are not subject to this window.

Very old jobs may have been moved to archival storage. Those still appear in `list_files()`, but
carry `archived` instead of a download URL rather than a link that would fail — ask us for a
restore if you need one.

### Removed in 0.2.0

The AI clinical report (an Opus-written PDF) was removed from the product on 2026-08-04, and with
it `generate_report()`, `wait_for_report()` and `download_report()`. The deliverable is the R stage
output — proportions, gene scores and plots — via `get_proportions()` and `list_files()`.

Calling a removed method raises `B2SCRemovedError`, which names the replacement and shows the
equivalent code. It subclasses both `B2SCError` and `AttributeError`, so `hasattr()` feature
detection correctly reports the method as absent while a direct call still explains itself.

The `wait_for_report=` **parameter** of `wait_for_completion` was only renamed, not removed — it
always waited for the R stage. It still works and warns.

Errors raise `B2SCError` (`.status`, `.code`); a poll timeout raises `B2SCTimeout`. A `409` from
`get_proportions` means the results are not produced yet — retry.

> **Data residency:** genomic expression data is sensitive. Analysis runs in AWS us-east-1;
> confirm that placement with WittGen before sending patient-derived data. Note the API does not
> send your data to any third-party model provider — the AI report that did was removed in 0.2.0.

## License

Apache-2.0 — see [LICENSE](LICENSE). This client SDK is open source; the WittGen B2SC **model
and service** it talks to remain proprietary. Copyright 2026 WittGen Biotechnologies.
