baseqdb 0.9.0__tar.gz → 0.10.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: baseqdb
3
- Version: 0.9.0
3
+ Version: 0.10.0
4
4
  Summary: Your baSeq datasets, analyses and results from Python, with lamindb's names
5
5
  Project-URL: Homepage, https://baseq.bio
6
6
  Project-URL: Documentation, https://baseq.bio/docs/python-and-r.html
@@ -34,6 +34,8 @@ bq.Analysis.filter(kind="bulk").to_dataframe() # the analyses you can open, new
34
34
  ds = bq.Dataset.get("kTIqMTTm89P2gbNU0000") # one version, by its uid
35
35
  folder = ds.cache() # its files, downloaded once
36
36
  sheet = ds.sample_sheet # a DataFrame
37
+ bq.Gene.standardize(["ENSG00000109906.15"]) # ['ZBTB16']: genes named as baSeq names them
38
+ genes = bq.Gene.df("human") # every gene: id, symbol, biotype
37
39
 
38
40
  analysis = ds.analyses[0]
39
41
  analysis.steps.to_dataframe() # each step, its settings, when it ran
@@ -13,6 +13,8 @@ bq.Analysis.filter(kind="bulk").to_dataframe() # the analyses you can open, new
13
13
  ds = bq.Dataset.get("kTIqMTTm89P2gbNU0000") # one version, by its uid
14
14
  folder = ds.cache() # its files, downloaded once
15
15
  sheet = ds.sample_sheet # a DataFrame
16
+ bq.Gene.standardize(["ENSG00000109906.15"]) # ['ZBTB16']: genes named as baSeq names them
17
+ genes = bq.Gene.df("human") # every gene: id, symbol, biotype
16
18
 
17
19
  analysis = ds.analyses[0]
18
20
  analysis.steps.to_dataframe() # each step, its settings, when it ran
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "baseqdb"
7
- version = "0.9.0"
7
+ version = "0.10.0"
8
8
  description = "Your baSeq datasets, analyses and results from Python, with lamindb's names"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -8,6 +8,7 @@ Python, with lamindb's names; and, with a key that may add data, new datasets an
8
8
  ds = bq.Dataset.get("kTIqMTTm89P2gbNU0000") # one version, by its uid
9
9
  folder = ds.cache() # its files, downloaded once
10
10
  bq.Dataset(["counts.tsv"], name=ds.name, kind="bulk").save() # its next version
11
+ bq.Gene.standardize(["ENSG00000109906.15", "fkbp5"]) # ['ZBTB16', 'FKBP5'], as baSeq names them
11
12
 
12
13
  bq.track() # this script as one run: what it reads, adds and saves
13
14
  bq.Record("dge.csv").save() # a file of the run
@@ -18,9 +19,10 @@ with it, what it found said at the end. `baseqdb init` writes that guide into a
18
19
  Docs: https://baseq.bio/docs/python-and-r.html
19
20
  """
20
21
  from baseqdb._client import VERSION as __version__, BaseqError, login, me
22
+ from baseqdb._genes import Gene
21
23
  from baseqdb._records import (Analysis, Dataset, File, Files, Finding, GeneHit, Record, Results, Run, Step, Table,
22
24
  Tables)
23
25
  from baseqdb._track import finish, track
24
26
 
25
27
  __all__ = ["login", "me", "BaseqError", "Dataset", "File", "Files", "Analysis", "Step", "Finding", "GeneHit", "Table",
26
- "Tables", "Record", "Run", "Results", "track", "finish", "__version__"]
28
+ "Tables", "Record", "Run", "Results", "Gene", "track", "finish", "__version__"]
@@ -9,7 +9,7 @@ from pathlib import Path
9
9
  import httpx
10
10
 
11
11
  DEFAULT_URL = "https://baseq.bio"
12
- VERSION = "0.9.0"
12
+ VERSION = "0.10.0"
13
13
  RUN_HEADER = "X-Baseq-Run"
14
14
  RUN_FILE = Path(".baseqdb") / "run" # an agent's task, begun with `baseqdb track`, for every command in the folder
15
15
  _state: dict = {"client": None, "base": None, "run": None}
@@ -231,6 +231,29 @@ def download(path: str, target: Path, sha256: str | None = None, read: dict | No
231
231
  return target
232
232
 
233
233
 
234
+ def download_current(path: str, target: Path, **params) -> Path:
235
+ """``path`` saved as ``target``, and again only when baSeq has changed it: the copy's ETag goes with the request,
236
+ and the copy stays when the answer is 304 (Not Modified)."""
237
+ tag_file = target.with_name(target.name + ".etag")
238
+ headers = {}
239
+ if target.is_file() and tag_file.is_file():
240
+ headers["If-None-Match"] = tag_file.read_text(encoding="utf-8").strip()
241
+ http, base = _http()
242
+ response = http.get(base + path, params={k: v for k, v in params.items() if v is not None}, headers=headers)
243
+ if response.status_code == 304:
244
+ return target
245
+ _raise(response)
246
+ target.parent.mkdir(parents=True, exist_ok=True)
247
+ part = target.with_name(target.name + ".part")
248
+ part.write_bytes(response.content)
249
+ os.replace(part, target)
250
+ if response.headers.get("ETag"):
251
+ tag_file.write_text(response.headers["ETag"], encoding="utf-8")
252
+ else:
253
+ tag_file.unlink(missing_ok=True)
254
+ return target
255
+
256
+
234
257
  def cache_dir() -> Path:
235
258
  """Where downloads are kept: BASEQDB_CACHE, or ~/.cache/baseqdb."""
236
259
  return Path(os.environ.get("BASEQDB_CACHE") or Path.home() / ".cache" / "baseqdb")
@@ -0,0 +1,72 @@
1
+ """Gene names, with bionty's names: ``Gene.standardize`` gives a table's genes their symbols (or Ensembl ids) as baSeq
2
+ names them, and ``Gene.df`` is every gene of one organism, kept on this computer. Both from the gene reference baSeq
3
+ uses: Ensembl release 114, for human, mouse, cattle, zebrafish and salmon."""
4
+ from __future__ import annotations
5
+
6
+ import re
7
+
8
+ from baseqdb import _client as _c
9
+ from baseqdb._records import _pandas
10
+
11
+ PART = 50_000 # names a request, the most the API takes
12
+ _LONGEST = 120 # no gene's name is longer: a longer one is kept as written without asking
13
+ RETURN_FIELDS = ("symbol", "ensembl_gene_id")
14
+
15
+
16
+ def _text(value) -> str | None:
17
+ """A name as text, or None for a missing value (None, NaN, pandas' NA)."""
18
+ if value is None:
19
+ return None
20
+ try:
21
+ if value != value:
22
+ return None
23
+ except TypeError:
24
+ return None
25
+ return str(value)
26
+
27
+
28
+ class Gene:
29
+ """Genes as baSeq's gene reference has them (Ensembl 114: human, mouse, cattle, zebrafish and salmon)."""
30
+
31
+ @staticmethod
32
+ def standardize(names, return_field: str = "symbol", return_mapper: bool = False, organism: str | None = None):
33
+ """Each name as baSeq names the gene, as bionty's ``Gene.standardize``: its symbol, or with
34
+ ``return_field="ensembl_gene_id"`` its Ensembl id. A name may be an Ensembl id with or without its version
35
+ (ENSG00000109906.15 is ZBTB16), a symbol in any case, or another of its names; a symbol several genes share
36
+ means the one on the primary assembly. A name the reference does not have, or one that several genes share,
37
+ is kept as written. The names tell their organism, as an analysis's genes do; ``organism`` ("human",
38
+ "mouse", "cattle", "zebrafish" or "salmon") says it when they cannot.
39
+
40
+ A list in the order of ``names`` (a list, a pandas Index or Series; one name gives one name), or with
41
+ ``return_mapper=True`` a dict of the names that changed."""
42
+ if return_field not in RETURN_FIELDS:
43
+ raise ValueError(f"return_field is {' or '.join(map(repr, RETURN_FIELDS))}, not {return_field!r}")
44
+ single = isinstance(names, str)
45
+ given = [names] if single else list(names)
46
+ texts = [_text(v) for v in given]
47
+ asked = [t for t in dict.fromkeys(texts) if t and len(t) <= _LONGEST]
48
+ named: dict[str, str] = {}
49
+ for i in range(0, len(asked), PART):
50
+ part = asked[i:i + PART]
51
+ answer = _c.post("/genes/standardize", json={"names": part, "organism": organism,
52
+ "return_field": return_field})
53
+ organism = organism or answer["organism"] or None # the later parts as the first was told
54
+ named.update(zip(part, answer["names"]))
55
+ changed = [named[t] if t in named and named[t] != t else None for t in texts]
56
+ if return_mapper:
57
+ return {v: new for v, new in zip(given, changed) if new is not None}
58
+ out = [v if new is None else new for v, new in zip(given, changed)]
59
+ return out[0] if single else out
60
+
61
+ @staticmethod
62
+ def df(organism: str):
63
+ """Every gene of one organism baSeq's reference has, as a pandas DataFrame, as bionty's
64
+ ``Gene.public(organism).df()``: ensembl_gene_id, symbol (missing when Ensembl gives none), biotype, and
65
+ primary_assembly (False for a copy on a haplotype or a patch). Downloaded once and kept in baseqdb's cache
66
+ (genes/), for as long as the reference stays the same."""
67
+ pd = _pandas()
68
+ key = re.sub(r"[^a-z0-9]+", "_", str(organism).strip().lower()).strip("_") or "organism"
69
+ path = _c.download_current("/genes", _c.cache_dir() / "genes" / f"{key}.tsv", organism=organism)
70
+ table = pd.read_csv(path, sep="\t", dtype=str, keep_default_na=False, na_values=[""])
71
+ table["primary_assembly"] = table["primary_assembly"] == "1"
72
+ return table
@@ -60,6 +60,12 @@ data this way before asking the person which dataset they mean. In R, the same n
60
60
  `Dataset$get(uid)$cache()`, `ds$sample_sheet`, `Dataset$filter(condition = "dexamethasone")`, `Dataset$search("dex")`,
61
61
  `Analysis$filter()`.
62
62
 
63
+ ## Gene names
64
+
65
+ Count tables often name genes by their Ensembl id. `bq.Gene.standardize(names)` gives each its symbol as baSeq names
66
+ it (with `return_field="ensembl_gene_id"`, the other way round), and `bq.Gene.df("human")` is every gene of an
67
+ organism; in R, `Gene$standardize()` and `Gene$df()`. A finding names its genes by symbol, as `--gene` does.
68
+
63
69
  ## Adding
64
70
 
65
71
  A new version of a dataset: the name and kind of one the person has, with a key that may add data.
File without changes
File without changes
File without changes