Architecture
apb-fasta is a thin composition layer. It owns no FASTA parsing, no peptide matching, and no result persistence — it owns the boundary between them.
Ownership
| Concern | Owner |
|---|---|
| FASTA reading, header interpretation, classification | protein_fasta |
| Peptide-to-protein matching (Aho-Corasick) | prozor |
| Result reading, writing, level and axis structure | apb2 |
| Translating between the two, and the operation lifecycle | apb_fasta |
Modules
graph TD
cli[cli.py]
api[api.py]
integration[integration.py]
config[configuration.py / errors.py]
calculation[calculation/]
cli --> api
api --> integration
integration --> config
config --> calculation
api.py— the public protein-bound lifecycle:FastaAnnotator, whichread()builds from FASTA files through protein_fasta, andFastaAnnotationResultintegration.py— the only module that reads or writes APB2 result values; extracts inputs, validates output names, and builds the deep replacementcalculation/— pure Polars in, pure Polars out; imports no APB2 and no storage frameworkconfiguration.py/errors.py— user-selected behavior and the single expected-failure typecli.py— composes APB2 result I/O and the lifecycle; holds no logic of its own
Enforced import direction
The layering above is a checked contract, not a convention. .importlinter declares it:
- an exhaustive
layerscontract overcli → api → integration → configuration | errors → calculation - a
forbiddencontract stoppingapb_fasta.calculationfrom importingapb2,anndata,mudata, orpandas
make architecture runs lint-imports and is part of make check, so an upward or sideways import fails the build.
Why calculations are isolated
Keeping calculation/ free of APB2 and storage types means the matching and protein-group logic is testable against plain frames, and a change to APB2's result layout touches exactly one module — integration.py.
Verification execution
Peptide verification normalizes feature sequences with Polars, searches distinct peptides across all levels once through Prozor, and uses native grouping and ordered joins to expand the results back to each feature axis. Reported protein assignments are split and resolved once per distinct assignment, rather than once per feature. Only peptide strings and minimal protein records cross the matching boundary; the complete protein metadata frame is not converted into Python row dictionaries.
Protein record identity is separate from the displayed identifier: two FASTA records with the same ID remain two matching proteins, and overlapping occurrences remain separate match sites. Repeated reported members retain their multiplicity. A missing assignment column produces null assignment diagnostics; an existing column with blank or missing values produces zero counts. Neither matching backend selection nor persisted output schemas change.
Measure verify_peptides separately from input loading and output writing. Concurrent workflow jobs, protein database size and unique peptide count affect elapsed time; vendor input bytes alone do not describe verification cost.
Immutability
FastaAnnotator validates and binds its protein frame at construction, then accepts each canonical APB2 result explicitly. The parameters, result, and every report are frozen data values. Each operation deep-copies its input result and validates every output name before writing anything. A refused operation leaves the input untouched.