seqforge 2026.7.1__py3-none-any.whl

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.
Files changed (124) hide show
  1. seqforge/__init__.py +16 -0
  2. seqforge/cli/__init__.py +38 -0
  3. seqforge/cli/__main__.py +8 -0
  4. seqforge/cli/_common.py +105 -0
  5. seqforge/cli/compose.py +119 -0
  6. seqforge/cli/eval.py +103 -0
  7. seqforge/cli/harvest.py +417 -0
  8. seqforge/cli/hook.py +247 -0
  9. seqforge/cli/io.py +502 -0
  10. seqforge/cli/kb.py +348 -0
  11. seqforge/cli/manifest.py +536 -0
  12. seqforge/cli/probe.py +43 -0
  13. seqforge/cli/processing.py +192 -0
  14. seqforge/cli/project.py +52 -0
  15. seqforge/cli/resolve.py +55 -0
  16. seqforge/cli/root.py +66 -0
  17. seqforge/cli/run.py +463 -0
  18. seqforge/cli/schema.py +41 -0
  19. seqforge/compose/__init__.py +28 -0
  20. seqforge/compose/core.py +515 -0
  21. seqforge/compose/gates.py +113 -0
  22. seqforge/compose/params.py +447 -0
  23. seqforge/e2e.py +1926 -0
  24. seqforge/evals/__init__.py +78 -0
  25. seqforge/evals/case.py +382 -0
  26. seqforge/evals/grade.py +300 -0
  27. seqforge/evals/run.py +420 -0
  28. seqforge/harvest/__init__.py +121 -0
  29. seqforge/harvest/extract.py +319 -0
  30. seqforge/harvest/fields.py +212 -0
  31. seqforge/harvest/normalize.py +537 -0
  32. seqforge/harvest/prep.py +41 -0
  33. seqforge/harvest/providers.py +321 -0
  34. seqforge/harvest/verify.py +251 -0
  35. seqforge/hooks/__init__.py +33 -0
  36. seqforge/hooks/guards.py +214 -0
  37. seqforge/io/__init__.py +61 -0
  38. seqforge/io/archive.py +450 -0
  39. seqforge/io/attributes.py +190 -0
  40. seqforge/io/biosample/attributes.json +6341 -0
  41. seqforge/io/efo/labels.json +55 -0
  42. seqforge/io/efo.py +138 -0
  43. seqforge/io/onlist.py +661 -0
  44. seqforge/io/onlists/3M-february-2018.codes.gz +0 -0
  45. seqforge/io/onlists/737K-arc-v1.codes.gz +0 -0
  46. seqforge/io/onlists/737K-august-2016.codes.gz +0 -0
  47. seqforge/io/onlists/bd-rhapsody-cls1-384.codes.gz +0 -0
  48. seqforge/io/onlists/bd-rhapsody-cls1.codes.gz +0 -0
  49. seqforge/io/onlists/bd-rhapsody-cls2-384.codes.gz +0 -0
  50. seqforge/io/onlists/bd-rhapsody-cls2.codes.gz +0 -0
  51. seqforge/io/onlists/bd-rhapsody-cls3-384.codes.gz +0 -0
  52. seqforge/io/onlists/bd-rhapsody-cls3.codes.gz +0 -0
  53. seqforge/io/onlists/index.json +74 -0
  54. seqforge/io/remote.py +659 -0
  55. seqforge/io/taxonomy.py +194 -0
  56. seqforge/kb/__init__.py +62 -0
  57. seqforge/kb/anchor.py +169 -0
  58. seqforge/kb/generate.py +147 -0
  59. seqforge/kb/loader.py +152 -0
  60. seqforge/kb/roundtrip.py +112 -0
  61. seqforge/kb/schema.py +422 -0
  62. seqforge/kb/specs/10x-3p-gex/spec.yaml +62 -0
  63. seqforge/kb/specs/10x-3p-gex-v2/README.md +41 -0
  64. seqforge/kb/specs/10x-3p-gex-v2/spec.yaml +83 -0
  65. seqforge/kb/specs/10x-3p-gex-v3/README.md +56 -0
  66. seqforge/kb/specs/10x-3p-gex-v3/spec.yaml +118 -0
  67. seqforge/kb/specs/10x-3p-gex-v3.1/README.md +56 -0
  68. seqforge/kb/specs/10x-3p-gex-v3.1/spec.yaml +124 -0
  69. seqforge/kb/specs/bd-rhapsody-wta/README.md +103 -0
  70. seqforge/kb/specs/bd-rhapsody-wta/spec.yaml +130 -0
  71. seqforge/kb/specs/bd-rhapsody-wta-enhanced/spec.yaml +99 -0
  72. seqforge/kb/specs/bd-rhapsody-wta-enhanced-v1/spec.yaml +93 -0
  73. seqforge/kb/specs/bd-rhapsody-wta-enhanced-v2/spec.yaml +81 -0
  74. seqforge/kb/specs/bulk-rnaseq-pe/README.md +35 -0
  75. seqforge/kb/specs/bulk-rnaseq-pe/spec.yaml +97 -0
  76. seqforge/kb/specs/splitseq/README.md +51 -0
  77. seqforge/kb/specs/splitseq/spec.yaml +157 -0
  78. seqforge/manifest/__init__.py +61 -0
  79. seqforge/manifest/fill.py +531 -0
  80. seqforge/manifest/hash.py +77 -0
  81. seqforge/manifest/instruct.py +114 -0
  82. seqforge/manifest/policy.py +409 -0
  83. seqforge/manifest/validate.py +274 -0
  84. seqforge/models/__init__.py +268 -0
  85. seqforge/models/assertion.py +68 -0
  86. seqforge/models/base.py +100 -0
  87. seqforge/models/blocker.py +71 -0
  88. seqforge/models/conflict.py +47 -0
  89. seqforge/models/dataset.py +320 -0
  90. seqforge/models/evidenced.py +54 -0
  91. seqforge/models/observation.py +157 -0
  92. seqforge/models/processing.py +231 -0
  93. seqforge/models/records.py +145 -0
  94. seqforge/models/resolve.py +216 -0
  95. seqforge/probe/__init__.py +46 -0
  96. seqforge/probe/core.py +232 -0
  97. seqforge/probe/signals.py +250 -0
  98. seqforge/probe/streaming.py +118 -0
  99. seqforge/project.py +177 -0
  100. seqforge/py.typed +0 -0
  101. seqforge/resolve/__init__.py +98 -0
  102. seqforge/resolve/assign.py +204 -0
  103. seqforge/resolve/cache.py +119 -0
  104. seqforge/resolve/confuse.py +215 -0
  105. seqforge/resolve/engine.py +646 -0
  106. seqforge/resolve/escalate.py +668 -0
  107. seqforge/resolve/evaluators.py +306 -0
  108. seqforge/resolve/geometry.py +89 -0
  109. seqforge/resolve/group.py +85 -0
  110. seqforge/resolve/records.py +550 -0
  111. seqforge/resolve/scoring.py +373 -0
  112. seqforge/resolve/window.py +206 -0
  113. seqforge/workflows/__init__.py +234 -0
  114. seqforge/workflows/cram.py +117 -0
  115. seqforge/workflows/h5ad.py +368 -0
  116. seqforge/workflows/map/star.smk +101 -0
  117. seqforge/workflows/map/starsolo.smk +360 -0
  118. seqforge/workflows/qc.py +157 -0
  119. seqforge/workspace.py +125 -0
  120. seqforge-2026.7.1.dist-info/METADATA +125 -0
  121. seqforge-2026.7.1.dist-info/RECORD +124 -0
  122. seqforge-2026.7.1.dist-info/WHEEL +4 -0
  123. seqforge-2026.7.1.dist-info/entry_points.txt +2 -0
  124. seqforge-2026.7.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,55 @@
1
+ {
2
+ "fetched": "2026-07-16",
3
+ "source": "https://www.ebi.ac.uk/ols4/api/ontologies/efo/terms/",
4
+ "terms": {
5
+ "EFO:0008896": {
6
+ "iri": "http://www.ebi.ac.uk/efo/EFO_0008896",
7
+ "name": "RNA-Seq",
8
+ "synonyms": []
9
+ },
10
+ "EFO:0009899": {
11
+ "iri": "http://www.ebi.ac.uk/efo/EFO_0009899",
12
+ "name": "10x 3' v2",
13
+ "synonyms": [
14
+ "10X 3' v2",
15
+ "10X 3' v2 sequencing",
16
+ "10X v2",
17
+ "10X v2 sequencing",
18
+ "10XV2",
19
+ "10x 3' v2 sequencing",
20
+ "10x v2",
21
+ "10x v2 sequencing",
22
+ "10x_v2"
23
+ ]
24
+ },
25
+ "EFO:0009919": {
26
+ "iri": "http://www.ebi.ac.uk/efo/EFO_0009919",
27
+ "name": "SPLiT-seq",
28
+ "synonyms": [
29
+ "split-pool ligation-based transcriptome sequencing"
30
+ ]
31
+ },
32
+ "EFO:0009922": {
33
+ "iri": "http://www.ebi.ac.uk/efo/EFO_0009922",
34
+ "name": "10x 3' v3",
35
+ "synonyms": [
36
+ "10X 3' v3",
37
+ "10X v3",
38
+ "10X v3 sequencing",
39
+ "10XV3",
40
+ "10x 3' v3 sequencing",
41
+ "10x v3",
42
+ "10x v3 sequencing"
43
+ ]
44
+ },
45
+ "EFO:0022980": {
46
+ "iri": "http://www.ebi.ac.uk/efo/EFO_0022980",
47
+ "name": "10x 3' v3.1",
48
+ "synonyms": [
49
+ "10X v3.1",
50
+ "10X v3.1 sequencing",
51
+ "10x 3' v3.1 sequencing"
52
+ ]
53
+ }
54
+ }
55
+ }
seqforge/io/efo.py ADDED
@@ -0,0 +1,138 @@
1
+ """EFO assay labels — what ``EFO:0009922`` is called, in the words of the people who own the term.
2
+
3
+ ``assay: EFO:0009922`` is good standardization and unreadable to a human, which was the first thing
4
+ the pilot's manifest got told off for. The fix is a name, and the name is not ours to write: it comes
5
+ from EFO via EBI's OLS4, is generated into ``efo/labels.json`` by ``seqforge io efo refresh``, and is
6
+ never typed by hand. The KB's ``spec.yaml`` files each carry a comment saying which term they mean —
7
+ comments are not checked, and a comment claiming a label is exactly the hand-maintained contract that
8
+ rots. ``kb lint`` now reads *this* file instead.
9
+
10
+ Shipped rather than fetched for the same reason the BioSample vocabulary is: a manifest must be
11
+ fillable on a compute node with no internet. Five terms, so the file is tiny; it grows by one entry
12
+ per KB technology, which is the rate at which anything here grows.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ from dataclasses import dataclass
19
+ from functools import cache
20
+ from pathlib import Path
21
+
22
+ #: EBI's Ontology Lookup Service, v4. The term IRI is **double**-URL-encoded in the path — OLS4
23
+ #: returns 404 for a singly-encoded one, which is the kind of detail that belongs in code and a test
24
+ #: rather than in somebody's memory.
25
+ OLS4_TERMS = "https://www.ebi.ac.uk/ols4/api/ontologies/efo/terms/"
26
+
27
+ #: How a CURIE becomes an IRI. EFO's own namespace; the OBO-style terms EFO borrows (UBERON, CL) use
28
+ #: a different one, which is why this is not a general CURIE expander and does not pretend to be.
29
+ EFO_IRI_BASE = "http://www.ebi.ac.uk/efo/"
30
+
31
+ _DATA = Path(__file__).parent / "efo" / "labels.json"
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class EfoTerm:
36
+ """One ontology term: its id, its label, and what else it is called."""
37
+
38
+ curie: str
39
+ name: str
40
+ iri: str
41
+ synonyms: tuple[str, ...] = ()
42
+
43
+
44
+ class TermUnknown(KeyError):
45
+ """A CURIE with no shipped label. Never a silently blank name."""
46
+
47
+
48
+ @cache
49
+ def load_terms() -> dict[str, EfoTerm]:
50
+ if not _DATA.is_file():
51
+ return {}
52
+ doc = json.loads(_DATA.read_text(encoding="utf-8"))
53
+ return {
54
+ curie: EfoTerm(
55
+ curie=curie,
56
+ name=str(meta["name"]),
57
+ iri=str(meta.get("iri", "")),
58
+ synonyms=tuple(meta.get("synonyms", ())),
59
+ )
60
+ for curie, meta in sorted(doc["terms"].items())
61
+ }
62
+
63
+
64
+ def has_term(curie: str) -> bool:
65
+ return curie in load_terms()
66
+
67
+
68
+ def term(curie: str) -> EfoTerm:
69
+ """The term ``curie`` names. Raises rather than returning a blank: an unlabelled assay is a bug.
70
+
71
+ It means a KB spec declares a CURIE nobody fetched a label for, which ``kb lint`` refuses — so
72
+ reaching this raise means the lint was skipped, and a manifest with an empty assay name would be
73
+ a worse outcome than a loud one.
74
+ """
75
+ try:
76
+ return load_terms()[curie]
77
+ except KeyError as exc:
78
+ raise TermUnknown(
79
+ f"no shipped EFO label for {curie!r}. Fetch it with `seqforge io efo refresh {curie}` "
80
+ f"and commit `io/efo/labels.json` — the label comes from EFO, never from us."
81
+ ) from exc
82
+
83
+
84
+ def iri_for(curie: str) -> str:
85
+ """``EFO:0009922`` -> ``http://www.ebi.ac.uk/efo/EFO_0009922``."""
86
+ return EFO_IRI_BASE + curie.replace(":", "_")
87
+
88
+
89
+ def parse_ols4_term(payload: dict[str, object]) -> EfoTerm:
90
+ """OLS4's JSON -> an :class:`EfoTerm`. The only reader of that shape.
91
+
92
+ An obsolete term is refused rather than shipped: EFO deprecates assay terms (it has replaced 10x
93
+ ids before), and silently pinning a manifest's vocabulary to a dead one is how a corpus ends up
94
+ self-consistent and wrong.
95
+ """
96
+ if payload.get("is_obsolete"):
97
+ raise TermUnknown(
98
+ f"EFO says {payload.get('short_form')!r} is obsolete. Refusing to ship it as an assay "
99
+ f"label: find the term that replaced it."
100
+ )
101
+ label = payload.get("label")
102
+ iri = payload.get("iri")
103
+ if not isinstance(label, str) or not isinstance(iri, str):
104
+ raise TermUnknown(f"OLS4 returned no label/iri for {payload.get('short_form')!r}")
105
+ short = str(payload.get("short_form") or "").replace("_", ":")
106
+ raw = payload.get("synonyms")
107
+ synonyms = tuple(sorted(s for s in raw if isinstance(s, str))) if isinstance(raw, list) else ()
108
+ return EfoTerm(curie=short, name=label, iri=iri, synonyms=synonyms)
109
+
110
+
111
+ def write_terms(terms: dict[str, EfoTerm], *, fetched: str) -> Path:
112
+ """The ONLY writer of ``efo/labels.json``, so it cannot drift from EFO by hand."""
113
+ _DATA.parent.mkdir(parents=True, exist_ok=True)
114
+ doc = {
115
+ "source": OLS4_TERMS,
116
+ "fetched": fetched,
117
+ "terms": {
118
+ t.curie: {"name": t.name, "iri": t.iri, "synonyms": list(t.synonyms)}
119
+ for t in sorted(terms.values(), key=lambda x: x.curie)
120
+ },
121
+ }
122
+ _DATA.write_text(json.dumps(doc, indent=1, sort_keys=True, ensure_ascii=False) + "\n")
123
+ load_terms.cache_clear()
124
+ return _DATA
125
+
126
+
127
+ __all__ = [
128
+ "OLS4_TERMS",
129
+ "EFO_IRI_BASE",
130
+ "EfoTerm",
131
+ "TermUnknown",
132
+ "load_terms",
133
+ "has_term",
134
+ "term",
135
+ "iri_for",
136
+ "parse_ols4_term",
137
+ "write_terms",
138
+ ]