orfdata 0.2.0__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {orfdata-0.2.0/orfdata.egg-info → orfdata-0.3.0}/PKG-INFO +9 -2
- {orfdata-0.2.0 → orfdata-0.3.0}/README.md +3 -1
- orfdata-0.3.0/docs/measurements.md +134 -0
- {orfdata-0.2.0 → orfdata-0.3.0}/orfdata/__init__.py +12 -0
- {orfdata-0.2.0 → orfdata-0.3.0}/orfdata/cli.py +8 -1
- orfdata-0.3.0/orfdata/data/datasets.json +1 -0
- orfdata-0.3.0/orfdata/dataset.py +281 -0
- orfdata-0.3.0/orfdata/dataset_cli.py +80 -0
- orfdata-0.3.0/orfdata/datasets.py +172 -0
- orfdata-0.3.0/orfdata/measurements.py +364 -0
- {orfdata-0.2.0 → orfdata-0.3.0}/orfdata/reference.py +25 -0
- orfdata-0.3.0/orfdata/version.py +1 -0
- {orfdata-0.2.0 → orfdata-0.3.0/orfdata.egg-info}/PKG-INFO +9 -2
- {orfdata-0.2.0 → orfdata-0.3.0}/orfdata.egg-info/SOURCES.txt +7 -0
- {orfdata-0.2.0 → orfdata-0.3.0}/orfdata.egg-info/requires.txt +7 -0
- {orfdata-0.2.0 → orfdata-0.3.0}/pyproject.toml +4 -2
- orfdata-0.3.0/tests/test_measurements.py +356 -0
- orfdata-0.2.0/orfdata/version.py +0 -1
- {orfdata-0.2.0 → orfdata-0.3.0}/LICENSE +0 -0
- {orfdata-0.2.0 → orfdata-0.3.0}/MANIFEST.in +0 -0
- {orfdata-0.2.0 → orfdata-0.3.0}/deploy.sh +0 -0
- {orfdata-0.2.0 → orfdata-0.3.0}/docs/design.md +0 -0
- {orfdata-0.2.0 → orfdata-0.3.0}/docs/reference.md +0 -0
- {orfdata-0.2.0 → orfdata-0.3.0}/docs/species.md +0 -0
- {orfdata-0.2.0 → orfdata-0.3.0}/lint.sh +0 -0
- {orfdata-0.2.0 → orfdata-0.3.0}/orfdata/build.py +0 -0
- {orfdata-0.2.0 → orfdata-0.3.0}/orfdata/catalog.py +0 -0
- {orfdata-0.2.0 → orfdata-0.3.0}/orfdata/data/orf-evidence/AUTHOR-LICENSE.txt +0 -0
- {orfdata-0.2.0 → orfdata-0.3.0}/orfdata/data/orf-evidence/NOTICE.txt +0 -0
- {orfdata-0.2.0 → orfdata-0.3.0}/orfdata/data/orf-evidence/catalog.json.gz +0 -0
- {orfdata-0.2.0 → orfdata-0.3.0}/orfdata/data/orf-evidence/manifest.json +0 -0
- {orfdata-0.2.0 → orfdata-0.3.0}/orfdata/data/uorf-evidence/AUTHOR-LICENSE.txt +0 -0
- {orfdata-0.2.0 → orfdata-0.3.0}/orfdata/data/uorf-evidence/NOTICE.txt +0 -0
- {orfdata-0.2.0 → orfdata-0.3.0}/orfdata/data/uorf-evidence/catalog.json.gz +0 -0
- {orfdata-0.2.0 → orfdata-0.3.0}/orfdata/data/uorf-evidence/manifest.json +0 -0
- {orfdata-0.2.0 → orfdata-0.3.0}/orfdata/initiation_context.py +0 -0
- {orfdata-0.2.0 → orfdata-0.3.0}/orfdata/model.py +0 -0
- {orfdata-0.2.0 → orfdata-0.3.0}/orfdata/sequence.py +0 -0
- {orfdata-0.2.0 → orfdata-0.3.0}/orfdata.egg-info/dependency_links.txt +0 -0
- {orfdata-0.2.0 → orfdata-0.3.0}/orfdata.egg-info/entry_points.txt +0 -0
- {orfdata-0.2.0 → orfdata-0.3.0}/orfdata.egg-info/top_level.txt +0 -0
- {orfdata-0.2.0 → orfdata-0.3.0}/setup.cfg +0 -0
- {orfdata-0.2.0 → orfdata-0.3.0}/test.sh +0 -0
- {orfdata-0.2.0 → orfdata-0.3.0}/tests/data/references/README.md +0 -0
- {orfdata-0.2.0 → orfdata-0.3.0}/tests/data/references/canis_lupus_familiaris.json +0 -0
- {orfdata-0.2.0 → orfdata-0.3.0}/tests/data/references/homo_sapiens.json +0 -0
- {orfdata-0.2.0 → orfdata-0.3.0}/tests/data/references/mus_musculus.json +0 -0
- {orfdata-0.2.0 → orfdata-0.3.0}/tests/data/references/saccharomyces_cerevisiae.json +0 -0
- {orfdata-0.2.0 → orfdata-0.3.0}/tests/test_initiation_context.py +0 -0
- {orfdata-0.2.0 → orfdata-0.3.0}/tests/test_reference_context.py +0 -0
- {orfdata-0.2.0 → orfdata-0.3.0}/tests/test_reference_orf.py +0 -0
- {orfdata-0.2.0 → orfdata-0.3.0}/tests/test_uorf_catalog.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: orfdata
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Reference open reading frames and experimental translation evidence on annotated transcripts
|
|
5
5
|
Author: OpenVax
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -11,6 +11,10 @@ Provides-Extra: pyensembl
|
|
|
11
11
|
Requires-Dist: pyensembl>=2.27.2; extra == "pyensembl"
|
|
12
12
|
Provides-Extra: build-data
|
|
13
13
|
Requires-Dist: openpyxl>=3.1; extra == "build-data"
|
|
14
|
+
Provides-Extra: data
|
|
15
|
+
Requires-Dist: datacache<2,>=1.25.1; extra == "data"
|
|
16
|
+
Provides-Extra: columnar
|
|
17
|
+
Requires-Dist: pyarrow>=14; extra == "columnar"
|
|
14
18
|
Provides-Extra: dev
|
|
15
19
|
Requires-Dist: pytest; extra == "dev"
|
|
16
20
|
Requires-Dist: ruff; extra == "dev"
|
|
@@ -18,6 +22,7 @@ Requires-Dist: build; extra == "dev"
|
|
|
18
22
|
Requires-Dist: twine; extra == "dev"
|
|
19
23
|
Requires-Dist: openpyxl>=3.1; extra == "dev"
|
|
20
24
|
Requires-Dist: pyensembl>=2.27.2; extra == "dev"
|
|
25
|
+
Requires-Dist: datacache<2,>=1.25.1; extra == "dev"
|
|
21
26
|
Dynamic: license-file
|
|
22
27
|
|
|
23
28
|
# orfdata
|
|
@@ -95,5 +100,7 @@ bind real human, mouse, dog and yeast reference slices; those fixtures are not
|
|
|
95
100
|
new experimental ORF catalogues. [Species and reference identity](docs/species.md).
|
|
96
101
|
|
|
97
102
|
Install with `pip install orfdata[pyensembl]`. See the
|
|
103
|
+
[measurement API](docs/measurements.md) for native units, explicit samples and
|
|
104
|
+
assays, and DataCache-managed downloads, and the
|
|
98
105
|
[quantitative, multispecies data roadmap](https://github.com/openvax/orfdata/issues/3)
|
|
99
|
-
for
|
|
106
|
+
for upcoming RNA abundance, ribosome occupancy and protein data releases.
|
|
@@ -73,5 +73,7 @@ bind real human, mouse, dog and yeast reference slices; those fixtures are not
|
|
|
73
73
|
new experimental ORF catalogues. [Species and reference identity](docs/species.md).
|
|
74
74
|
|
|
75
75
|
Install with `pip install orfdata[pyensembl]`. See the
|
|
76
|
+
[measurement API](docs/measurements.md) for native units, explicit samples and
|
|
77
|
+
assays, and DataCache-managed downloads, and the
|
|
76
78
|
[quantitative, multispecies data roadmap](https://github.com/openvax/orfdata/issues/3)
|
|
77
|
-
for
|
|
79
|
+
for upcoming RNA abundance, ribosome occupancy and protein data releases.
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# RNA, ribosome and protein measurements
|
|
2
|
+
|
|
3
|
+
ORFdata adds evidence to PyEnsembl's reference transcripts and proteins. Its
|
|
4
|
+
quantitative layer stores measurements of genes, transcripts, reference ORFs,
|
|
5
|
+
proteins, protein groups and peptides. A measurement stays at the resolution
|
|
6
|
+
the experiment supports. Parent-gene RNA abundance is useful context for an ORF;
|
|
7
|
+
it is not a separate measurement of each ORF in that gene.
|
|
8
|
+
|
|
9
|
+
Install `orfdata[data]` to manage datasets. The initial 0.3 registry is empty;
|
|
10
|
+
the [data roadmap](https://github.com/openvax/orfdata/issues/3) tracks source
|
|
11
|
+
imports. The bundled human ORF evidence catalogue remains available offline.
|
|
12
|
+
Its pooled PSM counts describe spectra, not sample-level protein abundance.
|
|
13
|
+
|
|
14
|
+
## Discover, download and query
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
from orfdata import DatasetRegistry, ScopedTarget
|
|
18
|
+
|
|
19
|
+
registry = DatasetRegistry() # Metadata only; no download.
|
|
20
|
+
releases = registry.list(species="dog", assay="digest_ms")
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
For a registered release, use `registry.info(name, version)` for its immutable
|
|
24
|
+
manifest, `registry.download(name, version)` to fetch it, and
|
|
25
|
+
`registry.open(name, version)` to read it offline. `inspect` reports missing,
|
|
26
|
+
available or invalid files without downloading or repairing them.
|
|
27
|
+
`download_metadata` returns the artifact's access terms and file hashes/sizes.
|
|
28
|
+
An invalid cache
|
|
29
|
+
requires an explicit `download(..., force=True)`; old reader paths survive.
|
|
30
|
+
Python download options such as timeout, retries and progress go to DataCache.
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
# dataset = registry.open(name, version)
|
|
34
|
+
# transcript is a PyEnsembl Transcript in the dataset's exact reference context.
|
|
35
|
+
target = ScopedTarget.from_pyensembl(
|
|
36
|
+
transcript, kind="transcript", namespace="ensembl:110"
|
|
37
|
+
)
|
|
38
|
+
for measurement in dataset.measurements(target=target):
|
|
39
|
+
print(measurement.value, measurement.unit, measurement.status)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Use `ScopedTarget.from_orf(bound_reference_orf, namespace=source_release)` for a
|
|
43
|
+
validated reference ORF. Namespaces and identifiers must match the source's
|
|
44
|
+
mapping table. The default PyEnsembl target uses its unversioned `id`; request
|
|
45
|
+
`versioned=True` when the source uses versioned identifiers. There is no implicit
|
|
46
|
+
mapping across annotation releases, gene isoforms, ORFs or species. A variant or
|
|
47
|
+
fusion product does not inherit a native reference protein measurement.
|
|
48
|
+
|
|
49
|
+
The CLI uses the same operations:
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
orfdata datasets list --species yeast
|
|
53
|
+
orfdata datasets info DATASET --version RELEASE
|
|
54
|
+
orfdata datasets download DATASET --version RELEASE
|
|
55
|
+
orfdata datasets inspect DATASET --version RELEASE
|
|
56
|
+
orfdata datasets query DATASET --version RELEASE --quantity protein_abundance
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Queries stream JSON lines. `--reference ref:SHA256` selects an exact context;
|
|
60
|
+
target queries also require `--target`, `--kind` and `--namespace`. Unqualified
|
|
61
|
+
sample/assay ID queries across multiple references are rejected. Existing
|
|
62
|
+
catalogue commands such as `orfdata --gene TPST1` retain their behavior.
|
|
63
|
+
|
|
64
|
+
## What each value means
|
|
65
|
+
|
|
66
|
+
`Measurement` retains the dataset/version, reference-scoped target, sample and
|
|
67
|
+
assay IDs, value, native unit, linear/log scale and log base, uncertainty and its
|
|
68
|
+
definition, detection limit, status, origin and provenance. Measured zero,
|
|
69
|
+
below detection, not measured, not mapped and missing are distinct. Censored or
|
|
70
|
+
missing values use `None`; native sentinels can remain in provenance.
|
|
71
|
+
|
|
72
|
+
RNA counts/TPM/FPKM, ribosome occupancy, source-defined translation efficiency,
|
|
73
|
+
LFQ/iBAQ/TMT values, peptide intensity, PSM counts, copies per cell and turnover
|
|
74
|
+
remain distinct. No conversion is automatic. Derived/predicted values name a
|
|
75
|
+
method and retain source/input provenance. An estimate from calibration remains
|
|
76
|
+
an estimate; an intensity/TPM ratio does not acquire molecule-ratio units.
|
|
77
|
+
|
|
78
|
+
`Sample` records biological units, pools and source labels. `Assay` separately
|
|
79
|
+
records technical replicates, runs, fractions, TMT plex/channel/bridge,
|
|
80
|
+
normalization and calibration. A sample's strain/breed belongs in its attributes;
|
|
81
|
+
it need not be the strain used to assemble its reference genome. `TargetMapping`
|
|
82
|
+
retains all candidate assignments or an unmapped reason. A protein-group
|
|
83
|
+
representative does not become a uniquely measured protein. Explicit sample
|
|
84
|
+
pairing and orthology comparisons follow in
|
|
85
|
+
[issue #6](https://github.com/openvax/orfdata/issues/6).
|
|
86
|
+
|
|
87
|
+
## Importing a source
|
|
88
|
+
|
|
89
|
+
Define a `DatasetManifest` with exact reference contexts, raw source URLs,
|
|
90
|
+
SHA-256 hashes and byte sizes, citations, file-specific access/redistribution
|
|
91
|
+
terms, searched/measurable coverage, processing software/version/parameters and
|
|
92
|
+
row accounting. Unavailable metadata stay unknown. Article licensing does not
|
|
93
|
+
substitute for the data files' terms.
|
|
94
|
+
|
|
95
|
+
An importer calls `QuantitativeDataset.create(path, manifest, samples=...,
|
|
96
|
+
assays=..., measurements=..., mappings=...)`. These inputs may be generators;
|
|
97
|
+
measurements are written incrementally. Every observation names a source asset
|
|
98
|
+
or earlier input measurements. Derived inputs precede outputs in the same
|
|
99
|
+
reference context. Duplicate IDs, broken foreign keys, wrong dataset releases
|
|
100
|
+
and cross-reference links fail the import. Failed builds do not replace data.
|
|
101
|
+
|
|
102
|
+
Use `build_dataset(manifest, builder, ...)` for a managed local import.
|
|
103
|
+
`builder(source_paths, output_path)` reads the verified inputs and creates that
|
|
104
|
+
SQLite dataset. DataCache publishes it atomically with input/output checksums
|
|
105
|
+
and a processing receipt. `source_paths={asset_id: local_file}` supports manually
|
|
106
|
+
acquired sources. Importing locally does not grant redistribution rights.
|
|
107
|
+
|
|
108
|
+
A `DatasetRegistry(definitions, cache_root=...)` registers releases as:
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
definitions = {
|
|
112
|
+
name: {
|
|
113
|
+
"default_version": version,
|
|
114
|
+
"versions": {version: {
|
|
115
|
+
"manifest": manifest.to_dict(),
|
|
116
|
+
"distribution": {"access_terms": terms, "redistributable": False},
|
|
117
|
+
"assets": {"dataset.sqlite": {
|
|
118
|
+
"url": url, "sha256": sha256, "size": size, "decompress": False,
|
|
119
|
+
}},
|
|
120
|
+
}},
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
CLI users can supply this JSON with `orfdata datasets --registry FILE ...`.
|
|
126
|
+
`ORFDATA_CACHE`, then `OPENVAX_DATA_CACHE`, then the platform's OpenVax cache
|
|
127
|
+
select the root. ORFdata owns its `orfdata/` child. Release cache identities
|
|
128
|
+
include manifest/reference and content hashes. SQLite partitions every table
|
|
129
|
+
by reference key, with composite foreign keys; human, mouse, dog, yeast and
|
|
130
|
+
arbitrary other taxa use the same schema. The core API needs neither pandas nor
|
|
131
|
+
PyArrow. Optional `orfdata[columnar]` enables `dataset.export_parquet(path)`;
|
|
132
|
+
query columns accompany full measurement JSON, preserving exact values and
|
|
133
|
+
provenance even when a floating-point analysis column cannot represent an
|
|
134
|
+
integer exactly.
|
|
@@ -12,7 +12,10 @@ from .catalog import (
|
|
|
12
12
|
load_catalog,
|
|
13
13
|
load_uorf_catalog,
|
|
14
14
|
)
|
|
15
|
+
from .dataset import QuantitativeDataset
|
|
16
|
+
from .datasets import DatasetRegistry, build_dataset
|
|
15
17
|
from .initiation_context import KOZAK_PREFERENCES, InitiationContext, annotate_initiation_context
|
|
18
|
+
from .measurements import Assay, DatasetManifest, Measurement, Sample, SourceAsset, TargetMapping
|
|
16
19
|
from .model import ORFModel, ReferenceORF
|
|
17
20
|
from .reference import ReferenceContext, ScopedTarget
|
|
18
21
|
from .version import __version__
|
|
@@ -28,6 +31,15 @@ __all__ = [
|
|
|
28
31
|
"ReferenceORF",
|
|
29
32
|
"ReferenceContext",
|
|
30
33
|
"ScopedTarget",
|
|
34
|
+
"QuantitativeDataset",
|
|
35
|
+
"DatasetRegistry",
|
|
36
|
+
"build_dataset",
|
|
37
|
+
"DatasetManifest",
|
|
38
|
+
"SourceAsset",
|
|
39
|
+
"Sample",
|
|
40
|
+
"Assay",
|
|
41
|
+
"Measurement",
|
|
42
|
+
"TargetMapping",
|
|
31
43
|
"load_catalog",
|
|
32
44
|
"UORFCatalog",
|
|
33
45
|
"UORFRecord",
|
|
@@ -11,7 +11,14 @@ from .catalog import EVIDENCE_METHODS, UORFCatalog, load_catalog
|
|
|
11
11
|
|
|
12
12
|
def run(argv=None, prog=None, *, catalog_loader=load_catalog):
|
|
13
13
|
"""Export reference records as JSON, TSV or FASTA."""
|
|
14
|
-
|
|
14
|
+
argv = list(sys.argv[1:] if argv is None else argv)
|
|
15
|
+
if argv and argv[0] == "datasets":
|
|
16
|
+
from .dataset_cli import run as dataset_run
|
|
17
|
+
|
|
18
|
+
return dataset_run(argv[1:], prog=(prog or "orfdata") + " datasets")
|
|
19
|
+
parser = argparse.ArgumentParser(
|
|
20
|
+
description=__doc__, prog=prog,
|
|
21
|
+
epilog="For quantitative data management, use 'orfdata datasets --help'.")
|
|
15
22
|
parser.add_argument("--catalog", help="Use a compatible local JSON/JSON.gz catalogue")
|
|
16
23
|
parser.add_argument("--gene", help="Exact gene symbol or Ensembl gene ID")
|
|
17
24
|
parser.add_argument("--transcript", help="Exact source transcript ID (unversioned)")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
"""Streaming, reference-partitioned quantitative datasets stored as SQLite."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import sqlite3
|
|
7
|
+
from contextlib import closing
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from .measurements import (
|
|
11
|
+
MEASUREMENT_STATUSES,
|
|
12
|
+
QUANTITIES,
|
|
13
|
+
Assay,
|
|
14
|
+
DatasetManifest,
|
|
15
|
+
Measurement,
|
|
16
|
+
Sample,
|
|
17
|
+
TargetMapping,
|
|
18
|
+
)
|
|
19
|
+
from .reference import ReferenceContext, ScopedTarget
|
|
20
|
+
|
|
21
|
+
_SCHEMA = """
|
|
22
|
+
CREATE TABLE metadata (name TEXT PRIMARY KEY, value TEXT NOT NULL);
|
|
23
|
+
CREATE TABLE reference (key TEXT PRIMARY KEY, record TEXT NOT NULL);
|
|
24
|
+
CREATE TABLE target (
|
|
25
|
+
reference_key TEXT NOT NULL REFERENCES reference(key),
|
|
26
|
+
key TEXT NOT NULL, kind TEXT NOT NULL, namespace TEXT NOT NULL,
|
|
27
|
+
identifier TEXT NOT NULL, PRIMARY KEY(reference_key, key));
|
|
28
|
+
CREATE TABLE sample (
|
|
29
|
+
reference_key TEXT NOT NULL REFERENCES reference(key),
|
|
30
|
+
id TEXT NOT NULL, record TEXT NOT NULL, PRIMARY KEY(reference_key, id));
|
|
31
|
+
CREATE TABLE assay (
|
|
32
|
+
reference_key TEXT NOT NULL, id TEXT NOT NULL, sample_id TEXT NOT NULL,
|
|
33
|
+
record TEXT NOT NULL, PRIMARY KEY(reference_key, id),
|
|
34
|
+
UNIQUE(reference_key, id, sample_id),
|
|
35
|
+
FOREIGN KEY(reference_key, sample_id) REFERENCES sample(reference_key, id));
|
|
36
|
+
CREATE TABLE mapping (
|
|
37
|
+
reference_key TEXT NOT NULL REFERENCES reference(key),
|
|
38
|
+
id TEXT NOT NULL, record TEXT NOT NULL, PRIMARY KEY(reference_key, id));
|
|
39
|
+
CREATE TABLE measurement (
|
|
40
|
+
reference_key TEXT NOT NULL, id TEXT NOT NULL, target_key TEXT NOT NULL,
|
|
41
|
+
sample_id TEXT NOT NULL, assay_id TEXT NOT NULL, quantity TEXT NOT NULL,
|
|
42
|
+
status TEXT NOT NULL, record TEXT NOT NULL, PRIMARY KEY(reference_key, id),
|
|
43
|
+
FOREIGN KEY(reference_key, target_key) REFERENCES target(reference_key, key),
|
|
44
|
+
FOREIGN KEY(reference_key, assay_id, sample_id)
|
|
45
|
+
REFERENCES assay(reference_key, id, sample_id));
|
|
46
|
+
CREATE TABLE input_measurement (
|
|
47
|
+
reference_key TEXT NOT NULL, output_id TEXT NOT NULL, input_id TEXT NOT NULL,
|
|
48
|
+
PRIMARY KEY(reference_key, output_id, input_id),
|
|
49
|
+
FOREIGN KEY(reference_key, output_id) REFERENCES measurement(reference_key, id),
|
|
50
|
+
FOREIGN KEY(reference_key, input_id) REFERENCES measurement(reference_key, id));
|
|
51
|
+
CREATE INDEX target_lookup ON target(reference_key, namespace, kind, identifier);
|
|
52
|
+
CREATE INDEX observation_lookup ON measurement(reference_key, target_key, sample_id, assay_id);
|
|
53
|
+
CREATE INDEX assay_lookup ON measurement(reference_key, assay_id, quantity, status);
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _json(value):
|
|
58
|
+
return json.dumps(value, sort_keys=True, separators=(",", ":"), allow_nan=False)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _target_key(target):
|
|
62
|
+
return hashlib.sha256(_json(target.key).encode()).hexdigest()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class QuantitativeDataset:
|
|
66
|
+
"""A read-only release. Constructing or querying it never downloads data.
|
|
67
|
+
|
|
68
|
+
Local IDs are unique only within one dataset release AND reference context.
|
|
69
|
+
Iterators preserve that context even when IDs collide between species.
|
|
70
|
+
Registry-managed files are checked against pinned hashes before opening.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
def __init__(self, path):
|
|
74
|
+
self.path = Path(path).resolve()
|
|
75
|
+
with closing(self._connect()) as connection:
|
|
76
|
+
if connection.execute("PRAGMA user_version").fetchone()[0] != 1:
|
|
77
|
+
raise ValueError("Unsupported quantitative database schema")
|
|
78
|
+
row = connection.execute("SELECT value FROM metadata WHERE name='manifest'").fetchone()
|
|
79
|
+
if row is None:
|
|
80
|
+
raise ValueError("Missing quantitative manifest")
|
|
81
|
+
self.manifest = DatasetManifest.from_dict(json.loads(row[0]))
|
|
82
|
+
self._references = {r.key: r for r in self.manifest.references}
|
|
83
|
+
|
|
84
|
+
def _connect(self):
|
|
85
|
+
# mode=ro prevents SQLite's default creation of an empty missing database.
|
|
86
|
+
connection = sqlite3.connect(self.path.as_uri() + "?mode=ro", uri=True)
|
|
87
|
+
connection.execute("PRAGMA query_only=ON")
|
|
88
|
+
return connection
|
|
89
|
+
|
|
90
|
+
@classmethod
|
|
91
|
+
def create(cls, path, manifest, *, samples, assays, measurements, mappings=()):
|
|
92
|
+
"""Write a new dataset from iterators, validating scope and foreign keys.
|
|
93
|
+
|
|
94
|
+
Never overwrite an existing file. An interrupted/invalid build removes
|
|
95
|
+
only the new file it created. For atomic managed publication, call this
|
|
96
|
+
inside DataCache's materialize builder. Derived observations must follow
|
|
97
|
+
their input observations, preventing cyclic or dangling provenance.
|
|
98
|
+
"""
|
|
99
|
+
if not isinstance(manifest, DatasetManifest):
|
|
100
|
+
raise TypeError("manifest must be a DatasetManifest")
|
|
101
|
+
path = Path(path)
|
|
102
|
+
descriptor = os.open(path, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600)
|
|
103
|
+
os.close(descriptor)
|
|
104
|
+
connection = None
|
|
105
|
+
try:
|
|
106
|
+
connection = sqlite3.connect(path)
|
|
107
|
+
connection.execute("PRAGMA foreign_keys=ON")
|
|
108
|
+
connection.executescript(_SCHEMA)
|
|
109
|
+
connection.execute("PRAGMA user_version=1")
|
|
110
|
+
connection.execute("INSERT INTO metadata VALUES ('manifest', ?)",
|
|
111
|
+
(_json(manifest.to_dict()),))
|
|
112
|
+
for reference in manifest.references:
|
|
113
|
+
connection.execute("INSERT INTO reference VALUES (?, ?)",
|
|
114
|
+
(reference.key, _json(reference.to_dict())))
|
|
115
|
+
for sample in samples:
|
|
116
|
+
if not isinstance(sample, Sample):
|
|
117
|
+
raise TypeError("samples must contain Samples")
|
|
118
|
+
connection.execute("INSERT INTO sample VALUES (?, ?, ?)",
|
|
119
|
+
(sample.reference.key, sample.sample_id, _json(sample.to_dict())))
|
|
120
|
+
for assay in assays:
|
|
121
|
+
if not isinstance(assay, Assay):
|
|
122
|
+
raise TypeError("assays must contain Assays")
|
|
123
|
+
if assay.kind not in manifest.assay_kinds:
|
|
124
|
+
raise ValueError("Assay kind absent from manifest coverage")
|
|
125
|
+
connection.execute("INSERT INTO assay VALUES (?, ?, ?, ?)",
|
|
126
|
+
(assay.reference.key, assay.assay_id, assay.sample_id,
|
|
127
|
+
_json(assay.to_dict())))
|
|
128
|
+
for mapping in mappings:
|
|
129
|
+
if not isinstance(mapping, TargetMapping):
|
|
130
|
+
raise TypeError("mappings must contain TargetMappings")
|
|
131
|
+
connection.execute("INSERT INTO mapping VALUES (?, ?, ?)",
|
|
132
|
+
(mapping.reference.key, mapping.mapping_id,
|
|
133
|
+
_json(mapping.to_dict())))
|
|
134
|
+
source_ids = {s.asset_id for s in manifest.sources}
|
|
135
|
+
for observation in measurements:
|
|
136
|
+
if not isinstance(observation, Measurement):
|
|
137
|
+
raise TypeError("measurements must contain Measurements")
|
|
138
|
+
if (observation.dataset_id, observation.dataset_version) != (manifest.dataset_id, manifest.version):
|
|
139
|
+
raise ValueError("Measurement belongs to a different dataset release")
|
|
140
|
+
source = observation.provenance.get("source_asset")
|
|
141
|
+
if source is not None and source not in source_ids:
|
|
142
|
+
raise ValueError("Measurement provenance names an unknown source asset")
|
|
143
|
+
if source is None and not observation.input_measurements:
|
|
144
|
+
raise ValueError("Measurement requires a source_asset or input measurements")
|
|
145
|
+
reference_key = observation.target.reference.key
|
|
146
|
+
for identifier in observation.input_measurements:
|
|
147
|
+
if connection.execute(
|
|
148
|
+
"SELECT 1 FROM measurement WHERE reference_key=? AND id=?",
|
|
149
|
+
(reference_key, identifier),
|
|
150
|
+
).fetchone() is None:
|
|
151
|
+
raise ValueError("Derived inputs must precede outputs in the same reference context")
|
|
152
|
+
target = observation.target
|
|
153
|
+
target_key = _target_key(target)
|
|
154
|
+
connection.execute("INSERT OR IGNORE INTO target VALUES (?, ?, ?, ?, ?)",
|
|
155
|
+
(reference_key, target_key, target.kind,
|
|
156
|
+
target.namespace, target.identifier))
|
|
157
|
+
record = observation.to_dict()
|
|
158
|
+
record.pop("target")
|
|
159
|
+
connection.execute("INSERT INTO measurement VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
|
160
|
+
(reference_key, observation.measurement_id, target_key,
|
|
161
|
+
observation.sample_id, observation.assay_id,
|
|
162
|
+
observation.quantity, observation.status, _json(record)))
|
|
163
|
+
connection.executemany("INSERT INTO input_measurement VALUES (?, ?, ?)",
|
|
164
|
+
((reference_key, observation.measurement_id, identifier)
|
|
165
|
+
for identifier in observation.input_measurements))
|
|
166
|
+
counts = {table: connection.execute(f"SELECT count(*) FROM {table}").fetchone()[0]
|
|
167
|
+
for table in ("sample", "assay", "mapping", "target", "measurement")}
|
|
168
|
+
connection.execute("INSERT INTO metadata VALUES ('counts', ?)", (_json(counts),))
|
|
169
|
+
connection.commit()
|
|
170
|
+
connection.close()
|
|
171
|
+
connection = None
|
|
172
|
+
return cls(path)
|
|
173
|
+
except BaseException:
|
|
174
|
+
if connection is not None:
|
|
175
|
+
connection.close()
|
|
176
|
+
path.unlink()
|
|
177
|
+
raise
|
|
178
|
+
|
|
179
|
+
def _scope(self, reference):
|
|
180
|
+
if reference is None:
|
|
181
|
+
return None
|
|
182
|
+
key = reference.key if isinstance(reference, ReferenceContext) else reference
|
|
183
|
+
if key not in self._references:
|
|
184
|
+
raise ValueError("Reference context is not present in this dataset")
|
|
185
|
+
return key
|
|
186
|
+
|
|
187
|
+
def _records(self, table, cls, *, reference=None):
|
|
188
|
+
scope = self._scope(reference)
|
|
189
|
+
query = f"SELECT record FROM {table}"
|
|
190
|
+
params = ()
|
|
191
|
+
if scope is not None:
|
|
192
|
+
query += " WHERE reference_key=?"
|
|
193
|
+
params = (scope,)
|
|
194
|
+
query += " ORDER BY reference_key, id"
|
|
195
|
+
with closing(self._connect()) as connection:
|
|
196
|
+
for row in connection.execute(query, params):
|
|
197
|
+
yield cls.from_dict(json.loads(row[0]))
|
|
198
|
+
|
|
199
|
+
def samples(self, *, reference=None):
|
|
200
|
+
return self._records("sample", Sample, reference=reference)
|
|
201
|
+
|
|
202
|
+
def assays(self, *, reference=None):
|
|
203
|
+
return self._records("assay", Assay, reference=reference)
|
|
204
|
+
|
|
205
|
+
def mappings(self, *, reference=None):
|
|
206
|
+
return self._records("mapping", TargetMapping, reference=reference)
|
|
207
|
+
|
|
208
|
+
def measurements(self, *, target=None, reference=None, sample_id=None,
|
|
209
|
+
assay_id=None, quantity=None, status=None):
|
|
210
|
+
"""Iterate native observations; a target match is exact, never orthology.
|
|
211
|
+
|
|
212
|
+
Unqualified IDs across multiple references are rejected. To browse all
|
|
213
|
+
species together, omit ID filters; each result still carries its scope.
|
|
214
|
+
"""
|
|
215
|
+
if quantity is not None and quantity not in QUANTITIES:
|
|
216
|
+
raise ValueError("Unknown measurement quantity")
|
|
217
|
+
if status is not None and status not in MEASUREMENT_STATUSES:
|
|
218
|
+
raise ValueError("Unknown measurement status")
|
|
219
|
+
scope = self._scope(reference)
|
|
220
|
+
if target is not None:
|
|
221
|
+
if not isinstance(target, ScopedTarget):
|
|
222
|
+
raise TypeError("target must be a ScopedTarget")
|
|
223
|
+
target_scope = self._scope(target.reference)
|
|
224
|
+
if scope is not None and scope != target_scope:
|
|
225
|
+
raise ValueError("Target and requested reference disagree")
|
|
226
|
+
scope = target_scope
|
|
227
|
+
if scope is None and len(self._references) > 1 and (sample_id or assay_id):
|
|
228
|
+
raise ValueError("An ID query across multiple references requires an explicit reference")
|
|
229
|
+
filters = dict(reference_key=scope, target_key=_target_key(target) if target else None,
|
|
230
|
+
sample_id=sample_id, assay_id=assay_id, quantity=quantity, status=status)
|
|
231
|
+
selected = [(key, value) for key, value in filters.items() if value is not None]
|
|
232
|
+
query = ("SELECT m.record, m.reference_key, t.kind, t.namespace, t.identifier "
|
|
233
|
+
"FROM measurement m JOIN target t ON m.reference_key=t.reference_key "
|
|
234
|
+
"AND m.target_key=t.key")
|
|
235
|
+
if selected:
|
|
236
|
+
query += " WHERE " + " AND ".join(f"m.{key}=?" for key, _ in selected)
|
|
237
|
+
query += " ORDER BY m.reference_key, m.id"
|
|
238
|
+
with closing(self._connect()) as connection:
|
|
239
|
+
for row in connection.execute(query, [value for _, value in selected]):
|
|
240
|
+
values = json.loads(row[0])
|
|
241
|
+
values["target"] = ScopedTarget(self._references[row[1]], row[2], row[4], row[3])
|
|
242
|
+
yield Measurement(**values)
|
|
243
|
+
|
|
244
|
+
@property
|
|
245
|
+
def counts(self):
|
|
246
|
+
with closing(self._connect()) as connection:
|
|
247
|
+
result = json.loads(connection.execute(
|
|
248
|
+
"SELECT value FROM metadata WHERE name='counts'").fetchone()[0])
|
|
249
|
+
return result
|
|
250
|
+
|
|
251
|
+
def export_parquet(self, path, **query):
|
|
252
|
+
"""Export query results in bounded batches; requires orfdata[columnar]."""
|
|
253
|
+
try:
|
|
254
|
+
import pyarrow as pa
|
|
255
|
+
import pyarrow.parquet as pq
|
|
256
|
+
except ImportError as exc:
|
|
257
|
+
raise ImportError("Install orfdata[columnar] to export Parquet") from exc
|
|
258
|
+
schema = pa.schema([(name, pa.string()) for name in
|
|
259
|
+
("dataset_id", "version", "reference_key", "measurement_id",
|
|
260
|
+
"sample_id", "assay_id", "target_kind", "target_namespace",
|
|
261
|
+
"target_identifier", "quantity", "unit", "status", "origin",
|
|
262
|
+
"scale", "measurement_json")] + [("value_float64", pa.float64())])
|
|
263
|
+
with pq.ParquetWriter(path, schema) as writer:
|
|
264
|
+
batch = []
|
|
265
|
+
for value in self.measurements(**query):
|
|
266
|
+
batch.append(dict(dataset_id=self.manifest.dataset_id, version=self.manifest.version,
|
|
267
|
+
reference_key=value.target.reference.key,
|
|
268
|
+
measurement_id=value.measurement_id,
|
|
269
|
+
sample_id=value.sample_id, assay_id=value.assay_id,
|
|
270
|
+
target_kind=value.target.kind,
|
|
271
|
+
target_namespace=value.target.namespace,
|
|
272
|
+
target_identifier=value.target.identifier, quantity=value.quantity,
|
|
273
|
+
unit=value.unit, status=value.status, origin=value.origin,
|
|
274
|
+
scale=value.scale,
|
|
275
|
+
value_float64=float(value.value) if value.value is not None else None,
|
|
276
|
+
measurement_json=_json(value.to_dict())))
|
|
277
|
+
if len(batch) == 4096:
|
|
278
|
+
writer.write_table(pa.Table.from_pylist(batch, schema=schema))
|
|
279
|
+
batch = []
|
|
280
|
+
if batch:
|
|
281
|
+
writer.write_table(pa.Table.from_pylist(batch, schema=schema))
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Discover, download, inspect and query quantitative datasets."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import sqlite3
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from .datasets import DatasetRegistry
|
|
9
|
+
from .reference import ScopedTarget
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def run(argv=None, prog=None):
|
|
13
|
+
parser = argparse.ArgumentParser(description=__doc__, prog=prog)
|
|
14
|
+
parser.add_argument("--registry", help="Local registry JSON (otherwise use packaged releases)")
|
|
15
|
+
parser.add_argument("--cache-root", help="Override the OpenVax data cache root")
|
|
16
|
+
commands = parser.add_subparsers(dest="command", required=True)
|
|
17
|
+
listing = commands.add_parser("list", help="List published releases and assay coverage")
|
|
18
|
+
listing.add_argument("--species")
|
|
19
|
+
listing.add_argument("--taxon-id", type=int)
|
|
20
|
+
listing.add_argument("--assay")
|
|
21
|
+
for command in ("info", "download", "inspect", "query"):
|
|
22
|
+
subparser = commands.add_parser(command)
|
|
23
|
+
subparser.add_argument("name")
|
|
24
|
+
subparser.add_argument("--version")
|
|
25
|
+
if command == "download":
|
|
26
|
+
subparser.add_argument("--force", action="store_true")
|
|
27
|
+
subparser.add_argument("--timeout", type=float)
|
|
28
|
+
subparser.add_argument("--quiet", action="store_true")
|
|
29
|
+
subparser.add_argument("--source-path", help="Use a local dataset.sqlite with the registered hash")
|
|
30
|
+
if command == "query":
|
|
31
|
+
subparser.add_argument("--reference", help="Exact ref:SHA256 context key")
|
|
32
|
+
subparser.add_argument("--sample")
|
|
33
|
+
subparser.add_argument("--assay-id")
|
|
34
|
+
subparser.add_argument("--quantity")
|
|
35
|
+
subparser.add_argument("--status")
|
|
36
|
+
subparser.add_argument("--target")
|
|
37
|
+
subparser.add_argument("--kind")
|
|
38
|
+
subparser.add_argument("--namespace")
|
|
39
|
+
args = parser.parse_args(argv)
|
|
40
|
+
try:
|
|
41
|
+
definitions = None
|
|
42
|
+
if args.registry:
|
|
43
|
+
with open(args.registry) as handle:
|
|
44
|
+
definitions = json.load(handle)
|
|
45
|
+
registry = DatasetRegistry(definitions, cache_root=args.cache_root)
|
|
46
|
+
if args.command == "list":
|
|
47
|
+
result = [m.to_dict() for m in registry.list(
|
|
48
|
+
species=args.species, taxon_id=args.taxon_id, assay=args.assay)]
|
|
49
|
+
elif args.command == "info":
|
|
50
|
+
result = registry.info(args.name, args.version).to_dict()
|
|
51
|
+
result["download"] = registry.download_metadata(args.name, args.version)
|
|
52
|
+
elif args.command == "inspect":
|
|
53
|
+
inspected = registry.inspect(args.name, args.version)
|
|
54
|
+
result = dict(status=inspected.status, path=str(inspected.path),
|
|
55
|
+
error=str(inspected.error) if inspected.error else None)
|
|
56
|
+
elif args.command == "download":
|
|
57
|
+
options = dict(show_progress=not args.quiet)
|
|
58
|
+
if args.timeout is not None:
|
|
59
|
+
options["timeout"] = args.timeout
|
|
60
|
+
sources = {"dataset.sqlite": args.source_path} if args.source_path else None
|
|
61
|
+
result = registry.download(args.name, args.version, force=args.force,
|
|
62
|
+
source_paths=sources, **options)
|
|
63
|
+
else:
|
|
64
|
+
dataset = registry.open(args.name, args.version)
|
|
65
|
+
target = None
|
|
66
|
+
if any((args.target, args.kind, args.namespace)):
|
|
67
|
+
if not all((args.target, args.kind, args.namespace, args.reference)):
|
|
68
|
+
parser.error("Target queries require --target, --kind, --namespace and --reference")
|
|
69
|
+
references = {r.key: r for r in dataset.manifest.references}
|
|
70
|
+
target = ScopedTarget(references[args.reference], args.kind, args.target, args.namespace)
|
|
71
|
+
for measurement in dataset.measurements(
|
|
72
|
+
target=target, reference=args.reference, sample_id=args.sample,
|
|
73
|
+
assay_id=args.assay_id, quantity=args.quantity, status=args.status,
|
|
74
|
+
):
|
|
75
|
+
sys.stdout.write(json.dumps(measurement.to_dict()) + "\n")
|
|
76
|
+
return
|
|
77
|
+
json.dump(result, sys.stdout, indent=2)
|
|
78
|
+
sys.stdout.write("\n")
|
|
79
|
+
except (ValueError, TypeError, KeyError, OSError, ImportError, sqlite3.Error) as exc:
|
|
80
|
+
parser.error(str(exc))
|