EvoX Genesis
EvoX Genesis turns a high-level software objective into a persistent recursive software world: finite-lived agents recursively develop software, while only accepted consequences persist across develop
翻譯準備中
Python library for reading and writing PEFF (PSI Extended FASTA Format) files. PEFF is a superset of FASTA used in proteomics that carries rich per-entry annotations.
翻譯準備中
取自儲存庫 README
正在載入完整 README。
檢視完整 README ↗A pure-Python library for reading and writing PEFF (PSI Extended FASTA Format) files — the proteomics community's FASTA extension for carrying rich per-entry annotations (PTMs, sequence variants, processed forms, and more) directly in the sequence header. It's for anyone building or consuming protein sequence databases that need more than a bare FASTA header can hold.
pefftacular parses PEFF into typed, structured objects instead of leaving you to regex the header yourself, and it's permissive by default: malformed-but-recoverable files still parse, with spec violations reported as warnings rather than hard failures.
Documentation: https://tacular-omics.github.io/pefftacular/
read_peff for the whole file at once, PeffReader to stream entries lazily without loading everything into memory.PeffWarning (opt in to warnings.simplefilter("error", PeffWarning) for strict parsing) while PeffParseError still carries a line, context, and a repair hint for structural failures.pip install pefftacular
Dev install:
just install
read_peff — load everything into memory at once:
from pefftacular import read_peff
header, entries = read_peff("proteins.peff")
for entry in entries:
print(entry.db_unique_id, entry.pname, len(entry.sequence))
PeffReader — iterate lazily without loading the full file:
from pefftacular import PeffReader
with PeffReader("proteins.peff") as reader:
file_header = reader.header
for entry in reader:
process(entry)
Both accept gzip, bzip2 and xz compressed files (proteins.peff.gz) directly, detected
from the magic bytes. read_peff, PeffReader, to_records and write_peff take a
pandas-style compression argument: "infer" (the default: magic bytes when reading, the
.gz/.bz2/.xz suffix when writing), "gzip", "bz2" or "xz" to force a format
(an open handle must then be binary), or None for plain text.
read_peff and PeffReader yield SequenceEntry objects with these fields:
| Field | Type | Description |
|---|---|---|
prefix | str | Database prefix (e.g. sp, tr) |
db_unique_id | str | Accession (e.g. P12345) |
sequence | str | Amino acid sequence |
pname | str | None | Protein name (\\PName=) |
gname | str | None | Gene name (\\GName=) |
ncbi_tax_id | int | None | NCBI taxonomy ID (\\NcbiTaxId=) |
length | int | None | Sequence length (\\Length=) |
sv | int | None | Sequence version (\\SV=) |
ev | int | None | Entry version (\\EV=) |
pe | int | None | Protein existence level (\\PE=) |
variant_simple | tuple[VariantSimple, ...] | Simple sequence variants |
variant_complex | tuple[VariantComplex, ...] | Multi-residue variants (start, end, new sequence, optional tag) |
mod_res_unimod | tuple[ModResUnimod, ...] | UniMod modification sites |
mod_res_psi | tuple[ModResPsi, ...] | PSI-MOD modification sites |
mod_res | tuple[ModRes, ...] | Other named modification sites |
processed | tuple[Processed, ...] | Processed sequence forms |
custom_values |
Variants:
from pefftacular import read_peff
_, entries = read_peff("proteins.peff")
entry = entries[0]
for v in entry.variant_simple:
print(v.position, v.new_amino_acid, v.tag)
# e.g. 42, "K", "rs12345"
Modifications (UniMod):
for mod in entry.mod_res_unimod:
print(mod.positions, mod.accession, mod.name)
# e.g. (17,), "21", "Phospho"
Modifications (PSI-MOD):
for mod in entry.mod_res_psi:
print(mod.positions, mod.accession, mod.name)
# e.g. (17,), "MOD:00696", "phosphorylated residue"
Processed forms:
for proc in entry.processed:
print(proc.start_pos, proc.end_pos, proc.accession, proc.name)
# e.g. 1, 24, "PRO_0000012345", "Signal peptide"
Custom keys (declared via # CustomKeyDef= in the header):
When the database header declares a custom key, entry values for that key are
parsed using its RegExp / FieldNames / FieldTypes and exposed as typed
fields on entry.custom_values. The original item text is preserved in raw
for lossless round-trips.
Header excerpt:
# CustomKeyDef=(KeyName=SecondaryStructure|Description="..."|ConceptCURIE=BAO:0000014|RegExp="([0-9]+)\|([0-9]+)\|([A-Za-z]+:[0-9]+)?\|(.+)"|FieldNames=StartPosition,EndPosition,CURIE,Description|FieldTypes=integer,integer,string,string)
Entry usage:
>cu:P00001 \SecondaryStructure=(10|20|ncithesaurus:C47937|Helix)
Access:
ss = entry.custom_values["SecondaryStructure"]
ss[0].fields["StartPosition"] # 10 (int)
ss[0].fields["Description"] # "Helix"
Supported FieldTypes are XSD basic types (string, integer, decimal,
boolean, date, time) plus enumeration(a|b|c). Coercion failures and
enumeration mismatches emit PeffWarning (a UserWarning subclass) and fall back to the raw string.
If no RegExp is declared, the value is split on | and zipped with
FieldNames.
Other non-standard keys (no CustomKeyDef registered) still land in
entry.extra as raw strings:
value = entry.extra.get("MyCustomKey")
from pefftacular import SequenceEntry
entry = SequenceEntry.from_fasta(
"sp|P31946|1433B_HUMAN 14-3-3 protein beta/alpha OS=Homo sapiens OX=9606 GN=YWHAB PE=1 SV=3",
"MTMDKSELVQKAKLAEQAERYDDMAAAMK",
)
entry.prefix, entry.db_unique_id, entry.id # ("sp", "P31946", "1433B_HUMAN")
header, sequence = entry.to_fasta() # back to a plain FASTA record
entry.to_proforma() # \ModResPsi sites, e.g. "MS[MOD:00046]TK..."
entry.to_proforma(mods="unimod") # from \ModResUnimod sites
entry.to_proforma(variants=entry.variant_simple[:1]) # with a substitution applied
entry.to_proforma(errors="skip") # None instead of PeffError (e.g. sites past the end)
These use plain strings (pefftacular has no dependencies). With fastatacular, pass
(record.raw_header, record.sequence). See From FASTA and
ProForma strings.
to_records(source) returns one plain dict per entry, so any data-frame library can
take the result directly. pefftacular does not ship or require pandas or polars;
install whichever you use. (The test suite runs these examples only when the library is
installed.)
import pandas as pd
import polars as pl
from pefftacular import to_records
records = to_records("proteins.peff") # a path, an open text handle, or entries
df = pd.DataFrame(records)
with_variants = df[df["variant_simple"].notna()] # entries that have \VariantSimple
human = pl.DataFrame(records).filter(pl.col("ncbi_tax_id") == 9606)
PeffReader.to_records() and SequenceEntry.to_record() give the same dicts. The file
header is not included. Every record has these keys, in this order
(pefftacular.RECORD_KEYS); an absent value is None:
| key | type | from |
|---|---|---|
prefix, db_unique_id | str | the identifier prefix:db_unique_id |
id, db_unique_id_key | str | \ID, \DbUniqueId |
pname, gname, tax_name, comment | str | \PName, \GName, \TaxName, \Comment |
ncbi_tax_id, length, sv, ev, pe | int | \NcbiTaxId, \Length, \SV, \EV, \PE |
decoy | bool | \Decoy |
variant_simple, variant_complex, mod_res_unimod, mod_res_psi, mod_res, processed, disulfide_bond, proteoform | str | the annotation's PEFF value text, e.g. (12|L)(30|*) |
custom_values, extra | str | header-declared custom keys / unknown keys as \Key=value text |
sequence | str | the residues |
The same fields in fastatacular records have other names where each package follows its own model. Rename these to put both in one frame:
| field | fastatacular | pefftacular |
|---|---|---|
| database prefix | prefix | prefix |
| accession | accession | db_unique_id |
| entry name | entry_name | id |
| protein name, gene | pname, gname | pname, gname |
| organism name | os_name | tax_name |
| taxon id, PE, SV | ncbi_tax_id, pe, sv | ncbi_tax_id, pe, sv |
| other keys | extra, KEY=value pairs | extra, \Key=value pairs |
| length, residues | length, sequence | length, sequence |
Build a header and entries, then write:
from pefftacular import DatabaseHeader, FileHeader, SequenceEntry, write_peff
db_header = DatabaseHeader(
prefix="sp",
db_name="SwissProt",
db_version="2024_01",
number_of_entries=1,
)
file_header = FileHeader(
peff_version="1.0",
databases=(db_header,),
)
entry = SequenceEntry(
prefix="sp",
db_unique_id="P12345",
sequence="MKTIIALSYIFCLVFA",
pname="Example protein",
gname="EXMP",
)
write_peff(file_header, [entry], "output.peff")
dest can be a file path string, a pathlib.Path, or a text-mode file object.
entries can be any iterable, including a generator: it is consumed once, as a stream.
Every written line is parsed back and compared with the entry, so a value that would
read back differently raises PeffWriteError instead of corrupting the file. For
entries that came unchanged from read_peff, pass verify=False to skip that check
(about 3x faster writes):
header, entries = read_peff("proteins.peff.gz")
write_peff(header, (e for e in entries if e.variant_simple), "variants.peff", verify=False)
Every exception derives from PeffError (a ValueError subclass), so you can
catch any failure with one clause. Parse errors carry structured, actionable
detail — .line, .context, and a .hint — and attach the offending text and
the hint as exception notes, so they also show up in tracebacks:
from pefftacular import PeffError, PeffParseError, read_peff
try:
header, entries = read_peff("malformed.peff")
except PeffParseError as e:
print(e.line) # 1-based line number where it failed
print(e.context) # the exact offending text
print(e.hint) # a short suggestion for how to fix it
except PeffError:
... # any other pefftacular failure
write_peff validates its input before writing anything and raises
PeffWriteError (also a PeffError), with a .hint and the bad entry's 0-based
.index (message prefix Entry N: ), for a missing header or
an entry with an empty prefix, db_unique_id or sequence. File-system failures
(missing directory, no permission) are not wrapped: they raise the usual
OSError (FileNotFoundError, PermissionError).
from pefftacular import PeffWriteError
try:
write_peff(file_header, entries, "output.peff")
except PeffWriteError as e: # invalid header/entries
print(e, e.hint)
except OSError as e: # could not open/write the file
print(e)
Reading is permissive: the data is always returned, but anything that
violates a PEFF MUST rule (out-of-range positions, missing required fields,
NumberOfEntries mismatches, un-coercible custom values, …) is reported through
the PeffWarning category. Promote them to errors when you want strict parsing:
import warnings
from pefftacular import PeffWarning, read_peff
warnings.simplefilter("error", PeffWarning)
header, entries = read_peff("suspect.peff") # now raises on any spec violation
The library follows the standard logging convention (it attaches a
NullHandler and never configures logging itself). Enable a behavioral trace —
useful when scripting or debugging with an AI coding agent:
import logging
logging.basicConfig(level=logging.DEBUG)
logging.getLogger("pefftacular").setLevel(logging.DEBUG)
Milestones (entries read/written) log at INFO; file open, header parse, and
entry counts log at DEBUG, under the pefftacular.parser / pefftacular.writer
loggers.
Contributor and AI-agent guidance lives in CLAUDE.md. The one
command to run before committing is just check (formatting, lint, types, and
tests — the same gate CI enforces); just fix auto-applies formatting.
just install # install dependencies
just check # format-check + lint + type-check + test (pre-commit gate)
just fix # auto-fix lint + formatting
just test # run tests
just test-file tests/test_errors.py # run a single test file
just cov # run tests with coverage
just build # build the package
just clean # remove cache files
Run just with no arguments to list every recipe.
If you use pefftacular in research, please cite the archived software release. Machine-readable citation metadata is available in CITATION.cff; GitHub's Cite this repository menu can render it as APA or BibTeX. DOI: 10.5281/zenodo.22925639.
Supported by NIH grants R01AG077046, R01MH132570, R01MH100175, R01HL165168 and U01AG088679.
dict[str, tuple[CustomKeyValue, ...]] |
Header-declared custom keys, parsed by their CustomKeyDef |
extra | dict[str, str] | Non-standard keys with no CustomKeyDef |