gff3-validator 0.1.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.
- gff3_validator-0.1.0/LICENSE +21 -0
- gff3_validator-0.1.0/PKG-INFO +331 -0
- gff3_validator-0.1.0/README.md +302 -0
- gff3_validator-0.1.0/gff3_validator/__init__.py +25 -0
- gff3_validator-0.1.0/gff3_validator/__main__.py +5 -0
- gff3_validator-0.1.0/gff3_validator/catalogue.yaml +1537 -0
- gff3_validator-0.1.0/gff3_validator/checks/__init__.py +1 -0
- gff3_validator-0.1.0/gff3_validator/checks/attributes.py +262 -0
- gff3_validator-0.1.0/gff3_validator/checks/biology.py +517 -0
- gff3_validator-0.1.0/gff3_validator/checks/directives.py +170 -0
- gff3_validator-0.1.0/gff3_validator/checks/structure.py +436 -0
- gff3_validator-0.1.0/gff3_validator/checks/syntax.py +204 -0
- gff3_validator-0.1.0/gff3_validator/cli.py +84 -0
- gff3_validator-0.1.0/gff3_validator/codons.py +182 -0
- gff3_validator-0.1.0/gff3_validator/engine.py +316 -0
- gff3_validator-0.1.0/gff3_validator/findings.py +32 -0
- gff3_validator-0.1.0/gff3_validator/genome.py +284 -0
- gff3_validator-0.1.0/gff3_validator/header.py +33 -0
- gff3_validator-0.1.0/gff3_validator/reader.py +72 -0
- gff3_validator-0.1.0/gff3_validator/report.py +385 -0
- gff3_validator-0.1.0/gff3_validator/rules.py +147 -0
- gff3_validator-0.1.0/gff3_validator/web.py +106 -0
- gff3_validator-0.1.0/pyproject.toml +62 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Adam Wright and David Molik (FAIR-bioHeaders contributors)
|
|
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,331 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gff3-validator
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: One-stop GFF3 validator: GFF3 1.26 syntax, structure and Sequence Ontology rules, plus optional FAIR-bioHeaders (FHGFF3) header checks
|
|
5
|
+
License: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Keywords: GFF3,validator,Sequence Ontology,genome annotation,FAIR
|
|
8
|
+
Author: Adam Wright
|
|
9
|
+
Author-email: adam.wright@oicr.on.ca
|
|
10
|
+
Requires-Python: >=3.9,<4.0
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Provides-Extra: headers
|
|
20
|
+
Requires-Dist: fair-bioheaders (>=0.4,<0.5) ; extra == "headers"
|
|
21
|
+
Requires-Dist: pyyaml (>=6.0.1,<7.0.0)
|
|
22
|
+
Project-URL: Bug Tracker, https://github.com/FAIR-bioHeaders/gff3-validator/issues
|
|
23
|
+
Project-URL: Documentation, https://github.com/FAIR-bioHeaders/gff3-validator/blob/main/docs/rules.md
|
|
24
|
+
Project-URL: Homepage, https://github.com/FAIR-bioHeaders/gff3-validator
|
|
25
|
+
Project-URL: Repository, https://github.com/FAIR-bioHeaders/gff3-validator
|
|
26
|
+
Project-URL: Specification, https://github.com/FAIR-bioHeaders/FHR-Specification/blob/main/specs/009-gff3-validator/spec.md
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# gff3-validator
|
|
30
|
+
|
|
31
|
+
A one-stop validator for [GFF3](https://github.com/The-Sequence-Ontology/Specifications/blob/master/gff3.md)
|
|
32
|
+
annotation files, built by FAIR-bioHeaders with the Sequence Ontology (SO) group.
|
|
33
|
+
|
|
34
|
+
> **Status: early release (0.1.0).** The rule catalogue is a draft under
|
|
35
|
+
> review with the Sequence Ontology group
|
|
36
|
+
> ([FHR-Specification #62](https://github.com/FAIR-bioHeaders/FHR-Specification/issues/62)).
|
|
37
|
+
> The core syntax, attribute, directive and structure rules that the GFF3
|
|
38
|
+
> specification settles are implemented, and the optional biology rules run
|
|
39
|
+
> with `--genome`; Sequence Ontology and header rules are not yet. Install
|
|
40
|
+
> with `pip install gff3-validator`, or use it in the browser at
|
|
41
|
+
> https://fair-bioheaders.github.io/gff3-validator/. Rule ids, levels and
|
|
42
|
+
> output formats may still change before 1.0.
|
|
43
|
+
|
|
44
|
+
## Goal
|
|
45
|
+
|
|
46
|
+
One place, one name, three interfaces (web page, CLI, library) that tell you
|
|
47
|
+
whether a GFF3 file is valid against the GFF3 specification (version 1.26) and
|
|
48
|
+
the Sequence Ontology, and that explain every problem in plain language with
|
|
49
|
+
the rule id, the line and a suggested fix. When a file carries a FAIR-bioHeaders
|
|
50
|
+
(FHGFF3) provenance header, the validator also checks it and its link to the
|
|
51
|
+
genome. See [spec 009](https://github.com/FAIR-bioHeaders/FHR-Specification/blob/main/specs/009-gff3-validator/spec.md)
|
|
52
|
+
(copied in [specs/001-gff3-validator](specs/001-gff3-validator/spec.md)).
|
|
53
|
+
|
|
54
|
+
Plain GFF3 without a header is a first-class input: core validation does not
|
|
55
|
+
depend on FAIR-bioHeaders.
|
|
56
|
+
|
|
57
|
+
## Rule catalogue
|
|
58
|
+
|
|
59
|
+
Every check is a numbered rule in [rules/catalogue.yaml](rules/catalogue.yaml),
|
|
60
|
+
rendered as [docs/rules.md](docs/rules.md). Each rule has an id, level
|
|
61
|
+
(error, warning, info), layer (core, FHGFF3, biology), a reference into
|
|
62
|
+
GFF3 1.26 or SO, a bad example and a fix, its implementation status and its SO
|
|
63
|
+
review state. Open questions for SO are collected in
|
|
64
|
+
[docs/questions-for-SO.md](docs/questions-for-SO.md). A suggested fix is
|
|
65
|
+
guidance only; the validator never rewrites a file.
|
|
66
|
+
|
|
67
|
+
[docs/EVALUATION.md](docs/EVALUATION.md) reviews the 2024 FHGFF3 prototype, the
|
|
68
|
+
LinkML GFF3 model and existing validators (GenomeTools, AGAT, GFF3toolkit, the
|
|
69
|
+
SO-era Perl validator, NCBI table2asn), with a side-by-side run on small broken
|
|
70
|
+
files.
|
|
71
|
+
|
|
72
|
+
### Implemented rules
|
|
73
|
+
|
|
74
|
+
<!-- implemented-rules:start (generated by scripts/render_rules.py; do not edit) -->
|
|
75
|
+
68 of 88 catalogue rules are implemented:
|
|
76
|
+
|
|
77
|
+
- `GFF-SYN-001` (error): First line is the ##gff-version 3 directive
|
|
78
|
+
- `GFF-SYN-002` (error): Only one ##gff-version directive
|
|
79
|
+
- `GFF-SYN-003` (error): Feature lines have nine tab-separated columns
|
|
80
|
+
- `GFF-SYN-004` (error): Undefined columns are ".", not empty
|
|
81
|
+
- `GFF-SYN-005` (error): No unescaped control characters
|
|
82
|
+
- `GFF-SYN-006` (warning): File is UTF-8
|
|
83
|
+
- `GFF-SYN-007` (info): Possible end-of-line comment
|
|
84
|
+
- `GFF-SYN-008` (error): Percent-encoding is well formed
|
|
85
|
+
- `GFF-SYN-009` (warning): Characters encoded that need not be
|
|
86
|
+
- `GFF-SYN-010` (error): seqid uses the allowed characters
|
|
87
|
+
- `GFF-SYN-012` (error): type is defined
|
|
88
|
+
- `GFF-SYN-013` (error): start and end are integers
|
|
89
|
+
- `GFF-SYN-014` (error): start is at least 1
|
|
90
|
+
- `GFF-SYN-015` (error): start is not greater than end
|
|
91
|
+
- `GFF-SYN-016` (error): score is a number or "."
|
|
92
|
+
- `GFF-SYN-017` (error): strand is +, -, . or ?
|
|
93
|
+
- `GFF-SYN-018` (error): phase is 0, 1, 2 or "."
|
|
94
|
+
- `GFF-SYN-019` (error): CDS features have a phase
|
|
95
|
+
- `GFF-ATT-001` (error): Attributes are tag=value pairs separated by semicolons
|
|
96
|
+
- `GFF-ATT-002` (error): Tags and values are not empty
|
|
97
|
+
- `GFF-ATT-003` (info): Empty attribute pair or trailing semicolon
|
|
98
|
+
- `GFF-ATT-004` (error): Each tag appears once per line
|
|
99
|
+
- `GFF-ATT-005` (error): Reserved characters are escaped in tags and values
|
|
100
|
+
- `GFF-ATT-006` (error): Single-valued reserved tags have one value
|
|
101
|
+
- `GFF-ATT-007` (warning): Unknown reserved (upper-case) tag
|
|
102
|
+
- `GFF-ATT-008` (warning): Tag differs from a reserved tag only by case
|
|
103
|
+
- `GFF-ATT-009` (info): Quoted attribute value
|
|
104
|
+
- `GFF-ATT-010` (error): Target has the form "target_id start end [strand]"
|
|
105
|
+
- `GFF-ATT-011` (error): Gap is a list of operations
|
|
106
|
+
- `GFF-ATT-012` (warning): Gap is given with Target
|
|
107
|
+
- `GFF-ATT-013` (warning): Gap lengths agree with the feature and Target lengths
|
|
108
|
+
- `GFF-ATT-014` (error): Dbxref and Ontology_term values are DBTAG:ID
|
|
109
|
+
- `GFF-ATT-016` (warning): Is_circular is "true"
|
|
110
|
+
- `GFF-DIR-001` (error): ##sequence-region has the form: seqid start end
|
|
111
|
+
- `GFF-DIR-002` (error): One ##sequence-region per seqid
|
|
112
|
+
- `GFF-DIR-003` (error): ### closes all forward references
|
|
113
|
+
- `GFF-DIR-004` (error): Only FASTA after ##FASTA
|
|
114
|
+
- `GFF-DIR-005` (warning): FASTA section starts with ##FASTA
|
|
115
|
+
- `GFF-DIR-006` (warning): FASTA records are well formed
|
|
116
|
+
- `GFF-DIR-007` (warning): ##species is an NCBI Taxonomy URL
|
|
117
|
+
- `GFF-DIR-008` (warning): ##genome-build has a source and a build name
|
|
118
|
+
- `GFF-DIR-009` (info): Ontology directives are recorded but not fetched
|
|
119
|
+
- `GFF-DIR-010` (info): Unknown directive
|
|
120
|
+
- `GFF-STR-001` (error): IDs are unique, except for the lines of one discontinuous feature
|
|
121
|
+
- `GFF-STR-002` (warning): Lines of one feature share seqid and strand
|
|
122
|
+
- `GFF-STR-003` (warning): Lines of one feature have the same Parent
|
|
123
|
+
- `GFF-STR-004` (error): Parent refers to an ID in the file
|
|
124
|
+
- `GFF-STR-005` (error): Derives_from refers to an ID in the file
|
|
125
|
+
- `GFF-STR-006` (error): No Parent cycles
|
|
126
|
+
- `GFF-STR-007` (warning): No Derives_from cycles
|
|
127
|
+
- `GFF-STR-008` (error): Features are within their ##sequence-region
|
|
128
|
+
- `GFF-STR-009` (info): seqid without ##sequence-region
|
|
129
|
+
- `GFF-STR-010` (warning): seqids appear in the ##FASTA section
|
|
130
|
+
- `GFF-STR-011` (error): Features fit within the embedded sequence
|
|
131
|
+
- `BIO-001` (error): seqid is in the genome
|
|
132
|
+
- `BIO-002` (error): Features fit within the genome sequence
|
|
133
|
+
- `BIO-003` (warning): ##sequence-region agrees with the genome
|
|
134
|
+
- `BIO-004` (warning): CDS phases are consistent
|
|
135
|
+
- `BIO-005` (warning): CDS segments lie within exons
|
|
136
|
+
- `BIO-006` (warning): CDS starts with a start codon
|
|
137
|
+
- `BIO-007` (warning): CDS ends with a stop codon
|
|
138
|
+
- `BIO-008` (warning): No internal stop codons
|
|
139
|
+
- `BIO-009` (info): CDS length is a multiple of three
|
|
140
|
+
- `BIO-010` (warning): Strand is consistent within a gene
|
|
141
|
+
- `BIO-011` (info): Biology checks skipped
|
|
142
|
+
- `HDR-001` (error): FHGFF3 header present when required
|
|
143
|
+
- `HDR-002` (info): FHGFF3 header present but not validated
|
|
144
|
+
- `HDR-003` (info): Header checks skipped (--no-header)
|
|
145
|
+
<!-- implemented-rules:end -->
|
|
146
|
+
|
|
147
|
+
## Planned interfaces
|
|
148
|
+
|
|
149
|
+
- **CLI** `gff3-validate FILE [--genome genome.fa [--translation-table N]] [--format text|json|html|sarif]`, reading
|
|
150
|
+
plain, gzip/BGZF or stdin (`-`) input. Exit codes: 0 no errors, 1 errors
|
|
151
|
+
found, 2 usage error or unreadable input (the run is then incomplete).
|
|
152
|
+
- **Library** `gff3_validator.validate(path)` returning findings with rule id,
|
|
153
|
+
level, line, column, message and fix.
|
|
154
|
+
- **Web page** running the same engine in the browser through Pyodide, so
|
|
155
|
+
files never leave the computer (see [Use it in your browser](#use-it-in-your-browser)).
|
|
156
|
+
- **Reports** in text, JSON, HTML (one self-contained file) and SARIF 2.1.0
|
|
157
|
+
(for code-scanning annotations in CI).
|
|
158
|
+
- **Repository profiles** (NCBI, Ensembl, Alliance) layered on the core rules,
|
|
159
|
+
later and only with each repository's review.
|
|
160
|
+
- **Distribution** on PyPI and Bioconda, a Galaxy wrapper, an FHR-Nextflow
|
|
161
|
+
module and a container, after the first reviewed release.
|
|
162
|
+
|
|
163
|
+
Headers: a FAIR-bioHeaders header is validated when present; `--require-header`
|
|
164
|
+
makes it mandatory and `--no-header` skips header checks. Header checks need the
|
|
165
|
+
optional extra, `pip install "gff3-validator[headers]"` (FAIR-bioHeaders
|
|
166
|
+
toolkit 0.4). FHGFF3 support in the toolkit is not released yet, so the
|
|
167
|
+
skeleton only detects `#~` header lines and reports that header validation is
|
|
168
|
+
not implemented.
|
|
169
|
+
|
|
170
|
+
## Current skeleton
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
poetry install
|
|
174
|
+
poetry run gff3-validate tests/fixtures/valid/canonical_gene.gff3
|
|
175
|
+
poetry run gff3-validate --format json annotation.gff3.gz
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
The reader streams line by line and stops reading features at `##FASTA`;
|
|
179
|
+
FASTA records are read for their ids and lengths only, never kept. Per-line
|
|
180
|
+
rules keep no state. Findings for problems that can occur on most lines
|
|
181
|
+
(GFF-SYN-006, GFF-SYN-009, GFF-ATT-003, GFF-ATT-009) are reported once, at
|
|
182
|
+
the first occurrence, with a count, so they cannot crowd errors out of the
|
|
183
|
+
`--max-findings` cap.
|
|
184
|
+
|
|
185
|
+
### Memory
|
|
186
|
+
|
|
187
|
+
The structure rules (IDs, Parent and Derives_from references, cycles, bounds)
|
|
188
|
+
need state. Memory is O(number of distinct IDs) plus O(number of seqids):
|
|
189
|
+
|
|
190
|
+
- each ID gets an integer index and four packed array entries (first line,
|
|
191
|
+
seqid/type/strand combination, first Parent, Parent-list hash); features with
|
|
192
|
+
several Parents or with Derives_from keep the extra edges in sparse maps;
|
|
193
|
+
- features without an ID are not remembered after their line, except for a
|
|
194
|
+
reference to an ID not yet defined;
|
|
195
|
+
- forward references are kept per referenced ID (first line and count), not
|
|
196
|
+
per line, and are resolved when the ID appears or at the end of the file;
|
|
197
|
+
`###` only advances a counter, so a reference that crosses it is detected
|
|
198
|
+
when the ID is finally defined (GFF-DIR-003);
|
|
199
|
+
- per seqid: its first line, furthest start and end, and at most 100 lines
|
|
200
|
+
beyond its `##sequence-region` end (then a count);
|
|
201
|
+
- Parent and Derives_from cycles are found at the end of the file by an
|
|
202
|
+
iterative depth-first search (one byte of state per ID, no recursion).
|
|
203
|
+
|
|
204
|
+
On a synthetic file of 3.1 million lines (143,000 genes with two mRNAs, five
|
|
205
|
+
exons and four CDS segments each, 2.1 million distinct IDs) a run takes about
|
|
206
|
+
22 s and 380 MiB peak RSS on Python 3.13, and 40 s and 430 MiB on Python 3.9
|
|
207
|
+
(roughly 170 to 200 bytes per ID). The opt-in test regenerates such a file and
|
|
208
|
+
reports the numbers:
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
GFF3_VALIDATOR_PERFORMANCE_TEST=1 poetry run pytest -s tests/test_performance.py
|
|
212
|
+
# GFF3_VALIDATOR_PERFORMANCE_LINES=3000000 sets the size (default 2000000)
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Biology checks (`--genome`)
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
gff3-validate --genome genome.fa.gz annotation.gff3
|
|
219
|
+
gff3-validate --genome genome.fa --translation-table 11 bacterium.gff3
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
The biology rules (BIO-001 to BIO-011) run only with `--genome`; without it
|
|
223
|
+
the report lists the biology layer as not checked. They compare seqids and
|
|
224
|
+
lengths with the genome, check CDS phases, CDS within exons, start, stop and
|
|
225
|
+
internal stop codons, coding length and strand consistency. Codon checks use
|
|
226
|
+
an NCBI translation table: 1 by default (the table is never inferred from the
|
|
227
|
+
organism; bacteria, archaea and plastids need `--translation-table 11`).
|
|
228
|
+
Partial CDS have no standard marker in GFF3, so the codon rules are warnings.
|
|
229
|
+
Translation exceptions (selenocysteine, pyrrolysine, readthrough, stops
|
|
230
|
+
completed by polyadenylation) are exempt when the codon is covered by a
|
|
231
|
+
`recoded_codon` child of the CDS (or a subtype), following the SO/NCBI
|
|
232
|
+
discussion in SO-Ontologies#658 (pending SO confirmation), or by a legacy
|
|
233
|
+
`transl_except` attribute.
|
|
234
|
+
|
|
235
|
+
The genome is a FASTA file, plain or gzip/BGZF (detected from the magic bytes;
|
|
236
|
+
not stdin). Leading `;` lines, such as a FAIR-bioHeaders `;~` header, are
|
|
237
|
+
skipped. The validator reads the file once to build an in-memory index like
|
|
238
|
+
samtools faidx (name, length, offset, bases and bytes per line; memory
|
|
239
|
+
O(number of sequences)) and then reads CDS bases on demand, in 64 kb blocks
|
|
240
|
+
with a 2 MiB cache, so no chromosome is loaded whole. A gzip genome is first
|
|
241
|
+
decompressed to an anonymous temporary file in `TMPDIR`, which needs as much
|
|
242
|
+
free disk space as the uncompressed genome and is removed at the end. A genome
|
|
243
|
+
that cannot be indexed reliably (sequence before the first `>`, duplicate
|
|
244
|
+
names, lines of different lengths inside a record, non-sequence characters)
|
|
245
|
+
stops the run with exit code 2.
|
|
246
|
+
|
|
247
|
+
CDS segments (four integers per line), the exons of each transcript and the
|
|
248
|
+
recoded codons of each CDS are kept until the next `###` or the end of the
|
|
249
|
+
file, then judged and dropped. With `###` after each gene this is a few
|
|
250
|
+
genes' worth; without `###` it is about 40 bytes per CDS line and 20 bytes per
|
|
251
|
+
exon, plus about 300 bytes per transcript. On a synthetic file of 524,000
|
|
252
|
+
lines (23,809 genes on both strands, 47,618 CDS of 1,066 codons) with a
|
|
253
|
+
231 MiB genome, a run takes about 9 s and 97 MiB peak RSS on Python 3.13
|
|
254
|
+
(14 s and 104 MiB on 3.9); without `###` lines, 140 MiB. The opt-in
|
|
255
|
+
performance test includes this case
|
|
256
|
+
(`GFF3_VALIDATOR_PERFORMANCE_GENOME_LINES` sets its size).
|
|
257
|
+
|
|
258
|
+
## Use it in your browser
|
|
259
|
+
|
|
260
|
+
<https://fair-bioheaders.github.io/gff3-validator/> (once GitHub Pages is
|
|
261
|
+
enabled for this repository) runs the validator inside the page: the
|
|
262
|
+
`gff3-validator` wheel built from this repository, in Python compiled to
|
|
263
|
+
WebAssembly ([Pyodide](https://pyodide.org/)). Drop or pick a `.gff3` or
|
|
264
|
+
`.gff3.gz` file, optionally add a genome FASTA for the biology checks and a
|
|
265
|
+
translation table, choose how to treat a FAIR-bioHeaders header, and the page
|
|
266
|
+
shows the HTML report and offers it, the JSON and the SARIF report for
|
|
267
|
+
download. The reports are the ones `gff3-validate --format html|json|sarif`
|
|
268
|
+
writes for the same file and options; CI checks this on every fixture
|
|
269
|
+
(`web/test/parity.mjs`).
|
|
270
|
+
|
|
271
|
+
**Privacy.** Your files are read by the page and never uploaded. After the
|
|
272
|
+
page has loaded, the only requests are for the Python runtime and PyYAML from
|
|
273
|
+
one pinned Pyodide release on `cdn.jsdelivr.net` and for the validator wheel
|
|
274
|
+
from the page's own site. The page's Content-Security-Policy (a meta tag; the
|
|
275
|
+
validator runs in a Web Worker started from a `blob:` URL, which inherits it)
|
|
276
|
+
allows connections only to the site itself and to that Pyodide release, so the
|
|
277
|
+
page could not send a file elsewhere even by mistake. `pyodide.js` and
|
|
278
|
+
`pyodide-lock.json` are loaded with Subresource Integrity (sha384, in
|
|
279
|
+
[web/pyodide.json](web/pyodide.json)); the lock file pins the sha256 of
|
|
280
|
+
PyYAML, and the page checks the wheel against the sha256 in its
|
|
281
|
+
`manifest.json`. The rest of the Pyodide runtime (`pyodide.asm.mjs`, the
|
|
282
|
+
WebAssembly binary and the standard library) is loaded by `pyodide.js` from
|
|
283
|
+
the same pinned release without SRI, because Pyodide offers no way to pass
|
|
284
|
+
hashes for them.
|
|
285
|
+
|
|
286
|
+
**Size limits.** The annotation is read from disk in 1 MiB slices and is never
|
|
287
|
+
held in memory; memory grows with the number of feature IDs, as on the command
|
|
288
|
+
line. Measured in headless Chrome (Pyodide 314.0.7, Python 3.14) on the
|
|
289
|
+
synthetic files of the performance test:
|
|
290
|
+
|
|
291
|
+
| Input | Time | WebAssembly memory |
|
|
292
|
+
|---|---|---|
|
|
293
|
+
| 1.1 million lines, 90 MB plain (1.05 million IDs) | 18 s | 155 MiB |
|
|
294
|
+
| 3.3 million lines, 28 MB gzip (270 MB uncompressed, 3.15 million IDs) | 56 s | 387 MiB |
|
|
295
|
+
| 524,000 lines (42 MB) with a 231 MiB genome, plain or gzip | 18 s | 90 to 108 MiB, plus the genome |
|
|
296
|
+
|
|
297
|
+
That is two to three times slower than the CLI. A full vertebrate annotation
|
|
298
|
+
(about 3 million lines) works. Memory grows by about 120 bytes per ID and
|
|
299
|
+
WebAssembly memory cannot exceed 4 GiB (browsers may stop a tab sooner), so
|
|
300
|
+
files several times larger than those measured here (not tested) need the
|
|
301
|
+
command line. The genome is copied into the page's memory (a gzip genome is also
|
|
302
|
+
decompressed there), so genomes of a few hundred MB are fine and multi-GB
|
|
303
|
+
genomes are not; use `gff3-validate --genome` for those.
|
|
304
|
+
|
|
305
|
+
Build and try the page locally:
|
|
306
|
+
|
|
307
|
+
```bash
|
|
308
|
+
poetry run python scripts/build_web.py # builds the wheel and web/dist
|
|
309
|
+
python -m http.server -d web/dist 8000 # then open http://localhost:8000/
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
`.github/workflows/pages.yml` builds `web/dist`, runs the parity check and
|
|
313
|
+
deploys to GitHub Pages on every push to `main` (and on demand). To bump
|
|
314
|
+
Pyodide, run `poetry run python scripts/build_web.py --pin VERSION` (it
|
|
315
|
+
downloads `pyodide.js` and `pyodide-lock.json` of that release, computes the
|
|
316
|
+
SRI hashes, checks that PyYAML is included and rewrites `web/pyodide.json`),
|
|
317
|
+
then `cd web/test && npm install --save-exact pyodide@VERSION`, rebuild and run
|
|
318
|
+
`node web/test/parity.mjs --python "poetry run python"`.
|
|
319
|
+
|
|
320
|
+
## Development
|
|
321
|
+
|
|
322
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md). The rule catalogue is the source of
|
|
323
|
+
truth: edit `rules/catalogue.yaml`, then run `python scripts/render_rules.py` to
|
|
324
|
+
regenerate `docs/rules.md`, the packaged copy and the list above (CI checks for
|
|
325
|
+
drift with `--check`).
|
|
326
|
+
|
|
327
|
+
## License
|
|
328
|
+
|
|
329
|
+
[MIT](LICENSE). Copyright (c) 2026 Adam Wright and David Molik
|
|
330
|
+
(FAIR-bioHeaders contributors).
|
|
331
|
+
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
# gff3-validator
|
|
2
|
+
|
|
3
|
+
A one-stop validator for [GFF3](https://github.com/The-Sequence-Ontology/Specifications/blob/master/gff3.md)
|
|
4
|
+
annotation files, built by FAIR-bioHeaders with the Sequence Ontology (SO) group.
|
|
5
|
+
|
|
6
|
+
> **Status: early release (0.1.0).** The rule catalogue is a draft under
|
|
7
|
+
> review with the Sequence Ontology group
|
|
8
|
+
> ([FHR-Specification #62](https://github.com/FAIR-bioHeaders/FHR-Specification/issues/62)).
|
|
9
|
+
> The core syntax, attribute, directive and structure rules that the GFF3
|
|
10
|
+
> specification settles are implemented, and the optional biology rules run
|
|
11
|
+
> with `--genome`; Sequence Ontology and header rules are not yet. Install
|
|
12
|
+
> with `pip install gff3-validator`, or use it in the browser at
|
|
13
|
+
> https://fair-bioheaders.github.io/gff3-validator/. Rule ids, levels and
|
|
14
|
+
> output formats may still change before 1.0.
|
|
15
|
+
|
|
16
|
+
## Goal
|
|
17
|
+
|
|
18
|
+
One place, one name, three interfaces (web page, CLI, library) that tell you
|
|
19
|
+
whether a GFF3 file is valid against the GFF3 specification (version 1.26) and
|
|
20
|
+
the Sequence Ontology, and that explain every problem in plain language with
|
|
21
|
+
the rule id, the line and a suggested fix. When a file carries a FAIR-bioHeaders
|
|
22
|
+
(FHGFF3) provenance header, the validator also checks it and its link to the
|
|
23
|
+
genome. See [spec 009](https://github.com/FAIR-bioHeaders/FHR-Specification/blob/main/specs/009-gff3-validator/spec.md)
|
|
24
|
+
(copied in [specs/001-gff3-validator](specs/001-gff3-validator/spec.md)).
|
|
25
|
+
|
|
26
|
+
Plain GFF3 without a header is a first-class input: core validation does not
|
|
27
|
+
depend on FAIR-bioHeaders.
|
|
28
|
+
|
|
29
|
+
## Rule catalogue
|
|
30
|
+
|
|
31
|
+
Every check is a numbered rule in [rules/catalogue.yaml](rules/catalogue.yaml),
|
|
32
|
+
rendered as [docs/rules.md](docs/rules.md). Each rule has an id, level
|
|
33
|
+
(error, warning, info), layer (core, FHGFF3, biology), a reference into
|
|
34
|
+
GFF3 1.26 or SO, a bad example and a fix, its implementation status and its SO
|
|
35
|
+
review state. Open questions for SO are collected in
|
|
36
|
+
[docs/questions-for-SO.md](docs/questions-for-SO.md). A suggested fix is
|
|
37
|
+
guidance only; the validator never rewrites a file.
|
|
38
|
+
|
|
39
|
+
[docs/EVALUATION.md](docs/EVALUATION.md) reviews the 2024 FHGFF3 prototype, the
|
|
40
|
+
LinkML GFF3 model and existing validators (GenomeTools, AGAT, GFF3toolkit, the
|
|
41
|
+
SO-era Perl validator, NCBI table2asn), with a side-by-side run on small broken
|
|
42
|
+
files.
|
|
43
|
+
|
|
44
|
+
### Implemented rules
|
|
45
|
+
|
|
46
|
+
<!-- implemented-rules:start (generated by scripts/render_rules.py; do not edit) -->
|
|
47
|
+
68 of 88 catalogue rules are implemented:
|
|
48
|
+
|
|
49
|
+
- `GFF-SYN-001` (error): First line is the ##gff-version 3 directive
|
|
50
|
+
- `GFF-SYN-002` (error): Only one ##gff-version directive
|
|
51
|
+
- `GFF-SYN-003` (error): Feature lines have nine tab-separated columns
|
|
52
|
+
- `GFF-SYN-004` (error): Undefined columns are ".", not empty
|
|
53
|
+
- `GFF-SYN-005` (error): No unescaped control characters
|
|
54
|
+
- `GFF-SYN-006` (warning): File is UTF-8
|
|
55
|
+
- `GFF-SYN-007` (info): Possible end-of-line comment
|
|
56
|
+
- `GFF-SYN-008` (error): Percent-encoding is well formed
|
|
57
|
+
- `GFF-SYN-009` (warning): Characters encoded that need not be
|
|
58
|
+
- `GFF-SYN-010` (error): seqid uses the allowed characters
|
|
59
|
+
- `GFF-SYN-012` (error): type is defined
|
|
60
|
+
- `GFF-SYN-013` (error): start and end are integers
|
|
61
|
+
- `GFF-SYN-014` (error): start is at least 1
|
|
62
|
+
- `GFF-SYN-015` (error): start is not greater than end
|
|
63
|
+
- `GFF-SYN-016` (error): score is a number or "."
|
|
64
|
+
- `GFF-SYN-017` (error): strand is +, -, . or ?
|
|
65
|
+
- `GFF-SYN-018` (error): phase is 0, 1, 2 or "."
|
|
66
|
+
- `GFF-SYN-019` (error): CDS features have a phase
|
|
67
|
+
- `GFF-ATT-001` (error): Attributes are tag=value pairs separated by semicolons
|
|
68
|
+
- `GFF-ATT-002` (error): Tags and values are not empty
|
|
69
|
+
- `GFF-ATT-003` (info): Empty attribute pair or trailing semicolon
|
|
70
|
+
- `GFF-ATT-004` (error): Each tag appears once per line
|
|
71
|
+
- `GFF-ATT-005` (error): Reserved characters are escaped in tags and values
|
|
72
|
+
- `GFF-ATT-006` (error): Single-valued reserved tags have one value
|
|
73
|
+
- `GFF-ATT-007` (warning): Unknown reserved (upper-case) tag
|
|
74
|
+
- `GFF-ATT-008` (warning): Tag differs from a reserved tag only by case
|
|
75
|
+
- `GFF-ATT-009` (info): Quoted attribute value
|
|
76
|
+
- `GFF-ATT-010` (error): Target has the form "target_id start end [strand]"
|
|
77
|
+
- `GFF-ATT-011` (error): Gap is a list of operations
|
|
78
|
+
- `GFF-ATT-012` (warning): Gap is given with Target
|
|
79
|
+
- `GFF-ATT-013` (warning): Gap lengths agree with the feature and Target lengths
|
|
80
|
+
- `GFF-ATT-014` (error): Dbxref and Ontology_term values are DBTAG:ID
|
|
81
|
+
- `GFF-ATT-016` (warning): Is_circular is "true"
|
|
82
|
+
- `GFF-DIR-001` (error): ##sequence-region has the form: seqid start end
|
|
83
|
+
- `GFF-DIR-002` (error): One ##sequence-region per seqid
|
|
84
|
+
- `GFF-DIR-003` (error): ### closes all forward references
|
|
85
|
+
- `GFF-DIR-004` (error): Only FASTA after ##FASTA
|
|
86
|
+
- `GFF-DIR-005` (warning): FASTA section starts with ##FASTA
|
|
87
|
+
- `GFF-DIR-006` (warning): FASTA records are well formed
|
|
88
|
+
- `GFF-DIR-007` (warning): ##species is an NCBI Taxonomy URL
|
|
89
|
+
- `GFF-DIR-008` (warning): ##genome-build has a source and a build name
|
|
90
|
+
- `GFF-DIR-009` (info): Ontology directives are recorded but not fetched
|
|
91
|
+
- `GFF-DIR-010` (info): Unknown directive
|
|
92
|
+
- `GFF-STR-001` (error): IDs are unique, except for the lines of one discontinuous feature
|
|
93
|
+
- `GFF-STR-002` (warning): Lines of one feature share seqid and strand
|
|
94
|
+
- `GFF-STR-003` (warning): Lines of one feature have the same Parent
|
|
95
|
+
- `GFF-STR-004` (error): Parent refers to an ID in the file
|
|
96
|
+
- `GFF-STR-005` (error): Derives_from refers to an ID in the file
|
|
97
|
+
- `GFF-STR-006` (error): No Parent cycles
|
|
98
|
+
- `GFF-STR-007` (warning): No Derives_from cycles
|
|
99
|
+
- `GFF-STR-008` (error): Features are within their ##sequence-region
|
|
100
|
+
- `GFF-STR-009` (info): seqid without ##sequence-region
|
|
101
|
+
- `GFF-STR-010` (warning): seqids appear in the ##FASTA section
|
|
102
|
+
- `GFF-STR-011` (error): Features fit within the embedded sequence
|
|
103
|
+
- `BIO-001` (error): seqid is in the genome
|
|
104
|
+
- `BIO-002` (error): Features fit within the genome sequence
|
|
105
|
+
- `BIO-003` (warning): ##sequence-region agrees with the genome
|
|
106
|
+
- `BIO-004` (warning): CDS phases are consistent
|
|
107
|
+
- `BIO-005` (warning): CDS segments lie within exons
|
|
108
|
+
- `BIO-006` (warning): CDS starts with a start codon
|
|
109
|
+
- `BIO-007` (warning): CDS ends with a stop codon
|
|
110
|
+
- `BIO-008` (warning): No internal stop codons
|
|
111
|
+
- `BIO-009` (info): CDS length is a multiple of three
|
|
112
|
+
- `BIO-010` (warning): Strand is consistent within a gene
|
|
113
|
+
- `BIO-011` (info): Biology checks skipped
|
|
114
|
+
- `HDR-001` (error): FHGFF3 header present when required
|
|
115
|
+
- `HDR-002` (info): FHGFF3 header present but not validated
|
|
116
|
+
- `HDR-003` (info): Header checks skipped (--no-header)
|
|
117
|
+
<!-- implemented-rules:end -->
|
|
118
|
+
|
|
119
|
+
## Planned interfaces
|
|
120
|
+
|
|
121
|
+
- **CLI** `gff3-validate FILE [--genome genome.fa [--translation-table N]] [--format text|json|html|sarif]`, reading
|
|
122
|
+
plain, gzip/BGZF or stdin (`-`) input. Exit codes: 0 no errors, 1 errors
|
|
123
|
+
found, 2 usage error or unreadable input (the run is then incomplete).
|
|
124
|
+
- **Library** `gff3_validator.validate(path)` returning findings with rule id,
|
|
125
|
+
level, line, column, message and fix.
|
|
126
|
+
- **Web page** running the same engine in the browser through Pyodide, so
|
|
127
|
+
files never leave the computer (see [Use it in your browser](#use-it-in-your-browser)).
|
|
128
|
+
- **Reports** in text, JSON, HTML (one self-contained file) and SARIF 2.1.0
|
|
129
|
+
(for code-scanning annotations in CI).
|
|
130
|
+
- **Repository profiles** (NCBI, Ensembl, Alliance) layered on the core rules,
|
|
131
|
+
later and only with each repository's review.
|
|
132
|
+
- **Distribution** on PyPI and Bioconda, a Galaxy wrapper, an FHR-Nextflow
|
|
133
|
+
module and a container, after the first reviewed release.
|
|
134
|
+
|
|
135
|
+
Headers: a FAIR-bioHeaders header is validated when present; `--require-header`
|
|
136
|
+
makes it mandatory and `--no-header` skips header checks. Header checks need the
|
|
137
|
+
optional extra, `pip install "gff3-validator[headers]"` (FAIR-bioHeaders
|
|
138
|
+
toolkit 0.4). FHGFF3 support in the toolkit is not released yet, so the
|
|
139
|
+
skeleton only detects `#~` header lines and reports that header validation is
|
|
140
|
+
not implemented.
|
|
141
|
+
|
|
142
|
+
## Current skeleton
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
poetry install
|
|
146
|
+
poetry run gff3-validate tests/fixtures/valid/canonical_gene.gff3
|
|
147
|
+
poetry run gff3-validate --format json annotation.gff3.gz
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
The reader streams line by line and stops reading features at `##FASTA`;
|
|
151
|
+
FASTA records are read for their ids and lengths only, never kept. Per-line
|
|
152
|
+
rules keep no state. Findings for problems that can occur on most lines
|
|
153
|
+
(GFF-SYN-006, GFF-SYN-009, GFF-ATT-003, GFF-ATT-009) are reported once, at
|
|
154
|
+
the first occurrence, with a count, so they cannot crowd errors out of the
|
|
155
|
+
`--max-findings` cap.
|
|
156
|
+
|
|
157
|
+
### Memory
|
|
158
|
+
|
|
159
|
+
The structure rules (IDs, Parent and Derives_from references, cycles, bounds)
|
|
160
|
+
need state. Memory is O(number of distinct IDs) plus O(number of seqids):
|
|
161
|
+
|
|
162
|
+
- each ID gets an integer index and four packed array entries (first line,
|
|
163
|
+
seqid/type/strand combination, first Parent, Parent-list hash); features with
|
|
164
|
+
several Parents or with Derives_from keep the extra edges in sparse maps;
|
|
165
|
+
- features without an ID are not remembered after their line, except for a
|
|
166
|
+
reference to an ID not yet defined;
|
|
167
|
+
- forward references are kept per referenced ID (first line and count), not
|
|
168
|
+
per line, and are resolved when the ID appears or at the end of the file;
|
|
169
|
+
`###` only advances a counter, so a reference that crosses it is detected
|
|
170
|
+
when the ID is finally defined (GFF-DIR-003);
|
|
171
|
+
- per seqid: its first line, furthest start and end, and at most 100 lines
|
|
172
|
+
beyond its `##sequence-region` end (then a count);
|
|
173
|
+
- Parent and Derives_from cycles are found at the end of the file by an
|
|
174
|
+
iterative depth-first search (one byte of state per ID, no recursion).
|
|
175
|
+
|
|
176
|
+
On a synthetic file of 3.1 million lines (143,000 genes with two mRNAs, five
|
|
177
|
+
exons and four CDS segments each, 2.1 million distinct IDs) a run takes about
|
|
178
|
+
22 s and 380 MiB peak RSS on Python 3.13, and 40 s and 430 MiB on Python 3.9
|
|
179
|
+
(roughly 170 to 200 bytes per ID). The opt-in test regenerates such a file and
|
|
180
|
+
reports the numbers:
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
GFF3_VALIDATOR_PERFORMANCE_TEST=1 poetry run pytest -s tests/test_performance.py
|
|
184
|
+
# GFF3_VALIDATOR_PERFORMANCE_LINES=3000000 sets the size (default 2000000)
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Biology checks (`--genome`)
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
gff3-validate --genome genome.fa.gz annotation.gff3
|
|
191
|
+
gff3-validate --genome genome.fa --translation-table 11 bacterium.gff3
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
The biology rules (BIO-001 to BIO-011) run only with `--genome`; without it
|
|
195
|
+
the report lists the biology layer as not checked. They compare seqids and
|
|
196
|
+
lengths with the genome, check CDS phases, CDS within exons, start, stop and
|
|
197
|
+
internal stop codons, coding length and strand consistency. Codon checks use
|
|
198
|
+
an NCBI translation table: 1 by default (the table is never inferred from the
|
|
199
|
+
organism; bacteria, archaea and plastids need `--translation-table 11`).
|
|
200
|
+
Partial CDS have no standard marker in GFF3, so the codon rules are warnings.
|
|
201
|
+
Translation exceptions (selenocysteine, pyrrolysine, readthrough, stops
|
|
202
|
+
completed by polyadenylation) are exempt when the codon is covered by a
|
|
203
|
+
`recoded_codon` child of the CDS (or a subtype), following the SO/NCBI
|
|
204
|
+
discussion in SO-Ontologies#658 (pending SO confirmation), or by a legacy
|
|
205
|
+
`transl_except` attribute.
|
|
206
|
+
|
|
207
|
+
The genome is a FASTA file, plain or gzip/BGZF (detected from the magic bytes;
|
|
208
|
+
not stdin). Leading `;` lines, such as a FAIR-bioHeaders `;~` header, are
|
|
209
|
+
skipped. The validator reads the file once to build an in-memory index like
|
|
210
|
+
samtools faidx (name, length, offset, bases and bytes per line; memory
|
|
211
|
+
O(number of sequences)) and then reads CDS bases on demand, in 64 kb blocks
|
|
212
|
+
with a 2 MiB cache, so no chromosome is loaded whole. A gzip genome is first
|
|
213
|
+
decompressed to an anonymous temporary file in `TMPDIR`, which needs as much
|
|
214
|
+
free disk space as the uncompressed genome and is removed at the end. A genome
|
|
215
|
+
that cannot be indexed reliably (sequence before the first `>`, duplicate
|
|
216
|
+
names, lines of different lengths inside a record, non-sequence characters)
|
|
217
|
+
stops the run with exit code 2.
|
|
218
|
+
|
|
219
|
+
CDS segments (four integers per line), the exons of each transcript and the
|
|
220
|
+
recoded codons of each CDS are kept until the next `###` or the end of the
|
|
221
|
+
file, then judged and dropped. With `###` after each gene this is a few
|
|
222
|
+
genes' worth; without `###` it is about 40 bytes per CDS line and 20 bytes per
|
|
223
|
+
exon, plus about 300 bytes per transcript. On a synthetic file of 524,000
|
|
224
|
+
lines (23,809 genes on both strands, 47,618 CDS of 1,066 codons) with a
|
|
225
|
+
231 MiB genome, a run takes about 9 s and 97 MiB peak RSS on Python 3.13
|
|
226
|
+
(14 s and 104 MiB on 3.9); without `###` lines, 140 MiB. The opt-in
|
|
227
|
+
performance test includes this case
|
|
228
|
+
(`GFF3_VALIDATOR_PERFORMANCE_GENOME_LINES` sets its size).
|
|
229
|
+
|
|
230
|
+
## Use it in your browser
|
|
231
|
+
|
|
232
|
+
<https://fair-bioheaders.github.io/gff3-validator/> (once GitHub Pages is
|
|
233
|
+
enabled for this repository) runs the validator inside the page: the
|
|
234
|
+
`gff3-validator` wheel built from this repository, in Python compiled to
|
|
235
|
+
WebAssembly ([Pyodide](https://pyodide.org/)). Drop or pick a `.gff3` or
|
|
236
|
+
`.gff3.gz` file, optionally add a genome FASTA for the biology checks and a
|
|
237
|
+
translation table, choose how to treat a FAIR-bioHeaders header, and the page
|
|
238
|
+
shows the HTML report and offers it, the JSON and the SARIF report for
|
|
239
|
+
download. The reports are the ones `gff3-validate --format html|json|sarif`
|
|
240
|
+
writes for the same file and options; CI checks this on every fixture
|
|
241
|
+
(`web/test/parity.mjs`).
|
|
242
|
+
|
|
243
|
+
**Privacy.** Your files are read by the page and never uploaded. After the
|
|
244
|
+
page has loaded, the only requests are for the Python runtime and PyYAML from
|
|
245
|
+
one pinned Pyodide release on `cdn.jsdelivr.net` and for the validator wheel
|
|
246
|
+
from the page's own site. The page's Content-Security-Policy (a meta tag; the
|
|
247
|
+
validator runs in a Web Worker started from a `blob:` URL, which inherits it)
|
|
248
|
+
allows connections only to the site itself and to that Pyodide release, so the
|
|
249
|
+
page could not send a file elsewhere even by mistake. `pyodide.js` and
|
|
250
|
+
`pyodide-lock.json` are loaded with Subresource Integrity (sha384, in
|
|
251
|
+
[web/pyodide.json](web/pyodide.json)); the lock file pins the sha256 of
|
|
252
|
+
PyYAML, and the page checks the wheel against the sha256 in its
|
|
253
|
+
`manifest.json`. The rest of the Pyodide runtime (`pyodide.asm.mjs`, the
|
|
254
|
+
WebAssembly binary and the standard library) is loaded by `pyodide.js` from
|
|
255
|
+
the same pinned release without SRI, because Pyodide offers no way to pass
|
|
256
|
+
hashes for them.
|
|
257
|
+
|
|
258
|
+
**Size limits.** The annotation is read from disk in 1 MiB slices and is never
|
|
259
|
+
held in memory; memory grows with the number of feature IDs, as on the command
|
|
260
|
+
line. Measured in headless Chrome (Pyodide 314.0.7, Python 3.14) on the
|
|
261
|
+
synthetic files of the performance test:
|
|
262
|
+
|
|
263
|
+
| Input | Time | WebAssembly memory |
|
|
264
|
+
|---|---|---|
|
|
265
|
+
| 1.1 million lines, 90 MB plain (1.05 million IDs) | 18 s | 155 MiB |
|
|
266
|
+
| 3.3 million lines, 28 MB gzip (270 MB uncompressed, 3.15 million IDs) | 56 s | 387 MiB |
|
|
267
|
+
| 524,000 lines (42 MB) with a 231 MiB genome, plain or gzip | 18 s | 90 to 108 MiB, plus the genome |
|
|
268
|
+
|
|
269
|
+
That is two to three times slower than the CLI. A full vertebrate annotation
|
|
270
|
+
(about 3 million lines) works. Memory grows by about 120 bytes per ID and
|
|
271
|
+
WebAssembly memory cannot exceed 4 GiB (browsers may stop a tab sooner), so
|
|
272
|
+
files several times larger than those measured here (not tested) need the
|
|
273
|
+
command line. The genome is copied into the page's memory (a gzip genome is also
|
|
274
|
+
decompressed there), so genomes of a few hundred MB are fine and multi-GB
|
|
275
|
+
genomes are not; use `gff3-validate --genome` for those.
|
|
276
|
+
|
|
277
|
+
Build and try the page locally:
|
|
278
|
+
|
|
279
|
+
```bash
|
|
280
|
+
poetry run python scripts/build_web.py # builds the wheel and web/dist
|
|
281
|
+
python -m http.server -d web/dist 8000 # then open http://localhost:8000/
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
`.github/workflows/pages.yml` builds `web/dist`, runs the parity check and
|
|
285
|
+
deploys to GitHub Pages on every push to `main` (and on demand). To bump
|
|
286
|
+
Pyodide, run `poetry run python scripts/build_web.py --pin VERSION` (it
|
|
287
|
+
downloads `pyodide.js` and `pyodide-lock.json` of that release, computes the
|
|
288
|
+
SRI hashes, checks that PyYAML is included and rewrites `web/pyodide.json`),
|
|
289
|
+
then `cd web/test && npm install --save-exact pyodide@VERSION`, rebuild and run
|
|
290
|
+
`node web/test/parity.mjs --python "poetry run python"`.
|
|
291
|
+
|
|
292
|
+
## Development
|
|
293
|
+
|
|
294
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md). The rule catalogue is the source of
|
|
295
|
+
truth: edit `rules/catalogue.yaml`, then run `python scripts/render_rules.py` to
|
|
296
|
+
regenerate `docs/rules.md`, the packaged copy and the list above (CI checks for
|
|
297
|
+
drift with `--check`).
|
|
298
|
+
|
|
299
|
+
## License
|
|
300
|
+
|
|
301
|
+
[MIT](LICENSE). Copyright (c) 2026 Adam Wright and David Molik
|
|
302
|
+
(FAIR-bioHeaders contributors).
|