orfdata 0.1.0__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.
orfdata/cli.py ADDED
@@ -0,0 +1,156 @@
1
+ """Query and export the offline ORF evidence reference."""
2
+
3
+ import argparse
4
+ import csv
5
+ import json
6
+ import re
7
+ import sys
8
+
9
+ from .catalog import EVIDENCE_METHODS, UORFCatalog, load_catalog
10
+
11
+
12
+ def run(argv=None, prog=None, *, catalog_loader=load_catalog):
13
+ """Export reference records as JSON, TSV or FASTA."""
14
+ parser = argparse.ArgumentParser(description=__doc__, prog=prog)
15
+ parser.add_argument("--catalog", help="Use a compatible local JSON/JSON.gz catalogue")
16
+ parser.add_argument("--gene", help="Exact gene symbol or Ensembl gene ID")
17
+ parser.add_argument("--transcript", help="Exact source transcript ID (unversioned)")
18
+ parser.add_argument("--orf-id", help="Exact catalogue ORF identifier")
19
+ parser.add_argument("--evidence", choices=EVIDENCE_METHODS)
20
+ parser.add_argument(
21
+ "--require-unique", action="store_true", help="Require one protein mapping in the MS source"
22
+ )
23
+ parser.add_argument(
24
+ "--require-reviewed",
25
+ action="store_true",
26
+ help="Require convincing reviewed spectra (benchmark scores 4-5)",
27
+ )
28
+ parser.add_argument("--biotype", help="Source ORF class, e.g. uORF, lncRNA, intORF, dORF")
29
+ parser.add_argument("--region", help="1-based inclusive interval: chr7:66205483-66205522")
30
+ parser.add_argument(
31
+ "--position", help="Map a 1-based variant position: chr7:66205483 (JSON only)"
32
+ )
33
+ parser.add_argument(
34
+ "--include-initiation-context",
35
+ action="store_true",
36
+ help="Include spliced start flanks in locus lookup",
37
+ )
38
+ parser.add_argument("--ref", help="Forward-genomic SNV REF; requires --alt and --position")
39
+ parser.add_argument("--alt", help="Forward-genomic SNV ALT; compare initiation contexts")
40
+ parser.add_argument(
41
+ "--genome-build", help="Required for genomic queries; packaged data use GRCh38"
42
+ )
43
+ parser.add_argument("--format", choices=("json", "tsv", "fasta"), default="json")
44
+ args = parser.parse_args(argv)
45
+ if args.region and args.position:
46
+ parser.error("Choose --region or --position")
47
+ if args.position and args.format != "json":
48
+ parser.error("--position requires --format json")
49
+ if (args.ref is not None or args.alt is not None) and (
50
+ not args.ref or not args.alt or not args.position
51
+ ):
52
+ parser.error("SNV annotation requires --position, --ref and --alt")
53
+ if args.ref and (
54
+ args.ref not in ("A", "C", "G", "T")
55
+ or args.alt not in ("A", "C", "G", "T")
56
+ or args.ref == args.alt
57
+ ):
58
+ parser.error("Provide distinct single-base forward-genomic REF and ALT")
59
+ try:
60
+ catalog = catalog_loader(args.catalog)
61
+ options = {
62
+ "gene": args.gene,
63
+ "transcript_id": args.transcript,
64
+ "evidence": args.evidence,
65
+ "require_unique": args.require_unique,
66
+ "require_reviewed": args.require_reviewed,
67
+ "biotype": args.biotype,
68
+ "genome_build": args.genome_build,
69
+ "include_initiation_context": args.include_initiation_context or bool(args.ref),
70
+ }
71
+ locus = args.region or args.position
72
+ if locus:
73
+ pattern = r"([^:]+):([0-9]+)-([0-9]+)" if args.region else r"([^:]+):([0-9]+)"
74
+ match = re.fullmatch(pattern, locus)
75
+ if match is None:
76
+ raise ValueError(f"Invalid locus: {locus}")
77
+ start = int(match.group(2))
78
+ options.update(
79
+ contig=match.group(1),
80
+ start=start,
81
+ end=int(match.group(3)) if args.region else start,
82
+ )
83
+ records = catalog.query(**options)
84
+ if args.orf_id:
85
+ catalog.get(args.orf_id) # A misspelled ID is an error, not empty evidence.
86
+ records = tuple(r for r in records if r.orf_id == args.orf_id)
87
+ initiation_annotations = None
88
+ if args.position:
89
+ initiation_annotations = []
90
+ for r in records:
91
+ annotation = r.map_initiation_position(
92
+ options["start"], genome_build=args.genome_build
93
+ )
94
+ if args.ref and annotation is not None:
95
+ annotation = r.annotate_initiation_snv(
96
+ options["start"], args.ref, args.alt, genome_build=args.genome_build
97
+ )
98
+ initiation_annotations.append(annotation)
99
+ if args.ref and not any(a is not None for a in initiation_annotations):
100
+ raise ValueError(
101
+ "No mapped initiation context covers the SNV; coding consequences require full transcript reconstruction"
102
+ )
103
+ except (ValueError, KeyError, OSError) as exc:
104
+ parser.error(str(exc))
105
+ if args.format == "json":
106
+ output = {
107
+ "schema": "isovar.uorf_query.v1"
108
+ if isinstance(catalog, UORFCatalog)
109
+ else "orfdata.query.v1",
110
+ "metadata": catalog.metadata,
111
+ "records": [r.to_dict() for r in records],
112
+ }
113
+ if args.position:
114
+ output["position_annotations"] = [
115
+ r.map_genomic_position(options["start"], genome_build=args.genome_build)
116
+ for r in records
117
+ ]
118
+ output["initiation_annotations"] = initiation_annotations
119
+ json.dump(output, sys.stdout, indent=2)
120
+ sys.stdout.write("\n")
121
+ elif args.format == "fasta":
122
+ for r in records:
123
+ sys.stdout.write(
124
+ f">{r.orf_id} gene={r.gene_name} transcript={r.transcript_id} assembly={r.genome_build} biotype={r.biotype}\n{r.protein_sequence}\n"
125
+ )
126
+ else:
127
+ fields = (
128
+ "orf_id",
129
+ "gene_id",
130
+ "gene_name",
131
+ "transcript_id",
132
+ "genome_build",
133
+ "taxon_id",
134
+ "species",
135
+ "translation_table",
136
+ "source",
137
+ "contig",
138
+ "strand",
139
+ "biotype",
140
+ "protein_sequence",
141
+ "coding_blocks_json",
142
+ "ribosome_studies_json",
143
+ "peptides_json",
144
+ "initiation_context_json",
145
+ )
146
+ writer = csv.DictWriter(sys.stdout, fieldnames=fields, delimiter="\t", lineterminator="\n")
147
+ writer.writeheader()
148
+ for r in records:
149
+ row = r.to_dict()
150
+ for field in ("coding_blocks", "ribosome_studies", "peptides", "initiation_context"):
151
+ row[field + "_json"] = json.dumps(row.pop(field), separators=(",", ":"))
152
+ writer.writerow(row)
153
+
154
+
155
+ if __name__ == "__main__":
156
+ run()
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Leron Kok
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,32 @@
1
+ orfdata ncORF evidence catalogue, dataset version 2026-10-11.1.
2
+
3
+ The orfdata API and builder (derived from Isovar) are Apache-2.0. Source data retain their own licenses.
4
+
5
+ GENCODE Phase I models and legacy MS annotations, plus HLA mapping/count data:
6
+ Copyright (c) 2025 Leron Kok. MIT license, included as AUTHOR-LICENSE.txt.
7
+ VanHeeschLab/deutsch_kok_et_al_2024, commit
8
+ b65ae0c09a53e9efe083d806d617581e6139bdde.
9
+ https://github.com/VanHeeschLab/deutsch_kok_et_al_2024
10
+ Associated studies: Mudge et al. 2022, DOI 10.1038/s41587-022-01369-0;
11
+ Deutsch et al. 2026, DOI 10.1038/s41586-026-10459-x.
12
+
13
+ Community spectrum review input:
14
+ Aaron Wacholder and the community assessment authors, archived dataset version 1.
15
+ https://doi.org/10.6084/m9.figshare.30131869.v1
16
+ License: Creative Commons Attribution 4.0 International (CC BY 4.0).
17
+ https://creativecommons.org/licenses/by/4.0/
18
+ Associated study: Wacholder et al. 2026, DOI 10.1038/s41467-025-68002-x.
19
+
20
+ Changes: included all complete stopped source ncORF classes; normalized coordinates and source metadata;
21
+ joined sequence-compatible peptide observations; retained individual reviews,
22
+ source attribution and unknown mapping counts; calculated peptide intervals and
23
+ conservative review labels. Full original article text and supplementary
24
+ curation tables are not redistributed. Source URLs, SHA256 pins, evidence scope
25
+ and exclusions are in catalog metadata and manifest.json.
26
+
27
+ Initiation-context sequence: Ensembl release 101, human GRCh38 GTF/cDNA/ncRNA.
28
+ Ensembl-generated data are available without restriction:
29
+ https://www.ensembl.org/info/about/legal/disclaimer.html
30
+ Changes: exact ORF path/protein validation; extracted transcript-oriented start
31
+ flanks and genomic placements; added partial-context/positional Kozak priors.
32
+ Sequence priors are not measurements of translation or protein abundance.
@@ -0,0 +1,30 @@
1
+ {
2
+ "catalog_sha256": "06bc0bf3f81c25a7356f71a38581ff5704c05dcb75752454bc1ca75075ab8c09",
3
+ "dataset_version": "2026-10-11.1",
4
+ "excluded_assignments": [],
5
+ "schema": "orfdata.manifest.v1",
6
+ "source_sha256": {
7
+ "author-hla-counts.json": "b1e8e79e74e90d205224cc86663ebf9d64ed97cf0e0290cd86117756ee46a4b6",
8
+ "author-hla-mappings.tsv": "7f97f4ea675ac6abc97217c865019d73135358f448533ad8d98c9479fee9fb8f",
9
+ "ensembl101.cdna.fa.gz": "519cb4398f7a7904531a0eaaf16a8252f0487f101e00087f6316017bd51a1093",
10
+ "ensembl101.gtf.gz": "6f63da87a7f2f7b5e6940f7e2bd9dd2e7aee37494430e6ac7274f7ef8dc214ba",
11
+ "ensembl101.ncrna.fa.gz": "6bbf091adb1214942b6f258aa830393b27ad7cc49f8ee2fe18260a7b46a0cc88",
12
+ "filtered_peptides.tar.bz2": "a97d73b08b4ee7376a064f9fee3480b3d73639ebe0bd238a6dd0099df6a9e30c",
13
+ "media-4_with_dot_products.csv": "9e2cbb54765f8a9886cdcbf0bdfefa772f4623f54291add2bf3c47c26b17250f",
14
+ "ncorf_list.xlsx": "ca2e2f9389052c62f48ec6f81d53ca88c64152edb5940e556813d4a7bf1e6c62",
15
+ "peptide_sample_msrun_counts.tar.bz2": "290658de32de2579d17f0b75c3fcc39b4d50c5e6ce1ceea9ce9f411ccc8a98ba"
16
+ },
17
+ "statistics": {
18
+ "excluded_legacy_assignments": 0,
19
+ "hla_orfs": 1881,
20
+ "initiation_context_complete": 7041,
21
+ "initiation_context_partial": 223,
22
+ "initiation_context_unavailable": 0,
23
+ "ms_orfs": 2186,
24
+ "peptides": 4315,
25
+ "records": 7264,
26
+ "reviewed_ms_orfs": 44,
27
+ "ribosome_orfs": 7264,
28
+ "shotgun_orfs": 73
29
+ }
30
+ }
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Leron Kok
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,32 @@
1
+ Isovar uORF evidence catalogue, dataset version 2026-10-08.2.
2
+
3
+ The Isovar API and builder are Apache-2.0. Source data retain their own licenses.
4
+
5
+ GENCODE Phase I models and legacy MS annotations, plus HLA mapping/count data:
6
+ Copyright (c) 2025 Leron Kok. MIT license, included as AUTHOR-LICENSE.txt.
7
+ VanHeeschLab/deutsch_kok_et_al_2024, commit
8
+ b65ae0c09a53e9efe083d806d617581e6139bdde.
9
+ https://github.com/VanHeeschLab/deutsch_kok_et_al_2024
10
+ Associated studies: Mudge et al. 2022, DOI 10.1038/s41587-022-01369-0;
11
+ Deutsch et al. 2026, DOI 10.1038/s41586-026-10459-x.
12
+
13
+ Community spectrum review input:
14
+ Aaron Wacholder and the community assessment authors, archived dataset version 1.
15
+ https://doi.org/10.6084/m9.figshare.30131869.v1
16
+ License: Creative Commons Attribution 4.0 International (CC BY 4.0).
17
+ https://creativecommons.org/licenses/by/4.0/
18
+ Associated study: Wacholder et al. 2026, DOI 10.1038/s41467-025-68002-x.
19
+
20
+ Changes: selected uORF/uoORF models; normalized coordinates and source metadata;
21
+ joined sequence-compatible peptide observations; retained individual reviews,
22
+ source attribution and unknown mapping counts; calculated peptide intervals and
23
+ conservative review labels. Full original article text and supplementary
24
+ curation tables are not redistributed. Source URLs, SHA256 pins, evidence scope
25
+ and exclusions are in catalog metadata and manifest.json.
26
+
27
+ Initiation-context sequence: Ensembl release 101, human GRCh38 GTF/cDNA.
28
+ Ensembl-generated data are available without restriction:
29
+ https://www.ensembl.org/info/about/legal/disclaimer.html
30
+ Changes: exact ORF path/protein validation; extracted transcript-oriented start
31
+ flanks and genomic placements; added partial-context/positional Kozak priors.
32
+ Sequence priors are not measurements of translation or protein abundance.
@@ -0,0 +1,29 @@
1
+ {
2
+ "catalog_sha256": "408d79631214f3872b1fb886bf2aa117228d4ae6179d59113af8ce336244bedd",
3
+ "dataset_version": "2026-10-08.2",
4
+ "excluded_assignments": [],
5
+ "schema": "isovar.uorf_manifest.v1",
6
+ "source_sha256": {
7
+ "author-hla-counts.json": "b1e8e79e74e90d205224cc86663ebf9d64ed97cf0e0290cd86117756ee46a4b6",
8
+ "author-hla-mappings.tsv": "7f97f4ea675ac6abc97217c865019d73135358f448533ad8d98c9479fee9fb8f",
9
+ "ensembl101.cdna.fa.gz": "519cb4398f7a7904531a0eaaf16a8252f0487f101e00087f6316017bd51a1093",
10
+ "ensembl101.gtf.gz": "6f63da87a7f2f7b5e6940f7e2bd9dd2e7aee37494430e6ac7274f7ef8dc214ba",
11
+ "filtered_peptides.tar.bz2": "a97d73b08b4ee7376a064f9fee3480b3d73639ebe0bd238a6dd0099df6a9e30c",
12
+ "media-4_with_dot_products.csv": "9e2cbb54765f8a9886cdcbf0bdfefa772f4623f54291add2bf3c47c26b17250f",
13
+ "ncorf_list.xlsx": "ca2e2f9389052c62f48ec6f81d53ca88c64152edb5940e556813d4a7bf1e6c62",
14
+ "peptide_sample_msrun_counts.tar.bz2": "290658de32de2579d17f0b75c3fcc39b4d50c5e6ce1ceea9ce9f411ccc8a98ba"
15
+ },
16
+ "statistics": {
17
+ "excluded_legacy_assignments": 0,
18
+ "hla_orfs": 1059,
19
+ "initiation_context_complete": 3598,
20
+ "initiation_context_partial": 173,
21
+ "initiation_context_unavailable": 0,
22
+ "ms_orfs": 1219,
23
+ "peptides": 2414,
24
+ "records": 3771,
25
+ "reviewed_ms_orfs": 16,
26
+ "ribosome_orfs": 3771,
27
+ "shotgun_orfs": 31
28
+ }
29
+ }
@@ -0,0 +1,178 @@
1
+ """Loose, positional initiation priors, separate from translation observations."""
2
+
3
+ from dataclasses import asdict, dataclass
4
+ from typing import Optional
5
+
6
+ # A of ATG is +1; there is no position zero. The outer GCC is optional.
7
+ KOZAK_PREFERENCES = {
8
+ -9: "G",
9
+ -8: "C",
10
+ -7: "C",
11
+ -6: "G",
12
+ -5: "C",
13
+ -4: "C",
14
+ -3: "AG",
15
+ -2: "C",
16
+ -1: "C",
17
+ 4: "G",
18
+ }
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class InitiationContext:
23
+ """Transcript-oriented bases surrounding a proposed initiation codon.
24
+
25
+ Missing flanks are empty/short strings, never inferred from protein
26
+ sequence or intronic genomic adjacency. Ambiguous bases remain unknown.
27
+ ``genomic_positions`` are optional 1-based placements in sequence order.
28
+ ``start_offset`` is measured from the supplied transcript sequence; it
29
+ is not a verified cap distance unless ``five_prime_complete`` is True.
30
+ """
31
+
32
+ upstream: str
33
+ start_codon: str
34
+ downstream: str
35
+ genomic_positions: tuple = ()
36
+ source: Optional[str] = None
37
+ source_transcript_id: Optional[str] = None
38
+ start_offset: Optional[int] = None
39
+ five_prime_complete: Optional[bool] = None
40
+ unavailable_reason: Optional[str] = None
41
+
42
+ def __post_init__(self):
43
+ sequence = self.upstream + self.start_codon + self.downstream
44
+ if (
45
+ len(self.upstream) > 9
46
+ or len(self.downstream) > 6
47
+ or any(b not in "ACGTRYSWKMBDHVN" for b in sequence)
48
+ or (len(self.start_codon) != 3 and not self.unavailable_reason)
49
+ ):
50
+ raise ValueError("Invalid initiation-context sequence")
51
+ if self.unavailable_reason and sequence:
52
+ raise ValueError("Unavailable context must not carry unvalidated sequence")
53
+ if self.genomic_positions and (
54
+ len(self.genomic_positions) != len(sequence)
55
+ or any(type(p) is not int or p < 1 for p in self.genomic_positions)
56
+ or len(set(self.genomic_positions)) != len(self.genomic_positions)
57
+ ):
58
+ raise ValueError("Invalid initiation-context genomic placements")
59
+ if self.start_offset is not None and (
60
+ type(self.start_offset) is not int or self.start_offset < len(self.upstream)
61
+ ):
62
+ raise ValueError("Invalid initiation-context start offset")
63
+ if self.five_prime_complete is not None and type(self.five_prime_complete) is not bool:
64
+ raise ValueError("five_prime_complete must be True, False or None")
65
+
66
+ @property
67
+ def relative_positions(self):
68
+ """Kozak numbering for every retained base, without a position zero."""
69
+ return tuple(range(-len(self.upstream), 0)) + tuple(
70
+ range(1, len(self.start_codon) + len(self.downstream) + 1)
71
+ )
72
+
73
+ @property
74
+ def assessment(self):
75
+ """Return observed preferences, missingness and a qualitative prior.
76
+
77
+ Match counts are descriptive, not calibrated probabilities or a
78
+ translation threshold. A mismatch never establishes no initiation.
79
+ Non-ATG starts are retained without predicting their efficiency.
80
+ """
81
+ sequence = self.upstream + self.start_codon + self.downstream
82
+ bases = dict(zip(self.relative_positions, sequence))
83
+ positions = []
84
+ for position, preferred in KOZAK_PREFERENCES.items():
85
+ base = bases.get(position)
86
+ positions.append(
87
+ {
88
+ "position": position,
89
+ "base": base,
90
+ "preferred": preferred,
91
+ "matches": base in preferred if base is not None and base in "ACGT" else None,
92
+ }
93
+ )
94
+ key = [p for p in positions if p["position"] in (-3, 4)]
95
+ observed = sum(p["matches"] is not None for p in key)
96
+ matches = sum(p["matches"] is True for p in key)
97
+ if self.unavailable_reason:
98
+ status, interpretation = "unavailable", "sequence_context_unavailable"
99
+ else:
100
+ status = (
101
+ "complete"
102
+ if len(self.upstream) == 9
103
+ and len(self.downstream) == 6
104
+ and all(b in "ACGT" for b in sequence)
105
+ else "partial"
106
+ )
107
+ interpretation = (
108
+ "both_key_preferences_present"
109
+ if matches == 2
110
+ else "one_key_preference_present"
111
+ if matches == 1
112
+ else "no_observed_key_preference; initiation_still_possible"
113
+ if observed
114
+ else "key_positions_unknown; initiation_unresolved"
115
+ )
116
+ codon = self.start_codon
117
+ atg = None if len(codon) != 3 or any(b not in "ACGT" for b in codon) else codon == "ATG"
118
+ boundary = (
119
+ (
120
+ "at_verified_5prime_end; conventional_scanning_uncertain"
121
+ if self.five_prime_complete
122
+ else "at_supplied_sequence_start; true_5prime_end_unverified"
123
+ )
124
+ if self.start_offset == 0
125
+ else None
126
+ )
127
+ return {
128
+ "policy": "isovar.initiation_context.v1",
129
+ "status": status,
130
+ "display": self.upstream + "[" + codon + "]" + self.downstream if sequence else None,
131
+ "is_atg": atg,
132
+ "kozak_positions": positions,
133
+ "key_positions_observed": observed,
134
+ "key_positions_matching": matches,
135
+ "interpretation": interpretation,
136
+ "sequence_boundary_note": boundary,
137
+ "cap_distance_nt": self.start_offset if self.five_prime_complete is True else None,
138
+ "translation_evidence": "sequence_prior_only; not_observed",
139
+ }
140
+
141
+ def to_dict(self):
142
+ """Serialize raw sequence/provenance and the reproducible assessment."""
143
+ result = asdict(self)
144
+ result["assessment"] = self.assessment
145
+ return result
146
+
147
+
148
+ def annotate_initiation_context(sequence, start, *, five_prime_complete=None):
149
+ """Extract up to 9 upstream and 6 downstream bases from a supplied RNA.
150
+
151
+ Parameters
152
+ ----------
153
+ sequence : str
154
+ Transcript-oriented sequence, accepting RNA U or DNA T and IUPAC
155
+ ambiguity. It need not be a complete mature mRNA.
156
+ start : int
157
+ Zero-based offset of a complete proposed initiation codon. ATG is
158
+ not required; retained non-ATG codons receive no efficiency claim.
159
+ five_prime_complete : bool or None
160
+ Caller evidence that the sequence begins at the biological 5-prime
161
+ end. The default leaves the cap distance unknown.
162
+
163
+ Returns
164
+ -------
165
+ InitiationContext
166
+ Observed flanks and a loose positional assessment. Partial contexts
167
+ and ATG at the first three bases are retained, never filtered out.
168
+ """
169
+ sequence = sequence.upper().replace("U", "T")
170
+ if type(start) is not int or not 0 <= start <= len(sequence) - 3:
171
+ raise ValueError("start must identify a complete codon in the sequence")
172
+ return InitiationContext(
173
+ sequence[max(0, start - 9) : start],
174
+ sequence[start : start + 3],
175
+ sequence[start + 3 : start + 9],
176
+ start_offset=start,
177
+ five_prime_complete=five_prime_complete,
178
+ )