Skip to content

API reference

Generated from the docstrings in the source, so it cannot fall out of step with the code.

pymzlib.pride

pride

PRIDE Archive access, backed by mzLib's PrideArchiveClient.

The PRIDE Archive (https://www.ebi.ac.uk/pride/archive/) is EBI's public proteomics data repository. This module lets a Python user list what is in a project and pull files down, using the same paging, URL-resolution, and safe-download logic that mzLib uses in C#.

>>> import pymzlib
>>> files = pymzlib.pride.list_files("PXD000001")
>>> len(files)
8
>>> files[0].file_name
'PRIDE_Exp_Complete_Ac_22134.pride.mztab.gz'
>>> raw = [f for f in files if f.category == "RAW"]
>>> [f.file_name for f in raw]
['TMT_Erwinia_1uLSike_Top10HCD_isol2_45stepped_60min_01.raw']
>>> # Not run in CI: downloads 220 MB from EBI.
>>> pymzlib.pride.download("PXD000001", "downloads", category="RAW")   # doctest: +SKIP

ProjectNotFoundError

Bases: PyMzLibError

No project with that accession exists, or it has no files.

PRIDE answers an unknown accession with an empty result rather than a 404, so earlier versions of pyMzLib returned an empty list. That was a mistake: an empty list is indistinguishable from "this project genuinely has nothing matching", so a typo'd accession produced a script that reported "0 files, done" and moved on. A wrong answer that looks like a right answer is worse than an error.

Source code in pkg/python/src/pymzlib/pride.py
class ProjectNotFoundError(_bridge.PyMzLibError):
    """No project with that accession exists, or it has no files.

    PRIDE answers an unknown accession with an empty result rather than a 404, so earlier versions
    of pyMzLib returned an empty list. That was a mistake: an empty list is indistinguishable from
    "this project genuinely has nothing matching", so a typo'd accession produced a script that
    reported "0 files, done" and moved on. A wrong answer that looks like a right answer is worse
    than an error.
    """

PrideFile dataclass

One file belonging to a PRIDE Archive project.

Attributes:

Name Type Description
file_name str

The file's name, e.g. "run1.raw".

file_size_bytes int

Size in bytes as reported by PRIDE.

checksum str

The repository's checksum, or "" if it provides none.

category str

The file category, e.g. "RAW", "PEAK", "SEARCH", "OTHER".

https_url str | None

A direct HTTPS download URL, or None when the file is only reachable by a protocol that cannot be fetched over HTTPS (Aspera-only files).

category_accession str

The category's PRIDE controlled-vocabulary accession, e.g. "PRIDE:0000410".

locations list[dict[str, str]]

Every published location as {"accession", "name", "value"} dicts, for callers that want the raw controlled-vocabulary terms.

submission_date datetime | None

When the project was submitted, as a timezone-aware timestamp. None when PRIDE omitted it (the wire carries mzLib's 0001-01-01 placeholder).

publication_date datetime | None

When the project was made public; as submission_date.

updated_date datetime | None

When the project record last changed; as submission_date.

project_accession str

The accession the file was listed under.

Source code in pkg/python/src/pymzlib/pride.py
@dataclass(frozen=True)
class PrideFile:
    """One file belonging to a PRIDE Archive project.

    Attributes:
        file_name: The file's name, e.g. ``"run1.raw"``.
        file_size_bytes: Size in bytes as reported by PRIDE.
        checksum: The repository's checksum, or ``""`` if it provides none.
        category: The file category, e.g. ``"RAW"``, ``"PEAK"``, ``"SEARCH"``, ``"OTHER"``.
        https_url: A direct HTTPS download URL, or ``None`` when the file is only reachable
            by a protocol that cannot be fetched over HTTPS (Aspera-only files).
        category_accession: The category's PRIDE controlled-vocabulary accession, e.g.
            ``"PRIDE:0000410"``.
        locations: Every published location as ``{"accession", "name", "value"}`` dicts, for
            callers that want the raw controlled-vocabulary terms.
        submission_date: When the project was submitted, as a timezone-aware timestamp. ``None``
            when PRIDE omitted it (the wire carries mzLib's ``0001-01-01`` placeholder).
        publication_date: When the project was made public; as ``submission_date``.
        updated_date: When the project record last changed; as ``submission_date``.
        project_accession: The accession the file was listed under.
    """

    file_name: str
    file_size_bytes: int
    checksum: str
    category: str
    category_accession: str
    https_url: str | None
    locations: list[dict[str, str]] = field(default_factory=list)
    submission_date: datetime | None = None
    publication_date: datetime | None = None
    updated_date: datetime | None = None
    project_accession: str = ""

    @property
    def size_mb(self) -> float:
        """The file size in megabytes, for the common case of eyeballing a manifest."""
        return self.file_size_bytes / 1_000_000

    @property
    def extension(self) -> str:
        """The file's lowercase extension including the dot, e.g. ``".raw"``. Empty if none."""
        return Path(self.file_name).suffix.lower()

    @property
    def downloadable(self) -> bool:
        """Whether this file can be fetched by :func:`download` (i.e. has an HTTPS location)."""
        return self.https_url is not None

    def as_dict(self) -> dict[str, Any]:
        """Return every attribute, **including the computed ones**, as a plain dict.

        Use this rather than ``vars(f)`` when building a table. ``size_mb``, ``extension`` and
        ``downloadable`` are properties, so ``vars()`` and ``dataclasses.asdict()`` both skip
        them — which silently produced a DataFrame missing the three attributes the
        documentation pushes hardest, including the ``downloadable`` flag used to filter out
        files that cannot be fetched.

        Examples:
            >>> # Not run in CI: pandas is not a pyMzLib dependency.
            >>> import pandas as pd                                    # doctest: +SKIP
            >>> df = pd.DataFrame([f.as_dict() for f in files])        # doctest: +SKIP
        """
        record = asdict(self)
        record["size_mb"] = self.size_mb
        record["extension"] = self.extension
        record["downloadable"] = self.downloadable
        return record

    @classmethod
    def _from_wire(cls, payload: dict[str, Any], project_accession: str = "") -> "PrideFile":
        return cls(
            file_name=payload.get("file_name", ""),
            file_size_bytes=int(payload.get("file_size_bytes", 0)),
            checksum=payload.get("checksum", ""),
            category=payload.get("category", ""),
            category_accession=payload.get("category_accession", ""),
            https_url=payload.get("https_url"),
            locations=list(payload.get("locations") or []),
            submission_date=_parse_timestamp(payload.get("submission_date")),
            publication_date=_parse_timestamp(payload.get("publication_date")),
            updated_date=_parse_timestamp(payload.get("updated_date")),
            project_accession=project_accession,
        )
size_mb property
size_mb: float

The file size in megabytes, for the common case of eyeballing a manifest.

extension property
extension: str

The file's lowercase extension including the dot, e.g. ".raw". Empty if none.

downloadable property
downloadable: bool

Whether this file can be fetched by :func:download (i.e. has an HTTPS location).

as_dict
as_dict() -> dict[str, Any]

Return every attribute, including the computed ones, as a plain dict.

Use this rather than vars(f) when building a table. size_mb, extension and downloadable are properties, so vars() and dataclasses.asdict() both skip them — which silently produced a DataFrame missing the three attributes the documentation pushes hardest, including the downloadable flag used to filter out files that cannot be fetched.

Examples:

>>> # Not run in CI: pandas is not a pyMzLib dependency.
>>> import pandas as pd
>>> df = pd.DataFrame([f.as_dict() for f in files])
Source code in pkg/python/src/pymzlib/pride.py
def as_dict(self) -> dict[str, Any]:
    """Return every attribute, **including the computed ones**, as a plain dict.

    Use this rather than ``vars(f)`` when building a table. ``size_mb``, ``extension`` and
    ``downloadable`` are properties, so ``vars()`` and ``dataclasses.asdict()`` both skip
    them — which silently produced a DataFrame missing the three attributes the
    documentation pushes hardest, including the ``downloadable`` flag used to filter out
    files that cannot be fetched.

    Examples:
        >>> # Not run in CI: pandas is not a pyMzLib dependency.
        >>> import pandas as pd                                    # doctest: +SKIP
        >>> df = pd.DataFrame([f.as_dict() for f in files])        # doctest: +SKIP
    """
    record = asdict(self)
    record["size_mb"] = self.size_mb
    record["extension"] = self.extension
    record["downloadable"] = self.downloadable
    return record

PrideFtpFile dataclass

One file found by walking a PRIDE project's FTP directory tree — the complete listing.

This is what :func:list_ftp_files returns, and the difference from :class:PrideFile is the whole point: the FTP walk sees everything the project holds, including the files PRIDE's REST manifest omits and files nested in subdirectories. The trade-off is the size: PRIDE's directory index rounds it to about three significant figures, so it is approximate_size_bytes — a good project-size estimate, not the exact number of bytes you will transfer.

Attributes:

Name Type Description
relative_path str

Path relative to the project's FTP root, e.g. "run1.raw" or, for a file in a subdirectory, "generated/summary.mztab".

file_name str

The bare file name — the last segment of relative_path.

url str

The HTTPS URL the file can be downloaded from.

approximate_size_bytes int

PRIDE's rounded index size in bytes. For the exact transfer size of one file, issue an HTTP HEAD against url and read its Content-Length.

project_accession str

The accession this file was listed under.

Source code in pkg/python/src/pymzlib/pride.py
@dataclass(frozen=True)
class PrideFtpFile:
    """One file found by walking a PRIDE project's **FTP directory tree** — the complete listing.

    This is what :func:`list_ftp_files` returns, and the difference from :class:`PrideFile` is the
    whole point: the FTP walk sees everything the project holds, including the files PRIDE's REST
    manifest omits and files nested in subdirectories. The trade-off is the size: PRIDE's directory
    index rounds it to about three significant figures, so it is ``approximate_size_bytes`` — a good
    project-size estimate, not the exact number of bytes you will transfer.

    Attributes:
        relative_path: Path relative to the project's FTP root, e.g. ``"run1.raw"`` or, for a file
            in a subdirectory, ``"generated/summary.mztab"``.
        file_name: The bare file name — the last segment of ``relative_path``.
        url: The HTTPS URL the file can be downloaded from.
        approximate_size_bytes: PRIDE's rounded index size in bytes. For the exact transfer size of
            one file, issue an HTTP HEAD against ``url`` and read its ``Content-Length``.
        project_accession: The accession this file was listed under.
    """

    relative_path: str
    file_name: str
    url: str
    approximate_size_bytes: int
    project_accession: str = ""

    @property
    def approximate_size_mb(self) -> float:
        """The approximate size in megabytes, for eyeballing a project's footprint."""
        return self.approximate_size_bytes / 1_000_000

    @property
    def extension(self) -> str:
        """The file's lowercase extension including the dot, e.g. ``".raw"``. Empty if none."""
        return Path(self.file_name).suffix.lower()

    def as_dict(self) -> dict[str, Any]:
        """Return every attribute, **including the computed ones**, as a plain dict for a DataFrame."""
        record = asdict(self)
        record["approximate_size_mb"] = self.approximate_size_mb
        record["extension"] = self.extension
        return record

    @classmethod
    def _from_wire(cls, payload: dict[str, Any], project_accession: str = "") -> "PrideFtpFile":
        return cls(
            relative_path=payload.get("relative_path", ""),
            file_name=payload.get("file_name", ""),
            url=payload.get("url", ""),
            approximate_size_bytes=int(payload.get("approximate_size_bytes", 0)),
            project_accession=project_accession,
        )
approximate_size_mb property
approximate_size_mb: float

The approximate size in megabytes, for eyeballing a project's footprint.

extension property
extension: str

The file's lowercase extension including the dot, e.g. ".raw". Empty if none.

as_dict
as_dict() -> dict[str, Any]

Return every attribute, including the computed ones, as a plain dict for a DataFrame.

Source code in pkg/python/src/pymzlib/pride.py
def as_dict(self) -> dict[str, Any]:
    """Return every attribute, **including the computed ones**, as a plain dict for a DataFrame."""
    record = asdict(self)
    record["approximate_size_mb"] = self.approximate_size_mb
    record["extension"] = self.extension
    return record

PrideProjectSearchResult dataclass

One hit from :func:search.

This is not a project's full metadata, and the two are not interchangeable. PRIDE serves search from a separate Elasticsearch projection in which every controlled-vocabulary field has been flattened to a display string: the same project reports its instruments as ["Q Exactive"] here and as structured terms with accessions from the metadata endpoint, contacts collapse from ten-field objects to a display name, and publications to a single pre-formatted citation string. That is a property of PRIDE's wire, not a simplification chosen here — the accessions are simply not sent. Follow :attr:accession when you need the vocabulary.

A zero or an empty list means "not reported", never a measured zero. PRIDE omits nothing as null, so absence arrives as 0, "" or [], and several fields are genuinely sparse — sampled across 1,600 hits, :attr:project_tags was populated on 2.6%, :attr:sdrf on 2.4%, :attr:other_omics_links on 18%, and the bot/hub/organic trio on under half. Do not read a download_count of 0 as "nobody downloaded it".

Attributes:

Name Type Description
accession str

The project accession — the key to everything else in this module. Usually a PXD accession, but search also returns legacy PRD and affinity PAD ones, so treat it as an opaque key rather than assuming a prefix.

title str

The project title.

project_description str

The submitter's free-text description.

sample_processing_protocol str

How the sample was prepared, as free text.

data_processing_protocol str

How the data were searched and processed, as free text.

doi str

The dataset DOI, or "" if PRIDE has not minted one.

submission_type str

"COMPLETE", "PARTIAL", "AFFINITY", or "PRIDE" for legacy submissions. Not a closed set — PRIDE has added values before.

sdrf str

The project's SDRF metadata as a single space-joined bag of term values, flattened by the search index. Not a file, filename or URL — nothing can be fetched with it and the row/column structure is gone. For a real SDRF see :mod:pymzlib.sdrf.

submission_date date | None

A calendar :class:~datetime.date, not a timestamp: PRIDE's search index sends no time and no offset. None when PRIDE reported none.

publication_date date | None

As submission_date.

updated_date date | None

As submission_date.

project_tags list[str]

PRIDE's coarse classification tags.

keywords list[str]

The submitter's free-text keywords. May contain empty and whitespace-only strings — PRIDE ships them on roughly 9% of hits. They are passed through rather than filtered, because dropping them here would make this module disagree with mzLib and with the Rust and R bindings about what a project's keywords are. Filter before joining.

submitters list[str]

Display names, flattened from the structured contact objects the metadata endpoint returns.

lab_pis list[str]

Display names of the lab heads, flattened the same way.

affiliations list[str]

Display strings.

instruments list[str]

Display names only; the controlled-vocabulary accessions are not sent.

softwares list[str]

Display names.

quantification_methods list[str]

Display names, e.g. "TMT".

sample_attributes list[str]

Sample characteristics by display value (e.g. "liver"). Flattened: which characteristic each value describes is not recoverable from a search hit.

organisms list[str]

Display names, e.g. "Homo sapiens (human)".

organism_parts list[str]

Display names.

diseases list[str]

Display names.

references list[str]

Publications, each a single pre-formatted citation string. A PubMed ID or DOI cannot be read out of one without parsing the string PRIDE assembled.

experiment_types list[str]

e.g. "Data-independent acquisition".

project_file_names list[str]

File names only — a search convenience, not the manifest. It carries no sizes, categories or download locations. Use :func:list_files, or :func:list_ftp_files for the complete list, to act on the files.

other_omics_links list[str]

Links to related datasets in other omics repositories.

highlights dict[str, list[str]]

Why this project matched, keyed by the field each match was found in, with the matched terms wrapped in <em> markup. The keys vary per hit and per query. This is the one thing search returns that the metadata endpoint cannot.

yearly_downloads list[dict[str, Any]]

{"year", "count"} dicts: year a string such as "2025", count the downloads in that year. Empty when not reported.

download_count int

Total downloads. 0 means not reported, never a measured zero.

avg_downloads_per_file float

Mean downloads per file. 0 means not reported.

percentile int

Download-popularity percentile within PRIDE. 0 means not reported.

bot_count int

Downloads attributed to crawlers. 0 means not reported.

hub_count int

Downloads attributed to institutional or aggregating hubs. 0 means not reported.

organic_count int

Downloads attributed to ordinary human traffic. 0 means not reported.

Source code in pkg/python/src/pymzlib/pride.py
@dataclass(frozen=True)
class PrideProjectSearchResult:
    """One hit from :func:`search`.

    **This is not a project's full metadata, and the two are not interchangeable.** PRIDE serves
    search from a separate Elasticsearch projection in which every controlled-vocabulary field has
    been **flattened to a display string**: the same project reports its instruments as
    ``["Q Exactive"]`` here and as structured terms with accessions from the metadata endpoint,
    contacts collapse from ten-field objects to a display name, and publications to a single
    pre-formatted citation string. That is a property of PRIDE's wire, not a simplification chosen
    here — the accessions are simply not sent. Follow :attr:`accession` when you need the
    vocabulary.

    **A zero or an empty list means "not reported", never a measured zero.** PRIDE omits nothing as
    null, so absence arrives as ``0``, ``""`` or ``[]``, and several fields are genuinely sparse —
    sampled across 1,600 hits, :attr:`project_tags` was populated on 2.6%, :attr:`sdrf` on 2.4%,
    :attr:`other_omics_links` on 18%, and the bot/hub/organic trio on under half. Do not read a
    ``download_count`` of 0 as "nobody downloaded it".

    Attributes:
        accession: The project accession — the key to everything else in this module. Usually a
            ``PXD`` accession, but search also returns legacy ``PRD`` and affinity ``PAD`` ones, so
            treat it as an opaque key rather than assuming a prefix.
        title: The project title.
        project_description: The submitter's free-text description.
        sample_processing_protocol: How the sample was prepared, as free text.
        data_processing_protocol: How the data were searched and processed, as free text.
        doi: The dataset DOI, or ``""`` if PRIDE has not minted one.
        submission_type: ``"COMPLETE"``, ``"PARTIAL"``, ``"AFFINITY"``, or ``"PRIDE"`` for legacy
            submissions. Not a closed set — PRIDE has added values before.
        sdrf: The project's SDRF metadata as a single space-joined bag of term *values*, flattened
            by the search index. **Not a file, filename or URL** — nothing can be fetched with it
            and the row/column structure is gone. For a real SDRF see :mod:`pymzlib.sdrf`.
        submission_date: A calendar :class:`~datetime.date`, not a timestamp: PRIDE's search index
            sends no time and no offset. ``None`` when PRIDE reported none.
        publication_date: As ``submission_date``.
        updated_date: As ``submission_date``.
        project_tags: PRIDE's coarse classification tags.
        keywords: The submitter's free-text keywords. **May contain empty and whitespace-only
            strings** — PRIDE ships them on roughly 9% of hits. They are passed through rather than
            filtered, because dropping them here would make this module disagree with mzLib and with
            the Rust and R bindings about what a project's keywords are. Filter before joining.
        submitters: Display names, flattened from the structured contact objects the metadata
            endpoint returns.
        lab_pis: Display names of the lab heads, flattened the same way.
        affiliations: Display strings.
        instruments: Display names only; the controlled-vocabulary accessions are not sent.
        softwares: Display names.
        quantification_methods: Display names, e.g. ``"TMT"``.
        sample_attributes: Sample characteristics by display *value* (e.g. ``"liver"``). Flattened:
            which characteristic each value describes is **not recoverable** from a search hit.
        organisms: Display names, e.g. ``"Homo sapiens (human)"``.
        organism_parts: Display names.
        diseases: Display names.
        references: Publications, each a single pre-formatted citation string. A PubMed ID or DOI
            cannot be read out of one without parsing the string PRIDE assembled.
        experiment_types: e.g. ``"Data-independent acquisition"``.
        project_file_names: File *names* only — a search convenience, **not the manifest**. It
            carries no sizes, categories or download locations. Use :func:`list_files`, or
            :func:`list_ftp_files` for the complete list, to act on the files.
        other_omics_links: Links to related datasets in other omics repositories.
        highlights: Why this project matched, keyed by the field each match was found in, with the
            matched terms wrapped in ``<em>`` markup. The keys vary per hit and per query. This is
            the one thing search returns that the metadata endpoint cannot.
        yearly_downloads: ``{"year", "count"}`` dicts: ``year`` a string such as ``"2025"``,
            ``count`` the downloads in that year. Empty when not reported.
        download_count: Total downloads. ``0`` means not reported, never a measured zero.
        avg_downloads_per_file: Mean downloads per file. ``0`` means not reported.
        percentile: Download-popularity percentile within PRIDE. ``0`` means not reported.
        bot_count: Downloads attributed to crawlers. ``0`` means not reported.
        hub_count: Downloads attributed to institutional or aggregating hubs. ``0`` means not
            reported.
        organic_count: Downloads attributed to ordinary human traffic. ``0`` means not reported.
    """

    accession: str
    title: str = ""
    project_description: str = ""
    sample_processing_protocol: str = ""
    data_processing_protocol: str = ""
    doi: str = ""
    submission_type: str = ""
    sdrf: str = ""
    submission_date: date | None = None
    publication_date: date | None = None
    updated_date: date | None = None
    project_tags: list[str] = field(default_factory=list)
    keywords: list[str] = field(default_factory=list)
    submitters: list[str] = field(default_factory=list)
    lab_pis: list[str] = field(default_factory=list)
    affiliations: list[str] = field(default_factory=list)
    instruments: list[str] = field(default_factory=list)
    softwares: list[str] = field(default_factory=list)
    quantification_methods: list[str] = field(default_factory=list)
    sample_attributes: list[str] = field(default_factory=list)
    organisms: list[str] = field(default_factory=list)
    organism_parts: list[str] = field(default_factory=list)
    diseases: list[str] = field(default_factory=list)
    references: list[str] = field(default_factory=list)
    experiment_types: list[str] = field(default_factory=list)
    project_file_names: list[str] = field(default_factory=list)
    other_omics_links: list[str] = field(default_factory=list)
    highlights: dict[str, list[str]] = field(default_factory=dict)
    yearly_downloads: list[dict[str, Any]] = field(default_factory=list)
    download_count: int = 0
    avg_downloads_per_file: float = 0.0
    percentile: int = 0
    bot_count: int = 0
    hub_count: int = 0
    organic_count: int = 0

    @property
    def matched_fields(self) -> list[str]:
        """Which PRIDE fields the query hit, from :attr:`highlights`. Empty if PRIDE reported none."""
        return sorted(self.highlights)

    def as_dict(self) -> dict[str, Any]:
        """Every attribute, **including the computed ones**, as a plain dict.

        Use this rather than ``vars()`` when building a table: ``matched_fields`` is a property, so
        ``dataclasses.asdict()`` silently omits it. Same reasoning as :meth:`PrideFile.as_dict`.
        """
        record = asdict(self)
        record["matched_fields"] = self.matched_fields
        return record

    @classmethod
    def _from_wire(cls, payload: dict[str, Any]) -> "PrideProjectSearchResult":
        def strings(name: str) -> list[str]:
            return [str(x) for x in (payload.get(name) or [])]

        return cls(
            accession=payload.get("accession", ""),
            title=payload.get("title", ""),
            project_description=payload.get("project_description", ""),
            sample_processing_protocol=payload.get("sample_processing_protocol", ""),
            data_processing_protocol=payload.get("data_processing_protocol", ""),
            doi=payload.get("doi", ""),
            submission_type=payload.get("submission_type", ""),
            sdrf=payload.get("sdrf", ""),
            submission_date=_parse_date(payload.get("submission_date")),
            publication_date=_parse_date(payload.get("publication_date")),
            updated_date=_parse_date(payload.get("updated_date")),
            project_tags=strings("project_tags"),
            keywords=strings("keywords"),
            submitters=strings("submitters"),
            lab_pis=strings("lab_pis"),
            affiliations=strings("affiliations"),
            instruments=strings("instruments"),
            softwares=strings("softwares"),
            quantification_methods=strings("quantification_methods"),
            sample_attributes=strings("sample_attributes"),
            organisms=strings("organisms"),
            organism_parts=strings("organism_parts"),
            diseases=strings("diseases"),
            references=strings("references"),
            experiment_types=strings("experiment_types"),
            project_file_names=strings("project_file_names"),
            other_omics_links=strings("other_omics_links"),
            highlights={
                str(k): [str(v) for v in (vs or [])]
                for k, vs in (payload.get("highlights") or {}).items()
            },
            yearly_downloads=list(payload.get("yearly_downloads") or []),
            download_count=int(payload.get("download_count", 0)),
            avg_downloads_per_file=float(payload.get("avg_downloads_per_file", 0.0)),
            percentile=int(payload.get("percentile", 0)),
            bot_count=int(payload.get("bot_count", 0)),
            hub_count=int(payload.get("hub_count", 0)),
            organic_count=int(payload.get("organic_count", 0)),
        )
matched_fields property
matched_fields: list[str]

Which PRIDE fields the query hit, from :attr:highlights. Empty if PRIDE reported none.

as_dict
as_dict() -> dict[str, Any]

Every attribute, including the computed ones, as a plain dict.

Use this rather than vars() when building a table: matched_fields is a property, so dataclasses.asdict() silently omits it. Same reasoning as :meth:PrideFile.as_dict.

Source code in pkg/python/src/pymzlib/pride.py
def as_dict(self) -> dict[str, Any]:
    """Every attribute, **including the computed ones**, as a plain dict.

    Use this rather than ``vars()`` when building a table: ``matched_fields`` is a property, so
    ``dataclasses.asdict()`` silently omits it. Same reasoning as :meth:`PrideFile.as_dict`.
    """
    record = asdict(self)
    record["matched_fields"] = self.matched_fields
    return record

list_files

list_files(accession: str, page_size: int = 100, timeout: float | None = 300) -> list[PrideFile]

Return the file manifest of a PRIDE Archive project.

This is what PRIDE's REST API publishes, which is not always everything in the project. For PXD000001 the API returns 8 files while the FTP tree holds 13, and the five it omits include the two largest: ...60min_01-20141210.mzML (450 MB) and the matching .mzXML (472 MB), exactly the modern open-format conversions most people want. The omission is PRIDE's, not mzLib's. If completeness matters, use :func:list_ftp_files, which walks the project's FTP directory and returns everything it actually holds.

Paging is handled for you: however many pages the project spans, you get one list.

Parameters:

Name Type Description Default
accession str

The project accession, e.g. "PXD000001".

required
page_size int

How many files to request per underlying API call. Only affects how the manifest is fetched, never what you get back.

100
timeout float | None

Seconds to allow for the whole fetch.

300

Returns:

Type Description
list[PrideFile]

Every file in the project, in repository order. An unknown accession yields an empty

list[PrideFile]

list rather than an error — that is PRIDE's own behavior, preserved here.

Raises:

Type Description
UsageError

the accession is blank or the page size is not positive.

BridgeError

PRIDE returned an error status or was unreachable.

Source code in pkg/python/src/pymzlib/pride.py
def list_files(accession: str, page_size: int = 100, timeout: float | None = 300) -> list[PrideFile]:
    """Return the file manifest of a PRIDE Archive project.

    **This is what PRIDE's REST API publishes, which is not always everything in the
    project.** For PXD000001 the API returns **8** files while the FTP tree holds **13**, and
    the five it omits include the two largest: ``...60min_01-20141210.mzML`` (450 MB) and the
    matching ``.mzXML`` (472 MB), exactly the modern open-format conversions most people want.
    The omission is PRIDE's, not mzLib's. **If completeness matters, use :func:`list_ftp_files`,**
    which walks the project's FTP directory and returns everything it actually holds.

    Paging is handled for you: however many pages the project spans, you get one list.

    Args:
        accession: The project accession, e.g. ``"PXD000001"``.
        page_size: How many files to request per underlying API call. Only affects how the
            manifest is fetched, never what you get back.
        timeout: Seconds to allow for the whole fetch.

    Returns:
        Every file in the project, in repository order. An unknown accession yields an empty
        list rather than an error — that is PRIDE's own behavior, preserved here.

    Raises:
        UsageError: the accession is blank or the page size is not positive.
        BridgeError: PRIDE returned an error status or was unreachable.
    """
    canonical = _normalise_accession(accession)
    if isinstance(page_size, bool) or not isinstance(page_size, int):
        raise _bridge.UsageError(
            f"page_size must be a whole number; got {type(page_size).__name__} ({page_size!r})."
        )
    if page_size <= 0:
        raise _bridge.UsageError(f"page_size must be positive; got {page_size}.")
    if page_size > 2_147_483_647:
        raise _bridge.UsageError(f"page_size is larger than the API allows; got {page_size}.")

    data = _bridge.invoke(
        "pride", "files",
        "--accession", canonical,
        "--page-size", str(page_size),
        timeout=timeout,
    )
    files = [PrideFile._from_wire(item, canonical) for item in data.get("files", [])]

    if not files:
        raise ProjectNotFoundError(
            f"PRIDE returned no files for '{canonical}'. Either the accession does not exist "
            "(check for a typo) or the project is private. PRIDE does not distinguish the two, "
            "so neither can pyMzLib."
        )
    return files

list_ftp_files

list_ftp_files(accession: str, timeout: float | None = 300) -> list[PrideFtpFile]

Return the complete file list of a PRIDE project, read from its FTP directory tree.

This is the authoritative counterpart to :func:list_files. Where list_files returns PRIDE's REST manifest — which is knowingly incomplete, omitting for PXD000001 five of the project's 13 files, including the two largest — this walks the FTP directory (subdirectories included) and returns everything the project actually holds. Reach for it whenever completeness or a true project size matters; use :func:list_files when you want the rich metadata (category, checksum, controlled-vocabulary locations) that the REST manifest carries and the directory index does not.

This is a listing surface only. :func:download and :func:download_files operate on the REST manifest, so a file that appears only here — the whole point of this function — is not accepted by them; fetch it directly from its :attr:PrideFtpFile.url with an ordinary HTTPS client (e.g. urllib.request.urlretrieve(f.url, f.file_name)).

The sizes are approximate: PRIDE's directory index rounds them (see :attr:PrideFtpFile.approximate_size_bytes), so :func:approximate_total_size_bytes is an estimate — but an estimate over the whole project, unlike :func:total_size_bytes.

>>> # Not run in CI: no "pride ftp-files" recording to replay yet.
>>> ftp = pymzlib.pride.list_ftp_files("PXD000001")           # doctest: +SKIP
>>> len(ftp)                                                  # doctest: +SKIP
13
>>> nested = [f.relative_path for f in ftp if "/" in f.relative_path]   # doctest: +SKIP

Parameters:

Name Type Description Default
accession str

The project accession, e.g. "PXD000001".

required
timeout float | None

Seconds to allow for the whole walk, which spans one request per directory.

300

Returns:

Type Description
list[PrideFtpFile]

Every file under the project's FTP root, subdirectories included, in the order the walk

list[PrideFtpFile]

encounters them. Never empty — an empty result is raised as an error (see below).

Raises:

Type Description
UsageError

the accession is blank or malformed.

ProjectNotFoundError

no project has that accession (or it lacks the publication date that locates its FTP directory), or the directory listed no files. Same "no such project" signal :func:list_files raises, so one except catches both — the difference from list_files is only how it is detected (mzLib resolves the project before walking, so a typo fails loudly rather than looking like an empty project).

ServiceUnavailableError / BridgeError

PRIDE was unreachable, or a directory fetch failed.

Source code in pkg/python/src/pymzlib/pride.py
def list_ftp_files(accession: str, timeout: float | None = 300) -> list[PrideFtpFile]:
    """Return the **complete** file list of a PRIDE project, read from its FTP directory tree.

    This is the authoritative counterpart to :func:`list_files`. Where ``list_files`` returns
    PRIDE's REST manifest — which is knowingly incomplete, omitting for PXD000001 five of the
    project's 13 files, including the two largest — this walks the FTP directory (subdirectories
    included) and returns everything the project actually holds. Reach for it whenever completeness
    or a true project size matters; use :func:`list_files` when you want the rich metadata (category,
    checksum, controlled-vocabulary locations) that the REST manifest carries and the directory
    index does not.

    This is a **listing** surface only. :func:`download` and :func:`download_files` operate on the
    REST manifest, so a file that appears *only* here — the whole point of this function — is not
    accepted by them; fetch it directly from its :attr:`PrideFtpFile.url` with an ordinary HTTPS
    client (e.g. ``urllib.request.urlretrieve(f.url, f.file_name)``).

    The sizes are approximate: PRIDE's directory index rounds them (see
    :attr:`PrideFtpFile.approximate_size_bytes`), so :func:`approximate_total_size_bytes` is an
    estimate — but an estimate over the *whole* project, unlike :func:`total_size_bytes`.

        >>> # Not run in CI: no "pride ftp-files" recording to replay yet.
        >>> ftp = pymzlib.pride.list_ftp_files("PXD000001")           # doctest: +SKIP
        >>> len(ftp)                                                  # doctest: +SKIP
        13
        >>> nested = [f.relative_path for f in ftp if "/" in f.relative_path]   # doctest: +SKIP

    Args:
        accession: The project accession, e.g. ``"PXD000001"``.
        timeout: Seconds to allow for the whole walk, which spans one request per directory.

    Returns:
        Every file under the project's FTP root, subdirectories included, in the order the walk
        encounters them. Never empty — an empty result is raised as an error (see below).

    Raises:
        UsageError: the accession is blank or malformed.
        ProjectNotFoundError: no project has that accession (or it lacks the publication date that
            locates its FTP directory), or the directory listed no files. Same "no such project"
            signal :func:`list_files` raises, so one ``except`` catches both — the difference from
            ``list_files`` is only *how* it is detected (mzLib resolves the project before walking,
            so a typo fails loudly rather than looking like an empty project).
        ServiceUnavailableError / BridgeError: PRIDE was unreachable, or a directory fetch failed.
    """
    canonical = _normalise_accession(accession)

    try:
        data = _bridge.invoke(
            "pride", "ftp-files",
            "--accession", canonical,
            timeout=timeout,
        )
    except _bridge.BridgeError as exc:
        # mzLib resolves the project (and its publication date) before walking, so an unknown
        # accession comes back as an MzLibException rather than an empty list. Re-map it to the same
        # ProjectNotFoundError that list_files() raises so callers catch one type for "not there".
        # ServiceUnavailableError (a BridgeError subclass) has a different error_type and is left to
        # propagate — an outage is not a missing project.
        #
        # Only the two MzLibExceptions that mean "not there" are re-mapped, matched on mzLib's own
        # wording (PrideArchiveClient.GetProjectAsync and ListFtpFilesAsync at the pin). Every other
        # MzLibException - a cyclic FTP listing that exceeded the depth limit, for one - is a broken
        # listing, not a missing project, and keeps its BridgeError.
        if exc.error_type == "MzLibException" and any(
            marker in str(exc) for marker in _PROJECT_NOT_FOUND_MARKERS
        ):
            raise ProjectNotFoundError(
                f"PRIDE has no project '{canonical}' (or it lacks the publication date needed to "
                "locate its FTP directory). Check for a typo — a private project looks the same."
            ) from exc
        raise

    files = [PrideFtpFile._from_wire(item, canonical) for item in data.get("files", [])]
    if not files:
        # The project resolved but its FTP directory listed nothing. Almost always PRIDE changing
        # its autoindex format, never a real published project. Raise rather than hand back [], the
        # same "0 files, done" trap list_files() defends against.
        raise ProjectNotFoundError(
            f"The FTP directory for '{canonical}' listed no files. Either the project is genuinely "
            "empty or PRIDE's directory-index format has changed; list_files() may still return "
            "its REST manifest."
        )
    return files

download

download(accession: str, destination: str | Path, category: str | None = None, extensions: Sequence[str] | None = None, overwrite: bool = True, timeout: float | None = None) -> list[Path]

Download a project's files, optionally filtered, and return where they landed.

Files are streamed to a temporary name and moved into place only once complete, so an interrupted download never leaves a truncated file behind.

Parameters:

Name Type Description Default
accession str

The project accession, e.g. "PXD000001".

required
destination str | Path

Directory to write into. Created if it does not exist.

required
category str | None

Keep only files of this category, e.g. "RAW". None keeps all.

None
extensions Sequence[str] | None

Keep only files with these extensions, e.g. [".raw", ".mzML"]. None keeps all. Combined with category as AND.

None
overwrite bool

When False, a file already present at the destination is left alone and not re-fetched — a cheap resume for a large project.

True
timeout float | None

Seconds to allow. None (the default) waits as long as it takes, which is usually what you want for multi-gigabyte projects.

None

Returns:

Type Description
list[Path]

The paths written, in manifest order.

Raises:

Type Description
UsageError

the accession or destination is blank.

BridgeError

a request failed, or a selected file has no HTTPS location.

Source code in pkg/python/src/pymzlib/pride.py
def download(
    accession: str,
    destination: str | Path,
    category: str | None = None,
    extensions: Sequence[str] | None = None,
    overwrite: bool = True,
    timeout: float | None = None,
) -> list[Path]:
    """Download a project's files, optionally filtered, and return where they landed.

    Files are streamed to a temporary name and moved into place only once complete, so an
    interrupted download never leaves a truncated file behind.

    Args:
        accession: The project accession, e.g. ``"PXD000001"``.
        destination: Directory to write into. Created if it does not exist.
        category: Keep only files of this category, e.g. ``"RAW"``. ``None`` keeps all.
        extensions: Keep only files with these extensions, e.g. ``[".raw", ".mzML"]``.
            ``None`` keeps all. Combined with ``category`` as AND.
        overwrite: When ``False``, a file already present at the destination is left alone
            and not re-fetched — a cheap resume for a large project.
        timeout: Seconds to allow. ``None`` (the default) waits as long as it takes, which
            is usually what you want for multi-gigabyte projects.

    Returns:
        The paths written, in manifest order.

    Raises:
        UsageError: the accession or destination is blank.
        BridgeError: a request failed, or a selected file has no HTTPS location.
    """
    canonical = _normalise_accession(accession)
    target = _normalise_destination(destination)
    wanted = _normalise_extensions(extensions)

    args = ["pride", "download", "--accession", canonical, "--dest", str(target)]
    if category is not None:
        if not isinstance(category, str):
            raise _bridge.UsageError(
                f"category must be a string like 'RAW'; got {type(category).__name__} ({category!r})."
            )
        if not category.strip():
            raise _bridge.UsageError(
                "category is empty. Omit it to download every category, rather than passing a "
                "blank value — a filter that selects nothing must not silently select everything."
            )
        args += ["--category", _reject_flag_like("category", category.strip())]
    if wanted:
        args += ["--ext", _reject_flag_like("extensions", ",".join(wanted))]
    if not overwrite:
        args.append("--no-overwrite")

    data = _bridge.invoke(*args, timeout=timeout)
    written = [Path(p) for p in data.get("paths", [])]

    # Same doctrine as list_files and download_files, which this function was inconsistent with:
    # a filter that matched nothing is nearly always a filter that does not mean what its author
    # thought, and reporting success with an empty list lets a batch script carry on as though
    # the work had been done.
    if not written and (category is not None or wanted):
        raise _bridge.UsageError(
            f"No file in {canonical} matched "
            f"{'category ' + repr(category) if category else ''}"
            f"{' and ' if category and wanted else ''}"
            f"{'extensions ' + repr(wanted) if wanted else ''}. "
            "Use list_files() to see what the project actually contains — note that "
            "compressed files such as 'x.mgf.gz' have the extension '.gz'."
        )
    return written

download_files

download_files(files: Iterable[PrideFile], destination: str | Path, overwrite: bool = True, timeout: float | None = None) -> list[Path]

Download exactly the files you selected, and nothing else.

This is the counterpart to :func:list_files, and usually the one you want. Filter the manifest however you like — in Python, with the full expressiveness of Python — and hand the result straight back:

>>> files = list_files("PXD000001")
>>> small = [f for f in files if f.size_mb < 5 and f.downloadable]
>>> len(small)
3
>>> # Not run in CI: downloads from EBI.
>>> download_files(small, "downloads")                          # doctest: +SKIP

:func:download's category and extensions filters can only express what they were built to express; "under 5 MB", "the three newest", or "everything except the MGF" cannot be said in that vocabulary at all. They can all be said in a list comprehension.

Parameters:

Name Type Description Default
files Iterable[PrideFile]

The :class:PrideFile objects to fetch, from one project.

required
destination str | Path

Directory to write into. Created if it does not exist.

required
overwrite bool

When False, files already present are left alone and not re-fetched.

True
timeout float | None

Seconds to allow. None waits as long as it takes.

None

Returns:

Type Description
list[Path]

The paths written, in the order the repository lists them.

Raises:

Type Description
UsageError

the selection is empty, spans several projects, or includes a file with no HTTPS location.

Source code in pkg/python/src/pymzlib/pride.py
def download_files(
    files: Iterable[PrideFile],
    destination: str | Path,
    overwrite: bool = True,
    timeout: float | None = None,
) -> list[Path]:
    """Download exactly the files you selected, and nothing else.

    This is the counterpart to :func:`list_files`, and usually the one you want. Filter the
    manifest however you like — in Python, with the full expressiveness of Python — and hand the
    result straight back:

        >>> files = list_files("PXD000001")
        >>> small = [f for f in files if f.size_mb < 5 and f.downloadable]
        >>> len(small)
        3
        >>> # Not run in CI: downloads from EBI.
        >>> download_files(small, "downloads")                          # doctest: +SKIP

    :func:`download`'s ``category`` and ``extensions`` filters can only express what they were
    built to express; "under 5 MB", "the three newest", or "everything except the MGF" cannot be
    said in that vocabulary at all. They can all be said in a list comprehension.

    Args:
        files: The :class:`PrideFile` objects to fetch, from one project.
        destination: Directory to write into. Created if it does not exist.
        overwrite: When ``False``, files already present are left alone and not re-fetched.
        timeout: Seconds to allow. ``None`` waits as long as it takes.

    Returns:
        The paths written, in the order the repository lists them.

    Raises:
        UsageError: the selection is empty, spans several projects, or includes a file with no
            HTTPS location.
    """
    target = _normalise_destination(destination)
    selected = list(files)

    if not selected:
        raise _bridge.UsageError(
            "No files selected. An empty selection is almost always a filter that did not match "
            "what you expected, so pyMzLib refuses it rather than reporting success."
        )
    for item in selected:
        if not isinstance(item, PrideFile):
            raise _bridge.UsageError(
                f"download_files expects PrideFile objects from list_files(); got "
                f"{type(item).__name__} ({item!r})."
            )

    unreachable = [f.file_name for f in selected if not f.downloadable]
    if unreachable:
        raise _bridge.UsageError(
            f"{len(unreachable)} of {len(selected)} selected files have no HTTPS location and "
            f"cannot be downloaded (e.g. {unreachable[0]!r}). Filter on `.downloadable` first."
        )

    accessions = {f.project_accession for f in selected if f.project_accession}
    if len(accessions) > 1:
        raise _bridge.UsageError(
            f"All files must come from one project; got {sorted(accessions)}."
        )
    if not accessions:
        raise _bridge.UsageError(
            "These PrideFile objects carry no project accession, so pyMzLib cannot tell which "
            "project to fetch from. Obtain them from list_files()."
        )

    args = [
        "pride", "download",
        "--accession", accessions.pop(),
        "--dest", str(target),
        "--names-from-stdin",
    ]
    if not overwrite:
        args.append("--no-overwrite")

    # The selection travels on stdin rather than argv: a few thousand names would blow the ~32 KB
    # command-line ceiling. The framing is newline-delimited, which is *almost* general — a POSIX
    # file name may legally contain a newline, so such a name would split into two and silently
    # select the wrong files. PRIDE has never published one, but "never seen it" is not a contract,
    # so it is refused explicitly rather than mis-parsed quietly.
    embedded_newline = [f.file_name for f in selected if "\n" in f.file_name or "\r" in f.file_name]
    if embedded_newline:
        raise _bridge.UsageError(
            f"Cannot select {embedded_newline[0]!r}: the file name contains a line break, which the "
            "selection format cannot represent. Please open an issue — this is a limitation worth "
            "fixing properly if a real repository ever publishes such a name."
        )

    payload = "\n".join(f.file_name for f in selected)
    data = _bridge.invoke(*args, stdin=payload, timeout=timeout)
    return [Path(p) for p in data.get("paths", [])]

total_size_bytes

total_size_bytes(files: Iterable[PrideFile]) -> int

Sum the sizes of some files.

This is the size PRIDE reports, which is not the number of bytes you will transfer. For compressed files PRIDE frequently reports the decompressed size: in PXD000001 the reported size of PRIDE_Exp_Complete_Ac_22134.pride.mgf.gz is 16,448,103 bytes, exactly what gzip -l gives as its uncompressed length, while the actual download is 5,984,662 bytes, 2.75x smaller.

It is also a sum over an incomplete manifest. For PXD000001 this returns 0.51 GB; the project on disk is 1.44 GB, because PRIDE's API omits five files including the two largest (see :func:list_files). The two errors run in opposite directions and do not cancel. For a size that covers the whole project, use :func:approximate_total_size_bytes over :func:list_ftp_files.

files = list_files("PXD000001") round(total_size_bytes(files) / 1e9, 2) 0.51

Source code in pkg/python/src/pymzlib/pride.py
def total_size_bytes(files: Iterable[PrideFile]) -> int:
    """Sum the sizes of some files.

    **This is the size PRIDE reports, which is not the number of bytes you will transfer.**
    For compressed files PRIDE frequently reports the *decompressed* size: in PXD000001 the
    reported size of ``PRIDE_Exp_Complete_Ac_22134.pride.mgf.gz`` is 16,448,103 bytes, exactly
    what ``gzip -l`` gives as its uncompressed length, while the actual download is 5,984,662
    bytes, 2.75x smaller.

    **It is also a sum over an incomplete manifest.** For PXD000001 this returns 0.51 GB; the
    project on disk is 1.44 GB, because PRIDE's API omits five files including the two largest
    (see :func:`list_files`). The two errors run in opposite directions and do **not** cancel. For
    a size that covers the whole project, use :func:`approximate_total_size_bytes` over
    :func:`list_ftp_files`.

    >>> files = list_files("PXD000001")
    >>> round(total_size_bytes(files) / 1e9, 2)
    0.51
    """
    return sum(f.file_size_bytes for f in files)

approximate_total_size_bytes

approximate_total_size_bytes(files: Iterable[PrideFtpFile]) -> int

Sum the approximate sizes of some FTP files.

This is the honest project-size number, and the counterpart to :func:total_size_bytes with the trade-offs reversed. It sums over the complete FTP listing (:func:list_ftp_files), so no files are missing — but each size is PRIDE's directory-index value, rounded to about three significant figures, so the total is an estimate, not an exact byte count. For PXD000001 it lands near the true 1.44 GB, where :func:total_size_bytes reports 0.51 GB over the incomplete REST manifest. When you need the exact bytes for one file, HTTP HEAD its :attr:PrideFtpFile.url and read Content-Length.

Not run in CI: no "pride ftp-files" recording to replay yet.

ftp = list_ftp_files("PXD000001") # doctest: +SKIP approximate_total_size_bytes(ftp) / 1e9 # doctest: +SKIP 1.44

Source code in pkg/python/src/pymzlib/pride.py
def approximate_total_size_bytes(files: Iterable[PrideFtpFile]) -> int:
    """Sum the approximate sizes of some FTP files.

    This is the honest project-size number, and the counterpart to :func:`total_size_bytes` with the
    trade-offs reversed. It sums over the **complete** FTP listing (:func:`list_ftp_files`), so no
    files are missing — but each size is PRIDE's directory-index value, rounded to about three
    significant figures, so the total is an **estimate**, not an exact byte count. For PXD000001 it
    lands near the true 1.44 GB, where :func:`total_size_bytes` reports 0.51 GB over the incomplete
    REST manifest. When you need the exact bytes for one file, HTTP HEAD its
    :attr:`PrideFtpFile.url` and read ``Content-Length``.

    >>> # Not run in CI: no "pride ftp-files" recording to replay yet.
    >>> ftp = list_ftp_files("PXD000001")                       # doctest: +SKIP
    >>> approximate_total_size_bytes(ftp) / 1e9                 # doctest: +SKIP
    1.44
    """
    return sum(f.approximate_size_bytes for f in files)

search

search(keyword: str, page_size: int = 100, timeout: float | None = 300) -> list[PrideProjectSearchResult]

Find PRIDE projects by keyword.

The discovery entry point. Every other function here takes an accession you already have; this is the one that produces them, so you can go from a subject to a dataset without leaving Python.

Paging is handled for you: however many pages the result set spans, you get one list, with no accession repeated.

Parameters:

Name Type Description Default
keyword str

What to search for, e.g. "phosphoproteome". Matched across titles, descriptions, keywords, organisms and more — :attr:~PrideProjectSearchResult.highlights on each hit says which fields actually matched.

required
page_size int

How many projects to request per underlying API call. Only affects how the result set is fetched, never what you get back.

100
timeout float | None

Seconds to allow for the whole fetch.

300

Returns:

Type Description
list[PrideProjectSearchResult]

Every matching project. An empty list is a real answer — PRIDE reports no hits as an

list[PrideProjectSearchResult]

empty result rather than an error, so unlike :func:list_files this does not raise

list[PrideProjectSearchResult]

class:ProjectNotFoundError: there is no accession here that could have been a typo.

Raises:

Type Description
UsageError

the keyword is blank, over MAX_KEYWORD_LENGTH, or begins with -; or the page size is not positive.

BridgeError

PRIDE returned an error status or was unreachable.

Note

PRIDE pages a live index with no stable cursor, so a result set that changes during a multi-page fetch shifts its own paging. A project published mid-fetch is served on two pages and deduplicated, so it comes back once; a project removed mid-fetch can fall between two pages and be missed. A search whose hits fit on one page cannot be affected.

Examples:

>>> hits = search("plasmodium falciparum schizont")
>>> hits[0].accession, hits[0].organisms
('PXD070842', ['Homo sapiens (human)', 'Plasmodium falciparum (isolate 3d7)'])
>>> hits[0].matched_fields
['references', 'title']
>>> # Not run in CI: no recording of this project's file list to replay.
>>> files = list_files(hits[0].accession)
Source code in pkg/python/src/pymzlib/pride.py
def search(
    keyword: str,
    page_size: int = 100,
    timeout: float | None = 300,
) -> list[PrideProjectSearchResult]:
    """Find PRIDE projects by keyword.

    **The discovery entry point.** Every other function here takes an accession you already have;
    this is the one that produces them, so you can go from a subject to a dataset without leaving
    Python.

    Paging is handled for you: however many pages the result set spans, you get one list, with no
    accession repeated.

    Args:
        keyword: What to search for, e.g. ``"phosphoproteome"``. Matched across titles,
            descriptions, keywords, organisms and more — :attr:`~PrideProjectSearchResult.highlights`
            on each hit says which fields actually matched.
        page_size: How many projects to request per underlying API call. Only affects how the
            result set is fetched, never what you get back.
        timeout: Seconds to allow for the whole fetch.

    Returns:
        Every matching project. **An empty list is a real answer** — PRIDE reports no hits as an
        empty result rather than an error, so unlike :func:`list_files` this does not raise
        :class:`ProjectNotFoundError`: there is no accession here that could have been a typo.

    Raises:
        UsageError: the keyword is blank, over ``MAX_KEYWORD_LENGTH``, or begins with ``-``; or the
            page size is not positive.
        BridgeError: PRIDE returned an error status or was unreachable.

    Note:
        PRIDE pages a **live index** with no stable cursor, so a result set that changes during a
        multi-page fetch shifts its own paging. A project published mid-fetch is served on two pages
        and deduplicated, so it comes back once; a project *removed* mid-fetch can fall between two
        pages and be missed. A search whose hits fit on one page cannot be affected.

    Examples:
        >>> hits = search("plasmodium falciparum schizont")
        >>> hits[0].accession, hits[0].organisms
        ('PXD070842', ['Homo sapiens (human)', 'Plasmodium falciparum (isolate 3d7)'])
        >>> hits[0].matched_fields
        ['references', 'title']
        >>> # Not run in CI: no recording of this project's file list to replay.
        >>> files = list_files(hits[0].accession)                  # doctest: +SKIP
    """
    if not isinstance(keyword, str) or not keyword.strip():
        raise _bridge.UsageError("A search keyword is required, e.g. 'phosphoproteome'.")

    canonical = _reject_flag_like("keyword", keyword.strip())
    if len(canonical) > MAX_KEYWORD_LENGTH:
        raise _bridge.UsageError(
            f"keyword may be at most {MAX_KEYWORD_LENGTH} characters; got {len(canonical)}. "
            "PRIDE answers a longer keyword with HTTP 500, which cannot be told apart from the "
            "service being down."
        )

    if isinstance(page_size, bool) or not isinstance(page_size, int):
        raise _bridge.UsageError(
            f"page_size must be a whole number; got {type(page_size).__name__} ({page_size!r})."
        )
    if page_size <= 0:
        raise _bridge.UsageError(f"page_size must be positive; got {page_size}.")
    if page_size > 2_147_483_647:
        raise _bridge.UsageError(f"page_size is larger than the API allows; got {page_size}.")

    data = _bridge.invoke(
        "pride", "search",
        "--keyword", canonical,
        "--page-size", str(page_size),
        timeout=timeout,
    )
    return [PrideProjectSearchResult._from_wire(item) for item in data.get("results", [])]

pymzlib.peptidoform

peptidoform

Peptidoform-level questions: digest an annotated protein and fragment its peptides.

The question this answers is the one a mass spectrometrist actually asks — what fragments would I see for this protein's peptides? — in one call:

>>> import pymzlib
>>> digest = pymzlib.peptidoform.fragments("P02768")
>>> digest.accession, digest.sequence_length
('P02768', 609)

The defaults are opinions, not placeholders. Tryptic with the proline rule, two missed cleavages, ETD, both termini, UniProt's annotated modifications applied. They are the choices this lab makes when it does not have a reason to choose otherwise, so the common question needs no parameters — and every one of them is reachable, because the point is to open the doors, not to hide them.

:func:convert rewrites full sequences from one notation to another with mzLib's SequenceConversionService: a MetaMorpheus full sequence [UniProt:N-acetylserine on S]SEQK becomes [UNIMOD:1]SEQK in Unimod notation.

Fragment dataclass

One backbone fragment ion.

Attributes:

Name Type Description
product_type str

The ion series, e.g. "c" or "zDot" for ETD, "b"/"y" for CID.

fragment_number int

Position in the series — c3 is the third from the N-terminus.

neutral_mass float

Monoisotopic neutral mass in daltons. Not an m/z: no proton has been added and no charge assumed.

Fragments deliberately expose no mz(). Converting one correctly requires the fixed charge within this fragment's span — a c or z ion carries only the permanently charged modifications on the residues it contains, not the whole peptide's :attr:Peptide.fixed_charges. For an unmodified or neutrally-modified peptide, (neutral_mass + z * PROTON_MASS) / z is correct; for a fragment bearing a quaternary-ammonium modification (e.g. trimethyllysine) it is not, and per-fragment charge accounting is not yet provided.

neutral_loss float

Neutral loss in daltons, 0.0 when there is none.

residue_position int

One-based residue position in the peptide.

Source code in pkg/python/src/pymzlib/peptidoform.py
@dataclass(frozen=True)
class Fragment:
    """One backbone fragment ion.

    Attributes:
        product_type: The ion series, e.g. ``"c"`` or ``"zDot"`` for ETD, ``"b"``/``"y"`` for CID.
        fragment_number: Position in the series — ``c3`` is the third from the N-terminus.
        neutral_mass: Monoisotopic neutral mass in daltons. **Not** an m/z: no proton has been
            added and no charge assumed.

            Fragments deliberately expose no ``mz()``. Converting one correctly requires the fixed
            charge *within this fragment's span* — a c or z ion carries only the permanently
            charged modifications on the residues it contains, not the whole peptide's
            :attr:`Peptide.fixed_charges`. For an unmodified or neutrally-modified peptide,
            ``(neutral_mass + z * PROTON_MASS) / z`` is correct; for a fragment bearing a
            quaternary-ammonium modification (e.g. trimethyllysine) it is not, and per-fragment
            charge accounting is not yet provided.
        neutral_loss: Neutral loss in daltons, ``0.0`` when there is none.
        residue_position: One-based residue position in the peptide.
    """

    product_type: str
    fragment_number: int
    neutral_mass: float
    neutral_loss: float
    residue_position: int

Peptide dataclass

One digested peptide, with its modifications and fragment ions.

Attributes:

Name Type Description
base_sequence str

The bare amino-acid sequence.

full_sequence str

The sequence with modifications written inline, as mzLib renders them.

monoisotopic_mass float

The neutral monoisotopic mass, modifications included.

one_based_start / one_based_end

Position within the parent protein.

missed_cleavages int

How many cleavage sites the peptide spans.

fixed_charges int

Charges the peptide carries before any protonation, from modifications that leave a permanently charged residue. :meth:mz accounts for these.

modifications list[dict[str, Any]]

Each applied modification. one_based_residue indexes the peptide's own residues and is None for a terminal modification, which carries terminus instead. (mzLib's internal dictionary reserves slot 1 for the N-terminus, so its keys are one past the residue they modify; that is corrected here rather than passed on.)

fragments list[Fragment]

The fragment ions for the requested dissociation type.

Source code in pkg/python/src/pymzlib/peptidoform.py
@dataclass(frozen=True)
class Peptide:
    """One digested peptide, with its modifications and fragment ions.

    Attributes:
        base_sequence: The bare amino-acid sequence.
        full_sequence: The sequence with modifications written inline, as mzLib renders them.
        monoisotopic_mass: The neutral monoisotopic mass, modifications included.
        one_based_start / one_based_end: Position within the parent protein.
        missed_cleavages: How many cleavage sites the peptide spans.
        fixed_charges: Charges the peptide carries before any protonation, from modifications
            that leave a permanently charged residue. :meth:`mz` accounts for these.
        modifications: Each applied modification. ``one_based_residue`` indexes the peptide's own
            residues and is ``None`` for a terminal modification, which carries ``terminus``
            instead. (mzLib's internal dictionary reserves slot 1 for the N-terminus, so its keys
            are one past the residue they modify; that is corrected here rather than passed on.)
        fragments: The fragment ions for the requested dissociation type.
    """

    base_sequence: str
    full_sequence: str
    monoisotopic_mass: float
    length: int
    one_based_start: int
    one_based_end: int
    missed_cleavages: int
    fixed_charges: int = 0
    modifications: list[dict[str, Any]] = field(default_factory=list)
    fragments: list[Fragment] = field(default_factory=list)

    @property
    def is_modified(self) -> bool:
        """Whether this peptide carries at least one modification."""
        return bool(self.modifications)

    def mz(self, charge: int) -> float:
        """Return the m/z of the intact peptide at a given total charge.

        Two conventions are handled explicitly here, because getting either wrong is invisible in
        the answer.

        **The proton mass (1.007276), not the hydrogen atom (1.007825).** The difference is
        0.55 mDa — 1.1 ppm at m/z 500, which on an Orbitrap is a match versus a miss. Libraries
        differ on this and rarely say which they used.

        **Fixed charges are not double-counted.** Some modifications leave the residue permanently
        charged: trimethylation of a lysine ε-amine gives a quaternary ammonium, and UniProt
        records the delta as 43.054227 — C₃H₇ *minus an electron* — rather than the neutral
        43.054775. So :attr:`monoisotopic_mass` already carries that charge, and only
        ``charge - fixed_charges`` protons are added. Adding a full complement would put a 2+
        trimethylated peptide half a Thomson high, on the most important histone modification
        there is.

        A peptide with a fixed charge is therefore observable at that charge with no protonation
        at all, which is why ``charge`` may not be below :attr:`fixed_charges`.

        Args:
            charge: The total charge state, at least :attr:`fixed_charges` and at least 1.
        """
        if not isinstance(charge, int) or isinstance(charge, bool) or charge < 1:
            raise _bridge.UsageError(f"charge must be a positive whole number; got {charge!r}.")
        if charge < self.fixed_charges:
            raise _bridge.UsageError(
                f"This peptide already carries {self.fixed_charges} fixed charge(s) from its "
                f"modifications, so it cannot be observed at charge {charge}."
            )
        return (self.monoisotopic_mass + (charge - self.fixed_charges) * PROTON_MASS) / charge

    @classmethod
    def _from_wire(cls, payload: dict[str, Any]) -> "Peptide":
        return cls(
            base_sequence=payload.get("base_sequence", ""),
            full_sequence=payload.get("full_sequence", ""),
            monoisotopic_mass=float(payload.get("monoisotopic_mass", 0.0)),
            length=int(payload.get("length", 0)),
            one_based_start=int(payload.get("one_based_start", 0)),
            one_based_end=int(payload.get("one_based_end", 0)),
            missed_cleavages=int(payload.get("missed_cleavages", 0)),
            fixed_charges=int(payload.get("fixed_charges", 0)),
            modifications=list(payload.get("modifications") or []),
            fragments=[
                Fragment(
                    product_type=f.get("product_type", ""),
                    fragment_number=int(f.get("fragment_number", 0)),
                    neutral_mass=float(f.get("neutral_mass", 0.0)),
                    neutral_loss=float(f.get("neutral_loss", 0.0)),
                    residue_position=int(f.get("residue_position", 0)),
                )
                for f in (payload.get("fragments") or [])
            ],
        )
is_modified property
is_modified: bool

Whether this peptide carries at least one modification.

mz
mz(charge: int) -> float

Return the m/z of the intact peptide at a given total charge.

Two conventions are handled explicitly here, because getting either wrong is invisible in the answer.

The proton mass (1.007276), not the hydrogen atom (1.007825). The difference is 0.55 mDa — 1.1 ppm at m/z 500, which on an Orbitrap is a match versus a miss. Libraries differ on this and rarely say which they used.

Fixed charges are not double-counted. Some modifications leave the residue permanently charged: trimethylation of a lysine ε-amine gives a quaternary ammonium, and UniProt records the delta as 43.054227 — C₃H₇ minus an electron — rather than the neutral 43.054775. So :attr:monoisotopic_mass already carries that charge, and only charge - fixed_charges protons are added. Adding a full complement would put a 2+ trimethylated peptide half a Thomson high, on the most important histone modification there is.

A peptide with a fixed charge is therefore observable at that charge with no protonation at all, which is why charge may not be below :attr:fixed_charges.

Parameters:

Name Type Description Default
charge int

The total charge state, at least :attr:fixed_charges and at least 1.

required
Source code in pkg/python/src/pymzlib/peptidoform.py
def mz(self, charge: int) -> float:
    """Return the m/z of the intact peptide at a given total charge.

    Two conventions are handled explicitly here, because getting either wrong is invisible in
    the answer.

    **The proton mass (1.007276), not the hydrogen atom (1.007825).** The difference is
    0.55 mDa — 1.1 ppm at m/z 500, which on an Orbitrap is a match versus a miss. Libraries
    differ on this and rarely say which they used.

    **Fixed charges are not double-counted.** Some modifications leave the residue permanently
    charged: trimethylation of a lysine ε-amine gives a quaternary ammonium, and UniProt
    records the delta as 43.054227 — C₃H₇ *minus an electron* — rather than the neutral
    43.054775. So :attr:`monoisotopic_mass` already carries that charge, and only
    ``charge - fixed_charges`` protons are added. Adding a full complement would put a 2+
    trimethylated peptide half a Thomson high, on the most important histone modification
    there is.

    A peptide with a fixed charge is therefore observable at that charge with no protonation
    at all, which is why ``charge`` may not be below :attr:`fixed_charges`.

    Args:
        charge: The total charge state, at least :attr:`fixed_charges` and at least 1.
    """
    if not isinstance(charge, int) or isinstance(charge, bool) or charge < 1:
        raise _bridge.UsageError(f"charge must be a positive whole number; got {charge!r}.")
    if charge < self.fixed_charges:
        raise _bridge.UsageError(
            f"This peptide already carries {self.fixed_charges} fixed charge(s) from its "
            f"modifications, so it cannot be observed at charge {charge}."
        )
    return (self.monoisotopic_mass + (charge - self.fixed_charges) * PROTON_MASS) / charge

ModificationCensus dataclass

What UniProt annotates, and what could actually be used.

mzLib loads only modified residue and lipid moiety-binding region annotations; every other feature type is dropped on feature type alone, before any mass lookup. So the census sees the world at feature-type granularity — one entry per type in :attr:by_type, never per modification name. On serum albumin the 24 excluded features all sit under the single type glycosylation site; at UniProt's finer name level 22 of those 24 are specifically N-linked (Glc) (glycation) lysine, which does have a defined mass — but the census never surfaces that name, so "22" is a fact you confirm by reading the UniProt entry, not a number this class reports. Read the exclusion as "wrong feature type", not "no defined mass".

The exclusion is still correct: glycation and glycosylation are labile, heterogeneous adducts, so assigning one an exact mass and a clean fragment ladder would describe a species you cannot observe. What this class exists for is that you should not have to guess it happened: for serum albumin, 14 sites are applied out of 38 annotated, and without this the 14 arrives with no indication that a rule was ever applied. See smith-chem-wisc/mzLib#1112.

Attributes:

Name Type Description
sites int

Distinct residue positions carrying at least one modification. A histone lists several alternatives at one residue — K9me1, K9me2, K9me3, K9ac are four modifications at one site — so this is always the smaller number and is not a modification count.

applied int

Modifications actually placed on the protein.

annotated int

Modification-like features UniProt lists.

by_type list[dict[str, Any]]

One entry per feature type, with count and whether it was loaded.

unresolved list[str]

Modification names UniProt annotated that could not be resolved to a mass — usually because the name is absent from UniProt's own ptmlist. These vanish silently otherwise: on histone H3.1, seven N6-lactoyllysine sites were dropped while the type summary still reported "modified residue … loaded".

Source code in pkg/python/src/pymzlib/peptidoform.py
@dataclass(frozen=True)
class ModificationCensus:
    """What UniProt annotates, and what could actually be used.

    mzLib loads only ``modified residue`` and ``lipid moiety-binding region`` annotations; every
    other feature type is dropped **on feature type alone**, before any mass lookup. So the census
    sees the world at feature-*type* granularity — one entry per type in :attr:`by_type`, never per
    modification *name*. On serum albumin the 24 excluded features all sit under the single type
    ``glycosylation site``; at UniProt's finer name level 22 of those 24 are specifically
    ``N-linked (Glc) (glycation) lysine``, which *does* have a defined mass — but the census never
    surfaces that name, so "22" is a fact you confirm by reading the UniProt entry, not a number
    this class reports. Read the exclusion as "wrong feature type", **not** "no defined mass".

    The exclusion is still correct: glycation and glycosylation are labile, heterogeneous adducts,
    so assigning one an exact mass and a clean fragment ladder would describe a species you cannot
    observe. What this class exists for is that you should not have to *guess* it happened: for
    serum albumin, 14 sites are applied out of 38 annotated, and without this the 14 arrives with
    no indication that a rule was ever applied. See smith-chem-wisc/mzLib#1112.

    Attributes:
        sites: Distinct residue positions carrying at least one modification. A histone lists
            several alternatives at one residue — K9me1, K9me2, K9me3, K9ac are four
            modifications at one site — so this is always the smaller number and is not a
            modification count.
        applied: Modifications actually placed on the protein.
        annotated: Modification-like features UniProt lists.
        by_type: One entry per feature type, with ``count`` and whether it was ``loaded``.
        unresolved: Modification names UniProt annotated that could not be resolved to a mass —
            usually because the name is absent from UniProt's own ptmlist. These vanish silently
            otherwise: on histone H3.1, seven N6-lactoyllysine sites were dropped while the type
            summary still reported "modified residue … loaded".
    """

    sites: int
    applied: int
    annotated: int
    unresolved: list[str] = field(default_factory=list)
    by_type: list[dict[str, Any]] = field(default_factory=list)

    @property
    def excluded(self) -> int:
        """Annotated features mzLib did not apply, dropped on feature type (not for want of mass)."""
        return max(0, self.annotated - self.applied)

    def explain(self) -> str:
        """A one-paragraph, human-readable account of what was used and what was not.

        It names the excluded feature *types* and their counts — the only granularity the census
        has. It never reports a modification-*name*-level breakdown (e.g. "22 of 24 are glycation"),
        because :attr:`by_type` does not carry names; such a figure comes from reading the UniProt
        entry, not from this census.
        """
        if not self.excluded:
            return (
                f"All {self.annotated} annotated modifications were applied, across "
                f"{self.sites} residue positions."
            )
        sentences = [
            f"{self.applied} of {self.annotated} annotated modifications were applied, across "
            f"{self.sites} residue positions."
        ]
        excluded_types = [t for t in self.by_type if not t.get("loaded")]
        if excluded_types:
            named = ", ".join(f"{t['count']} × {t['type']}" for t in excluded_types)
            sentences.append(
                f"Excluded by type: {named} — mzLib loads only 'modified residue' "
                "and 'lipid moiety-binding region' annotations, so these were dropped on "
                "feature type alone. The exclusion is usually right: a glycation or "
                "glycosylation annotation describes a labile, heterogeneous adduct, so "
                "assigning it one exact mass and a clean fragment ladder would invent a "
                "species you cannot observe. But the reason is not reported, and the "
                "qualifier is not read: some annotations are marked 'in vitro' and some "
                "exist only in disease variants, which are different "
                "grounds for exclusion needing different judgements from you. Read the "
                "annotations on the UniProt entry before concluding anything about a "
                "specific site; this census can only tell you the count "
                "(smith-chem-wisc/mzLib#1112)."
            )
        if self.unresolved:
            sentences.append(
                f"Could not be resolved to a mass: {', '.join(self.unresolved)} — annotated by "
                "UniProt but absent from its own modification list, so they were dropped."
            )
        return " ".join(sentences)
excluded property
excluded: int

Annotated features mzLib did not apply, dropped on feature type (not for want of mass).

explain
explain() -> str

A one-paragraph, human-readable account of what was used and what was not.

It names the excluded feature types and their counts — the only granularity the census has. It never reports a modification-name-level breakdown (e.g. "22 of 24 are glycation"), because :attr:by_type does not carry names; such a figure comes from reading the UniProt entry, not from this census.

Source code in pkg/python/src/pymzlib/peptidoform.py
def explain(self) -> str:
    """A one-paragraph, human-readable account of what was used and what was not.

    It names the excluded feature *types* and their counts — the only granularity the census
    has. It never reports a modification-*name*-level breakdown (e.g. "22 of 24 are glycation"),
    because :attr:`by_type` does not carry names; such a figure comes from reading the UniProt
    entry, not from this census.
    """
    if not self.excluded:
        return (
            f"All {self.annotated} annotated modifications were applied, across "
            f"{self.sites} residue positions."
        )
    sentences = [
        f"{self.applied} of {self.annotated} annotated modifications were applied, across "
        f"{self.sites} residue positions."
    ]
    excluded_types = [t for t in self.by_type if not t.get("loaded")]
    if excluded_types:
        named = ", ".join(f"{t['count']} × {t['type']}" for t in excluded_types)
        sentences.append(
            f"Excluded by type: {named} — mzLib loads only 'modified residue' "
            "and 'lipid moiety-binding region' annotations, so these were dropped on "
            "feature type alone. The exclusion is usually right: a glycation or "
            "glycosylation annotation describes a labile, heterogeneous adduct, so "
            "assigning it one exact mass and a clean fragment ladder would invent a "
            "species you cannot observe. But the reason is not reported, and the "
            "qualifier is not read: some annotations are marked 'in vitro' and some "
            "exist only in disease variants, which are different "
            "grounds for exclusion needing different judgements from you. Read the "
            "annotations on the UniProt entry before concluding anything about a "
            "specific site; this census can only tell you the count "
            "(smith-chem-wisc/mzLib#1112)."
        )
    if self.unresolved:
        sentences.append(
            f"Could not be resolved to a mass: {', '.join(self.unresolved)} — annotated by "
            "UniProt but absent from its own modification list, so they were dropped."
        )
    return " ".join(sentences)

Digest dataclass

The result of digesting a protein and fragmenting its peptides.

Attributes:

Name Type Description
accession str

The accession UniProt returned.

name str

UniProt entry name, e.g. "ALBU_HUMAN".

full_name str

Recommended protein name, e.g. "Albumin".

organism str

Scientific name of the source organism.

sequence_length int

Length of the full precursor sequence, in residues.

protease str

The protease used, as mzLib names it.

dissociation str

The dissociation type used, which decides the fragment series.

terminus str

Which fragment termini were generated: "Both", "N" or "C".

modifications_applied bool

False when modifications=False was passed.

max_modifications int

The max_modifications cap, echoed, in modifications per peptidoform.

max_isoforms int

The max_isoforms cap, echoed, in peptidoforms per peptide position.

peptides_at_cap int

Peptide positions whose peptidoforms reached max_isoforms. Non-zero means the list is truncated; :attr:truncated is the same fact as a yes/no.

modification_census ModificationCensus

What UniProt annotates against what mzLib applied: distinct residue positions carrying a modification (sites), modifications loaded (applied), modification-like features annotated (annotated), names that resolved to no mass (unresolved), and a per-feature-type summary (by_type). See :class:ModificationCensus.

peptides list[Peptide]

One :class:Peptide per distinct peptidoform, in digestion order.

Source code in pkg/python/src/pymzlib/peptidoform.py
@dataclass(frozen=True)
class Digest:
    """The result of digesting a protein and fragmenting its peptides.

    Attributes:
        accession: The accession UniProt returned.
        name: UniProt entry name, e.g. ``"ALBU_HUMAN"``.
        full_name: Recommended protein name, e.g. ``"Albumin"``.
        organism: Scientific name of the source organism.
        sequence_length: Length of the full precursor sequence, in residues.
        protease: The protease used, as mzLib names it.
        dissociation: The dissociation type used, which decides the fragment series.
        terminus: Which fragment termini were generated: ``"Both"``, ``"N"`` or ``"C"``.
        modifications_applied: ``False`` when ``modifications=False`` was passed.
        max_modifications: The ``max_modifications`` cap, echoed, in modifications per peptidoform.
        max_isoforms: The ``max_isoforms`` cap, echoed, in peptidoforms per peptide position.
        peptides_at_cap: Peptide positions whose peptidoforms reached ``max_isoforms``. Non-zero
            means the list is **truncated**; :attr:`truncated` is the same fact as a yes/no.
        modification_census: What UniProt annotates against what mzLib applied: distinct residue
            positions carrying a modification (``sites``), modifications loaded (``applied``),
            modification-like features annotated (``annotated``), names that resolved to no mass
            (``unresolved``), and a per-feature-type summary (``by_type``). See
            :class:`ModificationCensus`.
        peptides: One :class:`Peptide` per distinct peptidoform, in digestion order.
    """

    accession: str
    name: str
    full_name: str
    organism: str
    sequence_length: int
    protease: str
    dissociation: str
    terminus: str
    modifications_applied: bool
    max_modifications: int
    max_isoforms: int
    peptides_at_cap: int
    modification_census: ModificationCensus
    peptides: list[Peptide] = field(default_factory=list)

    @property
    def truncated(self) -> bool:
        """Whether any peptide hit the isoform cap, meaning the result is incomplete.

        A short answer and a truncated answer look identical from the outside. Check this before
        treating a Peptidoform list as exhaustive.
        """
        return self.peptides_at_cap > 0

    @property
    def modified_peptides(self) -> list[Peptide]:
        """Only the peptides carrying at least one modification."""
        return [p for p in self.peptides if p.is_modified]

    @property
    def fragment_count(self) -> int:
        """Total fragment ions across every peptide."""
        return sum(len(p.fragments) for p in self.peptides)
truncated property
truncated: bool

Whether any peptide hit the isoform cap, meaning the result is incomplete.

A short answer and a truncated answer look identical from the outside. Check this before treating a Peptidoform list as exhaustive.

modified_peptides property
modified_peptides: list[Peptide]

Only the peptides carrying at least one modification.

fragment_count property
fragment_count: int

Total fragment ions across every peptide.

ConvertedSequence dataclass

One input sequence and what mzLib made of it.

Attributes:

Name Type Description
input str

The sequence exactly as given.

output str | None

The sequence in the target notation, or None when mzLib could not convert it (status == "failed").

status str

mzLib's verdict. "converted": an output and nothing recorded against it. "converted_with_warnings": an output, but mzLib recorded a warning, an error or an incompatible item (a modification dropped, a character skipped); read :attr:incompatible_items and :attr:warnings. "failed": no output.

failure_reason str | None

mzLib's ConversionFailureReason ("InvalidSequence", "IncompatibleModifications", "UnsupportedDirection", "UnknownFormat"), or None when mzLib recorded none. A failed row can have none: the Unimod target under ReturnNull names only the :attr:incompatible_items.

incompatible_items list[str]

The modifications (or other elements) mzLib could not write in the target, as mzLib describes them ("Made Up:Not a modification on K @3(K)").

warnings list[str]

mzLib's non-fatal messages for this input.

errors list[str]

mzLib's error messages for this input.

Source code in pkg/python/src/pymzlib/peptidoform.py
@dataclass(frozen=True)
class ConvertedSequence:
    """One input sequence and what mzLib made of it.

    Attributes:
        input: The sequence exactly as given.
        output: The sequence in the target notation, or ``None`` when mzLib could not convert it
            (``status == "failed"``).
        status: mzLib's verdict. ``"converted"``: an output and nothing recorded against it.
            ``"converted_with_warnings"``: an output, but mzLib recorded a warning, an error or an
            incompatible item (a modification dropped, a character skipped); read
            :attr:`incompatible_items` and :attr:`warnings`. ``"failed"``: no output.
        failure_reason: mzLib's ``ConversionFailureReason`` (``"InvalidSequence"``,
            ``"IncompatibleModifications"``, ``"UnsupportedDirection"``, ``"UnknownFormat"``), or
            ``None`` when mzLib recorded none. A failed row can have none: the Unimod target under
            ``ReturnNull`` names only the :attr:`incompatible_items`.
        incompatible_items: The modifications (or other elements) mzLib could not write in the
            target, as mzLib describes them (``"Made Up:Not a modification on K @3(K)"``).
        warnings: mzLib's non-fatal messages for this input.
        errors: mzLib's error messages for this input.
    """

    input: str
    output: str | None
    status: str
    failure_reason: str | None
    incompatible_items: list[str]
    warnings: list[str]
    errors: list[str]

    @property
    def ok(self) -> bool:
        """Whether mzLib converted it with nothing recorded against it (``status == "converted"``)."""
        return self.status == "converted"
ok property
ok: bool

Whether mzLib converted it with nothing recorded against it (status == "converted").

SequenceConversions dataclass

Bases: _Table

What :func:convert returned: one row per input sequence, in input order.

columns maps input, output, status, failure_reason, incompatible_items, warnings and errors to one value per input, ready for pandas.DataFrame(result.columns). :attr:sequences is the same rows as :class:ConvertedSequence objects.

Attributes:

Name Type Description
source_format str

The source notation, spelled as mzLib registered it ("mzLib").

target_format str

The target notation, spelled as mzLib registered it ("Unimod").

mode str

The SequenceConversionHandlingMode used.

source_formats list[str]

Every source notation mzLib has registered, sorted.

target_formats list[str]

Every target notation mzLib has registered, sorted.

record_count int

Rows, one per input sequence, duplicates included.

converted_count int

Sequences with status "converted".

warned_count int

Sequences with status "converted_with_warnings".

failed_count int

Sequences with status "failed".

column_names list[str]

The table's columns, in order.

columns dict[str, list[Any]]

Column name to one value per input.

caveats list[str]

What a status does and does not promise, per target. Read these once.

Source code in pkg/python/src/pymzlib/peptidoform.py
@dataclass(frozen=True)
class SequenceConversions(_Table):
    """What :func:`convert` returned: one row per input sequence, in input order.

    ``columns`` maps ``input``, ``output``, ``status``, ``failure_reason``, ``incompatible_items``,
    ``warnings`` and ``errors`` to one value per input, ready for
    ``pandas.DataFrame(result.columns)``. :attr:`sequences` is the same rows as
    :class:`ConvertedSequence` objects.

    Attributes:
        source_format: The source notation, spelled as mzLib registered it (``"mzLib"``).
        target_format: The target notation, spelled as mzLib registered it (``"Unimod"``).
        mode: The ``SequenceConversionHandlingMode`` used.
        source_formats: Every source notation mzLib has registered, sorted.
        target_formats: Every target notation mzLib has registered, sorted.
        record_count: Rows, one per input sequence, duplicates included.
        converted_count: Sequences with status ``"converted"``.
        warned_count: Sequences with status ``"converted_with_warnings"``.
        failed_count: Sequences with status ``"failed"``.
        column_names: The table's columns, in order.
        columns: Column name to one value per input.
        caveats: What a status does and does not promise, per target. Read these once.
    """

    source_format: str
    target_format: str
    mode: str
    source_formats: list[str]
    target_formats: list[str]
    record_count: int
    converted_count: int
    warned_count: int
    failed_count: int
    column_names: list[str]
    columns: dict[str, list[Any]]
    caveats: list[str] = field(default_factory=list)

    @property
    def outputs(self) -> list[str | None]:
        """The converted sequences in input order, ``None`` where a row failed."""
        return list(self.columns.get("output") or [])

    @property
    def sequences(self) -> list[ConvertedSequence]:
        """Every row as a :class:`ConvertedSequence`, in input order."""
        return [
            ConvertedSequence(
                input=row["input"],
                output=row["output"],
                status=row["status"],
                failure_reason=row["failure_reason"],
                incompatible_items=list(row["incompatible_items"] or []),
                warnings=list(row["warnings"] or []),
                errors=list(row["errors"] or []),
            )
            for row in self.records
        ]

    @property
    def not_converted(self) -> list[ConvertedSequence]:
        """The rows whose status is not ``"converted"``: failed, or converted with warnings."""
        return [s for s in self.sequences if not s.ok]

    @classmethod
    def _from_wire(cls, data: Mapping[str, Any]) -> "SequenceConversions":
        return cls(
            source_format=data.get("source_format", ""),
            target_format=data.get("target_format", ""),
            mode=data.get("mode", ""),
            source_formats=list(data.get("source_formats") or []),
            target_formats=list(data.get("target_formats") or []),
            record_count=int(data.get("record_count", 0)),
            converted_count=int(data.get("converted_count", 0)),
            warned_count=int(data.get("warned_count", 0)),
            failed_count=int(data.get("failed_count", 0)),
            column_names=list(data.get("column_names") or []),
            columns={k: list(v) for k, v in (data.get("columns") or {}).items()},
            caveats=list(data.get("caveats") or []),
        )
outputs property
outputs: list[str | None]

The converted sequences in input order, None where a row failed.

sequences property
sequences: list[ConvertedSequence]

Every row as a :class:ConvertedSequence, in input order.

not_converted property
not_converted: list[ConvertedSequence]

The rows whose status is not "converted": failed, or converted with warnings.

fragments

fragments(accession: str, protease: str = 'trypsin|P', dissociation: str = 'ETD', modifications: bool = True, missed_cleavages: int = 2, min_length: int = 7, max_length: int | None = None, max_modifications: int = 2, max_isoforms: int = 1024, terminus: str = 'Both', timeout: float | None = 300) -> Digest

Fetch a UniProt entry, digest it, and fragment every peptide.

Parameters:

Name Type Description Default
accession str

A UniProtKB accession, e.g. "P02768".

required
protease str

Read this if you are coming from MaxQuant or Mascot. mzLib's "trypsin|P" applies the classic Keil rule — cleave after K/R except before proline — and is the default here because it is what a mass spectrometrist usually means. mzLib's plain "trypsin" cleaves before proline too. That is the reverse of the MaxQuant and Mascot convention, where Trypsin/P denotes ignoring the proline rule. On serum albumin the two differ by 7 peptides out of about 200 (195 vs 202; tryptic, 2 missed cleavages, min length 7) - a small count hiding a large semantic difference, since which peptides you get changes wherever a K/R precedes a proline.

'trypsin|P'
dissociation str

"HCD"/"CID" (b and y ions), "ETD", and the rest of mzLib's dissociation types.

"ETD" returns the c and zDot series (radical N-Ca cleavage yields c/z, not the b/y of vibrational activation). Note that z ions are suppressed N-terminal to proline while the complementary c ions are not, leaving about 4% of the c series unobservable (smith-chem-wisc/mzLib#1110).

'ETD'
modifications bool

Apply UniProt's annotated modifications.

Pass False for the bare sequences — a clean control, since the digest's distinct backbones are unchanged and only the modified variants of them go away. The peptidoform count drops a long way; on albumin from 303 to 195.

This once carried a caveat saying False was not a clean control, because it discarded UniProt's whole feature table and with it the signal-peptide and propeptide boundaries mzLib digests at — costing albumin two peptides and changing the peptide list, not just the modifications on it (issue #8). That was true and is no longer: verified against the published bridge, both peptides are present either way and albumin gives 195 distinct base sequences with modifications on or off.

True
missed_cleavages int

Maximum missed cleavage sites per peptide.

2
min_length int

Shortest peptide to keep, in residues. The default of 7 silently discards shorter peptides — roughly a third of a histone digest — so pass min_length=1 when you mean every peptide.

7
max_length int | None

Longest peptide to keep, in residues. None means unbounded.

None
max_modifications int

Maximum modifications considered per peptide. Modification isoforms are enumerated combinatorially: histone H3.1 yields 49 bare tryptic peptides, 2,563 at two modifications and 7,040 at three.

2
max_isoforms int

Maximum modification isoforms per peptide position. mzLib's default of 1024 truncates silently when it binds — on H3.1 at four modifications it discards about 30% of the Peptidoforms (13,700 down to 9,536). :attr:Digest.peptides_at_cap reports how many peptides hit it, so a truncated answer is visible rather than merely short.

1024
terminus str

"Both", "N" or "C".

'Both'
timeout float | None

Seconds to allow. Large proteins with many modification isoforms take longer.

300

Returns:

Name Type Description
A Digest

class:Digest. Check :attr:Digest.modification_census before trusting a

Digest

modification count — it reports what was annotated as well as what was applied.

Raises:

Type Description
UsageError

the accession, protease, dissociation type or terminus is not recognised.

ServiceUnavailableError

UniProt was unreachable.

Examples:

>>> d = fragments("P02768")
>>> print(d.modification_census.explain())
14 of 38 annotated modifications were applied, across 14 residue positions. Excluded ...
Source code in pkg/python/src/pymzlib/peptidoform.py
def fragments(
    accession: str,
    protease: str = "trypsin|P",
    dissociation: str = "ETD",
    modifications: bool = True,
    missed_cleavages: int = 2,
    min_length: int = 7,
    max_length: int | None = None,
    max_modifications: int = 2,
    max_isoforms: int = 1024,
    terminus: str = "Both",
    timeout: float | None = 300,
) -> Digest:
    """Fetch a UniProt entry, digest it, and fragment every peptide.

    Args:
        accession: A UniProtKB accession, e.g. ``"P02768"``.
        protease: **Read this if you are coming from MaxQuant or Mascot.** mzLib's ``"trypsin|P"``
            applies the classic Keil rule — cleave after K/R *except* before proline — and is the
            default here because it is what a mass spectrometrist usually means. mzLib's plain
            ``"trypsin"`` cleaves before proline too. That is the **reverse** of the MaxQuant and
            Mascot convention, where ``Trypsin/P`` denotes ignoring the proline rule. On serum
            albumin the two differ by 7 peptides out of about 200 (195 vs 202; tryptic, 2
            missed cleavages, min length 7) - a small count hiding a large semantic
            difference, since which peptides you get changes wherever a K/R precedes a
            proline.
        dissociation: ``"HCD"``/``"CID"`` (b and y ions), ``"ETD"``, and the rest of mzLib's
            dissociation types.

            ``"ETD"`` returns the c and zDot series (radical N-Ca cleavage yields c/z*, not the
            b/y of vibrational activation). Note that z* ions are suppressed N-terminal to
            proline while the complementary c ions are not, leaving about 4% of the c series
            unobservable (smith-chem-wisc/mzLib#1110).
        modifications: Apply UniProt's annotated modifications.

            Pass ``False`` for the bare sequences — a clean control, since the digest's
            distinct backbones are unchanged and only the modified variants of them go
            away. The peptidoform count drops a long way; on albumin from 303 to 195.

            This once carried a caveat saying ``False`` was *not* a clean control, because
            it discarded UniProt's whole feature table and with it the signal-peptide and
            propeptide boundaries mzLib digests at — costing albumin two peptides and
            changing the peptide list, not just the modifications on it (issue #8). That
            was true and is no longer: verified against the published bridge, both peptides
            are present either way and albumin gives 195 distinct base sequences with
            modifications on or off.
        missed_cleavages: Maximum missed cleavage sites per peptide.
        min_length: Shortest peptide to keep, in residues. The default of 7 silently discards shorter
            peptides — roughly a third of a histone digest — so pass ``min_length=1`` when you
            mean *every* peptide.
        max_length: Longest peptide to keep, in residues. ``None`` means unbounded.
        max_modifications: Maximum modifications considered per peptide. Modification isoforms
            are enumerated combinatorially: histone H3.1 yields 49 bare tryptic peptides, 2,563
            at two modifications and 7,040 at three.
        max_isoforms: Maximum modification isoforms per peptide position. mzLib's default of 1024
            **truncates silently** when it binds — on H3.1 at four modifications it discards
            about 30% of the Peptidoforms (13,700 down to 9,536). :attr:`Digest.peptides_at_cap`
            reports how many peptides hit it, so a truncated answer is visible rather than
            merely short.
        terminus: ``"Both"``, ``"N"`` or ``"C"``.
        timeout: Seconds to allow. Large proteins with many modification isoforms take longer.

    Returns:
        A :class:`Digest`. Check :attr:`Digest.modification_census` before trusting a
        modification count — it reports what was annotated as well as what was applied.

    Raises:
        UsageError: the accession, protease, dissociation type or terminus is not recognised.
        ServiceUnavailableError: UniProt was unreachable.

    Examples:
        >>> d = fragments("P02768")
        >>> print(d.modification_census.explain())                     # doctest: +ELLIPSIS
        14 of 38 annotated modifications were applied, across 14 residue positions. Excluded ...
    """
    if not isinstance(accession, str) or not accession.strip():
        raise _bridge.UsageError("A UniProt accession is required, e.g. 'P02768'.")
    canonical = accession.strip().upper()
    if not _ACCESSION.match(canonical):
        raise _bridge.UsageError(
            f"'{accession}' is not a valid UniProtKB accession. They look like 'P02768' or "
            "'A0A0B4J2D5' — see https://www.uniprot.org/help/accession_numbers."
        )
    # UniProt's accessions are upper-case and its API is case-sensitive, so the validated
    # canonical form is what gets sent, not the caller's original casing.
    if max_length is not None and (isinstance(max_length, bool) or not isinstance(max_length, int)):
        raise _bridge.UsageError(f"max_length must be a whole number or None; got {max_length!r}.")
    for name, value in (("missed_cleavages", missed_cleavages), ("min_length", min_length),
                        ("max_modifications", max_modifications), ("max_isoforms", max_isoforms)):
        if isinstance(value, bool) or not isinstance(value, int) or value < 0:
            raise _bridge.UsageError(f"{name} must be a non-negative whole number; got {value!r}.")
    if max_isoforms < 1:
        raise _bridge.UsageError(f"max_isoforms must be at least 1; got {max_isoforms}.")

    args = [
        "peptidoform", "fragments",
        "--accession", canonical,
        "--protease", protease,
        "--dissociation", dissociation,
        "--terminus", terminus,
        "--missed-cleavages", str(missed_cleavages),
        "--min-length", str(min_length),
        "--max-length", str(max_length if max_length else 0),
        "--max-mods", str(max_modifications),
        "--max-isoforms", str(max_isoforms),
    ]
    if not modifications:
        args.append("--no-modifications")

    data = _bridge.invoke(*args, timeout=timeout)

    census = ModificationCensus(
        sites=int(data.get("annotated_modification_sites", 0)),
        applied=int(data.get("annotated_modifications_loaded", 0)),
        annotated=int(data.get("uniprot_annotated_features", 0)),
        by_type=list(data.get("uniprot_features_by_type") or []),
        unresolved=list(data.get("unresolved_modifications") or []),
    )

    return Digest(
        accession=data.get("accession", ""),
        name=data.get("name", "") or "",
        full_name=data.get("full_name", "") or "",
        organism=data.get("organism", "") or "",
        sequence_length=int(data.get("sequence_length", 0)),
        protease=data.get("protease", ""),
        dissociation=data.get("dissociation", ""),
        terminus=data.get("terminus", ""),
        modifications_applied=bool(data.get("modifications_applied", False)),
        max_modifications=int(data.get("max_modifications", 0)),
        max_isoforms=int(data.get("max_modification_isoforms", 0)),
        peptides_at_cap=int(data.get("peptides_at_isoform_cap", 0)),
        modification_census=census,
        peptides=[Peptide._from_wire(p) for p in (data.get("peptides") or [])],
    )

convert

convert(sequences: Iterable[str], *, source: str = 'mzLib', target: str = 'Unimod', mode: str = 'ReturnNull', threads: int = 1, timeout: float | None = 120) -> SequenceConversions

Convert full sequences from one notation to another with mzLib, one result per input.

Wraps mzLib's SequenceConversionService.Default (with ProForma registered). The main use is MetaMorpheus or mzLib full sequences to Unimod accessions: [UniProt:N-acetylserine on S]SEQK becomes [UNIMOD:1]SEQK. Nothing is converted in Python or in the bridge; every output and every status is mzLib's.

Choose Unimod, not ProForma, for UniProt-sourced modifications. mzLib's ProForma target does not resolve them and writes them back under their mzLib name, status "converted". See the caveats.

Parameters:

Name Type Description Default
sequences Iterable[str]

Full sequences, one result row each, in order, duplicates included. Sent to mzLib exactly as given. A blank entry or one with a line break is refused, because it would shift every later row. Split an ambiguous MetaMorpheus full sequence (|-joined candidates) first: mzLib joins the candidates into one sequence.

required
source str

The notation the sequences are in. One of mzLib's registered source formats (:attr:SequenceConversions.source_formats), matched case-insensitively.

'mzLib'
target str

The notation to write. One of mzLib's registered target formats (:attr:SequenceConversions.target_formats), matched case-insensitively.

'Unimod'
mode str

mzLib's SequenceConversionHandlingMode, one of :data:CONVERSION_MODES (case-insensitive). "ReturnNull" (default): a sequence mzLib cannot convert is a failed row. "RemoveIncompatibleElements" and "UsePrimarySequence": what the target cannot write is dropped and the row is "converted_with_warnings". "ThrowException": the first such sequence raises :class:UsageError naming it, and nothing is returned.

'ReturnNull'
threads int

Sequences converted at once. Default 1; -1 means every core. Same rows, in the same order, at any value.

1
timeout float | None

Seconds to allow. None waits indefinitely.

120

Returns:

Name Type Description
A SequenceConversions

class:SequenceConversions table, also as :attr:SequenceConversions.sequences.

Raises:

Type Description
UsageError

no sequences; a blank one; source or target not a format mzLib registered (the message lists them); mode not one of :data:CONVERSION_MODES; or, under mode="ThrowException", a sequence mzLib could not convert.

Examples:

UniProt and MetaMorpheus modifications to Unimod accessions, and one mzLib cannot map:

>>> result = convert([
...     "[UniProt:N-acetylserine on S]SEQK",
...     "PEPK[UniProt:N6,N6-dimethyllysine on K]R",
...     "PEPM[Common Variable:Oxidation on M]K",
...     "PEPK[Made Up:Not a modification on K]R",
... ])
>>> result.outputs
['[UNIMOD:1]SEQK', 'PEPK[UNIMOD:36]R', 'PEPM[UNIMOD:35]K', None]
>>> failed = result.not_converted[0]
>>> failed.status, failed.incompatible_items
('failed', ['Made Up:Not a modification on K @3(K)'])
Source code in pkg/python/src/pymzlib/peptidoform.py
def convert(
    sequences: Iterable[str],
    *,
    source: str = "mzLib",
    target: str = "Unimod",
    mode: str = "ReturnNull",
    threads: int = 1,
    timeout: float | None = 120,
) -> SequenceConversions:
    """Convert full sequences from one notation to another with mzLib, one result per input.

    Wraps mzLib's ``SequenceConversionService.Default`` (with ProForma registered). The main use is
    MetaMorpheus or mzLib full sequences to Unimod accessions: ``[UniProt:N-acetylserine on S]SEQK``
    becomes ``[UNIMOD:1]SEQK``. Nothing is converted in Python or in the bridge; every output and
    every status is mzLib's.

    **Choose Unimod, not ProForma, for UniProt-sourced modifications.** mzLib's ProForma target
    does not resolve them and writes them back under their mzLib name, status ``"converted"``. See
    the caveats.

    Args:
        sequences: Full sequences, one result row each, in order, duplicates included. Sent to
            mzLib exactly as given. A blank entry or one with a line break is refused, because it
            would shift every later row. Split an ambiguous MetaMorpheus full sequence
            (``|``-joined candidates) first: mzLib joins the candidates into one sequence.
        source: The notation the sequences are in. One of mzLib's registered source formats
            (:attr:`SequenceConversions.source_formats`), matched case-insensitively.
        target: The notation to write. One of mzLib's registered target formats
            (:attr:`SequenceConversions.target_formats`), matched case-insensitively.
        mode: mzLib's ``SequenceConversionHandlingMode``, one of :data:`CONVERSION_MODES`
            (case-insensitive).
            ``"ReturnNull"`` (default): a sequence mzLib cannot convert is a failed row.
            ``"RemoveIncompatibleElements"`` and ``"UsePrimarySequence"``: what the target cannot
            write is dropped and the row is ``"converted_with_warnings"``. ``"ThrowException"``:
            the first such sequence raises :class:`UsageError` naming it, and nothing is returned.
        threads: Sequences converted at once. Default 1; ``-1`` means every core. Same rows, in
            the same order, at any value.
        timeout: Seconds to allow. ``None`` waits indefinitely.

    Returns:
        A :class:`SequenceConversions` table, also as :attr:`SequenceConversions.sequences`.

    Raises:
        UsageError: no sequences; a blank one; ``source`` or ``target`` not a format mzLib
            registered (the message lists them); ``mode`` not one of :data:`CONVERSION_MODES`; or,
            under ``mode="ThrowException"``, a sequence mzLib could not convert.

    Examples:
        UniProt and MetaMorpheus modifications to Unimod accessions, and one mzLib cannot map:

        >>> result = convert([
        ...     "[UniProt:N-acetylserine on S]SEQK",
        ...     "PEPK[UniProt:N6,N6-dimethyllysine on K]R",
        ...     "PEPM[Common Variable:Oxidation on M]K",
        ...     "PEPK[Made Up:Not a modification on K]R",
        ... ])
        >>> result.outputs
        ['[UNIMOD:1]SEQK', 'PEPK[UNIMOD:36]R', 'PEPM[UNIMOD:35]K', None]
        >>> failed = result.not_converted[0]
        >>> failed.status, failed.incompatible_items
        ('failed', ['Made Up:Not a modification on K @3(K)'])
    """
    lines = _sequence_lines(sequences)
    if not lines:
        raise _bridge.UsageError(
            "At least one sequence is required, e.g. ['[UniProt:N-acetylserine on S]SEQK']."
        )
    for name, value in (("source", source), ("target", target)):
        if not isinstance(value, str) or not value.strip():
            raise _bridge.UsageError(f"{name} must be a non-empty format name; got {value!r}.")
    canonical_mode = {m.lower(): m for m in CONVERSION_MODES}.get(mode.lower()) if isinstance(mode, str) else None
    if canonical_mode is None:
        raise _bridge.UsageError(
            f"mode must be one of {', '.join(CONVERSION_MODES)} (mzLib's "
            f"SequenceConversionHandlingMode); got {mode!r}."
        )
    if isinstance(threads, bool) or not isinstance(threads, int) or threads == 0 or threads < -1:
        raise _bridge.UsageError(f"threads must be 1 or more, or -1 for every core; got {threads!r}.")

    _bridge.require_verb("peptidoform convert", since=_CONVERT_SINCE)
    data = _bridge.invoke(
        "peptidoform", "convert",
        "--from", source, "--to", target, "--mode", canonical_mode, "--threads", str(threads),
        stdin="\n".join(lines) + "\n",
        timeout=timeout,
    )
    return SequenceConversions._from_wire(data)

pymzlib.flashlfq

flashlfq

Label-free quantification with FlashLFQ: quantify a search's peptides across mzML runs.

The question this answers is the one a quant workflow actually asks — given these identifications and these runs, how much of each peptide and protein is in each run? — in one call:

>>> import pymzlib
>>> result = pymzlib.flashlfq.quantify(
...     psms="AllPSMs.psmtsv",
...     spectra=["run_3.mzML", "run_4.mzML"],
...     match_between_runs=True,
... )
>>> result.peptide_count, result.protein_count
(2, 2)

The whole pipeline is mzLib's: the result file is read by mzLib's Readers, turned into FlashLFQ identifications by mzLib's own converter, and quantified by FlashLfqEngine. MetaMorpheus is not involved — mzLib does it alone.

Names follow mzLib and FlashLFQ deliberately, so a value here means the same thing it does in the FlashLFQ source, the MetaMorpheus output columns, and the FlashLFQ paper: match_between_runs, ppm_tolerance, mbr_ppm_tolerance, sequence, base_sequence, protein_groups, detection_types, ProteinGroup, FlashLfqResults.

.. note::

An MSFragger psm.tsv can now be quantified here. mzLib PR #1116 converts MSFragger's retention time to minutes at the reader, so the value FlashLFQ consumes is in the unit it expects; the former "MSFragger writes seconds, do not quantify it" warning no longer applies.

Three more limits worth knowing before you trust a number, in the "surface it, don't hide it" spirit of the rest of pyMzLib:

  • mzML only, for now. Convert .raw/.d to mzML first; a non-mzML path is rejected up front.
  • A protein intensity can be None. FlashLFQ's median-polish protein quant marks a protein NaN when its peptide matrix is degenerate (too few peptides per run, or identical intensities across runs — a real artifact documented in mzLib's own tests). NaN is not valid JSON, so it arrives here as None — "could not be quantified" — rather than a silently wrong number. A peptide intensity, by contrast, is 0.0 when missing, never None.
  • For match-between-runs, read the peaks, not the peptides. The peptide roll-up (:attr:FlashLfqResults.peptides, mirroring QuantifiedPeptides.tsv) reports far fewer MBR transfers than actually happened — a whole run's transfers can be absent. :attr:FlashLfqResults.peaks (and :meth:FlashLfqResults.mbr_peaks) is the complete surface; :attr:FlashLfqResults.mbr_peak_count is the number to trust.

SpectraFileInfo dataclass

One quantified run, mirroring mzLib's MassSpectrometry.SpectraFileInfo.

Attributes:

Name Type Description
file_name str

The run's base name (no directory, no extension) — the key used everywhere else here to look up this run's intensity.

full_path str

The mzML path as provided.

condition str

The sample-group label, or "" if none was given.

biological_replicate / technical_replicate / fraction

The experimental-design coordinates.

peak_count int

Chromatographic peaks quantified in this run.

mbr_peak_count int

Of those, how many were transferred by match-between-runs — peaks quantified in this run for a peptide that was never identified in it. Zero unless match_between_runs was set.

Source code in pkg/python/src/pymzlib/flashlfq.py
@dataclass(frozen=True)
class SpectraFileInfo:
    """One quantified run, mirroring mzLib's ``MassSpectrometry.SpectraFileInfo``.

    Attributes:
        file_name: The run's base name (no directory, no extension) — the key used everywhere else
            here to look up this run's intensity.
        full_path: The mzML path as provided.
        condition: The sample-group label, or ``""`` if none was given.
        biological_replicate / technical_replicate / fraction: The experimental-design coordinates.
        peak_count: Chromatographic peaks quantified in this run.
        mbr_peak_count: Of those, how many were transferred by match-between-runs — peaks quantified
            in this run for a peptide that was never identified *in* it. Zero unless
            ``match_between_runs`` was set.
    """

    file_name: str
    full_path: str
    condition: str
    biological_replicate: int
    technical_replicate: int
    fraction: int
    peak_count: int
    mbr_peak_count: int

    @classmethod
    def _from_wire(cls, payload: dict[str, Any]) -> "SpectraFileInfo":
        return cls(
            file_name=payload.get("file_name", ""),
            full_path=payload.get("full_path", ""),
            condition=payload.get("condition", ""),
            biological_replicate=int(payload.get("biological_replicate", 0)),
            technical_replicate=int(payload.get("technical_replicate", 0)),
            fraction=int(payload.get("fraction", 0)),
            peak_count=int(payload.get("peak_count", 0)),
            mbr_peak_count=int(payload.get("mbr_peak_count", 0)),
        )

Peptide dataclass

A quantified peptide, mirroring FlashLFQ's Peptide.

Attributes:

Name Type Description
sequence str

The full (modified) sequence, as FlashLFQ renders it — the identity FlashLFQ quantifies. Two different modification states of one base sequence are two peptides.

base_sequence str

The bare amino-acid sequence.

protein_groups str

The protein group(s) this peptide belongs to, ;-joined.

intensities dict[str, Any]

Run base name → intensity in that run. Missing is 0.0, never None — a null on the wire is resolved to 0.0 when parsed, so the invariant holds by construction rather than by hope (issue #7). Note too that where a peptide has several peaks in one run this roll-up reports one of them rather than their sum, so pivoting :attr:FlashLfqResults.peaks yourself will not reproduce these intensities exactly. Counting presence is unaffected; summing intensity is not. (unlike proteins). This roll-up mirrors FlashLFQ's QuantifiedPeptides.tsv and does not fully reflect match-between-runs — many transferred peptides read 0.0 / "NotDetected" here. For MBR-inclusive quantities use :attr:FlashLfqResults.peaks.

detection_types dict[str, str]

Run base name → how it was quantified there. Values FlashLFQ emits: "MSMS" (identified and quantified in this run), "MBR" (transferred by match-between-runs), "MSMSIdentifiedButNotQuantified" (an ID here but no usable peak), "MSMSAmbiguousPeakfinding" (more than one peptide fits the peak), and "NotDetected".

Source code in pkg/python/src/pymzlib/flashlfq.py
@dataclass(frozen=True)
class Peptide:
    """A quantified peptide, mirroring FlashLFQ's ``Peptide``.

    Attributes:
        sequence: The full (modified) sequence, as FlashLFQ renders it — the identity FlashLFQ
            quantifies. Two different modification states of one base sequence are two peptides.
        base_sequence: The bare amino-acid sequence.
        protein_groups: The protein group(s) this peptide belongs to, ``;``-joined.
        intensities: Run base name → intensity in that run. Missing is ``0.0``, **never ``None``** — a ``null``
            on the wire is resolved to ``0.0`` when parsed, so the invariant holds by
            construction rather than by hope (issue #7). Note too that where a peptide has
            several peaks in one run this roll-up reports **one** of them rather than their
            sum, so pivoting :attr:`FlashLfqResults.peaks` yourself will not reproduce these
            intensities exactly. Counting presence is unaffected; summing intensity is not.
            (unlike proteins). This roll-up mirrors FlashLFQ's ``QuantifiedPeptides.tsv`` and does
            **not** fully reflect match-between-runs — many transferred peptides read ``0.0`` /
            ``"NotDetected"`` here. For MBR-inclusive quantities use :attr:`FlashLfqResults.peaks`.
        detection_types: Run base name → how it was quantified there. Values FlashLFQ emits:
            ``"MSMS"`` (identified and quantified in this run), ``"MBR"`` (transferred by
            match-between-runs), ``"MSMSIdentifiedButNotQuantified"`` (an ID here but no usable
            peak), ``"MSMSAmbiguousPeakfinding"`` (more than one peptide fits the peak), and
            ``"NotDetected"``.
    """

    sequence: str
    base_sequence: str
    protein_groups: str
    intensities: dict[str, Any]
    detection_types: dict[str, str]

    def intensity(self, file_name: str) -> Any:
        """This peptide's intensity in the named run.

        **``0.0`` — not ``None`` — means "not quantified here."** (Only *protein* intensities are
        ever ``None``.) Treat ``0.0`` as missing, not as a measured absence: log-transforming it
        will mislead. And note a peptide that FlashLFQ *transferred* into this run by
        match-between-runs may still read ``0.0`` here — see :attr:`FlashLfqResults.peaks`. Returns
        ``0.0`` for a run that was never provided.
        """
        return self.intensities.get(file_name, 0.0)

    def detection_type(self, file_name: str) -> str:
        """How this peptide was quantified in the named run (``"NotDetected"`` if it was not)."""
        return self.detection_types.get(file_name, "NotDetected")

    @classmethod
    def _from_wire(cls, payload: dict[str, Any]) -> "Peptide":
        return cls(
            sequence=payload.get("sequence", ""),
            base_sequence=payload.get("base_sequence", ""),
            protein_groups=payload.get("protein_groups", ""),
            # A null crossing the wire becomes 0.0 here, so the documented invariant --
            # "missing is 0.0, never None" -- is true by construction rather than by hope.
            # The bridge routes every double through a finite check, so a NaN peptide
            # intensity arrives as null; leaving it None would make intensity() return None
            # for a peptide, which is supposed to be a protein-only condition and is exactly
            # what callers branch on to find unresolvable proteins. See issue #7.
            intensities={
                run: (0.0 if value is None else value)
                for run, value in (payload.get("intensities") or {}).items()
            },
            detection_types=dict(payload.get("detection_types") or {}),
        )
intensity
intensity(file_name: str) -> Any

This peptide's intensity in the named run.

0.0 — not None — means "not quantified here." (Only protein intensities are ever None.) Treat 0.0 as missing, not as a measured absence: log-transforming it will mislead. And note a peptide that FlashLFQ transferred into this run by match-between-runs may still read 0.0 here — see :attr:FlashLfqResults.peaks. Returns 0.0 for a run that was never provided.

Source code in pkg/python/src/pymzlib/flashlfq.py
def intensity(self, file_name: str) -> Any:
    """This peptide's intensity in the named run.

    **``0.0`` — not ``None`` — means "not quantified here."** (Only *protein* intensities are
    ever ``None``.) Treat ``0.0`` as missing, not as a measured absence: log-transforming it
    will mislead. And note a peptide that FlashLFQ *transferred* into this run by
    match-between-runs may still read ``0.0`` here — see :attr:`FlashLfqResults.peaks`. Returns
    ``0.0`` for a run that was never provided.
    """
    return self.intensities.get(file_name, 0.0)
detection_type
detection_type(file_name: str) -> str

How this peptide was quantified in the named run ("NotDetected" if it was not).

Source code in pkg/python/src/pymzlib/flashlfq.py
def detection_type(self, file_name: str) -> str:
    """How this peptide was quantified in the named run (``"NotDetected"`` if it was not)."""
    return self.detection_types.get(file_name, "NotDetected")

ProteinGroup dataclass

A quantified protein group, mirroring FlashLFQ's ProteinGroup.

Attributes:

Name Type Description
protein_group str

The protein group name (accession, or ;-joined accessions).

gene_name str

The gene name, when the result file carried one.

organism str

The organism, when the result file carried one.

intensities dict[str, Any]

Run base name → protein intensity in that run. May be None: FlashLFQ's median-polish protein quant emits NaN (returned here as None) when the peptide matrix for the protein is degenerate — too few peptides per run to resolve, or several runs reporting the same intensity — protecting you from a fabricated-looking number. Missing, as opposed to un-resolvable, is 0.0.

Source code in pkg/python/src/pymzlib/flashlfq.py
@dataclass(frozen=True)
class ProteinGroup:
    """A quantified protein group, mirroring FlashLFQ's ``ProteinGroup``.

    Attributes:
        protein_group: The protein group name (accession, or ``;``-joined accessions).
        gene_name: The gene name, when the result file carried one.
        organism: The organism, when the result file carried one.
        intensities: Run base name → protein intensity in that run. **May be ``None``**: FlashLFQ's
            median-polish protein quant emits NaN (returned here as ``None``) when the peptide matrix
            for the protein is degenerate — too few peptides per run to resolve, or several runs
            reporting the same intensity — protecting you from a fabricated-looking number. Missing,
            as opposed to un-resolvable, is ``0.0``.
    """

    protein_group: str
    gene_name: str
    organism: str
    intensities: dict[str, Any]

    def intensity(self, file_name: str) -> Any:
        """This protein's intensity in the named run.

        ``None`` means FlashLFQ could not resolve a number (a degenerate peptide matrix); ``0.0``
        means simply not measured in this run.
        """
        return self.intensities.get(file_name, 0.0)

    @classmethod
    def _from_wire(cls, payload: dict[str, Any]) -> "ProteinGroup":
        return cls(
            protein_group=payload.get("protein_group", ""),
            gene_name=payload.get("gene_name", ""),
            organism=payload.get("organism", ""),
            intensities=dict(payload.get("intensities") or {}),
        )
intensity
intensity(file_name: str) -> Any

This protein's intensity in the named run.

None means FlashLFQ could not resolve a number (a degenerate peptide matrix); 0.0 means simply not measured in this run.

Source code in pkg/python/src/pymzlib/flashlfq.py
def intensity(self, file_name: str) -> Any:
    """This protein's intensity in the named run.

    ``None`` means FlashLFQ could not resolve a number (a degenerate peptide matrix); ``0.0``
    means simply not measured in this run.
    """
    return self.intensities.get(file_name, 0.0)

Peak dataclass

One quantified chromatographic peak, mirroring FlashLFQ's ChromatographicPeak.

This is the surface to use for match-between-runs. Unlike the peptide roll-up (:attr:Peptide.intensities, which mirrors QuantifiedPeptides.tsv and drops most MBR transfers), the peaks fully represent every quantified peak, transferred or not. To build an MBR-inclusive peptide × run matrix, pivot these on (sequence, file_name).

Attributes:

Name Type Description
file_name str

The run this peak was measured in (base name).

sequence str

The full (modified) sequence of the peptide the peak was assigned to.

base_sequence str

The bare amino-acid sequence.

intensity Any

The peak's intensity (None only if FlashLFQ could not integrate it).

detection_type str

"MSMS", "MBR", "MSMSIdentifiedButNotQuantified", "MSMSAmbiguousPeakfinding" — the same vocabulary as :attr:Peptide.detection_types. Filter detection_type == "MBR" to see exactly the transferred peaks.

retention_time Any

Apex retention time in minutes, or None if the peak has no apex.

num_identifications int

How many peptides could explain this peak. > 1 means it is ambiguous and its intensity should be treated with care.

protein_groups str

The protein group(s) the assigned identification(s) belong to, ;-joined.

Source code in pkg/python/src/pymzlib/flashlfq.py
@dataclass(frozen=True)
class Peak:
    """One quantified chromatographic peak, mirroring FlashLFQ's ``ChromatographicPeak``.

    This is the surface to use for **match-between-runs**. Unlike the peptide roll-up
    (:attr:`Peptide.intensities`, which mirrors ``QuantifiedPeptides.tsv`` and drops most MBR
    transfers), the peaks fully represent every quantified peak, transferred or not. To build an
    MBR-inclusive peptide × run matrix, pivot these on ``(sequence, file_name)``.

    Attributes:
        file_name: The run this peak was measured in (base name).
        sequence: The full (modified) sequence of the peptide the peak was assigned to.
        base_sequence: The bare amino-acid sequence.
        intensity: The peak's intensity (``None`` only if FlashLFQ could not integrate it).
        detection_type: ``"MSMS"``, ``"MBR"``, ``"MSMSIdentifiedButNotQuantified"``,
            ``"MSMSAmbiguousPeakfinding"`` — the same vocabulary as :attr:`Peptide.detection_types`.
            Filter ``detection_type == "MBR"`` to see exactly the transferred peaks.
        retention_time: Apex retention time in minutes, or ``None`` if the peak has no apex.
        num_identifications: How many peptides could explain this peak. ``> 1`` means it is
            ambiguous and its intensity should be treated with care.
        protein_groups: The protein group(s) the assigned identification(s) belong to, ``;``-joined.
    """

    file_name: str
    sequence: str
    base_sequence: str
    intensity: Any
    detection_type: str
    retention_time: Any
    num_identifications: int
    protein_groups: str

    @property
    def is_mbr(self) -> bool:
        """Whether this peak was transferred by match-between-runs."""
        return self.detection_type == "MBR"

    @classmethod
    def _from_wire(cls, payload: dict[str, Any]) -> "Peak":
        return cls(
            file_name=payload.get("file_name", ""),
            sequence=payload.get("sequence", ""),
            base_sequence=payload.get("base_sequence", ""),
            intensity=payload.get("intensity"),
            detection_type=payload.get("detection_type", ""),
            retention_time=payload.get("retention_time"),
            num_identifications=int(payload.get("num_identifications", 0)),
            protein_groups=payload.get("protein_groups", ""),
        )
is_mbr property
is_mbr: bool

Whether this peak was transferred by match-between-runs.

FlashLfqResults dataclass

The result of a quantification run, mirroring mzLib's FlashLfqResults.

Attributes:

Name Type Description
psm_file str

The absolute path of the PSM result file that was quantified.

identification_count int

How many identifications were read from it.

parameters dict[str, Any]

The FlashLFQ parameters actually used, echoed back with their mzLib names.

spectra_files list[SpectraFileInfo]

One :class:SpectraFileInfo per run.

peptides list[Peptide]

One :class:Peptide per quantified modified sequence (mirrors QuantifiedPeptides.tsv; does not fully carry MBR — use :attr:peaks for that).

proteins list[ProteinGroup]

One :class:ProteinGroup per quantified protein group.

peaks list[Peak]

Every quantified :class:Peak across all runs (mirrors QuantifiedPeaks.tsv) — the complete surface, and the one to use for match-between-runs.

output_directory Any

Where the FlashLFQ TSVs were written, or None if none were.

peptide_count int

Distinct modified sequences quantified: the number of peptides in :attr:peptides.

protein_count int

Protein groups quantified: the number of protein groups in :attr:proteins.

Source code in pkg/python/src/pymzlib/flashlfq.py
@dataclass(frozen=True)
class FlashLfqResults:
    """The result of a quantification run, mirroring mzLib's ``FlashLfqResults``.

    Attributes:
        psm_file: The absolute path of the PSM result file that was quantified.
        identification_count: How many identifications were read from it.
        parameters: The FlashLFQ parameters actually used, echoed back with their mzLib names.
        spectra_files: One :class:`SpectraFileInfo` per run.
        peptides: One :class:`Peptide` per quantified modified sequence (mirrors
            ``QuantifiedPeptides.tsv``; does not fully carry MBR — use :attr:`peaks` for that).
        proteins: One :class:`ProteinGroup` per quantified protein group.
        peaks: Every quantified :class:`Peak` across all runs (mirrors ``QuantifiedPeaks.tsv``) —
            the complete surface, and the one to use for match-between-runs.
        output_directory: Where the FlashLFQ TSVs were written, or ``None`` if none were.
        peptide_count: Distinct modified sequences quantified: the number of peptides in
            :attr:`peptides`.
        protein_count: Protein groups quantified: the number of protein groups in :attr:`proteins`.
    """

    psm_file: str
    identification_count: int
    parameters: dict[str, Any]
    spectra_files: list[SpectraFileInfo]
    peptides: list[Peptide]
    proteins: list[ProteinGroup]
    peaks: list[Peak]
    output_directory: Any

    @property
    def peptide_count(self) -> int:
        """Number of quantified peptides."""
        return len(self.peptides)

    @property
    def protein_count(self) -> int:
        """Number of quantified protein groups."""
        return len(self.proteins)

    @property
    def mbr_peak_count(self) -> int:
        """Total match-between-runs peaks (transfers) across every run.

        This counts transferred **peaks**, not distinct peptides: one peptide rescued in two runs is
        two peaks here. For "how many peptides did MBR rescue," use :attr:`mbr_rescued_peptide_count`.
        Either way, do not count MBR from the peptide roll-up (:attr:`Peptide.detection_types`) — it
        under-counts. Zero unless ``match_between_runs`` was on.
        """
        return sum(f.mbr_peak_count for f in self.spectra_files)

    @property
    def mbr_peaks(self) -> list[Peak]:
        """Exactly the peaks transferred by match-between-runs (``detection_type == "MBR"``)."""
        return [p for p in self.peaks if p.is_mbr]

    @property
    def mbr_rescued_peptide_count(self) -> int:
        """Distinct peptides quantified in at least one run only by match-between-runs.

        Exactly: **the number of distinct ``sequence`` values among peaks whose
        ``detection_type`` is ``"MBR"``.** Stated in code terms because the prose version,
        "peptides quantified in at least one run *only* by match-between-runs", is subtly
        different and on real data the two diverge: peptides having **both** an MBR peak and
        a zero-intensity ``MSMS`` peak in the same run were identified there, so they are not
        rescues under the strict reading. On the K562 pair this returns 140 where the strict
        count is 135. Do not read ``mbr_rescued_peptide_count == mbr_peak_count`` as
        reassurance that nothing was double-counted; on that data both are 140, and they
        coincide only because every MBR peak happened to carry a distinct sequence.

        Distinct modified sequences among
        :attr:`mbr_peaks`. This equals :attr:`mbr_peak_count` only when no peptide was rescued in
        more than one run.
        """
        return len({p.sequence for p in self.mbr_peaks})

    @classmethod
    def _from_wire(cls, data: dict[str, Any]) -> "FlashLfqResults":
        return cls(
            psm_file=data.get("psm_file", ""),
            identification_count=int(data.get("identification_count", 0)),
            parameters=dict(data.get("parameters") or {}),
            spectra_files=[SpectraFileInfo._from_wire(f) for f in (data.get("spectra_files") or [])],
            peptides=[Peptide._from_wire(p) for p in (data.get("peptides") or [])],
            proteins=[ProteinGroup._from_wire(g) for g in (data.get("proteins") or [])],
            peaks=[Peak._from_wire(pk) for pk in (data.get("peaks") or [])],
            output_directory=data.get("output_directory"),
        )
peptide_count property
peptide_count: int

Number of quantified peptides.

protein_count property
protein_count: int

Number of quantified protein groups.

mbr_peak_count property
mbr_peak_count: int

Total match-between-runs peaks (transfers) across every run.

This counts transferred peaks, not distinct peptides: one peptide rescued in two runs is two peaks here. For "how many peptides did MBR rescue," use :attr:mbr_rescued_peptide_count. Either way, do not count MBR from the peptide roll-up (:attr:Peptide.detection_types) — it under-counts. Zero unless match_between_runs was on.

mbr_peaks property
mbr_peaks: list[Peak]

Exactly the peaks transferred by match-between-runs (detection_type == "MBR").

mbr_rescued_peptide_count property
mbr_rescued_peptide_count: int

Distinct peptides quantified in at least one run only by match-between-runs.

Exactly: the number of distinct sequence values among peaks whose detection_type is "MBR". Stated in code terms because the prose version, "peptides quantified in at least one run only by match-between-runs", is subtly different and on real data the two diverge: peptides having both an MBR peak and a zero-intensity MSMS peak in the same run were identified there, so they are not rescues under the strict reading. On the K562 pair this returns 140 where the strict count is 135. Do not read mbr_rescued_peptide_count == mbr_peak_count as reassurance that nothing was double-counted; on that data both are 140, and they coincide only because every MBR peak happened to carry a distinct sequence.

Distinct modified sequences among :attr:mbr_peaks. This equals :attr:mbr_peak_count only when no peptide was rescued in more than one run.

quantify

quantify(psms: str | PathLike[str], spectra: Sequence[SpectraInput], *, normalize: bool = False, ppm_tolerance: float = 10.0, isotope_ppm_tolerance: float = 5.0, integrate: bool = False, match_between_runs: bool = False, mbr_ppm_tolerance: float = 10.0, mbr_q_value_threshold: float = 0.05, use_shared_peptides_for_protein_quant: bool = False, bayesian_protein_quant: bool = False, use_pep_q_value: bool = False, max_threads: int = -1, output_directory: str | PathLike[str] | None = None, timeout: float | None = None) -> FlashLfqResults

Quantify a search's peptides across mzML runs with FlashLFQ.

Parameters:

Name Type Description Default
psms str | PathLike[str]

Path to a PSM result file — a MetaMorpheus .psmtsv/.osmtsv (full field set) or an MSFragger psm.tsv (retention time is converted to minutes upstream since mzLib PR #1116, so it quantifies correctly). Every run named in the file must have a matching mzML in spectra; FlashLFQ matches identifications to runs by base file name.

required
spectra Sequence[SpectraInput]

The mzML runs. Each entry is either a path ("run_1.mzML") or a mapping carrying the experimental design ({"path": "run_1.mzML", "condition": "treated", "biological_replicate": 1}). Base file names must be unique.

required
normalize bool

Normalize intensities across runs (FlashLFQ Normalize).

False
ppm_tolerance float

Mass tolerance for peak-finding, in ppm (PpmTolerance).

10.0
isotope_ppm_tolerance float

Mass tolerance for isotope-envelope matching, in ppm.

5.0
integrate bool

Integrate peak intensities rather than taking the apex. FlashLFQ recommends leaving this off.

False
match_between_runs bool

Quantify a peptide in a run where it was not identified, by transferring the identification from a run where it was (MatchBetweenRuns). The reason to reach for FlashLFQ; off by default because it makes assumptions worth opting into. Requires a complete, balanced design: every condition and biological replicate must carry the same set of fractions — a missing replicate or fraction breaks the complementarity MBR relies on. Read the transferred peaks from :attr:FlashLfqResults.peaks, not the peptide table.

False
mbr_ppm_tolerance float

Mass tolerance for MBR transfers, in ppm.

10.0
mbr_q_value_threshold float

The q-value cutoff below which an MBR transfer is accepted.

0.05
use_shared_peptides_for_protein_quant bool

Let peptides shared between protein groups contribute to protein quant (UseSharedPeptidesForProteinQuant).

False
bayesian_protein_quant bool

Also run FlashLFQ's Bayesian protein-fold-change engine. Its results are written to output_directory only; nothing on the returned object changes.

False
use_pep_q_value bool

Store each identification's PEP q-value as its q-value, instead of its q-value. It filters nothing here: FlashLFQ is given every identification either way. It changes the q-value FlashLFQ carries, which match-between-runs reads.

False
max_threads int

Worker threads; -1 lets FlashLFQ choose.

Before mzLib 1.0.592 this changed results, not only speed: with match-between-runs on and more than one thread, the peptide roll-up dropped some MBR intensities nondeterministically, so identical inputs gave different peptide and protein numbers (smith-chem-wisc/mzLib#1111). mzLib#1155 fixed the cause, the PEP training rows being assembled in thread-completion order, and this build includes it. pyMzLib has not re-measured its own K562 reproduction on this build, so max_threads=1 remains the conservative choice when a result must reproduce exactly.

-1
output_directory str | PathLike[str] | None

If given, FlashLFQ also writes QuantifiedPeaks.tsv, QuantifiedPeptides.tsv and QuantifiedProteins.tsv there, and, with bayesian_protein_quant, BayesianProteinQuant.tsv. That file is the only place the Bayesian results appear: they are not returned.

None
timeout float | None

Seconds to allow. Large experiments legitimately take a while; None waits indefinitely.

None

Returns:

Name Type Description
A FlashLfqResults

class:FlashLfqResults.

Raises:

Type Description
UsageError

an argument is malformed, a run is not mzML, an mzML is missing, or the PSM file names a run with no mzML provided.

BridgeError

FlashLFQ itself failed.

Source code in pkg/python/src/pymzlib/flashlfq.py
def quantify(
    psms: str | os.PathLike[str],
    spectra: Sequence[SpectraInput],
    *,
    normalize: bool = False,
    ppm_tolerance: float = 10.0,
    isotope_ppm_tolerance: float = 5.0,
    integrate: bool = False,
    match_between_runs: bool = False,
    mbr_ppm_tolerance: float = 10.0,
    mbr_q_value_threshold: float = 0.05,
    use_shared_peptides_for_protein_quant: bool = False,
    bayesian_protein_quant: bool = False,
    use_pep_q_value: bool = False,
    max_threads: int = -1,
    output_directory: str | os.PathLike[str] | None = None,
    timeout: float | None = None,
) -> FlashLfqResults:
    """Quantify a search's peptides across mzML runs with FlashLFQ.

    Args:
        psms: Path to a PSM result file — a MetaMorpheus ``.psmtsv``/``.osmtsv`` (full field set) or
            an MSFragger ``psm.tsv`` (retention time is converted to minutes upstream since mzLib
            PR #1116, so it quantifies correctly). Every run named in the file must have a matching
            mzML in ``spectra``; FlashLFQ matches identifications to runs by base file name.
        spectra: The mzML runs. Each entry is either a path (``"run_1.mzML"``) or a mapping
            carrying the experimental design (``{"path": "run_1.mzML", "condition": "treated",
            "biological_replicate": 1}``). Base file names must be unique.
        normalize: Normalize intensities across runs (FlashLFQ ``Normalize``).
        ppm_tolerance: Mass tolerance for peak-finding, in ppm (``PpmTolerance``).
        isotope_ppm_tolerance: Mass tolerance for isotope-envelope matching, in ppm.
        integrate: Integrate peak intensities rather than taking the apex. FlashLFQ recommends
            leaving this off.
        match_between_runs: Quantify a peptide in a run where it was not identified, by transferring
            the identification from a run where it was (``MatchBetweenRuns``). The reason to reach
            for FlashLFQ; off by default because it makes assumptions worth opting into. **Requires a
            complete, balanced design**: every condition and biological replicate must carry the same
            set of fractions — a missing replicate or fraction breaks the complementarity MBR relies
            on. Read the transferred peaks from :attr:`FlashLfqResults.peaks`, not the peptide table.
        mbr_ppm_tolerance: Mass tolerance for MBR transfers, in ppm.
        mbr_q_value_threshold: The q-value cutoff below which an MBR transfer is accepted.
        use_shared_peptides_for_protein_quant: Let peptides shared between protein groups contribute
            to protein quant (``UseSharedPeptidesForProteinQuant``).
        bayesian_protein_quant: Also run FlashLFQ's Bayesian protein-fold-change engine. Its
            results are written to ``output_directory`` only; nothing on the returned object changes.
        use_pep_q_value: Store each identification's PEP q-value as its q-value, instead of its
            q-value. **It filters nothing here**: FlashLFQ is given every identification either
            way. It changes the q-value FlashLFQ carries, which match-between-runs reads.
        max_threads: Worker threads; ``-1`` lets FlashLFQ choose.

            Before mzLib 1.0.592 this changed results, not only speed: with match-between-runs
            on and more than one thread, the peptide roll-up dropped some MBR intensities
            nondeterministically, so identical inputs gave different peptide and protein
            numbers (smith-chem-wisc/mzLib#1111). mzLib#1155 fixed the cause, the PEP
            training rows being assembled in thread-completion order, and this build includes
            it. pyMzLib has not re-measured its own K562 reproduction on this build, so
            ``max_threads=1`` remains the conservative choice when a result must reproduce
            exactly.
        output_directory: If given, FlashLFQ also writes ``QuantifiedPeaks.tsv``,
            ``QuantifiedPeptides.tsv`` and ``QuantifiedProteins.tsv`` there, and, with
            ``bayesian_protein_quant``, ``BayesianProteinQuant.tsv``. That file is the only place the
            Bayesian results appear: they are not returned.
        timeout: Seconds to allow. Large experiments legitimately take a while; ``None`` waits
            indefinitely.

    Returns:
        A :class:`FlashLfqResults`.

    Raises:
        UsageError: an argument is malformed, a run is not mzML, an mzML is missing, or the PSM
            file names a run with no mzML provided.
        BridgeError: FlashLFQ itself failed.
    """
    psms = _bridge.path_text(psms)
    if not psms:
        raise _bridge.UsageError("A PSM result file path is required, e.g. 'AllPSMs.psmtsv'.")

    stdin = _spectra_stdin(spectra)

    args: list[str] = ["quant", "flashlfq", "--psms", psms]
    if normalize:
        args.append("--normalize")
    args += ["--ppm", _number(ppm_tolerance, "ppm_tolerance")]
    args += ["--isotope-ppm", _number(isotope_ppm_tolerance, "isotope_ppm_tolerance")]
    if integrate:
        args.append("--integrate")
    if match_between_runs:
        args.append("--mbr")
    args += ["--mbr-ppm", _number(mbr_ppm_tolerance, "mbr_ppm_tolerance")]
    args += ["--mbr-q", _number(mbr_q_value_threshold, "mbr_q_value_threshold")]
    if use_shared_peptides_for_protein_quant:
        args.append("--shared-peptides")
    if bayesian_protein_quant:
        args.append("--bayesian")
    if use_pep_q_value:
        args.append("--use-pep-q")
    if isinstance(max_threads, bool) or not isinstance(max_threads, int):
        raise _bridge.UsageError(f"max_threads must be a whole number; got {max_threads!r}.")
    args += ["--threads", str(max_threads)]
    if output_directory is not None:
        output_directory = _bridge.path_text(output_directory)
        if not output_directory:
            raise _bridge.UsageError("output_directory must be a non-empty path or None.")
        args += ["--out", output_directory]

    data = _bridge.invoke(*args, stdin=stdin, timeout=timeout)
    return FlashLfqResults._from_wire(data)

median_polish

median_polish(peptides: str | PathLike[str], *, design: Sequence[DesignInput] | None = None, use_shared_peptides: bool = False, output_directory: str | PathLike[str] | None = None, timeout: float | None = None) -> list[ProteinGroup]

Roll a QuantifiedPeptides.tsv up to protein intensities with FlashLFQ's median polish.

This is the second half of :func:quantify on its own: given a peptide table FlashLFQ already wrote — its Intensity_<run> and Detection Type_<run> columns — it rebuilds the FlashLFQ peptide/protein object graph and runs the exact same median-polish protein quant (CalculateProteinResultsMedianPolish), without re-reading any mzML. Reach for it to re-quantify proteins under a different experimental design, or with shared peptides toggled, without paying for peak-finding again::

>>> import pymzlib
>>> proteins = pymzlib.flashlfq.median_polish(
...     "QuantifiedPeptides.tsv",
...     design=[
...         {"file_name": "run_3", "condition": "control", "biological_replicate": 0},
...         {"file_name": "run_4", "condition": "treated", "biological_replicate": 0},
...     ],
... )
>>> proteins[0].intensity("control_1")
3005.6

The returned objects are ordinary :class:ProteinGroup\ s, so their intensity semantics are the ones documented there and on :func:quantify: an intensity is None where median polish could not resolve a number (a degenerate peptide matrix), 0.0 where the protein was simply not measured in that sample.

Parameters:

Name Type Description Default
peptides str | PathLike[str]

Path to a FlashLFQ QuantifiedPeptides.tsv (the file :func:quantify writes as QuantifiedPeptides.tsv when given an output_directory). It must carry the Sequence, Base Sequence, Protein Groups and Intensity_<run> columns.

required
design Sequence[DesignInput] | None

The experimental design, one mapping per run. Each needs a file_name matching an Intensity_<file_name> column, plus any of condition, biological_replicate, technical_replicate, fraction (FlashLFQ's SpectraFileInfo names). Median polish groups measurements by condition and biological replicate, so this is how you tell it which runs are replicates of which sample — the whole reason to re-run it. If a design is given it must name every run in the table and only runs in the table. When None, each Intensity_ column becomes its own biological replicate with a blank condition — exactly what FlashLFQ assumes when it writes the file with no design.

None
use_shared_peptides bool

Let peptides shared between protein groups contribute to protein quant (FlashLFQ's UseSharedPeptidesForProteinQuant). Off by default; when off, a group with only shared peptides quantifies to 0.0.

False
output_directory str | PathLike[str] | None

If given, also write a FlashLFQ QuantifiedProteins.tsv there. Its sample labels match the keys of the returned objects' :attr:~ProteinGroup.intensities (the inverted labelling rule of smith-chem-wisc/mzLib#1128 was fixed by mzLib#1129, which this build includes), but its columns can come in a different order: the file lists conditions in first-appearance order, and the returned objects sort them. The returned list is the primary result and this file is a convenience.

None
timeout float | None

Seconds to allow; None waits indefinitely.

None

Returns:

Type Description
list[ProteinGroup]

A list of :class:ProteinGroup, ordered by protein group name. Each carries its per-sample

list[ProteinGroup]

attr:~ProteinGroup.intensities, keyed by the sample label (a run base name when no design

list[ProteinGroup]

is given, "condition_biorep" once a design groups runs).

Raises:

Type Description
UsageError

peptides is blank, the file is missing a required column or has no Intensity_ columns, or the design and the table's runs do not match.

BridgeError

the reconstruction or quantification itself failed.

Source code in pkg/python/src/pymzlib/flashlfq.py
def median_polish(
    peptides: str | os.PathLike[str],
    *,
    design: Sequence[DesignInput] | None = None,
    use_shared_peptides: bool = False,
    output_directory: str | os.PathLike[str] | None = None,
    timeout: float | None = None,
) -> list[ProteinGroup]:
    """Roll a ``QuantifiedPeptides.tsv`` up to protein intensities with FlashLFQ's median polish.

    This is the second half of :func:`quantify` on its own: given a peptide table FlashLFQ already
    wrote — its ``Intensity_<run>`` and ``Detection Type_<run>`` columns — it rebuilds the FlashLFQ
    peptide/protein object graph and runs the exact same median-polish protein quant
    (``CalculateProteinResultsMedianPolish``), without re-reading any mzML. Reach for it to
    re-quantify proteins under a different experimental design, or with shared peptides toggled,
    without paying for peak-finding again::

        >>> import pymzlib
        >>> proteins = pymzlib.flashlfq.median_polish(
        ...     "QuantifiedPeptides.tsv",
        ...     design=[
        ...         {"file_name": "run_3", "condition": "control", "biological_replicate": 0},
        ...         {"file_name": "run_4", "condition": "treated", "biological_replicate": 0},
        ...     ],
        ... )
        >>> proteins[0].intensity("control_1")
        3005.6

    The returned objects are ordinary :class:`ProteinGroup`\\ s, so their intensity semantics are the
    ones documented there and on :func:`quantify`: an intensity is ``None`` where median polish could
    not resolve a number (a degenerate peptide matrix), ``0.0`` where the protein was simply not
    measured in that sample.

    Args:
        peptides: Path to a FlashLFQ ``QuantifiedPeptides.tsv`` (the file :func:`quantify` writes as
            ``QuantifiedPeptides.tsv`` when given an ``output_directory``). It must carry the
            ``Sequence``, ``Base Sequence``, ``Protein Groups`` and ``Intensity_<run>`` columns.
        design: The experimental design, one mapping per run. Each needs a ``file_name`` matching an
            ``Intensity_<file_name>`` column, plus any of ``condition``, ``biological_replicate``,
            ``technical_replicate``, ``fraction`` (FlashLFQ's ``SpectraFileInfo`` names). Median
            polish groups measurements by condition and biological replicate, so **this is how you
            tell it which runs are replicates of which sample** — the whole reason to re-run it. If a
            design is given it must name every run in the table and only runs in the table. When
            ``None``, each ``Intensity_`` column becomes its own biological replicate with a blank
            condition — exactly what FlashLFQ assumes when it writes the file with no design.
        use_shared_peptides: Let peptides shared between protein groups contribute to protein quant
            (FlashLFQ's ``UseSharedPeptidesForProteinQuant``). Off by default; when off, a group with
            only shared peptides quantifies to ``0.0``.
        output_directory: If given, also write a FlashLFQ ``QuantifiedProteins.tsv`` there. Its
            sample labels match the keys of the returned objects' :attr:`~ProteinGroup.intensities`
            (the inverted labelling rule of smith-chem-wisc/mzLib#1128 was fixed by mzLib#1129,
            which this build includes), but its columns can come in a different order: the file
            lists conditions in first-appearance order, and the returned objects sort them. The
            returned list is the primary result and this file is a convenience.
        timeout: Seconds to allow; ``None`` waits indefinitely.

    Returns:
        A list of :class:`ProteinGroup`, ordered by protein group name. Each carries its per-sample
        :attr:`~ProteinGroup.intensities`, keyed by the sample label (a run base name when no design
        is given, ``"condition_biorep"`` once a design groups runs).

    Raises:
        UsageError: ``peptides`` is blank, the file is missing a required column or has no
            ``Intensity_`` columns, or the design and the table's runs do not match.
        BridgeError: the reconstruction or quantification itself failed.
    """
    peptides = _bridge.path_text(peptides)
    if not peptides:
        raise _bridge.UsageError("A quantified peptides file path is required, e.g. 'QuantifiedPeptides.tsv'.")

    stdin = _design_stdin(design) if design is not None else None

    args: list[str] = ["quant", "median-polish", "--peptides", peptides]
    if use_shared_peptides:
        args.append("--shared-peptides")
    if output_directory is not None:
        output_directory = _bridge.path_text(output_directory)
        if not output_directory:
            raise _bridge.UsageError("output_directory must be a non-empty path or None.")
        args += ["--out", output_directory]

    data = _bridge.invoke(*args, stdin=stdin, timeout=timeout)
    return [ProteinGroup._from_wire(g) for g in (data.get("proteins") or [])]

pymzlib.readers

readers

Read mass-spectrometry data files and proteomics search results: what a file is, what you can do with it, and its records.

Spectra files are read here too, not just search output. :func:read_spectra reads mzML, Thermo .raw, Bruker .d, timsTOF .d, MGF and msalign - scan headers always, peaks on request::

>>> import pymzlib
>>> scans = pymzlib.readers.read_spectra("sliced_ethcd.mzML", limit=3)
>>> scans.scan_count, scans.columns["retention_time"][:2]
(6, [38.92571663975, 38.92606115255])

mzLib recognises 38 file types in all - the instrument and deconvolution formats above, plus the output of a dozen search tools: MetaMorpheus, MSFragger, TopPIC, TopFD, MsPathFinderT, Crux, Casanovo, FlashDeconv, Dinosaur, DIA-NN, FlashLFQ, Pytheas, and any mzIdentML writer - and dispatches each to a parser it maintains. This module asks it what a path is::

>>> # Not run in CI: no "readers identify" recording to replay yet.
>>> info = pymzlib.readers.identify("psm.tsv")     # doctest: +SKIP
>>> info.file_type, info.views                     # doctest: +SKIP
('MsFraggerPsm', ['quantifiable'])

...and reads it, whatever it turns out to be::

>>> table = pymzlib.readers.read_records("ToppicPrsm_TopPICv1.6.2_prsm.tsv")
>>> table.record_type, len(table.column_names)
('ToppicPrsm', 36)

Every one of the 38 formats is readable - :func:read_records reads any of them. What differs between formats is not whether you can read them but what the columns mean, and that is what :attr:FileInfo.views tells you. It is tempting to describe mzLib as reading 38 formats into one uniform shape; it does not. They fall into disjoint families, and several belong to no family at all:

+---------------------+-------+------------------------+---------------------------------------+ | view | types | function | columns | +=====================+=======+========================+=======================================+ | "quantifiable" | 4 | :func:read_results | uniform: sequence, RT, charge, mass, | | | | | protein groups. What | | | | | :func:pymzlib.flashlfq.quantify | | | | | consumes. | +---------------------+-------+------------------------+---------------------------------------+ | "ms1_features" | 2 | :func:read_features | uniform: m/z, charge, RT range, | | | | | intensity, isotope count. | +---------------------+-------+------------------------+---------------------------------------+ | "spectral_match"| 6 | :func:read_matches | uniform: scan, sequences, accession, | | | | | decoy flag, modifications. | +---------------------+-------+------------------------+---------------------------------------+ | "spectra" | 7 | :func:read_spectra | uniform: scan headers, and peaks on | | | | | request. | +---------------------+-------+------------------------+---------------------------------------+ | (any) | 38 | :func:read_records | this format's own fields, under | | | | | mzLib's names. Not uniform. | +---------------------+-------+------------------------+---------------------------------------+

views == [] is a real and common answer - nineteen types have it. TopPIC, Crux, MSFragger's peptide and protein tables, the FlashDeconv formats and MetaMorpheus's protein-group and peptide tables (and their RNA counterparts) each parse into their own record type with nothing in common. mzLib reads them and so does :func:read_records; there is simply no uniform view to project them onto, and inventing one here would mean publishing a schema mzLib does not have.

So: use a typed view when you need numbers that mean the same thing across files, and :func:read_records when you need everything a format has. A .psmtsv read through :func:read_results gives 10 comparable columns; the same file through :func:read_records gives 73, including the q-values and scores the uniform view does not carry.

Call :func:formats for the whole table. It is enumerated from mzLib rather than transcribed, so it cannot drift from what mzLib actually dispatches.

Three things this module deliberately does not tell you, in the "surface it, don't hide it" spirit of the rest of pyMzLib:

  • Which tool wrote the file. mzLib has a Software property that looks like the answer and is not: readers carry their software constant on a constructor that mzLib's own file factory does not use, so the value is unset for everything the factory returns - and it is not reliably set on the other constructor either. Rather than reconstruct a plausible answer, there is no software field. :attr:FileInfo.file_type already names the tool.
  • Whether the numbers inside mean the same thing across formats. They do not, and this is the trap most likely to produce a wrong result. mzLib's result-file readers pass through whatever the tool wrote: MetaMorpheus retention times are in minutes and MSFragger's are too (mzLib PR #1116 converts them at the reader), but TopPIC's are still in seconds, and TopFD changed from seconds to minutes between v1.6.2 and v1.7.0 within the same file type. Likewise is_decoy is hardcoded False for MSFragger, which means "mzLib cannot tell" rather than "target" - MSFragger's psm.tsv carries no target/decoy column at all - so is_decoy arrives as None for that format rather than a fabricated False. monoisotopic_mass is the theoretical peptide mass in both formats, never the observed precursor mass. Identifying a file is safe; comparing raw fields across formats is not.

  • Anything about confidence. There is no q-value, PEP or score in this view, because IQuantifiableRecord carries only what FlashLFQ needs. Nothing you get back is FDR-filtered, even though every one of these files records confidence somewhere. Filter before you report.

Two quantification tables have typed functions of their own (mzLib 1.0.592, #1347). They belong to no uniform view - :func:read_records reads them too - but their per-sample values are dictionaries that :func:read_records cannot project, so these return them as long tables, one row per record per sample:

+-------------------------------------+------------------------------------------------------------+ | function | reads | +=====================================+============================================================+ | :func:read_protein_groups | MetaMorpheus AllQuantifiedProteinGroups.tsv: one row | | | per group per sample group - spectral count, intensity. | +-------------------------------------+------------------------------------------------------------+ | :func:read_quantified_peptides | FlashLFQ QuantifiedPeptides.tsv (and MetaMorpheus's | | | AllQuantifiedPeptides.tsv): one row per peptide per | | | sample - intensity, detection type. | +-------------------------------------+------------------------------------------------------------+ | :func:read_occupancy | the same protein-group table's PTM site occupancy: one row | | | per group, sample group, basis and modified site. | +-------------------------------------+------------------------------------------------------------+

Many files at once. Every read function has a _many twin - :func:read_spectra_many, :func:read_records_many, :func:identify_many and so on - that takes a list of paths and returns ONE long table, whose first two columns say which file each row came from (source_index, source_path), plus one :class:FileReport per file in :attr:ReadBatch.files. The list is read by one bridge process, threads files at a time, so two hundred runs cost one .NET start-up rather than two hundred. The answer is identical at any threads; the default is 1 because each file in flight is held whole in memory. There is deliberately no Python-side thread or process pool anywhere in this module: parallelism is the bridge's to express, once, where it can be counted.

Four ways a field can have no value, and each is named. A None in a table can mean four different things, and conflating them is how a missing column turns into a measured zero:

  • absent_fields - the function defines the field, but this file's format has no column for it: MBR Score in a current FlashLFQ peaks table (mzLib #1345), q_value in an MsPathFinderT targets file, apex intensity in a FLASHDeconv feature file. Every value in the column is None, whatever default mzLib filled in.
  • failed_fields - the field exists, but reading it threw for some rows (a UniProt-shaped accession parsed from a plain FASTA header). None in those rows.
  • excluded_fields - the field has no column shape (a dictionary, a nested object), or none without loss (an SDRF row's header and cells), so it does not cross in this function at all; each entry names the function that does carry it.
  • a None in one row, with the field in none of those lists - the value is genuinely missing for that row: the precursor of an MS1 scan, a blank intensity cell.

.. note::

That units mismatch was not hypothetical, and the fix shows where such things belong. Passing an MSFragger psm.tsv to :func:pymzlib.flashlfq.quantify used to return near-zero intensities, because FlashLFQ read the seconds as minutes and searched for each peptide about sixty times too late in the gradient. It was fixed upstream in mzLib (#1116 <https://github.com/smith-chem-wisc/mzLib/pull/1116>_, converting at the reader) rather than papered over here, so every mzLib consumer benefits and this library's caveat and retention_time_unit changed with it. That is the standing rule: a value whose meaning or availability is wrong is repaired in the core contract, and a binding discloses rather than repairs.

Format dataclass

One file type mzLib can recognise.

Attributes:

Name Type Description
file_type str

mzLib's SupportedFileType name, e.g. "MsFraggerPsm", "psmtsv".

extension Any

The extension or filename suffix mzLib dispatches on, e.g. "psm.tsv", "_ms1.feature". Not unique across file types - BrukerD and BrukerTimsTof are both .d (told apart by the directory's contents), and several formats share .tsv.

reader Any

The name of the mzLib class that parses it, for cross-referencing the mzLib source.

views list[str]

The uniform views this format supports - see the module docstring. Often empty.

Source code in pkg/python/src/pymzlib/readers.py
@dataclass(frozen=True)
class Format:
    """One file type mzLib can recognise.

    Attributes:
        file_type: mzLib's ``SupportedFileType`` name, e.g. ``"MsFraggerPsm"``, ``"psmtsv"``.
        extension: The extension or filename suffix mzLib dispatches on, e.g. ``"psm.tsv"``,
            ``"_ms1.feature"``. **Not unique across file types** - ``BrukerD`` and
            ``BrukerTimsTof`` are both ``.d`` (told apart by the directory's contents), and
            several formats share ``.tsv``.
        reader: The name of the mzLib class that parses it, for cross-referencing the mzLib source.
        views: The uniform views this format supports - see the module docstring. Often empty.
    """

    file_type: str
    extension: Any
    reader: Any
    views: list[str] = field(default_factory=list)

    @property
    def is_quantifiable(self) -> bool:
        """Whether this format offers the cross-format record view (and so feeds FlashLFQ)."""
        return QUANTIFIABLE in self.views

    @classmethod
    def _from_wire(cls, payload: dict[str, Any]) -> "Format":
        return cls(
            file_type=payload.get("file_type", ""),
            extension=payload.get("extension"),
            reader=payload.get("reader"),
            views=list(payload.get("views") or []),
        )
is_quantifiable property
is_quantifiable: bool

Whether this format offers the cross-format record view (and so feeds FlashLFQ).

ReadError dataclass

Why one file of a _many read could not be read, under on_error="skip".

Attributes:

Name Type Description
kind str

"usage" - the input was wrong: not found, not a type mzLib recognises, or not one this function reads; "correctness" - mzLib recognised the file and failed to parse it; or "service_unavailable". The same three-way split every pyMzLib error makes (:class:~pymzlib.UsageError, :class:~pymzlib.BridgeError, :class:~pymzlib.ServiceUnavailableError).

type str

What the failure would have been raised as on its own: "usage", or the .NET exception type, e.g. "MzLibException", "HeaderValidationException".

message str

mzLib's own message.

Source code in pkg/python/src/pymzlib/readers.py
@dataclass(frozen=True)
class ReadError:
    """Why one file of a ``_many`` read could not be read, under ``on_error="skip"``.

    Attributes:
        kind: ``"usage"`` - the input was wrong: not found, not a type mzLib recognises, or not one
            this function reads; ``"correctness"`` - mzLib recognised the file and failed to parse
            it; or ``"service_unavailable"``. The same three-way split every pyMzLib error makes
            (:class:`~pymzlib.UsageError`, :class:`~pymzlib.BridgeError`,
            :class:`~pymzlib.ServiceUnavailableError`).
        type: What the failure would have been raised as on its own: ``"usage"``, or the .NET
            exception type, e.g. ``"MzLibException"``, ``"HeaderValidationException"``.
        message: mzLib's own message.
    """

    kind: str
    type: str
    message: str

    @classmethod
    def _from_wire(cls, payload: dict[str, Any] | None) -> "ReadError | None":
        if not payload:
            return None
        return cls(
            kind=payload.get("kind", ""),
            type=payload.get("type", ""),
            message=payload.get("message", ""),
        )

SpectraSource dataclass

What a spectra file records about the run it came from (mzLib SourceFile, #1349).

The facts a batch or instrument confound is built from: which instrument, which unit of that model, and when. Every field is None when the file does not record it - MGF and msalign record none of them.

Attributes:

Name Type Description
instrument_model str | None

The instrument model's name, e.g. "Orbitrap Fusion Lumos". None: the file does not record it.

instrument_model_accession str | None

Its PSI-MS accession, e.g. "MS:1002416". mzML carries one; Thermo .raw records only the name, so it is None there rather than looked up. mzLib's rule is to match on the accession, never the name.

instrument_serial_number str | None

The instrument's serial number, verbatim, e.g. "FSN10189". Together with the model it tells two instruments of one model apart. Free text: a converter's placeholder such as "Serial Number N/A" is reported as written. None: the file does not record it.

acquisition_start_time str | None

When acquisition started, as ISO-8601 text, e.g. "2021-03-16T17:09:07Z". It ends in Z only when the source fixed the instant; otherwise it is the acquisition computer's wall-clock time with no offset, because the instant is unknown. None: the file does not record it.

acquisition_start_time_is_utc bool

True when :attr:acquisition_start_time is a UTC instant; False when it is local instrument-PC time (always, for Thermo .raw) or absent. A .raw and ProteoWizard's mzML of it can differ by the site's UTC offset, because ProteoWizard assumes the converting machine's time zone.

Source code in pkg/python/src/pymzlib/readers.py
@dataclass(frozen=True)
class SpectraSource:
    """What a spectra file records about the run it came from (mzLib ``SourceFile``, #1349).

    The facts a batch or instrument confound is built from: which instrument, which unit of that
    model, and when. Every field is ``None`` when the file does not record it - MGF and msalign
    record none of them.

    Attributes:
        instrument_model: The instrument model's name, e.g. ``"Orbitrap Fusion Lumos"``. ``None``:
            the file does not record it.
        instrument_model_accession: Its PSI-MS accession, e.g. ``"MS:1002416"``. mzML carries one;
            Thermo ``.raw`` records only the name, so it is ``None`` there rather than looked up.
            mzLib's rule is to **match on the accession, never the name**.
        instrument_serial_number: The instrument's serial number, verbatim, e.g. ``"FSN10189"``.
            Together with the model it tells two instruments of one model apart. Free text: a
            converter's placeholder such as ``"Serial Number N/A"`` is reported as written.
            ``None``: the file does not record it.
        acquisition_start_time: When acquisition started, as ISO-8601 text, e.g.
            ``"2021-03-16T17:09:07Z"``. It ends in ``Z`` only when the source fixed the instant;
            otherwise it is the acquisition computer's wall-clock time with no offset, because the
            instant is unknown. ``None``: the file does not record it.
        acquisition_start_time_is_utc: ``True`` when :attr:`acquisition_start_time` is a UTC
            instant; ``False`` when it is local instrument-PC time (always, for Thermo ``.raw``)
            or absent. A ``.raw`` and ProteoWizard's mzML of it can differ by the site's UTC
            offset, because ProteoWizard assumes the converting machine's time zone.
    """

    instrument_model: str | None = None
    instrument_model_accession: str | None = None
    instrument_serial_number: str | None = None
    acquisition_start_time: str | None = None
    acquisition_start_time_is_utc: bool = False

    @property
    def acquired_at(self) -> Any:
        """:attr:`acquisition_start_time` as a :class:`datetime.datetime`, or ``None``.

        Timezone-aware (UTC) when :attr:`acquisition_start_time_is_utc`, and **naive** otherwise:
        a local clock reading with no known offset must not be given one.
        """
        if not self.acquisition_start_time:
            return None
        import datetime

        text = self.acquisition_start_time
        if text.endswith("Z"):
            text = text[:-1] + "+00:00"
        return datetime.datetime.fromisoformat(text)

    @classmethod
    def _from_wire(cls, payload: dict[str, Any] | None) -> "SpectraSource | None":
        if not payload:
            return None
        return cls(
            instrument_model=payload.get("instrument_model"),
            instrument_model_accession=payload.get("instrument_model_accession"),
            instrument_serial_number=payload.get("instrument_serial_number"),
            acquisition_start_time=payload.get("acquisition_start_time"),
            acquisition_start_time_is_utc=bool(payload.get("acquisition_start_time_is_utc", False)),
        )
acquired_at property
acquired_at: Any

:attr:acquisition_start_time as a :class:datetime.datetime, or None.

Timezone-aware (UTC) when :attr:acquisition_start_time_is_utc, and naive otherwise: a local clock reading with no known offset must not be given one.

SkippedMatch dataclass

An mzIdentML identification item mzLib did not turn into a row, and why (mzLib #1313).

Attributes:

Name Type Description
spectrum_identification_item_id str

The item's id in the document.

spectrum_id str

The spectrum's nativeID, as written.

reason str

Why it was skipped, e.g. "crosslink identification", or a modification that does not resolve to a Unimod entry.

Source code in pkg/python/src/pymzlib/readers.py
@dataclass(frozen=True)
class SkippedMatch:
    """An mzIdentML identification item mzLib did not turn into a row, and why (mzLib #1313).

    Attributes:
        spectrum_identification_item_id: The item's ``id`` in the document.
        spectrum_id: The spectrum's nativeID, as written.
        reason: Why it was skipped, e.g. ``"crosslink identification"``, or a modification that
            does not resolve to a Unimod entry.
    """

    spectrum_identification_item_id: str
    spectrum_id: str
    reason: str

    @classmethod
    def _from_wire(cls, payload: dict[str, Any]) -> "SkippedMatch":
        return cls(
            spectrum_identification_item_id=payload.get("spectrum_identification_item_id", ""),
            spectrum_id=payload.get("spectrum_id", ""),
            reason=payload.get("reason", ""),
        )

FileReport dataclass

The per-file facts of one input to a _many read: one entry of :attr:ReadBatch.files.

One per input, in input order; a file's rows in the batch table are those whose source_index is its position. The fields are the ones a single-file read reports at its top level, under the same names, so what you learn about one reads the same for many.

The fields every reader reports come first; the rest belong to particular readers and are None elsewhere. For an input that failed under on_error="skip", :attr:error says why and the facts that could not be established are None or empty.

Attributes:

Name Type Description
path str

The absolute path of the input.

file_type str | None

mzLib's SupportedFileType name. None: the type could not be determined (a failed input).

reader str | None

The mzLib class that parsed the file. None: a failed input.

record_count int | None

Records mzLib read from the whole file, in the reader's own unit - matches, scans passing ms_order, groups, peptides. None: a failed input - not zero, because nothing was counted.

rows_not_read int | None

Data lines that did not become records, counted where one line is one record (the psmtsv family and MSFragger); mzLib drops a malformed line silently. None: not established for this format, or a failed input.

retention_time_unit str | None

The unit of the reader's time columns for this file: "minutes", "seconds" or "unknown". None: the reader has no time column, or it is :func:read_records_many, whose columns are the format's own.

caveats list[str]

What the reader cannot be trusted to mean for this file, each citing the mzLib source.

column_names list[str]

This file's columns, without the source_index/source_path pair the batch table adds.

absent_fields list[str]

Columns the reader defines but this file has no source for - its format has no such column - so every value in them is None. See the module docstring's "four ways a field can have no value".

failed_fields list[str]

"field: ExceptionType" for each column whose read threw on some rows; those cells are None.

excluded_fields list[dict[str, Any]]

Fields that have no column shape and so do not cross, each as {field, type, reason, verb}, where verb names the bridge command that does carry it (None if none does).

error ReadError | None

Why this input could not be read, under on_error="skip"; None when it was.

record_type str | None

:func:read_records_many only - the mzLib record class the columns belong to.

views list[str] | None

:func:read_records_many only - the uniform views this file also offers.

scan_count int | None

:func:read_spectra_many only - scans in the whole file, before ms_order.

source SpectraSource | None

:func:read_spectra_many only - the instrument and acquisition facts.

skipped_count int | None

mzIdentML items mzLib did not represent (:func:read_matches_many, :func:read_records_many). None: the format has no such list.

skipped list[SkippedMatch] | None

The same items, each a :class:SkippedMatch with its reason.

sample_labels list[str] | None

The quantification readers only - the file's sample labels, in header order.

truncated_cell_count int | None

:func:read_occupancy_many only - occupancy cells the writer cut short.

Source code in pkg/python/src/pymzlib/readers.py
@dataclass(frozen=True)
class FileReport:
    """The per-file facts of one input to a ``_many`` read: one entry of :attr:`ReadBatch.files`.

    One per input, **in input order**; a file's rows in the batch table are those whose
    ``source_index`` is its position. The fields are the ones a single-file read reports at its top
    level, under the same names, so what you learn about one reads the same for many.

    The fields every reader reports come first; the rest belong to particular readers and are
    ``None`` elsewhere. For an input that failed under ``on_error="skip"``, :attr:`error` says why
    and the facts that could not be established are ``None`` or empty.

    Attributes:
        path: The absolute path of the input.
        file_type: mzLib's ``SupportedFileType`` name. ``None``: the type could not be determined
            (a failed input).
        reader: The mzLib class that parsed the file. ``None``: a failed input.
        record_count: Records mzLib read from the whole file, in the reader's own unit - matches,
            scans passing ``ms_order``, groups, peptides. ``None``: a failed input - not zero,
            because nothing was counted.
        rows_not_read: Data lines that did not become records, counted where one line is one
            record (the psmtsv family and MSFragger); mzLib drops a malformed line silently.
            ``None``: not established for this format, or a failed input.
        retention_time_unit: The unit of the reader's time columns for this file: ``"minutes"``,
            ``"seconds"`` or ``"unknown"``. ``None``: the reader has no time column, or it is
            :func:`read_records_many`, whose columns are the format's own.
        caveats: What the reader cannot be trusted to mean for this file, each citing the mzLib
            source.
        column_names: This file's columns, without the ``source_index``/``source_path`` pair the
            batch table adds.
        absent_fields: Columns the reader defines but **this file has no source for** - its
            format has no such column - so every value in them is ``None``. See the module
            docstring's "four ways a field can have no value".
        failed_fields: ``"field: ExceptionType"`` for each column whose read threw on some rows;
            those cells are ``None``.
        excluded_fields: Fields that have no column shape and so do not cross, each as
            ``{field, type, reason, verb}``, where ``verb`` names the bridge command that does carry
            it (``None`` if none does).
        error: Why this input could not be read, under ``on_error="skip"``; ``None`` when it was.
        record_type: :func:`read_records_many` only - the mzLib record class the columns belong to.
        views: :func:`read_records_many` only - the uniform views this file also offers.
        scan_count: :func:`read_spectra_many` only - scans in the whole file, before ``ms_order``.
        source: :func:`read_spectra_many` only - the instrument and acquisition facts.
        skipped_count: mzIdentML items mzLib did not represent (:func:`read_matches_many`,
            :func:`read_records_many`). ``None``: the format has no such list.
        skipped: The same items, each a :class:`SkippedMatch` with its reason.
        sample_labels: The quantification readers only - the file's sample labels, in header
            order.
        truncated_cell_count: :func:`read_occupancy_many` only - occupancy cells the writer cut
            short.
    """

    path: str
    file_type: str | None = None
    reader: str | None = None
    record_count: int | None = None
    rows_not_read: int | None = None
    retention_time_unit: str | None = None
    caveats: list[str] = field(default_factory=list)
    column_names: list[str] = field(default_factory=list)
    absent_fields: list[str] = field(default_factory=list)
    failed_fields: list[str] = field(default_factory=list)
    excluded_fields: list[dict[str, Any]] = field(default_factory=list)
    error: ReadError | None = None
    record_type: str | None = None
    views: list[str] | None = None
    scan_count: int | None = None
    source: SpectraSource | None = None
    skipped_count: int | None = None
    skipped: list[SkippedMatch] | None = None
    sample_labels: list[str] | None = None
    truncated_cell_count: int | None = None

    @property
    def ok(self) -> bool:
        """Whether this input was read (``error`` is ``None``)."""
        return self.error is None

    @classmethod
    def _from_wire(cls, data: dict[str, Any]) -> "FileReport":
        count = data.get("record_count")
        views = data.get("views")
        labels = data.get("sample_labels")
        return cls(
            path=data.get("path", ""),
            file_type=data.get("file_type"),
            reader=data.get("reader"),
            record_count=None if count is None else int(count),
            rows_not_read=data.get("rows_not_read"),
            retention_time_unit=data.get("retention_time_unit"),
            caveats=list(data.get("caveats") or []),
            column_names=list(data.get("column_names") or []),
            absent_fields=list(data.get("absent_fields") or []),
            failed_fields=list(data.get("failed_fields") or []),
            excluded_fields=list(data.get("excluded_fields") or []),
            error=ReadError._from_wire(data.get("error")),
            record_type=data.get("record_type"),
            views=None if views is None else list(views),
            scan_count=data.get("scan_count"),
            source=SpectraSource._from_wire(data.get("source")),
            skipped_count=data.get("skipped_count"),
            skipped=_skipped(data.get("skipped")),
            sample_labels=None if labels is None else list(labels),
            truncated_cell_count=data.get("truncated_cell_count"),
        )
ok property
ok: bool

Whether this input was read (error is None).

FileInfo dataclass

What a particular file is, and what can be done with it.

Attributes:

Name Type Description
path str

The absolute path that was identified.

file_type str

mzLib's SupportedFileType name. Empty for an input of :func:identify_many that failed under on_error="skip".

extension Any

The extension mzLib dispatched on. None for a failed input.

reader Any

The mzLib class that would parse it. None for a failed input.

views list[str]

The uniform views this file supports - see the module docstring. Often empty, which means mzLib can read the file but offers no cross-format projection of it.

error ReadError | None

Always None from :func:identify, which raises instead. From :func:identify_many with on_error="skip", why this file could not be identified.

Source code in pkg/python/src/pymzlib/readers.py
@dataclass(frozen=True)
class FileInfo:
    """What a particular file is, and what can be done with it.

    Attributes:
        path: The absolute path that was identified.
        file_type: mzLib's ``SupportedFileType`` name. Empty for an input of
            :func:`identify_many` that failed under ``on_error="skip"``.
        extension: The extension mzLib dispatched on. ``None`` for a failed input.
        reader: The mzLib class that would parse it. ``None`` for a failed input.
        views: The uniform views this file supports - see the module docstring. Often empty, which
            means mzLib can read the file but offers no cross-format projection of it.
        error: Always ``None`` from :func:`identify`, which raises instead. From
            :func:`identify_many` with ``on_error="skip"``, why this file could not be identified.
    """

    path: str
    file_type: str
    extension: Any
    reader: Any
    views: list[str] = field(default_factory=list)
    error: ReadError | None = None

    @property
    def is_quantifiable(self) -> bool:
        """Whether this file offers the cross-format record view.

        When ``True``, the path can be passed straight to :func:`pymzlib.flashlfq.quantify` as
        ``psms``. When ``False``, mzLib can still read the file - it simply has no uniform view,
        so quantification would fail on it.
        """
        return QUANTIFIABLE in self.views

    @classmethod
    def _from_wire(cls, data: dict[str, Any]) -> "FileInfo":
        return cls(
            path=data.get("path", ""),
            file_type=data.get("file_type") or "",
            extension=data.get("extension"),
            reader=data.get("reader"),
            views=list(data.get("views") or []),
            error=ReadError._from_wire(data.get("error")),
        )
is_quantifiable property
is_quantifiable: bool

Whether this file offers the cross-format record view.

When True, the path can be passed straight to :func:pymzlib.flashlfq.quantify as psms. When False, mzLib can still read the file - it simply has no uniform view, so quantification would fail on it.

WrittenTable dataclass

Where a read wrote its table, when asked to write one instead of returning it.

Attributes:

Name Type Description
path str

The absolute path written.

format str

Always "tsv". Tab-separated, not comma-separated, because these fields contain commas - MSFragger's mapped proteins are a comma-separated list inside a single field, and joined accessions look the same. It is also what every mzLib reader and writer uses. Read it with csv.reader(f, delimiter="\t") or pandas.read_csv(path, sep="\t").

row_count int

Rows written, excluding the header - for a _many read, every file's rows.

Source code in pkg/python/src/pymzlib/readers.py
@dataclass(frozen=True)
class WrittenTable:
    """Where a read wrote its table, when asked to write one instead of returning it.

    Attributes:
        path: The absolute path written.
        format: Always ``"tsv"``. **Tab-separated, not comma-separated**, because these fields
            contain commas - MSFragger's mapped proteins are a comma-separated list inside a single
            field, and joined accessions look the same. It is also what every mzLib reader and
            writer uses. Read it with ``csv.reader(f, delimiter="\\t")`` or
            ``pandas.read_csv(path, sep="\\t")``.
        row_count: Rows written, excluding the header - for a ``_many`` read, every file's rows.
    """

    path: str
    format: str
    row_count: int

    @classmethod
    def _from_wire(cls, payload: dict[str, Any]) -> "WrittenTable":
        return cls(
            path=payload.get("path", ""),
            format=payload.get("format", ""),
            row_count=int(payload.get("row_count", 0)),
        )

ResultRecords dataclass

Bases: _Table

The uniform record view of a result file.

Attributes:

Name Type Description
path str

The absolute path that was read.

file_type str

mzLib's SupportedFileType name.

record_count int

Records in the whole file, regardless of limit or offset.

returned_count int

Records actually carried back in :attr:columns. Zero when out was given, since the table went to disk instead.

offset int

Records skipped: the offset that was applied.

truncated bool

Whether records were left behind, by either limit or offset. A short answer and a complete one must never look alike, so check this rather than assuming.

retention_time_unit str

The unit :attr:columns' retention_time carries for this format - "minutes", "seconds", or "unknown". mzLib does not normalise it, so this differs per format and you must convert before comparing two files. Provided as a value so you can convert programmatically instead of hard-coding a table.

rows_not_read Any

Data rows in the file that did not become records - mzLib drops a malformed row silently, so a non-zero value here means the file is partly unreadable and the table is incomplete. None when the count could not be established meaningfully.

caveats list[str]

What the uniform view cannot be trusted to mean for this format. Empty for some formats, not for others; each entry cites the mzLib source it came from. Worth printing before comparing anything across formats - this is where you learn that, e.g., TopPIC retention times are seconds while MetaMorpheus's and MSFragger's are minutes.

column_names list[str]

The field names, in order.

columns Any

Field name -> list of values, one entry per record - the shape pandas.DataFrame and polars.DataFrame both accept directly. None when out was given.

output Any

Where the table was written, or None if it was returned inline.

reader Any

The mzLib class that parsed the file, e.g. "PsmFromTsvFile".

absent_fields list[str]

Columns this view defines but this file's format has no column for, so every value is None: ["is_decoy"] for MSFragger and DIA-NN, which write no target/decoy label. See the module docstring's "four ways a field can have no value".

failed_fields list[str]

"field: ExceptionType" for each column whose read threw on some rows; those cells are None.

excluded_fields list[dict[str, Any]]

Fields with no column shape. Always empty for this view.

error ReadError | None

Always None: a file that cannot be read raises instead. Present because it is part of the per-file facts every reader reports (see :class:FileReport).

Source code in pkg/python/src/pymzlib/readers.py
@dataclass(frozen=True)
class ResultRecords(_Table):
    """The uniform record view of a result file.

    Attributes:
        path: The absolute path that was read.
        file_type: mzLib's ``SupportedFileType`` name.
        record_count: Records in the **whole file**, regardless of ``limit`` or ``offset``.
        returned_count: Records actually carried back in :attr:`columns`. Zero when ``out`` was
            given, since the table went to disk instead.
        offset: Records skipped: the offset that was applied.
        truncated: **Whether records were left behind**, by either ``limit`` or ``offset``. A short
            answer and a complete one must never look alike, so check this rather than assuming.
        retention_time_unit: The unit :attr:`columns`' ``retention_time`` carries for this format -
            ``"minutes"``, ``"seconds"``, or ``"unknown"``. mzLib does not normalise it, so this
            differs per format and you must convert before comparing two files. Provided as a value
            so you can convert programmatically instead of hard-coding a table.
        rows_not_read: Data rows in the file that did not become records - mzLib drops a malformed
            row silently, so a non-zero value here means the file is partly unreadable and the
            table is incomplete. ``None`` when the count could not be established meaningfully.
        caveats: **What the uniform view cannot be trusted to mean for this format.** Empty for
            some formats, not for others; each entry cites the mzLib source it came from. Worth
            printing before comparing anything across formats - this is where you learn that, e.g.,
            TopPIC retention times are seconds while MetaMorpheus's and MSFragger's are minutes.
        column_names: The field names, in order.
        columns: Field name -> list of values, one entry per record - the shape ``pandas.DataFrame``
            and ``polars.DataFrame`` both accept directly. ``None`` when ``out`` was given.
        output: Where the table was written, or ``None`` if it was returned inline.
        reader: The mzLib class that parsed the file, e.g. ``"PsmFromTsvFile"``.
        absent_fields: Columns this view defines but **this file's format has no column for**, so
            every value is ``None``: ``["is_decoy"]`` for MSFragger and DIA-NN, which write no
            target/decoy label. See the module docstring's "four ways a field can have no value".
        failed_fields: ``"field: ExceptionType"`` for each column whose read threw on some rows;
            those cells are ``None``.
        excluded_fields: Fields with no column shape. Always empty for this view.
        error: Always ``None``: a file that cannot be read raises instead. Present because it is
            part of the per-file facts every reader reports (see :class:`FileReport`).
    """

    path: str
    file_type: str
    record_count: int
    returned_count: int
    offset: int
    truncated: bool
    retention_time_unit: str
    rows_not_read: Any
    caveats: list[str] = field(default_factory=list)
    column_names: list[str] = field(default_factory=list)
    columns: Any = None
    output: Any = None
    reader: Any = None
    absent_fields: list[str] = field(default_factory=list)
    failed_fields: list[str] = field(default_factory=list)
    excluded_fields: list[dict[str, Any]] = field(default_factory=list)
    error: ReadError | None = None

    @property
    def retention_time_in_minutes(self) -> list[Any]:
        """``retention_time`` converted to minutes, whatever unit the format wrote.

        The conversion you would otherwise write by hand, using
        :attr:`retention_time_unit`. Raises if the unit is ``"unknown"`` rather than guessing -
        a silently unconverted axis is the specific mistake this module exists to prevent.
        """
        return self._to_minutes("read_results", "retention_time")

    @classmethod
    def _from_wire(cls, data: dict[str, Any]) -> "ResultRecords":
        return cls(
            path=data.get("path", ""),
            file_type=data.get("file_type", ""),
            record_count=int(data.get("record_count", 0)),
            returned_count=int(data.get("returned_count", 0)),
            offset=int(data.get("offset", 0)),
            truncated=bool(data.get("truncated", False)),
            retention_time_unit=data.get("retention_time_unit") or "unknown",
            rows_not_read=data.get("rows_not_read"),
            caveats=list(data.get("caveats") or []),
            column_names=list(data.get("column_names") or []),
            columns=data.get("columns"),
            output=_output(data),
            **_block(data),
        )
retention_time_in_minutes property
retention_time_in_minutes: list[Any]

retention_time converted to minutes, whatever unit the format wrote.

The conversion you would otherwise write by hand, using :attr:retention_time_unit. Raises if the unit is "unknown" rather than guessing - a silently unconverted axis is the specific mistake this module exists to prevent.

NativeRecords dataclass

Bases: _Table

A result file read into its own fields, whatever format it is.

What :func:read_records returns. Unlike every other result type in this module, the columns here are not uniform: they are the fields of this format's own mzLib record type, under mzLib's own names in snake_case. A TopPIC file gives you TopPIC's thirty-six columns; a Crux file gives you Crux's twenty-three. Always read :attr:column_names rather than assuming.

Attributes:

Name Type Description
path str

The absolute path that was read.

file_type str

mzLib's SupportedFileType name.

reader Any

The mzLib class that parsed it.

record_type str

The mzLib record class the columns came from, e.g. "ToppicPrsm". Cross- reference it against the mzLib source to find out what a column means.

record_count int

Records in the whole file, regardless of limit or offset.

returned_count int

Records carried back in :attr:columns. Zero when out was given.

offset int

Records skipped: the offset that was applied.

truncated bool

Whether records were left behind, by either limit or offset.

views list[str]

The uniform views this file also supports, if any - see the module docstring.

excluded_fields list[dict[str, Any]]

Fields of the record type that could not become columns, each as {field, type, reason, verb}. Nested objects and dictionaries have no faithful column shape, and inventing one would mean publishing a schema mzLib does not have. Listed rather than dropped, so an absent column is never mistaken for an absent field; verb names the bridge command that does carry it - sample_groups points at readers read-protein-groups (:func:read_protein_groups), mzIdentML's scores at readers read-matches (:func:read_matches with scores=True). An SDRF row's header and cells are here too, with the reason "use sdrf read": SDRF cells contain ; themselves, so a ;-joined list could not be split back. Read an SDRF with :func:pymzlib.sdrf.read.

failed_fields list[str]

Fields that raised while being read, with the exception type. Several mzLib properties are computed and assume a UniProt-style FASTA header - Crux's and MsPathFinderT's accession are protein_id.split("|")[1] - so on other databases they throw. Those cells arrive None instead of taking the whole read down, but a failure must not look like missing data, so it is named here.

absent_fields list[str]

Columns this file has no source for: the record type reads them from an optional column the file does not have, so every value is mzLib's default, and is None here. ["mbr_score"] on a current FlashLFQ peaks table (mzLib #1345); ["q_value", "pep_q_value"] on an MsPathFinderT targets file, where mzLib would otherwise report a q-value of 0 for every row.

column_names list[str]

The field names, in order - base-class fields first, then declared ones.

columns Any

Field name -> list of values, one entry per record. None when out was given.

output Any

Where the table was written, or None if it was returned inline.

rows_not_read Any

Data rows that did not become records, counted where one line is one record (the psmtsv family and MSFragger), since mzLib drops a malformed line silently. None: not established for this format.

retention_time_unit Any

Always None: the columns are this format's own, and several formats carry more than one time column in different units. Use a typed view when units matter.

caveats list[str]

Always empty for this function.

skipped_count int | None

mzIdentML only - identification items mzLib did not represent as records (crosslinks, unresolvable modifications, substitutions), so record_count + skipped_count is the items in the file. None for every other format, which keeps no such list.

skipped list[SkippedMatch] | None

The same items, each a :class:SkippedMatch with its reason. None with :attr:skipped_count.

error ReadError | None

Always None: a file that cannot be read raises instead.

Source code in pkg/python/src/pymzlib/readers.py
@dataclass(frozen=True)
class NativeRecords(_Table):
    """A result file read into **its own** fields, whatever format it is.

    What :func:`read_records` returns. Unlike every other result type in this module, the columns
    here are **not uniform**: they are the fields of this format's own mzLib record type, under
    mzLib's own names in snake_case. A TopPIC file gives you TopPIC's thirty-six columns; a Crux
    file gives you Crux's twenty-three. Always read :attr:`column_names` rather than assuming.

    Attributes:
        path: The absolute path that was read.
        file_type: mzLib's ``SupportedFileType`` name.
        reader: The mzLib class that parsed it.
        record_type: The mzLib record class the columns came from, e.g. ``"ToppicPrsm"``. Cross-
            reference it against the mzLib source to find out what a column means.
        record_count: Records in the **whole file**, regardless of ``limit`` or ``offset``.
        returned_count: Records carried back in :attr:`columns`. Zero when ``out`` was given.
        offset: Records skipped: the offset that was applied.
        truncated: Whether records were left behind, by either ``limit`` or ``offset``.
        views: The uniform views this file *also* supports, if any - see the module docstring.
        excluded_fields: **Fields of the record type that could not become columns**, each as
            ``{field, type, reason, verb}``. Nested objects and dictionaries have no faithful
            column shape, and inventing one would mean publishing a schema mzLib does not have.
            Listed rather than dropped, so an absent column is never mistaken for an absent field;
            ``verb`` names the bridge command that does carry it - ``sample_groups`` points at
            ``readers read-protein-groups`` (:func:`read_protein_groups`), mzIdentML's ``scores``
            at ``readers read-matches`` (:func:`read_matches` with ``scores=True``). An SDRF row's
            ``header`` and ``cells`` are here too, with the reason ``"use sdrf read"``: SDRF cells
            contain ``;`` themselves, so a ``;``-joined list could not be split back. Read an SDRF
            with :func:`pymzlib.sdrf.read`.
        failed_fields: Fields that **raised** while being read, with the exception type. Several
            mzLib properties are computed and assume a UniProt-style FASTA header - Crux's and
            MsPathFinderT's ``accession`` are ``protein_id.split("|")[1]`` - so on other databases
            they throw. Those cells arrive ``None`` instead of taking the whole read down, but a
            failure must not look like missing data, so it is named here.
        absent_fields: Columns **this file has no source for**: the record type reads them from an
            optional column the file does not have, so every value is mzLib's default, and is
            ``None`` here. ``["mbr_score"]`` on a current FlashLFQ peaks table (mzLib #1345);
            ``["q_value", "pep_q_value"]`` on an MsPathFinderT targets file, where mzLib would
            otherwise report a q-value of 0 for every row.
        column_names: The field names, in order - base-class fields first, then declared ones.
        columns: Field name -> list of values, one entry per record. ``None`` when ``out`` was given.
        output: Where the table was written, or ``None`` if it was returned inline.
        rows_not_read: Data rows that did not become records, counted where one line is one
            record (the psmtsv family and MSFragger), since mzLib drops a malformed line silently.
            ``None``: not established for this format.
        retention_time_unit: Always ``None``: the columns are this format's own, and several
            formats carry more than one time column in different units. Use a typed view when
            units matter.
        caveats: Always empty for this function.
        skipped_count: mzIdentML only - identification items mzLib did not represent as records
            (crosslinks, unresolvable modifications, substitutions), so ``record_count +
            skipped_count`` is the items in the file. ``None`` for every other format, which keeps
            no such list.
        skipped: The same items, each a :class:`SkippedMatch` with its reason. ``None`` with
            :attr:`skipped_count`.
        error: Always ``None``: a file that cannot be read raises instead.
    """

    path: str
    file_type: str
    reader: Any
    record_type: str
    record_count: int
    returned_count: int
    offset: int
    truncated: bool
    views: list[str] = field(default_factory=list)
    excluded_fields: list[dict[str, Any]] = field(default_factory=list)
    failed_fields: list[str] = field(default_factory=list)
    column_names: list[str] = field(default_factory=list)
    columns: Any = None
    output: Any = None
    absent_fields: list[str] = field(default_factory=list)
    rows_not_read: Any = None
    retention_time_unit: Any = None
    caveats: list[str] = field(default_factory=list)
    skipped_count: int | None = None
    skipped: list[SkippedMatch] | None = None
    error: ReadError | None = None

    @classmethod
    def _from_wire(cls, data: dict[str, Any]) -> "NativeRecords":
        return cls(
            path=data.get("path", ""),
            file_type=data.get("file_type", ""),
            reader=data.get("reader"),
            record_type=data.get("record_type", ""),
            record_count=int(data.get("record_count", 0)),
            returned_count=int(data.get("returned_count", 0)),
            offset=int(data.get("offset", 0)),
            truncated=bool(data.get("truncated", False)),
            views=list(data.get("views") or []),
            excluded_fields=list(data.get("excluded_fields") or []),
            failed_fields=list(data.get("failed_fields") or []),
            column_names=list(data.get("column_names") or []),
            columns=data.get("columns"),
            output=_output(data),
            absent_fields=list(data.get("absent_fields") or []),
            rows_not_read=data.get("rows_not_read"),
            retention_time_unit=data.get("retention_time_unit"),
            caveats=list(data.get("caveats") or []),
            skipped_count=data.get("skipped_count"),
            skipped=_skipped(data.get("skipped")),
            error=ReadError._from_wire(data.get("error")),
        )

FeatureRecords dataclass

Bases: _Table

Deconvolved MS1 features, in the cross-format ms1_features view.

What :func:read_features returns. Columns are mz, charge, retention_time_start, retention_time_end, intensity and number_of_isotopes - the same for every format that offers the view, so they are comparable across files, subject to :attr:retention_time_unit.

Attributes:

Name Type Description
path str

The absolute path that was read.

file_type str

mzLib's SupportedFileType name.

record_count int

Features in the whole file. For _ms1.feature this exceeds the file's line count, because mzLib expands each deconvolved feature into one row per charge state. See :attr:caveats.

returned_count int

Features carried back in :attr:columns. Zero when out was given.

offset int

Features skipped: the offset that was applied.

truncated bool

Whether features were left behind, by either limit or offset.

retention_time_unit str

"minutes", "seconds", or "unknown". It is genuinely "unknown" for _ms1.feature: TopFD wrote seconds through v1.6.2 and minutes from v1.7.0 without changing the file type. That is not a gap in this library; it is the honest state of the format.

caveats list[str]

What this view cannot be trusted to mean for this format, each citing the mzLib source it came from.

column_names list[str]

The field names, in order.

columns Any

Field name -> list of values. None when out was given.

output Any

Where the table was written, or None.

reader Any

The mzLib class that parsed the file, e.g. "Ms1FeatureFile".

rows_not_read Any

Always None: rows that did not become records are not counted for this view, because one line of an _ms1.feature is not one feature.

absent_fields list[str]

Columns this file has no source for, so every value is None: number_of_isotopes for every _ms1.feature (mzLib never sets it there), and intensity for a FLASHDeconv file, whose schema has no apex intensity - mzLib would otherwise report zero for every feature.

failed_fields list[str]

"field: ExceptionType" for each column whose read threw on some rows.

excluded_fields list[dict[str, Any]]

Always empty for this view.

error ReadError | None

Always None: a file that cannot be read raises instead.

Source code in pkg/python/src/pymzlib/readers.py
@dataclass(frozen=True)
class FeatureRecords(_Table):
    """Deconvolved MS1 features, in the cross-format ``ms1_features`` view.

    What :func:`read_features` returns. Columns are ``mz``, ``charge``, ``retention_time_start``,
    ``retention_time_end``, ``intensity`` and ``number_of_isotopes`` - the same for every format
    that offers the view, so they *are* comparable across files, subject to
    :attr:`retention_time_unit`.

    Attributes:
        path: The absolute path that was read.
        file_type: mzLib's ``SupportedFileType`` name.
        record_count: Features in the whole file. **For ``_ms1.feature`` this exceeds the file's
            line count**, because mzLib expands each deconvolved feature into one row per charge
            state. See :attr:`caveats`.
        returned_count: Features carried back in :attr:`columns`. Zero when ``out`` was given.
        offset: Features skipped: the offset that was applied.
        truncated: Whether features were left behind, by either ``limit`` or ``offset``.
        retention_time_unit: ``"minutes"``, ``"seconds"``, or ``"unknown"``. **It is genuinely
            ``"unknown"`` for ``_ms1.feature``**: TopFD wrote seconds through v1.6.2 and minutes
            from v1.7.0 without changing the file type. That is not a gap in this library; it is
            the honest state of the format.
        caveats: What this view cannot be trusted to mean for this format, each citing the mzLib
            source it came from.
        column_names: The field names, in order.
        columns: Field name -> list of values. ``None`` when ``out`` was given.
        output: Where the table was written, or ``None``.
        reader: The mzLib class that parsed the file, e.g. ``"Ms1FeatureFile"``.
        rows_not_read: Always ``None``: rows that did not become records are not counted for this
            view, because one line of an ``_ms1.feature`` is not one feature.
        absent_fields: Columns this file has no source for, so every value is ``None``:
            ``number_of_isotopes`` for every ``_ms1.feature`` (mzLib never sets it there), and
            ``intensity`` for a FLASHDeconv file, whose schema has no apex intensity - mzLib would
            otherwise report zero for every feature.
        failed_fields: ``"field: ExceptionType"`` for each column whose read threw on some rows.
        excluded_fields: Always empty for this view.
        error: Always ``None``: a file that cannot be read raises instead.
    """

    path: str
    file_type: str
    record_count: int
    returned_count: int
    offset: int
    truncated: bool
    retention_time_unit: str
    caveats: list[str] = field(default_factory=list)
    column_names: list[str] = field(default_factory=list)
    columns: Any = None
    output: Any = None
    reader: Any = None
    rows_not_read: Any = None
    absent_fields: list[str] = field(default_factory=list)
    failed_fields: list[str] = field(default_factory=list)
    excluded_fields: list[dict[str, Any]] = field(default_factory=list)
    error: ReadError | None = None

    @property
    def retention_time_start_in_minutes(self) -> list[Any]:
        """``retention_time_start`` in minutes, or a raised error if the unit is unknown.

        Raises rather than guessing. For ``_ms1.feature`` the unit genuinely is unknown, and a
        silently unconverted time axis is the specific mistake this module exists to prevent -
        mzLib's own deconvolution code guesses here, and this will not.
        """
        return self._to_minutes("read_features", "retention_time_start")

    @property
    def retention_time_end_in_minutes(self) -> list[Any]:
        """``retention_time_end`` in minutes, or a raised error if the unit is unknown."""
        return self._to_minutes("read_features", "retention_time_end")

    @classmethod
    def _from_wire(cls, data: dict[str, Any]) -> "FeatureRecords":
        return cls(
            path=data.get("path", ""),
            file_type=data.get("file_type", ""),
            record_count=int(data.get("record_count", 0)),
            returned_count=int(data.get("returned_count", 0)),
            offset=int(data.get("offset", 0)),
            truncated=bool(data.get("truncated", False)),
            retention_time_unit=data.get("retention_time_unit") or "unknown",
            caveats=list(data.get("caveats") or []),
            column_names=list(data.get("column_names") or []),
            columns=data.get("columns"),
            output=_output(data),
            rows_not_read=data.get("rows_not_read"),
            **_block(data),
        )
retention_time_start_in_minutes property
retention_time_start_in_minutes: list[Any]

retention_time_start in minutes, or a raised error if the unit is unknown.

Raises rather than guessing. For _ms1.feature the unit genuinely is unknown, and a silently unconverted time axis is the specific mistake this module exists to prevent - mzLib's own deconvolution code guesses here, and this will not.

retention_time_end_in_minutes property
retention_time_end_in_minutes: list[Any]

retention_time_end in minutes, or a raised error if the unit is unknown.

MatchRecords dataclass

Bases: _Table

Identifications, in the cross-format spectral_match view.

What :func:read_matches returns. Columns are file_name_without_extension, one_based_scan_number, base_sequence, full_sequence, accession, is_decoy, modifications, modification_count, q_value, rank and pass_threshold - and, with scores=True, match_index, score_name and score_value.

Nothing here is FDR-filtered. q_value is filled only by mzIdentML and by an MsPathFinderT file that carries a QValue column; :attr:absent_fields names it for every other file. Filter before you report.

Attributes:

Name Type Description
path str

The absolute path that was read.

file_type str

mzLib's SupportedFileType name.

record_count int

Matches in the whole file.

returned_count int

Matches carried back in :attr:columns - the unit limit and offset count in. Zero when out was given.

row_count int

Rows in :attr:columns: one per match, or with scores=True one per match and score, so it can exceed :attr:returned_count. Zero when out was given.

offset int

Matches skipped: the offset that was applied.

truncated bool

Whether matches were left behind, by either limit or offset.

caveats list[str]

What this view cannot be trusted to mean for this format - that MsPathFinderT infers decoys from an XXX name prefix, that Casanovo's scan numbers are mzTab indices, that mzIdentML lists every candidate rank.

column_names list[str]

The field names, in order.

columns Any

Field name -> list of values. None when out was given.

output Any

Where the table was written, or None.

reader Any

The mzLib class that parsed the file, e.g. "MzIdentMLResultFile".

scores_included bool

Whether scores=True was asked for, making the table long.

skipped_count int | None

mzIdentML only - identification items mzLib did not turn into rows (crosslinks, unresolvable modifications, substitutions); record_count + skipped_count is the items in the file. None for every other format, which keeps no such list.

skipped list[SkippedMatch] | None

The same items, each a :class:SkippedMatch with its reason. None with :attr:skipped_count.

rows_not_read Any

Always None: rows that did not become records are not counted for this view.

retention_time_unit Any

Always None: this view has no time column.

absent_fields list[str]

Columns this file's format has no source for, so every value is None: is_decoy for every format but MsPathFinderT; q_value for Casanovo and an MsPathFinderT file without a QValue column (mzLib would otherwise report 0, a perfect q-value); rank and pass_threshold for everything but mzIdentML; with scores=True, score_name and score_value for everything but mzIdentML.

failed_fields list[str]

"field: ExceptionType" for each column whose read threw on some rows - MsPathFinderT's accession on a FASTA without UniProt-style headers.

excluded_fields list[dict[str, Any]]

Always empty for this view.

error ReadError | None

Always None: a file that cannot be read raises instead.

Source code in pkg/python/src/pymzlib/readers.py
@dataclass(frozen=True)
class MatchRecords(_Table):
    """Identifications, in the cross-format ``spectral_match`` view.

    What :func:`read_matches` returns. Columns are ``file_name_without_extension``,
    ``one_based_scan_number``, ``base_sequence``, ``full_sequence``, ``accession``, ``is_decoy``,
    ``modifications``, ``modification_count``, ``q_value``, ``rank`` and ``pass_threshold`` - and,
    with ``scores=True``, ``match_index``, ``score_name`` and ``score_value``.

    **Nothing here is FDR-filtered.** ``q_value`` is filled only by mzIdentML and by an
    MsPathFinderT file that carries a ``QValue`` column; :attr:`absent_fields` names it for every
    other file. Filter before you report.

    Attributes:
        path: The absolute path that was read.
        file_type: mzLib's ``SupportedFileType`` name.
        record_count: Matches in the whole file.
        returned_count: Matches carried back in :attr:`columns` - the unit ``limit`` and
            ``offset`` count in. Zero when ``out`` was given.
        row_count: Rows in :attr:`columns`: one per match, or with ``scores=True`` one per match
            and score, so it can exceed :attr:`returned_count`. Zero when ``out`` was given.
        offset: Matches skipped: the offset that was applied.
        truncated: Whether matches were left behind, by either ``limit`` or ``offset``.
        caveats: What this view cannot be trusted to mean for this format - that MsPathFinderT
            infers decoys from an ``XXX`` name prefix, that Casanovo's scan numbers are mzTab
            indices, that mzIdentML lists every candidate rank.
        column_names: The field names, in order.
        columns: Field name -> list of values. ``None`` when ``out`` was given.
        output: Where the table was written, or ``None``.
        reader: The mzLib class that parsed the file, e.g. ``"MzIdentMLResultFile"``.
        scores_included: Whether ``scores=True`` was asked for, making the table long.
        skipped_count: mzIdentML only - identification items mzLib did not turn into rows
            (crosslinks, unresolvable modifications, substitutions); ``record_count +
            skipped_count`` is the items in the file. ``None`` for every other format, which keeps
            no such list.
        skipped: The same items, each a :class:`SkippedMatch` with its reason. ``None`` with
            :attr:`skipped_count`.
        rows_not_read: Always ``None``: rows that did not become records are not counted for this
            view.
        retention_time_unit: Always ``None``: this view has no time column.
        absent_fields: Columns this file's format has no source for, so every value is ``None``:
            ``is_decoy`` for every format but MsPathFinderT; ``q_value`` for Casanovo and an
            MsPathFinderT file without a ``QValue`` column (mzLib would otherwise report 0, a
            perfect q-value); ``rank`` and ``pass_threshold`` for everything but mzIdentML; with
            ``scores=True``, ``score_name`` and ``score_value`` for everything but mzIdentML.
        failed_fields: ``"field: ExceptionType"`` for each column whose read threw on some rows -
            MsPathFinderT's ``accession`` on a FASTA without UniProt-style headers.
        excluded_fields: Always empty for this view.
        error: Always ``None``: a file that cannot be read raises instead.
    """

    path: str
    file_type: str
    record_count: int
    returned_count: int
    offset: int
    truncated: bool
    row_count: int = 0
    caveats: list[str] = field(default_factory=list)
    column_names: list[str] = field(default_factory=list)
    columns: Any = None
    output: Any = None
    reader: Any = None
    scores_included: bool = False
    skipped_count: int | None = None
    skipped: list[SkippedMatch] | None = None
    rows_not_read: Any = None
    retention_time_unit: Any = None
    absent_fields: list[str] = field(default_factory=list)
    failed_fields: list[str] = field(default_factory=list)
    excluded_fields: list[dict[str, Any]] = field(default_factory=list)
    error: ReadError | None = None

    @classmethod
    def _from_wire(cls, data: dict[str, Any]) -> "MatchRecords":
        return cls(
            path=data.get("path", ""),
            file_type=data.get("file_type", ""),
            record_count=int(data.get("record_count", 0)),
            returned_count=int(data.get("returned_count", 0)),
            offset=int(data.get("offset", 0)),
            truncated=bool(data.get("truncated", False)),
            row_count=int(data.get("row_count", 0)),
            caveats=list(data.get("caveats") or []),
            column_names=list(data.get("column_names") or []),
            columns=data.get("columns"),
            output=_output(data),
            scores_included=bool(data.get("scores_included", False)),
            skipped_count=data.get("skipped_count"),
            skipped=_skipped(data.get("skipped")),
            rows_not_read=data.get("rows_not_read"),
            retention_time_unit=data.get("retention_time_unit"),
            **_block(data),
        )

ScanRecords dataclass

Bases: _Table

Scan headers - and optionally peaks - from a spectra file.

What :func:read_spectra returns. Retention times here are in minutes for every format: mzLib's spectra readers convert at the boundary, unlike its result-file readers, which pass the tool's own unit through untouched.

Attributes:

Name Type Description
path str

The absolute path that was read.

file_type str

mzLib's SupportedFileType name.

reader Any

The mzLib class that parsed it, e.g. "Mzml", "ThermoRawFileReader".

scan_count int

Scans in the whole file, before any ms_order filter. Reported alongside :attr:record_count so a filter that matched nothing can never look like an empty file.

ms_order Any

The MS level filtered to, or None if unfiltered.

record_count int

Scans that passed the ms_order filter.

returned_count int

Scans carried back in :attr:columns. Zero when out was given.

offset int

Scans skipped: the offset that was applied, counted after ms_order.

truncated bool

Whether scans were left behind, by either limit or offset.

peaks_included bool

Whether mz and intensity are present. When False, peak_count still tells you how many peaks each scan has.

retention_time_unit str

Always "minutes" for this view.

caveats list[str]

What this view cannot be trusted to mean for this format - that msalign holds deconvolved neutral masses rather than m/z, that MGF scan numbers come from a title line, that Bruker needs Windows-x64 native libraries.

column_names list[str]

The field names, in order.

columns Any

Field name -> list of values. When :attr:peaks_included, mz and intensity are each a list of lists - one array per scan. None when out was given.

output Any

Where the table was written, or None.

source SpectraSource | None

What the file records about the run - instrument model and serial number, and when acquisition started (mzLib #1349) - as a :class:SpectraSource. None only when the reader built no description at all; a field the file does not record is None inside it.

rows_not_read Any

Always None: rows that did not become records are not counted for this view.

absent_fields list[str]

Always empty for this view: a scan field a file does not record is None per scan, as for the precursor of an MS1 scan.

failed_fields list[str]

"field: ExceptionType" for each column whose read threw on some scans.

excluded_fields list[dict[str, Any]]

Always empty for this view.

error ReadError | None

Always None: a file that cannot be read raises instead.

Source code in pkg/python/src/pymzlib/readers.py
@dataclass(frozen=True)
class ScanRecords(_Table):
    """Scan headers - and optionally peaks - from a spectra file.

    What :func:`read_spectra` returns. Retention times here **are** in minutes for every format:
    mzLib's spectra readers convert at the boundary, unlike its result-file readers, which pass the
    tool's own unit through untouched.

    Attributes:
        path: The absolute path that was read.
        file_type: mzLib's ``SupportedFileType`` name.
        reader: The mzLib class that parsed it, e.g. ``"Mzml"``, ``"ThermoRawFileReader"``.
        scan_count: Scans in the **whole file**, before any ``ms_order`` filter. Reported alongside
            :attr:`record_count` so a filter that matched nothing can never look like an empty file.
        ms_order: The MS level filtered to, or ``None`` if unfiltered.
        record_count: Scans that passed the ``ms_order`` filter.
        returned_count: Scans carried back in :attr:`columns`. Zero when ``out`` was given.
        offset: Scans skipped: the offset that was applied, counted after ``ms_order``.
        truncated: Whether scans were left behind, by either ``limit`` or ``offset``.
        peaks_included: Whether ``mz`` and ``intensity`` are present. When ``False``, ``peak_count``
            still tells you how many peaks each scan has.
        retention_time_unit: Always ``"minutes"`` for this view.
        caveats: What this view cannot be trusted to mean for this format - that msalign holds
            deconvolved neutral masses rather than m/z, that MGF scan numbers come from a title
            line, that Bruker needs Windows-x64 native libraries.
        column_names: The field names, in order.
        columns: Field name -> list of values. When :attr:`peaks_included`, ``mz`` and ``intensity``
            are each a **list of lists** - one array per scan. ``None`` when ``out`` was given.
        output: Where the table was written, or ``None``.
        source: What the file records about the run - instrument model and serial number, and
            when acquisition started (mzLib #1349) - as a :class:`SpectraSource`. ``None`` only
            when the reader built no description at all; a field the file does not record is
            ``None`` inside it.
        rows_not_read: Always ``None``: rows that did not become records are not counted for this
            view.
        absent_fields: Always empty for this view: a scan field a file does not record is ``None``
            per scan, as for the precursor of an MS1 scan.
        failed_fields: ``"field: ExceptionType"`` for each column whose read threw on some scans.
        excluded_fields: Always empty for this view.
        error: Always ``None``: a file that cannot be read raises instead.
    """

    path: str
    file_type: str
    reader: Any
    scan_count: int
    record_count: int
    returned_count: int
    offset: int
    truncated: bool
    peaks_included: bool
    retention_time_unit: str
    ms_order: Any = None
    caveats: list[str] = field(default_factory=list)
    column_names: list[str] = field(default_factory=list)
    columns: Any = None
    output: Any = None
    source: SpectraSource | None = None
    rows_not_read: Any = None
    absent_fields: list[str] = field(default_factory=list)
    failed_fields: list[str] = field(default_factory=list)
    excluded_fields: list[dict[str, Any]] = field(default_factory=list)
    error: ReadError | None = None

    @property
    def total_ion_current(self) -> list[Any]:
        """The ``total_ion_current`` column, for the commonest plot there is."""
        columns = self._require_columns("read_spectra")
        return list(columns.get("total_ion_current") or [])

    @classmethod
    def _from_wire(cls, data: dict[str, Any]) -> "ScanRecords":
        block = _block(data)
        return cls(
            path=data.get("path", ""),
            file_type=data.get("file_type", ""),
            scan_count=int(data.get("scan_count", 0)),
            record_count=int(data.get("record_count", 0)),
            returned_count=int(data.get("returned_count", 0)),
            offset=int(data.get("offset", 0)),
            truncated=bool(data.get("truncated", False)),
            peaks_included=bool(data.get("peaks_included", False)),
            retention_time_unit=data.get("retention_time_unit") or "minutes",
            ms_order=data.get("ms_order"),
            caveats=list(data.get("caveats") or []),
            column_names=list(data.get("column_names") or []),
            columns=data.get("columns"),
            output=_output(data),
            source=SpectraSource._from_wire(data.get("source")),
            rows_not_read=data.get("rows_not_read"),
            **block,
        )
total_ion_current property
total_ion_current: list[Any]

The total_ion_current column, for the commonest plot there is.

ProteinGroupRecords dataclass

Bases: _Table

A MetaMorpheus protein-group table in long form: one row per group per sample group.

What :func:read_protein_groups returns. Columns: protein_group_name, gene, organism, decoy_contaminant_target, is_decoy, is_contaminant, is_entrapment, q_value, then sample_label, spectral_count and intensity.

Unfiltered. Decoys, contaminants and groups above 1% FDR are all rows, as MetaMorpheus writes them. Filter on q_value and decoy_contaminant_target before counting.

Attributes:

Name Type Description
path str

The absolute path that was read.

file_type str

"MetaMorpheusQuantifiedProteinGroups".

reader Any

"ProteinGroupFromTsvFile".

sample_labels list[str]

The file's sample-group labels, in header order, verbatim - e.g. "QE-002106_GM1_a-calib". Condition and replicate cannot be recovered from them; map them to your design yourself.

record_count int

Protein groups in the whole file - groups, not rows.

returned_count int

Groups carried back in :attr:columns - groups, not rows; the unit limit and offset count in. Zero when out was given.

row_count int

Rows in :attr:columns: returned groups times sample groups. Zero when out was given.

offset int

Groups skipped. limit and offset count groups, not rows.

truncated bool

Whether groups were left behind, by either limit or offset.

rows_not_read Any

Always None: rows that did not become records are not counted for this table - every data line is a record, or the file fails to read.

retention_time_unit Any

Always None: this table has no time column.

caveats list[str]

What this table cannot be trusted to mean, each citing the mzLib source.

column_names list[str]

The field names, in order.

columns Any

Field name -> list of values. intensity is None where the cell was blank (not quantified in that sample group), which is not zero. None when out was given.

output Any

Where the table was written, or None.

absent_fields list[str]

Columns this file has no column for, so every value is None - gene or organism when MetaMorpheus did not write them; sample_label, spectral_count and intensity when the file has no per-sample columns, in which case each group is one row.

failed_fields list[str]

"field: ExceptionType" for each column whose read threw on some rows.

excluded_fields list[dict[str, Any]]

count_occupancy and intensity_occupancy, whose verb is readers read-occupancy - :func:read_occupancy has them, one row per site.

error ReadError | None

Always None: a file that cannot be read raises instead.

Source code in pkg/python/src/pymzlib/readers.py
@dataclass(frozen=True)
class ProteinGroupRecords(_Table):
    """A MetaMorpheus protein-group table in **long** form: one row per group per sample group.

    What :func:`read_protein_groups` returns. Columns: ``protein_group_name``, ``gene``,
    ``organism``, ``decoy_contaminant_target``, ``is_decoy``, ``is_contaminant``,
    ``is_entrapment``, ``q_value``, then ``sample_label``, ``spectral_count`` and ``intensity``.

    **Unfiltered.** Decoys, contaminants and groups above 1% FDR are all rows, as MetaMorpheus
    writes them. Filter on ``q_value`` and ``decoy_contaminant_target`` before counting.

    Attributes:
        path: The absolute path that was read.
        file_type: ``"MetaMorpheusQuantifiedProteinGroups"``.
        reader: ``"ProteinGroupFromTsvFile"``.
        sample_labels: The file's sample-group labels, in header order, verbatim - e.g.
            ``"QE-002106_GM1_a-calib"``. Condition and replicate cannot be recovered from them;
            map them to your design yourself.
        record_count: Protein groups in the whole file - groups, not rows.
        returned_count: Groups carried back in :attr:`columns` - groups, not rows; the unit
            ``limit`` and ``offset`` count in. Zero when ``out`` was given.
        row_count: Rows in :attr:`columns`: returned groups times sample groups. Zero when ``out``
            was given.
        offset: Groups skipped. ``limit`` and ``offset`` count groups, not rows.
        truncated: Whether groups were left behind, by either ``limit`` or ``offset``.
        rows_not_read: Always ``None``: rows that did not become records are not counted for this
            table - every data line is a record, or the file fails to read.
        retention_time_unit: Always ``None``: this table has no time column.
        caveats: What this table cannot be trusted to mean, each citing the mzLib source.
        column_names: The field names, in order.
        columns: Field name -> list of values. ``intensity`` is ``None`` where the cell was blank
            (not quantified in that sample group), which is not zero. ``None`` when ``out`` was
            given.
        output: Where the table was written, or ``None``.
        absent_fields: Columns this file has no column for, so every value is ``None`` - ``gene``
            or ``organism`` when MetaMorpheus did not write them; ``sample_label``,
            ``spectral_count`` and ``intensity`` when the file has no per-sample columns, in which
            case each group is one row.
        failed_fields: ``"field: ExceptionType"`` for each column whose read threw on some rows.
        excluded_fields: ``count_occupancy`` and ``intensity_occupancy``, whose ``verb`` is
            ``readers read-occupancy`` - :func:`read_occupancy` has them, one row per site.
        error: Always ``None``: a file that cannot be read raises instead.
    """

    path: str
    file_type: str
    reader: Any
    record_count: int
    returned_count: int
    offset: int
    truncated: bool
    row_count: int = 0
    sample_labels: list[str] = field(default_factory=list)
    rows_not_read: Any = None
    retention_time_unit: Any = None
    caveats: list[str] = field(default_factory=list)
    column_names: list[str] = field(default_factory=list)
    columns: Any = None
    output: Any = None
    absent_fields: list[str] = field(default_factory=list)
    failed_fields: list[str] = field(default_factory=list)
    excluded_fields: list[dict[str, Any]] = field(default_factory=list)
    error: ReadError | None = None

    @classmethod
    def _from_wire(cls, data: dict[str, Any]) -> "ProteinGroupRecords":
        return cls(
            path=data.get("path", ""),
            file_type=data.get("file_type", ""),
            record_count=int(data.get("record_count", 0)),
            returned_count=int(data.get("returned_count", 0)),
            offset=int(data.get("offset", 0)),
            truncated=bool(data.get("truncated", False)),
            row_count=int(data.get("row_count", 0)),
            sample_labels=list(data.get("sample_labels") or []),
            rows_not_read=data.get("rows_not_read"),
            retention_time_unit=data.get("retention_time_unit"),
            caveats=list(data.get("caveats") or []),
            column_names=list(data.get("column_names") or []),
            columns=data.get("columns"),
            output=_output(data),
            **_block(data),
        )

QuantifiedPeptideRecords dataclass

Bases: _Table

A FlashLFQ peptide table in long form: one row per peptide per sample.

What :func:read_quantified_peptides returns. Columns: sequence (the full, modified sequence), base_sequence, peak_order, protein_groups, gene_names, organism, then sample_label, intensity, detection_type and retention_time.

An intensity of 0 is not a measurement. FlashLFQ writes a literal 0 for a peptide it did not quantify in a sample; detection_type ("MSMS", "MBR", "NotDetected", ...) is what tells the two apart.

Attributes:

Name Type Description
path str

The absolute path that was read.

file_type str

"FlashLFQQuantifiedPeptide".

reader Any

"QuantifiedPeptideFile".

sample_labels list[str]

The file's sample labels, in header order, verbatim.

record_count int

Peptides in the whole file - peptides, not rows.

returned_count int

Peptides carried back in :attr:columns - peptides, not rows. Zero when out was given.

row_count int

Rows in :attr:columns: returned peptides times samples. Zero when out was given.

offset int

Peptides skipped. limit and offset count peptides, not rows.

truncated bool

Whether peptides were left behind, by either limit or offset.

rows_not_read Any

Always None: rows that did not become records are not counted for this table - every data line is a record, or the file fails to read.

retention_time_unit Any

"minutes": the header says RetentionTime (min). The column is filled only by IsoTracker output; :attr:absent_fields names it otherwise.

caveats list[str]

What this table cannot be trusted to mean, each citing the mzLib source.

column_names list[str]

The field names, in order.

columns Any

Field name -> list of values. None when out was given.

output Any

Where the table was written, or None.

absent_fields list[str]

Columns this file has no column for, so every value is None - peak_order and retention_time for anything but IsoTracker output.

failed_fields list[str]

"field: ExceptionType" for each column whose read threw on some rows.

excluded_fields list[dict[str, Any]]

Always empty for this table.

error ReadError | None

Always None: a file that cannot be read raises instead.

Source code in pkg/python/src/pymzlib/readers.py
@dataclass(frozen=True)
class QuantifiedPeptideRecords(_Table):
    """A FlashLFQ peptide table in **long** form: one row per peptide per sample.

    What :func:`read_quantified_peptides` returns. Columns: ``sequence`` (the full, modified
    sequence), ``base_sequence``, ``peak_order``, ``protein_groups``, ``gene_names``,
    ``organism``, then ``sample_label``, ``intensity``, ``detection_type`` and ``retention_time``.

    **An intensity of 0 is not a measurement.** FlashLFQ writes a literal 0 for a peptide it did
    not quantify in a sample; ``detection_type`` (``"MSMS"``, ``"MBR"``, ``"NotDetected"``, ...)
    is what tells the two apart.

    Attributes:
        path: The absolute path that was read.
        file_type: ``"FlashLFQQuantifiedPeptide"``.
        reader: ``"QuantifiedPeptideFile"``.
        sample_labels: The file's sample labels, in header order, verbatim.
        record_count: Peptides in the whole file - peptides, not rows.
        returned_count: Peptides carried back in :attr:`columns` - peptides, not rows. Zero when
            ``out`` was given.
        row_count: Rows in :attr:`columns`: returned peptides times samples. Zero when ``out`` was
            given.
        offset: Peptides skipped. ``limit`` and ``offset`` count peptides, not rows.
        truncated: Whether peptides were left behind, by either ``limit`` or ``offset``.
        rows_not_read: Always ``None``: rows that did not become records are not counted for this
            table - every data line is a record, or the file fails to read.
        retention_time_unit: ``"minutes"``: the header says ``RetentionTime (min)``. The column is
            filled only by IsoTracker output; :attr:`absent_fields` names it otherwise.
        caveats: What this table cannot be trusted to mean, each citing the mzLib source.
        column_names: The field names, in order.
        columns: Field name -> list of values. ``None`` when ``out`` was given.
        output: Where the table was written, or ``None``.
        absent_fields: Columns this file has no column for, so every value is ``None`` -
            ``peak_order`` and ``retention_time`` for anything but IsoTracker output.
        failed_fields: ``"field: ExceptionType"`` for each column whose read threw on some rows.
        excluded_fields: Always empty for this table.
        error: Always ``None``: a file that cannot be read raises instead.
    """

    path: str
    file_type: str
    reader: Any
    record_count: int
    returned_count: int
    offset: int
    truncated: bool
    row_count: int = 0
    sample_labels: list[str] = field(default_factory=list)
    rows_not_read: Any = None
    retention_time_unit: Any = "minutes"
    caveats: list[str] = field(default_factory=list)
    column_names: list[str] = field(default_factory=list)
    columns: Any = None
    output: Any = None
    absent_fields: list[str] = field(default_factory=list)
    failed_fields: list[str] = field(default_factory=list)
    excluded_fields: list[dict[str, Any]] = field(default_factory=list)
    error: ReadError | None = None

    @classmethod
    def _from_wire(cls, data: dict[str, Any]) -> "QuantifiedPeptideRecords":
        return cls(
            path=data.get("path", ""),
            file_type=data.get("file_type", ""),
            record_count=int(data.get("record_count", 0)),
            returned_count=int(data.get("returned_count", 0)),
            offset=int(data.get("offset", 0)),
            truncated=bool(data.get("truncated", False)),
            row_count=int(data.get("row_count", 0)),
            sample_labels=list(data.get("sample_labels") or []),
            rows_not_read=data.get("rows_not_read"),
            retention_time_unit=data.get("retention_time_unit") or "minutes",
            caveats=list(data.get("caveats") or []),
            column_names=list(data.get("column_names") or []),
            columns=data.get("columns"),
            output=_output(data),
            **_block(data),
        )

OccupancyRecords dataclass

Bases: _Table

PTM site occupancy from a MetaMorpheus protein-group table: one row per modified site.

What :func:read_occupancy returns. Each row is one site in one occupancy cell - one group, one sample group, one basis. Columns: protein_group_name, sample_label, basis ("count" or "intensity"), entity_index, position, is_n_terminus, modification, fraction, numerator, denominator and cell_is_truncated.

Which number to trust depends on the basis. A "count" cell prints its fraction to two decimals, so use numerator / denominator (PSMs modified / PSMs covering the site); an "intensity" cell prints its numerator and denominator to four significant digits, so use fraction.

Attributes:

Name Type Description
path str

The absolute path that was read.

file_type str

"MetaMorpheusQuantifiedProteinGroups".

reader Any

"ProteinGroupFromTsvFile".

sample_labels list[str]

The file's sample-group labels, in header order.

truncated_cell_count int

Occupancy cells the writer cut short or replaced with "Output too long for Excel". Their complete sites are rows with cell_is_truncated set; a cut cell with no complete site gives no rows and is counted only here.

record_count int

Protein groups in the whole file - groups, not rows.

returned_count int

Groups carried back - groups, not rows. Zero when out was given.

row_count int

Site rows in :attr:columns: one per group, sample group, basis and modified site. Zero when out was given.

offset int

Groups skipped. limit and offset count groups, not rows.

truncated bool

Whether groups were left behind by limit or offset - not the same thing as a cell the writer cut short; see :attr:truncated_cell_count.

rows_not_read Any

Always None: rows that did not become records are not counted for this table - every data line is a record, or the file fails to read.

retention_time_unit Any

Always None: this table has no time column.

caveats list[str]

What this table cannot be trusted to mean.

column_names list[str]

The field names, in order.

columns Any

Field name -> list of values. None when out was given.

output Any

Where the table was written, or None.

absent_fields list[str]

Every site column, when the file has no occupancy columns at all (then there are no rows).

failed_fields list[str]

"count_occupancy: FormatException" (or intensity_occupancy) when a cell was not an occupancy cell at all: mzLib refuses to guess, and that cell gives no rows.

excluded_fields list[dict[str, Any]]

Always empty for this table.

error ReadError | None

Always None: a file that cannot be read raises instead.

Source code in pkg/python/src/pymzlib/readers.py
@dataclass(frozen=True)
class OccupancyRecords(_Table):
    """PTM site occupancy from a MetaMorpheus protein-group table: one row per modified site.

    What :func:`read_occupancy` returns. Each row is one site in one occupancy cell - one group,
    one sample group, one ``basis``. Columns: ``protein_group_name``, ``sample_label``, ``basis``
    (``"count"`` or ``"intensity"``), ``entity_index``, ``position``, ``is_n_terminus``,
    ``modification``, ``fraction``, ``numerator``, ``denominator`` and ``cell_is_truncated``.

    **Which number to trust depends on the basis.** A ``"count"`` cell prints its fraction to two
    decimals, so use ``numerator / denominator`` (PSMs modified / PSMs covering the site); an
    ``"intensity"`` cell prints its numerator and denominator to four significant digits, so use
    ``fraction``.

    Attributes:
        path: The absolute path that was read.
        file_type: ``"MetaMorpheusQuantifiedProteinGroups"``.
        reader: ``"ProteinGroupFromTsvFile"``.
        sample_labels: The file's sample-group labels, in header order.
        truncated_cell_count: Occupancy cells the writer cut short or replaced with ``"Output too
            long for Excel"``. Their complete sites are rows with ``cell_is_truncated`` set; a cut
            cell with no complete site gives no rows and is counted only here.
        record_count: Protein groups in the whole file - groups, not rows.
        returned_count: Groups carried back - groups, not rows. Zero when ``out`` was given.
        row_count: Site rows in :attr:`columns`: one per group, sample group, basis and modified
            site. Zero when ``out`` was given.
        offset: Groups skipped. ``limit`` and ``offset`` count groups, not rows.
        truncated: Whether groups were left behind by ``limit`` or ``offset`` - not the same thing
            as a cell the writer cut short; see :attr:`truncated_cell_count`.
        rows_not_read: Always ``None``: rows that did not become records are not counted for this
            table - every data line is a record, or the file fails to read.
        retention_time_unit: Always ``None``: this table has no time column.
        caveats: What this table cannot be trusted to mean.
        column_names: The field names, in order.
        columns: Field name -> list of values. ``None`` when ``out`` was given.
        output: Where the table was written, or ``None``.
        absent_fields: Every site column, when the file has no occupancy columns at all (then
            there are no rows).
        failed_fields: ``"count_occupancy: FormatException"`` (or ``intensity_occupancy``) when a
            cell was not an occupancy cell at all: mzLib refuses to guess, and that cell gives no
            rows.
        excluded_fields: Always empty for this table.
        error: Always ``None``: a file that cannot be read raises instead.
    """

    path: str
    file_type: str
    reader: Any
    record_count: int
    returned_count: int
    offset: int
    truncated: bool
    row_count: int = 0
    sample_labels: list[str] = field(default_factory=list)
    truncated_cell_count: int = 0
    rows_not_read: Any = None
    retention_time_unit: Any = None
    caveats: list[str] = field(default_factory=list)
    column_names: list[str] = field(default_factory=list)
    columns: Any = None
    output: Any = None
    absent_fields: list[str] = field(default_factory=list)
    failed_fields: list[str] = field(default_factory=list)
    excluded_fields: list[dict[str, Any]] = field(default_factory=list)
    error: ReadError | None = None

    @classmethod
    def _from_wire(cls, data: dict[str, Any]) -> "OccupancyRecords":
        return cls(
            path=data.get("path", ""),
            file_type=data.get("file_type", ""),
            record_count=int(data.get("record_count", 0)),
            returned_count=int(data.get("returned_count", 0)),
            offset=int(data.get("offset", 0)),
            truncated=bool(data.get("truncated", False)),
            row_count=int(data.get("row_count", 0)),
            sample_labels=list(data.get("sample_labels") or []),
            truncated_cell_count=int(data.get("truncated_cell_count") or 0),
            rows_not_read=data.get("rows_not_read"),
            retention_time_unit=data.get("retention_time_unit"),
            caveats=list(data.get("caveats") or []),
            column_names=list(data.get("column_names") or []),
            columns=data.get("columns"),
            output=_output(data),
            **_block(data),
        )

ReadBatch dataclass

Bases: _Table

Many files read into ONE long table: what every _many read function returns.

The table's first two columns say where each row came from: source_index, the file's 0-based position in the list you passed, and source_path, its absolute path. The rest are the single-file function's columns. Rows are grouped by file in input order, each file's rows in the file's own order - whatever threads was, so the same list always gives the same table. The per-file facts are in :attr:files, one :class:FileReport per input.

Attributes:

Name Type Description
verb str

The bridge command that produced it, e.g. "readers read-spectra".

file_count int

Files in the list you passed.

read_count int

Files read.

failed_count int

Files that could not be read - always 0 unless on_error="skip", since otherwise the first failure raises. Each one's :attr:FileReport.error says why.

on_error str

"fail" or "skip", as requested.

record_count int

Records read, summed over the files read, in the reader's own unit (scans, matches, groups, ...). Not always the row count: a long reader gives several rows per record.

returned_count int

Records carried back in :attr:columns, summed over the files, in the reader's own unit. Zero when out was given.

row_count int

Rows in :attr:columns - more than :attr:returned_count for a long reader (protein groups, peptides, occupancy, matches with scores). Zero when out was given.

column_names list[str]

source_index, source_path, then the reader's columns, in order.

columns Any

Column name -> list of values, one per row - the shape pandas.DataFrame and polars.DataFrame accept directly. None when out was given.

output Any

Where the table was written, or None if it was returned inline. Written one file at a time, so memory held at most threads files.

files list[FileReport]

One :class:FileReport per input, in input order.

ms_order Any

:func:read_spectra_many - the MS level every file was filtered to, or None.

peaks_included bool

:func:read_spectra_many - whether mz and intensity are present.

scores_included bool

:func:read_matches_many - whether the table is long by score.

Source code in pkg/python/src/pymzlib/readers.py
@dataclass(frozen=True)
class ReadBatch(_Table):
    """Many files read into ONE long table: what every ``_many`` read function returns.

    The table's first two columns say where each row came from: ``source_index``, the file's
    0-based position in the list you passed, and ``source_path``, its absolute path. The rest are
    the single-file function's columns. Rows are grouped by file in input order, each file's rows
    in the file's own order - **whatever** ``threads`` **was**, so the same list always gives the
    same table. The per-file facts are in :attr:`files`, one :class:`FileReport` per input.

    Attributes:
        verb: The bridge command that produced it, e.g. ``"readers read-spectra"``.
        file_count: Files in the list you passed.
        read_count: Files read.
        failed_count: Files that could not be read - always 0 unless ``on_error="skip"``, since
            otherwise the first failure raises. Each one's :attr:`FileReport.error` says why.
        on_error: ``"fail"`` or ``"skip"``, as requested.
        record_count: Records read, summed over the files read, in the reader's own unit (scans,
            matches, groups, ...). Not always the row count: a long reader gives several rows per
            record.
        returned_count: Records carried back in :attr:`columns`, summed over the files, in the
            reader's own unit. Zero when ``out`` was given.
        row_count: Rows in :attr:`columns` - more than :attr:`returned_count` for a long reader
            (protein groups, peptides, occupancy, matches with scores). Zero when ``out`` was
            given.
        column_names: ``source_index``, ``source_path``, then the reader's columns, in order.
        columns: Column name -> list of values, one per row - the shape ``pandas.DataFrame`` and
            ``polars.DataFrame`` accept directly. ``None`` when ``out`` was given.
        output: Where the table was written, or ``None`` if it was returned inline. Written one
            file at a time, so memory held at most ``threads`` files.
        files: One :class:`FileReport` per input, in input order.
        ms_order: :func:`read_spectra_many` - the MS level every file was filtered to, or ``None``.
        peaks_included: :func:`read_spectra_many` - whether ``mz`` and ``intensity`` are present.
        scores_included: :func:`read_matches_many` - whether the table is long by score.
    """

    verb: str
    file_count: int
    read_count: int
    failed_count: int
    on_error: str
    record_count: int
    returned_count: int
    row_count: int = 0
    column_names: list[str] = field(default_factory=list)
    columns: Any = None
    output: Any = None
    files: list[FileReport] = field(default_factory=list)
    ms_order: Any = None
    peaks_included: bool = False
    scores_included: bool = False

    @property
    def failed_files(self) -> list[FileReport]:
        """The inputs that could not be read, under ``on_error="skip"``. Empty otherwise."""
        return [report for report in self.files if report.error is not None]

    def in_minutes(self, column: str) -> list[Any]:
        """A time column in minutes, converting each file's rows by that file's own unit.

        The multi-file form of :attr:`ResultRecords.retention_time_in_minutes`: every file's
        :attr:`FileReport.retention_time_unit` is applied to that file's rows, so a batch that mixes
        formats comes back on one axis.

        Args:
            column: The time column, e.g. ``"retention_time"`` or ``"retention_time_start"``.

        Returns:
            The column's values in minutes, one per row.

        Raises:
            UsageError: the table went to ``out``, has no such column, or a file's unit is
                ``"unknown"`` - raised rather than guessed.
        """
        return self._to_minutes(self.verb, column)

    @classmethod
    def _from_wire(cls, verb: str, data: dict[str, Any]) -> "ReadBatch":
        return cls(
            verb=verb,
            file_count=int(data.get("file_count", 0)),
            read_count=int(data.get("read_count", 0)),
            failed_count=int(data.get("failed_count", 0)),
            on_error=data.get("on_error") or "fail",
            record_count=int(data.get("record_count", 0)),
            returned_count=int(data.get("returned_count", 0)),
            row_count=int(data.get("row_count", 0)),
            column_names=list(data.get("column_names") or []),
            columns=data.get("columns"),
            output=_output(data),
            files=[FileReport._from_wire(entry) for entry in (data.get("files") or [])],
            ms_order=data.get("ms_order"),
            peaks_included=bool(data.get("peaks_included", False)),
            scores_included=bool(data.get("scores_included", False)),
        )
failed_files property
failed_files: list[FileReport]

The inputs that could not be read, under on_error="skip". Empty otherwise.

in_minutes
in_minutes(column: str) -> list[Any]

A time column in minutes, converting each file's rows by that file's own unit.

The multi-file form of :attr:ResultRecords.retention_time_in_minutes: every file's :attr:FileReport.retention_time_unit is applied to that file's rows, so a batch that mixes formats comes back on one axis.

Parameters:

Name Type Description Default
column str

The time column, e.g. "retention_time" or "retention_time_start".

required

Returns:

Type Description
list[Any]

The column's values in minutes, one per row.

Raises:

Type Description
UsageError

the table went to out, has no such column, or a file's unit is "unknown" - raised rather than guessed.

Source code in pkg/python/src/pymzlib/readers.py
def in_minutes(self, column: str) -> list[Any]:
    """A time column in minutes, converting each file's rows by that file's own unit.

    The multi-file form of :attr:`ResultRecords.retention_time_in_minutes`: every file's
    :attr:`FileReport.retention_time_unit` is applied to that file's rows, so a batch that mixes
    formats comes back on one axis.

    Args:
        column: The time column, e.g. ``"retention_time"`` or ``"retention_time_start"``.

    Returns:
        The column's values in minutes, one per row.

    Raises:
        UsageError: the table went to ``out``, has no such column, or a file's unit is
            ``"unknown"`` - raised rather than guessed.
    """
    return self._to_minutes(self.verb, column)

IdentifyBatch dataclass

What :func:identify_many returns: one :class:FileInfo per path, in the order given.

Attributes:

Name Type Description
file_count int

Paths given.

read_count int

Paths identified.

failed_count int

Paths that could not be identified - always 0 unless on_error="skip".

on_error str

"fail" or "skip", as requested.

files list[FileInfo]

One :class:FileInfo per path, in input order; a failed one has :attr:FileInfo.error set.

Source code in pkg/python/src/pymzlib/readers.py
@dataclass(frozen=True)
class IdentifyBatch:
    """What :func:`identify_many` returns: one :class:`FileInfo` per path, in the order given.

    Attributes:
        file_count: Paths given.
        read_count: Paths identified.
        failed_count: Paths that could not be identified - always 0 unless ``on_error="skip"``.
        on_error: ``"fail"`` or ``"skip"``, as requested.
        files: One :class:`FileInfo` per path, in input order; a failed one has
            :attr:`FileInfo.error` set.
    """

    file_count: int
    read_count: int
    failed_count: int
    on_error: str
    files: list[FileInfo] = field(default_factory=list)

    @property
    def failed_files(self) -> list[FileInfo]:
        """The paths that could not be identified, under ``on_error="skip"``."""
        return [info for info in self.files if info.error is not None]

    @classmethod
    def _from_wire(cls, data: dict[str, Any]) -> "IdentifyBatch":
        return cls(
            file_count=int(data.get("file_count", 0)),
            read_count=int(data.get("read_count", 0)),
            failed_count=int(data.get("failed_count", 0)),
            on_error=data.get("on_error") or "fail",
            files=[FileInfo._from_wire(item) for item in (data.get("files") or [])],
        )
failed_files property
failed_files: list[FileInfo]

The paths that could not be identified, under on_error="skip".

formats

formats(timeout: float | None = 60) -> list[Format]

Every file type mzLib can recognise.

Enumerated from mzLib itself rather than from a list maintained here, so it reflects the installed version and cannot go stale.

Parameters:

Name Type Description Default
timeout float | None

Seconds to allow.

60

Returns:

Name Type Description
One list[Format]

class:Format per supported file type.

Examples:

>>> quantifiable = [f.file_type for f in formats() if f.is_quantifiable]
>>> quantifiable
['psmtsv', 'osmtsv', 'MsFraggerPsm', 'DiaNnReport']
Source code in pkg/python/src/pymzlib/readers.py
def formats(timeout: float | None = 60) -> list[Format]:
    """Every file type mzLib can recognise.

    Enumerated from mzLib itself rather than from a list maintained here, so it reflects the
    installed version and cannot go stale.

    Args:
        timeout: Seconds to allow.

    Returns:
        One :class:`Format` per supported file type.

    Examples:
        >>> quantifiable = [f.file_type for f in formats() if f.is_quantifiable]
        >>> quantifiable
        ['psmtsv', 'osmtsv', 'MsFraggerPsm', 'DiaNnReport']
    """
    data = _bridge.invoke("readers", "formats", timeout=timeout)
    return [Format._from_wire(item) for item in (data.get("formats") or [])]

identify

identify(path: str | PathLike[str], timeout: float | None = 60) -> FileInfo

Identify a result file without parsing its contents.

Cheap by design: mzLib resolves the type and stops, so identifying a million-row file costs no more than identifying an empty one. It is not, however, pure - mzLib disambiguates a bare .tsv by reading its first line, a .mztab by its first five, and a Bruker .d by which analysis file the directory holds. An unreadable file will therefore raise.

Parameters:

Name Type Description Default
path str | PathLike[str]

Path to a result or spectra file. A Bruker .d directory is also accepted.

required
timeout float | None

Seconds to allow.

60

Returns:

Name Type Description
A FileInfo

class:FileInfo naming the format and the views it supports.

Raises:

Type Description
UsageError

the path is blank, does not exist, or is not a file type mzLib recognises. mzLib has no "unknown" result - a file is dispatchable or it is an error - so use :func:formats to see what is supported, or catch this to test a file.

Examples:

>>> info = identify("PXD078927_msgf_1_1_0.mzid")
>>> info.file_type, info.views
('MzIdentML', ['spectral_match'])
Source code in pkg/python/src/pymzlib/readers.py
def identify(path: str | os.PathLike[str], timeout: float | None = 60) -> FileInfo:
    """Identify a result file without parsing its contents.

    Cheap by design: mzLib resolves the type and stops, so identifying a million-row file costs no
    more than identifying an empty one. It is not, however, *pure* - mzLib disambiguates a bare
    ``.tsv`` by reading its first line, a ``.mztab`` by its first five, and a Bruker ``.d`` by which
    analysis file the directory holds. An unreadable file will therefore raise.

    Args:
        path: Path to a result or spectra file. A Bruker ``.d`` directory is also accepted.
        timeout: Seconds to allow.

    Returns:
        A :class:`FileInfo` naming the format and the views it supports.

    Raises:
        UsageError: the path is blank, does not exist, or is not a file type mzLib recognises.
            mzLib has no "unknown" result - a file is dispatchable or it is an error - so use
            :func:`formats` to see what is supported, or catch this to test a file.

    Examples:
        >>> info = identify("PXD078927_msgf_1_1_0.mzid")
        >>> info.file_type, info.views
        ('MzIdentML', ['spectral_match'])
    """
    path = _bridge.path_text(path)
    if not path:
        raise _bridge.UsageError("A file path is required, e.g. 'AllPSMs.psmtsv'.")

    data = _bridge.invoke("readers", "identify", "--path", path, timeout=timeout)
    return FileInfo._from_wire(data)

identify_many

identify_many(paths: PathList, *, threads: int = 1, on_error: str = 'fail', timeout: float | None = 60) -> IdentifyBatch

Identify many files in one call, without parsing their contents.

:func:identify for a list: one bridge process, the answers in the order given. The cheap way to sort a directory of unknown files before reading any of them.

Parameters:

Name Type Description Default
paths PathList

The files to identify, as a list of str or pathlib.Path.

required
threads int

Files identified at once: 1 (the default), more, or -1 for one per core. The answer is the same at any value.

1
on_error str

"fail" (the default) raises on the first file that cannot be identified; "skip" answers it with :attr:FileInfo.error set and carries on.

'fail'
timeout float | None

Seconds to allow for the whole list.

60

Returns:

Name Type Description
An IdentifyBatch

class:IdentifyBatch, whose :attr:IdentifyBatch.files has one :class:FileInfo per

IdentifyBatch

path, in order.

Raises:

Type Description
UsageError

paths is not a list, is empty, or holds a blank entry; a path is repeated; threads or on_error is not a value listed above; or, with on_error="fail", a file is missing or is not one this function reads - the message then starts Input <i> ('<path>').

BridgeError

with on_error="fail", mzLib recognised a file and failed to parse it; the message names the input the same way.

Examples:

>>> # Not run in CI: the replay bridge answers single-path calls only.
>>> batch = identify_many(["run.mzML", "AllPSMs.psmtsv", "notes.txt"],
...                       on_error="skip")
>>> [f.file_type for f in batch.files]
['MzML', 'psmtsv', '']
>>> batch.failed_files[0].error.kind
'usage'
Source code in pkg/python/src/pymzlib/readers.py
def identify_many(
    paths: PathList,
    *,
    threads: int = 1,
    on_error: str = "fail",
    timeout: float | None = 60,
) -> IdentifyBatch:
    """Identify many files in one call, without parsing their contents.

    :func:`identify` for a list: one bridge process, the answers in the order given. The cheap way
    to sort a directory of unknown files before reading any of them.

    Args:
        paths: The files to identify, as a list of ``str`` or ``pathlib.Path``.
        threads: Files identified at once: ``1`` (the default), more, or ``-1`` for one per core.
            The answer is the same at any value.
        on_error: ``"fail"`` (the default) raises on the first file that cannot be identified;
            ``"skip"`` answers it with :attr:`FileInfo.error` set and carries on.
        timeout: Seconds to allow for the whole list.

    Returns:
        An :class:`IdentifyBatch`, whose :attr:`IdentifyBatch.files` has one :class:`FileInfo` per
        path, in order.

    Raises:
        UsageError: ``paths`` is not a list, is empty, or holds a blank entry; a path is
            repeated; ``threads`` or ``on_error`` is not a value listed above; or, with
            ``on_error="fail"``, a file is missing or is not one this function reads - the
            message then starts ``Input <i> ('<path>')``.
        BridgeError: with ``on_error="fail"``, mzLib recognised a file and failed to parse it;
            the message names the input the same way.

    Examples:
        >>> # Not run in CI: the replay bridge answers single-path calls only.
        >>> batch = identify_many(["run.mzML", "AllPSMs.psmtsv", "notes.txt"],
        ...                       on_error="skip")                             # doctest: +SKIP
        >>> [f.file_type for f in batch.files]                                 # doctest: +SKIP
        ['MzML', 'psmtsv', '']
        >>> batch.failed_files[0].error.kind                                   # doctest: +SKIP
        'usage'
    """
    args, stdin = _batch("identify", paths, threads=threads, on_error=on_error)
    data = _bridge.invoke(*args, stdin=stdin, timeout=timeout)
    return IdentifyBatch._from_wire(data)

read_results

read_results(path: str | PathLike[str], *, limit: int | None = None, offset: int = 0, out: str | PathLike[str] | None = None, timeout: float | None = None) -> ResultRecords

Read a result file into the uniform record view.

Only the four file types offering the "quantifiable" view can be read this way - check :func:identify first, or catch the error. A file without the view is rejected with a message naming the views it does have. :func:read_results_many reads a list of them into one table.

There is no default row limit. A result file can carry a million rows, and truncating by default would mean the ordinary call returns a table that looks complete and is not. For a large file use out rather than paging: see the note on offset below.

Parameters:

Name Type Description Default
path str | PathLike[str]

Path to a MetaMorpheus .psmtsv / .osmtsv, an MSFragger psm.tsv or a DIA-NN report.tsv.

required
limit int | None

Maximum records to return. None (the default) returns all of them. :attr:ResultRecords.truncated reports whether anything was left behind.

None
offset int

Records to skip. This is a window, not a cursor. mzLib materializes the whole file on every call - its readers look lazy and are not - so paging re-reads and re-parses the file once per page. For a large file, one call with out is right and a paging loop is quadratic.

0
out str | PathLike[str] | None

Write the records to this path as a tab-separated table and return only a summary, instead of carrying them back in the envelope. The intended path for large files, not an escape hatch. Tab-separated because these fields contain commas. The path must end in .tsv (any case); any other is a UsageError before a read.

None
timeout float | None

Seconds to allow. A large file legitimately takes a while; None waits indefinitely.

None

Returns:

Name Type Description
A ResultRecords

class:ResultRecords. Read :attr:ResultRecords.caveats before trusting a field across

ResultRecords

formats.

Raises:

Type Description
UsageError

the path is blank, missing, not a recognised format, or has no quantifiable view.

Examples:

>>> r = read_results("FraggerPsm_FragPipev21.1_psm.tsv", limit=2)
>>> r.record_count, r.returned_count, r.absent_fields
(5, 2, ['is_decoy'])

pandas is not a dependency of pyMzLib, so these lines are not executed:

>>> import pandas as pd
>>> pd.DataFrame(r.columns)
Source code in pkg/python/src/pymzlib/readers.py
def read_results(
    path: str | os.PathLike[str],
    *,
    limit: int | None = None,
    offset: int = 0,
    out: str | os.PathLike[str] | None = None,
    timeout: float | None = None,
) -> ResultRecords:
    """Read a result file into the uniform record view.

    Only the four file types offering the ``"quantifiable"`` view can be read this way - check
    :func:`identify` first, or catch the error. A file without the view is rejected with a message
    naming the views it does have. :func:`read_results_many` reads a list of them into one table.

    **There is no default row limit.** A result file can carry a million rows, and truncating by
    default would mean the ordinary call returns a table that looks complete and is not. For a large
    file use ``out`` rather than paging: see the note on ``offset`` below.

    Args:
        path: Path to a MetaMorpheus ``.psmtsv`` / ``.osmtsv``, an MSFragger ``psm.tsv`` or a
            DIA-NN ``report.tsv``.
        limit: Maximum records to return. ``None`` (the default) returns all of them.
            :attr:`ResultRecords.truncated` reports whether anything was left behind.
        offset: Records to skip. **This is a window, not a cursor.** mzLib materializes the whole
            file on every call - its readers look lazy and are not - so paging re-reads and
            re-parses the file once per page. For a large file, one call with ``out`` is right and
            a paging loop is quadratic.
        out: Write the records to this path as a **tab-separated** table and return only a summary,
            instead of carrying them back in the envelope. The intended path for large files, not
            an escape hatch. Tab-separated because these fields contain commas.
            The path must end in ``.tsv`` (any case); any other is a ``UsageError`` before a read.
        timeout: Seconds to allow. A large file legitimately takes a while; ``None`` waits
            indefinitely.

    Returns:
        A :class:`ResultRecords`. Read :attr:`ResultRecords.caveats` before trusting a field across
        formats.

    Raises:
        UsageError: the path is blank, missing, not a recognised format, or has no quantifiable view.

    Examples:
        >>> r = read_results("FraggerPsm_FragPipev21.1_psm.tsv", limit=2)
        >>> r.record_count, r.returned_count, r.absent_fields
        (5, 2, ['is_decoy'])

        pandas is not a dependency of pyMzLib, so these lines are not executed:

        >>> import pandas as pd                                      # doctest: +SKIP
        >>> pd.DataFrame(r.columns)                                  # doctest: +SKIP
    """
    args = _window("read-results", path, limit=limit, offset=offset, out=out)
    data = _bridge.invoke(*args, timeout=timeout)
    return ResultRecords._from_wire(data)

read_results_many

read_results_many(paths: PathList, *, out: str | PathLike[str] | None = None, threads: int = 1, on_error: str = 'fail', timeout: float | None = None) -> ReadBatch

Read many result files into one long table of the uniform record view.

:func:read_results for a list, which may mix the four quantifiable formats: the columns are the same for all of them. Their units need not be - use :meth:ReadBatch.in_minutes for retention_time - and each file's caveats are in its :class:FileReport.

Parameters:

Name Type Description Default
paths PathList

The files to read, as a list (or any iterable) of str or pathlib.Path. Read by ONE bridge process into ONE table, in this order; a file's position is its source_index. A repeated path is refused rather than read twice.

required
out str | PathLike[str] | None

Write the table to this path as tab-separated text and return only a summary (:attr:ReadBatch.output). Files are written one at a time, in order, so memory holds at most threads files however long the list is - the way to read hundreds. A batch that stops on an error removes its partial table. The path must end in .tsv (any case); any other is a UsageError before a read.

None
threads int

Files to read at once: 1 (the default), more, or -1 for one per core. The result is byte-identical at any value - files are always returned in input order - so this trades memory for speed and never changes an answer. The default is 1 because every reader holds a whole file in memory, so threads=8 can mean eight whole files at once.

1
on_error str

"fail" (the default) stops at the first file that cannot be read and raises, naming it; "skip" records the failure in that file's :attr:FileReport.error and reads the rest.

'fail'
timeout float | None

Seconds to allow for the whole batch. None (the default) waits indefinitely.

None

Returns:

Name Type Description
A ReadBatch

class:ReadBatch with the columns of :class:ResultRecords, after source_index and

ReadBatch

source_path.

Raises:

Type Description
UsageError

paths is not a list, is empty, or holds a blank entry; a path is repeated; threads or on_error is not a value listed above; or, with on_error="fail", a file is missing or is not one this function reads - the message then starts Input <i> ('<path>').

BridgeError

with on_error="fail", mzLib recognised a file and failed to parse it; the message names the input the same way.

Examples:

>>> # Not run in CI: the replay bridge answers single-path calls only.
>>> batch = read_results_many(["a/AllPSMs.psmtsv", "b/psm.tsv"], threads=2)
>>> [(f.file_type, f.record_count) for f in batch.files]
[('psmtsv', 8), ('MsFraggerPsm', 6)]
Source code in pkg/python/src/pymzlib/readers.py
def read_results_many(
    paths: PathList,
    *,
    out: str | os.PathLike[str] | None = None,
    threads: int = 1,
    on_error: str = "fail",
    timeout: float | None = None,
) -> ReadBatch:
    """Read many result files into one long table of the uniform record view.

    :func:`read_results` for a list, which may mix the four quantifiable formats: the columns are
    the same for all of them. Their units need not be - use :meth:`ReadBatch.in_minutes` for
    ``retention_time`` - and each file's caveats are in its :class:`FileReport`.

    Args:
        paths: The files to read, as a list (or any iterable) of ``str`` or ``pathlib.Path``.
            Read by ONE bridge process into ONE table, in this order; a file's position is its
            ``source_index``. A repeated path is refused rather than read twice.
        out: Write the table to this path as **tab-separated** text and return only a summary
            (:attr:`ReadBatch.output`). Files are written one at a time, in order, so memory holds
            at most ``threads`` files however long the list is - the way to read hundreds. A
            batch that stops on an error removes its partial table.
            The path must end in ``.tsv`` (any case); any other is a ``UsageError`` before a read.
        threads: Files to read at once: ``1`` (the default), more, or ``-1`` for one per core. The
            result is **byte-identical at any value** - files are always returned in input order -
            so this trades memory for speed and never changes an answer. The default is 1 because
            every reader holds a whole file in memory, so ``threads=8`` can mean eight whole files
            at once.
        on_error: ``"fail"`` (the default) stops at the first file that cannot be read and raises,
            naming it; ``"skip"`` records the failure in that file's :attr:`FileReport.error` and
            reads the rest.
        timeout: Seconds to allow for the whole batch. ``None`` (the default) waits indefinitely.

    Returns:
        A :class:`ReadBatch` with the columns of :class:`ResultRecords`, after ``source_index`` and
        ``source_path``.

    Raises:
        UsageError: ``paths`` is not a list, is empty, or holds a blank entry; a path is
            repeated; ``threads`` or ``on_error`` is not a value listed above; or, with
            ``on_error="fail"``, a file is missing or is not one this function reads - the
            message then starts ``Input <i> ('<path>')``.
        BridgeError: with ``on_error="fail"``, mzLib recognised a file and failed to parse it;
            the message names the input the same way.

    Examples:
        >>> # Not run in CI: the replay bridge answers single-path calls only.
        >>> batch = read_results_many(["a/AllPSMs.psmtsv", "b/psm.tsv"], threads=2)   # doctest: +SKIP
        >>> [(f.file_type, f.record_count) for f in batch.files]                      # doctest: +SKIP
        [('psmtsv', 8), ('MsFraggerPsm', 6)]
    """
    return _read_many("read-results", paths, out=out, threads=threads, on_error=on_error, timeout=timeout)

read_records

read_records(path: str | PathLike[str], *, limit: int | None = None, offset: int = 0, out: str | PathLike[str] | None = None, timeout: float | None = None) -> NativeRecords

Read any file mzLib recognises, into that format's own fields.

This is the exhaustive verb: if :func:identify succeeds on a path, this reads it. All thirty-eight file types, including the nineteen that belong to no cross-format view at all - TopPIC, Crux, MSFragger's peptide and protein tables, the FlashDeconv formats, MetaMorpheus's protein-group and peptide tables and their RNA counterparts - which no other function here can touch.

The columns are not uniform, by design. They are this format's own mzLib record fields, under mzLib's own names in snake_case: a TopPIC file gives thirty-six columns, a Crux file twenty-three, an experiment annotation five. Read :attr:NativeRecords.column_names, and use :func:read_results, :func:read_features or :func:read_matches when you need columns that mean the same thing across formats.

Nothing is silently dropped. A field that could not become a column is named in :attr:NativeRecords.excluded_fields, one that raised while being read in :attr:NativeRecords.failed_fields, and one this file's format has no column for in :attr:NativeRecords.absent_fields - so a missing value never has to be guessed at.

Parameters:

Name Type Description Default
path str | PathLike[str]

Path to any file mzLib recognises. A Bruker .d directory is also accepted.

required
limit int | None

Maximum records to return. None (the default) returns all of them.

None
offset int

Records to skip. A window, not a cursor - see :func:read_results.

0
out str | PathLike[str] | None

Write a tab-separated table here and return only a summary. The path must end in .tsv (any case); any other is a UsageError before a read.

None
timeout float | None

Seconds to allow. None waits indefinitely.

None

Returns:

Name Type Description
A NativeRecords

class:NativeRecords.

Raises:

Type Description
UsageError

the path is blank, missing, or not a file type mzLib recognises.

Examples:

>>> r = read_records("ToppicPrsm_TopPICv1.6.2_prsm.tsv")
>>> r.record_type, len(r.column_names), r.record_count
('ToppicPrsm', 36, 4)

pandas is not a dependency of pyMzLib, so this line is not executed:

>>> import pandas as pd
>>> pd.DataFrame(r.columns)[["e_value", "q_value_spectrum_level"]]
Source code in pkg/python/src/pymzlib/readers.py
def read_records(
    path: str | os.PathLike[str],
    *,
    limit: int | None = None,
    offset: int = 0,
    out: str | os.PathLike[str] | None = None,
    timeout: float | None = None,
) -> NativeRecords:
    """Read **any** file mzLib recognises, into that format's own fields.

    This is the exhaustive verb: if :func:`identify` succeeds on a path, this reads it. All
    thirty-eight file types, including the nineteen that belong to no cross-format view at all -
    TopPIC, Crux, MSFragger's peptide and protein tables, the FlashDeconv formats, MetaMorpheus's
    protein-group and peptide tables and their RNA counterparts - which no other
    function here can touch.

    **The columns are not uniform, by design.** They are this format's own mzLib record fields,
    under mzLib's own names in snake_case: a TopPIC file gives thirty-six columns, a Crux file
    twenty-three, an experiment annotation five. Read :attr:`NativeRecords.column_names`, and use
    :func:`read_results`, :func:`read_features` or :func:`read_matches` when you need columns that
    mean the same thing across formats.

    Nothing is silently dropped. A field that could not become a column is named in
    :attr:`NativeRecords.excluded_fields`, one that raised while being read in
    :attr:`NativeRecords.failed_fields`, and one this file's format has no column for in
    :attr:`NativeRecords.absent_fields` - so a missing value never has to be guessed at.

    Args:
        path: Path to any file mzLib recognises. A Bruker ``.d`` directory is also accepted.
        limit: Maximum records to return. ``None`` (the default) returns all of them.
        offset: Records to skip. A window, not a cursor - see :func:`read_results`.
        out: Write a **tab-separated** table here and return only a summary.
            The path must end in ``.tsv`` (any case); any other is a ``UsageError`` before a read.
        timeout: Seconds to allow. ``None`` waits indefinitely.

    Returns:
        A :class:`NativeRecords`.

    Raises:
        UsageError: the path is blank, missing, or not a file type mzLib recognises.

    Examples:
        >>> r = read_records("ToppicPrsm_TopPICv1.6.2_prsm.tsv")
        >>> r.record_type, len(r.column_names), r.record_count
        ('ToppicPrsm', 36, 4)

        pandas is not a dependency of pyMzLib, so this line is not executed:

        >>> import pandas as pd                                    # doctest: +SKIP
        >>> pd.DataFrame(r.columns)[["e_value", "q_value_spectrum_level"]]   # doctest: +SKIP
    """
    args = _window("read-records", path, limit=limit, offset=offset, out=out)
    data = _bridge.invoke(*args, timeout=timeout)
    return NativeRecords._from_wire(data)

read_records_many

read_records_many(paths: PathList, *, out: str | PathLike[str] | None = None, threads: int = 1, on_error: str = 'fail', timeout: float | None = None) -> ReadBatch

Read many files of ONE record type into one long table of their own fields.

:func:read_records for a list. Because the columns are the record type's own, every file must have the same record type - all mzIdentML, all FlashLFQ peaks tables, all TopPIC PrSMs. A mixed list is refused with a UsageError before anything is parsed, whatever on_error says, with the groups named so you can split it. Each file's record_type, absent_fields and mzIdentML skipped list are in its :class:FileReport.

Parameters:

Name Type Description Default
paths PathList

The files to read, as a list (or any iterable) of str or pathlib.Path. Read by ONE bridge process into ONE table, in this order; a file's position is its source_index. A repeated path is refused rather than read twice.

required
out str | PathLike[str] | None

Write the table to this path as tab-separated text and return only a summary (:attr:ReadBatch.output). Files are written one at a time, in order, so memory holds at most threads files however long the list is - the way to read hundreds. A batch that stops on an error removes its partial table. The path must end in .tsv (any case); any other is a UsageError before a read.

None
threads int

Files to read at once: 1 (the default), more, or -1 for one per core. The result is byte-identical at any value - files are always returned in input order - so this trades memory for speed and never changes an answer. The default is 1 because every reader holds a whole file in memory, so threads=8 can mean eight whole files at once.

1
on_error str

"fail" (the default) stops at the first file that cannot be read and raises, naming it; "skip" records the failure in that file's :attr:FileReport.error and reads the rest.

'fail'
timeout float | None

Seconds to allow for the whole batch. None (the default) waits indefinitely.

None

Returns:

Name Type Description
A ReadBatch

class:ReadBatch with the shared record type's columns after source_index and

ReadBatch

source_path.

Raises:

Type Description
UsageError

paths is not a list, is empty, or holds a blank entry; a path is repeated; threads or on_error is not a value listed above; or, with on_error="fail", a file is missing or is not one this function reads - the message then starts Input <i> ('<path>').

BridgeError

with on_error="fail", mzLib recognised a file and failed to parse it; the message names the input the same way.

Examples:

>>> # Not run in CI: the replay bridge answers single-path calls only.
>>> batch = read_records_many(sorted(Path("runs").glob("*.mzid.gz")), threads=4)
>>> batch.files[0].record_type, batch.record_count
('MzIdentMLRecord', 48213)
Source code in pkg/python/src/pymzlib/readers.py
def read_records_many(
    paths: PathList,
    *,
    out: str | os.PathLike[str] | None = None,
    threads: int = 1,
    on_error: str = "fail",
    timeout: float | None = None,
) -> ReadBatch:
    """Read many files of ONE record type into one long table of their own fields.

    :func:`read_records` for a list. Because the columns are the record type's own, **every file
    must have the same record type** - all mzIdentML, all FlashLFQ peaks tables, all TopPIC PrSMs.
    A mixed list is refused with a ``UsageError`` before anything is parsed, whatever ``on_error``
    says, with the groups named so you can split it.
    Each file's ``record_type``, ``absent_fields`` and mzIdentML ``skipped`` list are in its
    :class:`FileReport`.

    Args:
        paths: The files to read, as a list (or any iterable) of ``str`` or ``pathlib.Path``.
            Read by ONE bridge process into ONE table, in this order; a file's position is its
            ``source_index``. A repeated path is refused rather than read twice.
        out: Write the table to this path as **tab-separated** text and return only a summary
            (:attr:`ReadBatch.output`). Files are written one at a time, in order, so memory holds
            at most ``threads`` files however long the list is - the way to read hundreds. A
            batch that stops on an error removes its partial table.
            The path must end in ``.tsv`` (any case); any other is a ``UsageError`` before a read.
        threads: Files to read at once: ``1`` (the default), more, or ``-1`` for one per core. The
            result is **byte-identical at any value** - files are always returned in input order -
            so this trades memory for speed and never changes an answer. The default is 1 because
            every reader holds a whole file in memory, so ``threads=8`` can mean eight whole files
            at once.
        on_error: ``"fail"`` (the default) stops at the first file that cannot be read and raises,
            naming it; ``"skip"`` records the failure in that file's :attr:`FileReport.error` and
            reads the rest.
        timeout: Seconds to allow for the whole batch. ``None`` (the default) waits indefinitely.

    Returns:
        A :class:`ReadBatch` with the shared record type's columns after ``source_index`` and
        ``source_path``.

    Raises:
        UsageError: ``paths`` is not a list, is empty, or holds a blank entry; a path is
            repeated; ``threads`` or ``on_error`` is not a value listed above; or, with
            ``on_error="fail"``, a file is missing or is not one this function reads - the
            message then starts ``Input <i> ('<path>')``.
        BridgeError: with ``on_error="fail"``, mzLib recognised a file and failed to parse it;
            the message names the input the same way.

    Examples:
        >>> # Not run in CI: the replay bridge answers single-path calls only.
        >>> batch = read_records_many(sorted(Path("runs").glob("*.mzid.gz")), threads=4)   # doctest: +SKIP
        >>> batch.files[0].record_type, batch.record_count                                 # doctest: +SKIP
        ('MzIdentMLRecord', 48213)
    """
    return _read_many("read-records", paths, out=out, threads=threads, on_error=on_error, timeout=timeout)

read_features

read_features(path: str | PathLike[str], *, limit: int | None = None, offset: int = 0, out: str | PathLike[str] | None = None, timeout: float | None = None) -> FeatureRecords

Read deconvolved MS1 features, in the cross-format ms1_features view.

Two file types offer it: TopFD/FLASHDeconv _ms1.feature and Dinosaur .feature.tsv. A file without the view is rejected with a message naming the views it does have.

One row is not one line of the file for _ms1.feature. mzLib expands each deconvolved feature into one single-charge feature per charge in its recorded range, so a hundred-feature file can read as a thousand rows. Dinosaur is one-for-one. Both facts are in :attr:FeatureRecords.caveats, and :func:read_records gives the file's own rows either way.

Parameters:

Name Type Description Default
path str | PathLike[str]

Path to a _ms1.feature or Dinosaur .feature.tsv.

required
limit int | None

Maximum features to return. None returns all of them.

None
offset int

Features to skip.

0
out str | PathLike[str] | None

Write a tab-separated table here and return only a summary. The path must end in .tsv (any case); any other is a UsageError before a read.

None
timeout float | None

Seconds to allow.

None

Returns:

Name Type Description
A FeatureRecords

class:FeatureRecords. Check :attr:FeatureRecords.retention_time_unit before

FeatureRecords

comparing times - it is "unknown" for _ms1.feature and that is the honest answer.

Raises:

Type Description
UsageError

the path is blank, missing, unrecognised, or has no ms1_features view.

Examples:

>>> f = read_features("Ms1Feature_TopFDv1.6.2_ms1.feature", limit=5)
>>> f.record_count, f.returned_count, f.retention_time_unit
(25, 5, 'unknown')
>>> f.absent_fields
['number_of_isotopes']
Source code in pkg/python/src/pymzlib/readers.py
def read_features(
    path: str | os.PathLike[str],
    *,
    limit: int | None = None,
    offset: int = 0,
    out: str | os.PathLike[str] | None = None,
    timeout: float | None = None,
) -> FeatureRecords:
    """Read deconvolved MS1 features, in the cross-format ``ms1_features`` view.

    Two file types offer it: TopFD/FLASHDeconv ``_ms1.feature`` and Dinosaur ``.feature.tsv``. A
    file without the view is rejected with a message naming the views it does have.

    **One row is not one line of the file for ``_ms1.feature``.** mzLib expands each deconvolved
    feature into one single-charge feature per charge in its recorded range, so a hundred-feature
    file can read as a thousand rows. Dinosaur is one-for-one. Both facts are in
    :attr:`FeatureRecords.caveats`, and :func:`read_records` gives the file's own rows either way.

    Args:
        path: Path to a ``_ms1.feature`` or Dinosaur ``.feature.tsv``.
        limit: Maximum features to return. ``None`` returns all of them.
        offset: Features to skip.
        out: Write a tab-separated table here and return only a summary.
            The path must end in ``.tsv`` (any case); any other is a ``UsageError`` before a read.
        timeout: Seconds to allow.

    Returns:
        A :class:`FeatureRecords`. Check :attr:`FeatureRecords.retention_time_unit` before
        comparing times - it is ``"unknown"`` for ``_ms1.feature`` and that is the honest answer.

    Raises:
        UsageError: the path is blank, missing, unrecognised, or has no ``ms1_features`` view.

    Examples:
        >>> f = read_features("Ms1Feature_TopFDv1.6.2_ms1.feature", limit=5)
        >>> f.record_count, f.returned_count, f.retention_time_unit
        (25, 5, 'unknown')
        >>> f.absent_fields
        ['number_of_isotopes']
    """
    args = _window("read-features", path, limit=limit, offset=offset, out=out)
    data = _bridge.invoke(*args, timeout=timeout)
    return FeatureRecords._from_wire(data)

read_features_many

read_features_many(paths: PathList, *, out: str | PathLike[str] | None = None, threads: int = 1, on_error: str = 'fail', timeout: float | None = None) -> ReadBatch

Read many feature files into one long table of the ms1_features view.

:func:read_features for a list. Units differ per file - _ms1.feature times are genuinely of unknown unit - so :meth:ReadBatch.in_minutes raises rather than put two units on one axis.

Parameters:

Name Type Description Default
paths PathList

The files to read, as a list (or any iterable) of str or pathlib.Path. Read by ONE bridge process into ONE table, in this order; a file's position is its source_index. A repeated path is refused rather than read twice.

required
out str | PathLike[str] | None

Write the table to this path as tab-separated text and return only a summary (:attr:ReadBatch.output). Files are written one at a time, in order, so memory holds at most threads files however long the list is - the way to read hundreds. A batch that stops on an error removes its partial table. The path must end in .tsv (any case); any other is a UsageError before a read.

None
threads int

Files to read at once: 1 (the default), more, or -1 for one per core. The result is byte-identical at any value - files are always returned in input order - so this trades memory for speed and never changes an answer. The default is 1 because every reader holds a whole file in memory, so threads=8 can mean eight whole files at once.

1
on_error str

"fail" (the default) stops at the first file that cannot be read and raises, naming it; "skip" records the failure in that file's :attr:FileReport.error and reads the rest.

'fail'
timeout float | None

Seconds to allow for the whole batch. None (the default) waits indefinitely.

None

Returns:

Name Type Description
A ReadBatch

class:ReadBatch with the columns of :class:FeatureRecords, after source_index

ReadBatch

and source_path.

Raises:

Type Description
UsageError

paths is not a list, is empty, or holds a blank entry; a path is repeated; threads or on_error is not a value listed above; or, with on_error="fail", a file is missing or is not one this function reads - the message then starts Input <i> ('<path>').

BridgeError

with on_error="fail", mzLib recognised a file and failed to parse it; the message names the input the same way.

Examples:

>>> # Not run in CI: the replay bridge answers single-path calls only.
>>> batch = read_features_many(["a.feature.tsv", "b.feature.tsv"])
>>> batch.file_count, batch.read_count
(2, 2)
Source code in pkg/python/src/pymzlib/readers.py
def read_features_many(
    paths: PathList,
    *,
    out: str | os.PathLike[str] | None = None,
    threads: int = 1,
    on_error: str = "fail",
    timeout: float | None = None,
) -> ReadBatch:
    """Read many feature files into one long table of the ``ms1_features`` view.

    :func:`read_features` for a list. Units differ per file - ``_ms1.feature`` times are genuinely
    of unknown unit - so :meth:`ReadBatch.in_minutes` raises rather than put two units on one axis.

    Args:
        paths: The files to read, as a list (or any iterable) of ``str`` or ``pathlib.Path``.
            Read by ONE bridge process into ONE table, in this order; a file's position is its
            ``source_index``. A repeated path is refused rather than read twice.
        out: Write the table to this path as **tab-separated** text and return only a summary
            (:attr:`ReadBatch.output`). Files are written one at a time, in order, so memory holds
            at most ``threads`` files however long the list is - the way to read hundreds. A
            batch that stops on an error removes its partial table.
            The path must end in ``.tsv`` (any case); any other is a ``UsageError`` before a read.
        threads: Files to read at once: ``1`` (the default), more, or ``-1`` for one per core. The
            result is **byte-identical at any value** - files are always returned in input order -
            so this trades memory for speed and never changes an answer. The default is 1 because
            every reader holds a whole file in memory, so ``threads=8`` can mean eight whole files
            at once.
        on_error: ``"fail"`` (the default) stops at the first file that cannot be read and raises,
            naming it; ``"skip"`` records the failure in that file's :attr:`FileReport.error` and
            reads the rest.
        timeout: Seconds to allow for the whole batch. ``None`` (the default) waits indefinitely.

    Returns:
        A :class:`ReadBatch` with the columns of :class:`FeatureRecords`, after ``source_index``
        and ``source_path``.

    Raises:
        UsageError: ``paths`` is not a list, is empty, or holds a blank entry; a path is
            repeated; ``threads`` or ``on_error`` is not a value listed above; or, with
            ``on_error="fail"``, a file is missing or is not one this function reads - the
            message then starts ``Input <i> ('<path>')``.
        BridgeError: with ``on_error="fail"``, mzLib recognised a file and failed to parse it;
            the message names the input the same way.

    Examples:
        >>> # Not run in CI: the replay bridge answers single-path calls only.
        >>> batch = read_features_many(["a.feature.tsv", "b.feature.tsv"])      # doctest: +SKIP
        >>> batch.file_count, batch.read_count                                  # doctest: +SKIP
        (2, 2)
    """
    return _read_many("read-features", paths, out=out, threads=threads, on_error=on_error, timeout=timeout)

read_matches

read_matches(path: str | PathLike[str], *, limit: int | None = None, offset: int = 0, scores: bool = False, out: str | PathLike[str] | None = None, timeout: float | None = None) -> MatchRecords

Read identifications, in the cross-format spectral_match view.

Six file types offer it: MsPathFinderT's targets, decoys and combined results, Casanovo's .mztab, and mzIdentML (.mzid, and .mzid.gz read without unpacking it, mzLib #1313). These are the identification formats that share no file-level interface, so :func:read_results cannot reach them.

Nothing here is FDR-filtered. q_value is the only confidence column, filled by mzIdentML (the PSM-level q-value, mzLib #1306) and by an MsPathFinderT _IcTda.tsv; for every other file it is named in :attr:MatchRecords.absent_fields and None. mzIdentML also lists every candidate, not only accepted ones: filter on rank == 1 and pass_threshold before counting.

Parameters:

Name Type Description Default
path str | PathLike[str]

Path to an MsPathFinderT _IcTarget.tsv / _IcDecoy.tsv / _IcTda.tsv, a Casanovo .mztab, or an mzIdentML .mzid / .mzid.gz.

required
limit int | None

Maximum matches to return. None returns all of them.

None
offset int

Matches to skip.

0
scores bool

Add each search engine's own scores as long rows: one row per match and score, with match_index (the match's 0-based position among the file's matches), score_name (e.g. "MS-GF:SpecEValue") and score_value. Only mzIdentML records them (mzLib #1306); other files keep one row per match and name the two columns in absent_fields. limit and offset still count matches.

False
out str | PathLike[str] | None

Write a tab-separated table here and return only a summary. The path must end in .tsv (any case); any other is a UsageError before a read.

None
timeout float | None

Seconds to allow.

None

Returns:

Name Type Description
A MatchRecords

class:MatchRecords. Read :attr:MatchRecords.caveats before trusting is_decoy -

MatchRecords

it is inferred from a name prefix for MsPathFinderT and absent for Casanovo and mzIdentML.

Raises:

Type Description
UsageError

the path is blank, missing, unrecognised, or has no spectral_match view.

Examples:

>>> m = read_matches("MsPathFinderT_WithMods_IcTda.tsv")
>>> m.record_count, m.columns["modifications"][:3]
(5, ['37:Oxidation on M', '', '1:Acetylation on X;27:Acetylation on K'])
>>> s = read_matches("PXD078927_msgf_1_1_0.mzid", limit=1, scores=True)
>>> s.columns["score_name"][:3], s.columns["q_value"][0]
(['MS-GF:RawScore', 'MS-GF:DeNovoScore', 'MS-GF:SpecEValue'], 0)
Source code in pkg/python/src/pymzlib/readers.py
def read_matches(
    path: str | os.PathLike[str],
    *,
    limit: int | None = None,
    offset: int = 0,
    scores: bool = False,
    out: str | os.PathLike[str] | None = None,
    timeout: float | None = None,
) -> MatchRecords:
    """Read identifications, in the cross-format ``spectral_match`` view.

    Six file types offer it: MsPathFinderT's targets, decoys and combined results, Casanovo's
    ``.mztab``, and mzIdentML (``.mzid``, and ``.mzid.gz`` read without unpacking it, mzLib #1313).
    These are the identification formats that share no *file*-level interface, so
    :func:`read_results` cannot reach them.

    **Nothing here is FDR-filtered.** ``q_value`` is the only confidence column, filled by
    mzIdentML (the PSM-level q-value, mzLib #1306) and by an MsPathFinderT ``_IcTda.tsv``; for
    every other file it is named in :attr:`MatchRecords.absent_fields` and ``None``. mzIdentML
    also lists every candidate, not only accepted ones: filter on ``rank == 1`` and
    ``pass_threshold`` before counting.

    Args:
        path: Path to an MsPathFinderT ``_IcTarget.tsv`` / ``_IcDecoy.tsv`` / ``_IcTda.tsv``, a
            Casanovo ``.mztab``, or an mzIdentML ``.mzid`` / ``.mzid.gz``.
        limit: Maximum matches to return. ``None`` returns all of them.
        offset: Matches to skip.
        scores: Add each search engine's own scores as **long** rows: one row per match and
            score, with ``match_index`` (the match's 0-based position among the file's matches),
            ``score_name`` (e.g. ``"MS-GF:SpecEValue"``) and ``score_value``. Only mzIdentML
            records them (mzLib #1306); other files keep one row per match and name the two
            columns in ``absent_fields``. ``limit`` and ``offset`` still count matches.
        out: Write a tab-separated table here and return only a summary.
            The path must end in ``.tsv`` (any case); any other is a ``UsageError`` before a read.
        timeout: Seconds to allow.

    Returns:
        A :class:`MatchRecords`. Read :attr:`MatchRecords.caveats` before trusting ``is_decoy`` -
        it is inferred from a name prefix for MsPathFinderT and absent for Casanovo and mzIdentML.

    Raises:
        UsageError: the path is blank, missing, unrecognised, or has no ``spectral_match`` view.

    Examples:
        >>> m = read_matches("MsPathFinderT_WithMods_IcTda.tsv")
        >>> m.record_count, m.columns["modifications"][:3]
        (5, ['37:Oxidation on M', '', '1:Acetylation on X;27:Acetylation on K'])
        >>> s = read_matches("PXD078927_msgf_1_1_0.mzid", limit=1, scores=True)
        >>> s.columns["score_name"][:3], s.columns["q_value"][0]
        (['MS-GF:RawScore', 'MS-GF:DeNovoScore', 'MS-GF:SpecEValue'], 0)
    """
    args = _window("read-matches", path, limit=limit, offset=offset, out=out)
    if scores:
        args += ["--scores"]
    data = _bridge.invoke(*args, timeout=timeout)
    return MatchRecords._from_wire(data)

read_matches_many

read_matches_many(paths: PathList, *, scores: bool = False, out: str | PathLike[str] | None = None, threads: int = 1, on_error: str = 'fail', timeout: float | None = None) -> ReadBatch

Read many identification files into one long table of the spectral_match view.

:func:read_matches for a list - a directory of mzIdentML submissions, say. Each file's skipped items and absent_fields are in its :class:FileReport.

Parameters:

Name Type Description Default
paths PathList

The files to read, as a list (or any iterable) of str or pathlib.Path. Read by ONE bridge process into ONE table, in this order; a file's position is its source_index. A repeated path is refused rather than read twice.

required
out str | PathLike[str] | None

Write the table to this path as tab-separated text and return only a summary (:attr:ReadBatch.output). Files are written one at a time, in order, so memory holds at most threads files however long the list is - the way to read hundreds. A batch that stops on an error removes its partial table. The path must end in .tsv (any case); any other is a UsageError before a read.

None
threads int

Files to read at once: 1 (the default), more, or -1 for one per core. The result is byte-identical at any value - files are always returned in input order - so this trades memory for speed and never changes an answer. The default is 1 because every reader holds a whole file in memory, so threads=8 can mean eight whole files at once.

1
on_error str

"fail" (the default) stops at the first file that cannot be read and raises, naming it; "skip" records the failure in that file's :attr:FileReport.error and reads the rest.

'fail'
timeout float | None

Seconds to allow for the whole batch. None (the default) waits indefinitely.

None
scores bool

As for :func:read_matches: one row per match and score, for the files that record scores (mzIdentML).

False

Returns:

Name Type Description
A ReadBatch

class:ReadBatch with the columns of :class:MatchRecords, after source_index and

ReadBatch

source_path.

Raises:

Type Description
UsageError

paths is not a list, is empty, or holds a blank entry; a path is repeated; threads or on_error is not a value listed above; or, with on_error="fail", a file is missing or is not one this function reads - the message then starts Input <i> ('<path>').

BridgeError

with on_error="fail", mzLib recognised a file and failed to parse it; the message names the input the same way.

Examples:

>>> # Not run in CI: the replay bridge answers single-path calls only.
>>> batch = read_matches_many(["a.mzid", "b.mzid.gz"], scores=True)
>>> sorted(set(batch.columns["score_name"]))[:2]
['MS-GF:DeNovoScore', 'MS-GF:EValue']
Source code in pkg/python/src/pymzlib/readers.py
def read_matches_many(
    paths: PathList,
    *,
    scores: bool = False,
    out: str | os.PathLike[str] | None = None,
    threads: int = 1,
    on_error: str = "fail",
    timeout: float | None = None,
) -> ReadBatch:
    """Read many identification files into one long table of the ``spectral_match`` view.

    :func:`read_matches` for a list - a directory of mzIdentML submissions, say. Each file's
    ``skipped`` items and ``absent_fields`` are in its :class:`FileReport`.

    Args:
        paths: The files to read, as a list (or any iterable) of ``str`` or ``pathlib.Path``.
            Read by ONE bridge process into ONE table, in this order; a file's position is its
            ``source_index``. A repeated path is refused rather than read twice.
        out: Write the table to this path as **tab-separated** text and return only a summary
            (:attr:`ReadBatch.output`). Files are written one at a time, in order, so memory holds
            at most ``threads`` files however long the list is - the way to read hundreds. A
            batch that stops on an error removes its partial table.
            The path must end in ``.tsv`` (any case); any other is a ``UsageError`` before a read.
        threads: Files to read at once: ``1`` (the default), more, or ``-1`` for one per core. The
            result is **byte-identical at any value** - files are always returned in input order -
            so this trades memory for speed and never changes an answer. The default is 1 because
            every reader holds a whole file in memory, so ``threads=8`` can mean eight whole files
            at once.
        on_error: ``"fail"`` (the default) stops at the first file that cannot be read and raises,
            naming it; ``"skip"`` records the failure in that file's :attr:`FileReport.error` and
            reads the rest.
        timeout: Seconds to allow for the whole batch. ``None`` (the default) waits indefinitely.
        scores: As for :func:`read_matches`: one row per match and score, for the files that
            record scores (mzIdentML).

    Returns:
        A :class:`ReadBatch` with the columns of :class:`MatchRecords`, after ``source_index`` and
        ``source_path``.

    Raises:
        UsageError: ``paths`` is not a list, is empty, or holds a blank entry; a path is
            repeated; ``threads`` or ``on_error`` is not a value listed above; or, with
            ``on_error="fail"``, a file is missing or is not one this function reads - the
            message then starts ``Input <i> ('<path>')``.
        BridgeError: with ``on_error="fail"``, mzLib recognised a file and failed to parse it;
            the message names the input the same way.

    Examples:
        >>> # Not run in CI: the replay bridge answers single-path calls only.
        >>> batch = read_matches_many(["a.mzid", "b.mzid.gz"], scores=True)     # doctest: +SKIP
        >>> sorted(set(batch.columns["score_name"]))[:2]                        # doctest: +SKIP
        ['MS-GF:DeNovoScore', 'MS-GF:EValue']
    """
    return _read_many("read-matches", paths, out=out, threads=threads, on_error=on_error, timeout=timeout,
                      extra=("--scores",) if scores else ())

read_spectra

read_spectra(path: str | PathLike[str], *, limit: int | None = None, offset: int = 0, ms_order: int | None = None, peaks: bool = False, out: str | PathLike[str] | None = None, timeout: float | None = None) -> ScanRecords

Read the scans of a spectra file: headers always, peaks on request.

Seven file types offer the spectra view: .mzML, .mgf, _ms1.msalign, _ms2.msalign, Thermo .raw, Bruker .d and timsTOF .d. Each read also reports what the file records about the run - instrument model and serial number, and when acquisition started (:attr:ScanRecords.source, mzLib #1349).

Peaks are opt-in and should stay that way unless you need them. A scan header is tens of bytes; its peak list is thousands, and a mid-size mzML holds tens of thousands of scans. With peaks=True the mz and intensity columns each become a list of arrays, one per scan - so pair it with limit, ms_order or out.

Parameters:

Name Type Description Default
path str | PathLike[str]

Path to a spectra file. A Bruker .d directory is also accepted.

required
limit int | None

Maximum scans to return. None returns all of them.

None
offset int

Scans to skip, applied after ms_order.

0
ms_order int | None

Keep only scans at this MS level - 1 for survey scans, 2 for fragment scans. Applied before offset and limit, so ms_order=2, limit=10 means the first ten MS2 scans rather than the MS2 scans among the first ten.

None
peaks bool

Include the mz and intensity arrays. Off by default.

False
out str | PathLike[str] | None

Write a tab-separated table here and return only a summary. With peaks=True each cell holds a ;-joined list. The path must end in .tsv (any case); any other is a UsageError before a read.

None
timeout float | None

Seconds to allow. Reading a large .raw legitimately takes a while.

None

Returns:

Name Type Description
A ScanRecords

class:ScanRecords. Retention times are in minutes for every format.

Raises:

Type Description
UsageError

the path is blank, missing, unrecognised, has no spectra view, or ms_order is less than 1.

Examples:

>>> s = read_spectra("sliced_ethcd.mzML", ms_order=2, limit=2)
>>> s.scan_count, s.record_count, s.returned_count
(6, 5, 2)
>>> s.columns["selected_ion_mz"]
[548.453918457031, 796.765197753906]
>>> s.source.instrument_model, s.source.acquisition_start_time
('Orbitrap Fusion', '2021-03-16T17:09:07Z')
Source code in pkg/python/src/pymzlib/readers.py
def read_spectra(
    path: str | os.PathLike[str],
    *,
    limit: int | None = None,
    offset: int = 0,
    ms_order: int | None = None,
    peaks: bool = False,
    out: str | os.PathLike[str] | None = None,
    timeout: float | None = None,
) -> ScanRecords:
    """Read the scans of a spectra file: headers always, peaks on request.

    Seven file types offer the ``spectra`` view: ``.mzML``, ``.mgf``, ``_ms1.msalign``,
    ``_ms2.msalign``, Thermo ``.raw``, Bruker ``.d`` and timsTOF ``.d``. Each read also reports
    what the file records about the run - instrument model and serial number, and when acquisition
    started (:attr:`ScanRecords.source`, mzLib #1349).

    **Peaks are opt-in and should stay that way unless you need them.** A scan header is tens of
    bytes; its peak list is thousands, and a mid-size mzML holds tens of thousands of scans. With
    ``peaks=True`` the ``mz`` and ``intensity`` columns each become a list of arrays, one per scan -
    so pair it with ``limit``, ``ms_order`` or ``out``.

    Args:
        path: Path to a spectra file. A Bruker ``.d`` directory is also accepted.
        limit: Maximum scans to return. ``None`` returns all of them.
        offset: Scans to skip, applied **after** ``ms_order``.
        ms_order: Keep only scans at this MS level - ``1`` for survey scans, ``2`` for fragment
            scans. Applied before ``offset`` and ``limit``, so ``ms_order=2, limit=10`` means the
            first ten MS2 scans rather than the MS2 scans among the first ten.
        peaks: Include the ``mz`` and ``intensity`` arrays. Off by default.
        out: Write a tab-separated table here and return only a summary. With ``peaks=True`` each
            cell holds a ``;``-joined list.
            The path must end in ``.tsv`` (any case); any other is a ``UsageError`` before a read.
        timeout: Seconds to allow. Reading a large ``.raw`` legitimately takes a while.

    Returns:
        A :class:`ScanRecords`. Retention times are in minutes for every format.

    Raises:
        UsageError: the path is blank, missing, unrecognised, has no ``spectra`` view, or
            ``ms_order`` is less than 1.

    Examples:
        >>> s = read_spectra("sliced_ethcd.mzML", ms_order=2, limit=2)
        >>> s.scan_count, s.record_count, s.returned_count
        (6, 5, 2)
        >>> s.columns["selected_ion_mz"]
        [548.453918457031, 796.765197753906]
        >>> s.source.instrument_model, s.source.acquisition_start_time
        ('Orbitrap Fusion', '2021-03-16T17:09:07Z')
    """
    args = _window("read-spectra", path, limit=limit, offset=offset, out=out)
    args += list(_ms_order_args(ms_order, peaks))
    data = _bridge.invoke(*args, timeout=timeout)
    return ScanRecords._from_wire(data)

read_spectra_many

read_spectra_many(paths: PathList, *, ms_order: int | None = None, peaks: bool = False, out: str | PathLike[str] | None = None, threads: int = 1, on_error: str = 'fail', timeout: float | None = None) -> ReadBatch

Read the scans of many spectra files into one long table.

:func:read_spectra for a list - every run of an experiment, with each file's instrument, serial number and acquisition start in its :attr:FileReport.source. Retention times are minutes for every format, so the table is on one time axis.

Parameters:

Name Type Description Default
paths PathList

The files to read, as a list (or any iterable) of str or pathlib.Path. Read by ONE bridge process into ONE table, in this order; a file's position is its source_index. A repeated path is refused rather than read twice.

required
out str | PathLike[str] | None

Write the table to this path as tab-separated text and return only a summary (:attr:ReadBatch.output). Files are written one at a time, in order, so memory holds at most threads files however long the list is - the way to read hundreds. A batch that stops on an error removes its partial table. The path must end in .tsv (any case); any other is a UsageError before a read.

None
threads int

Files to read at once: 1 (the default), more, or -1 for one per core. The result is byte-identical at any value - files are always returned in input order - so this trades memory for speed and never changes an answer. The default is 1 because every reader holds a whole file in memory, so threads=8 can mean eight whole files at once.

1
on_error str

"fail" (the default) stops at the first file that cannot be read and raises, naming it; "skip" records the failure in that file's :attr:FileReport.error and reads the rest.

'fail'
timeout float | None

Seconds to allow for the whole batch. None (the default) waits indefinitely.

None
ms_order int | None

Keep only scans at this MS level, in every file.

None
peaks bool

Include the mz and intensity arrays. Off by default, and worth keeping off for a list: with peaks, write the batch to out.

False

Returns:

Name Type Description
A ReadBatch

class:ReadBatch with the columns of :class:ScanRecords, after source_index and

ReadBatch

source_path.

Raises:

Type Description
UsageError

paths is not a list, is empty, or holds a blank entry; a path is repeated; threads or on_error is not a value listed above; or, with on_error="fail", a file is missing or is not one this function reads - the message then starts Input <i> ('<path>').

BridgeError

with on_error="fail", mzLib recognised a file and failed to parse it; the message names the input the same way.

Examples:

>>> # Not run in CI: the replay bridge answers single-path calls only.
>>> runs = read_spectra_many(["run1.raw", "run2.raw"], ms_order=1, threads=2)
>>> [f.source.instrument_serial_number for f in runs.files]
['FSN20410', 'FSN20410']
Source code in pkg/python/src/pymzlib/readers.py
def read_spectra_many(
    paths: PathList,
    *,
    ms_order: int | None = None,
    peaks: bool = False,
    out: str | os.PathLike[str] | None = None,
    threads: int = 1,
    on_error: str = "fail",
    timeout: float | None = None,
) -> ReadBatch:
    """Read the scans of many spectra files into one long table.

    :func:`read_spectra` for a list - every run of an experiment, with each file's instrument,
    serial number and acquisition start in its :attr:`FileReport.source`. Retention times are
    minutes for every format, so the table is on one time axis.

    Args:
        paths: The files to read, as a list (or any iterable) of ``str`` or ``pathlib.Path``.
            Read by ONE bridge process into ONE table, in this order; a file's position is its
            ``source_index``. A repeated path is refused rather than read twice.
        out: Write the table to this path as **tab-separated** text and return only a summary
            (:attr:`ReadBatch.output`). Files are written one at a time, in order, so memory holds
            at most ``threads`` files however long the list is - the way to read hundreds. A
            batch that stops on an error removes its partial table.
            The path must end in ``.tsv`` (any case); any other is a ``UsageError`` before a read.
        threads: Files to read at once: ``1`` (the default), more, or ``-1`` for one per core. The
            result is **byte-identical at any value** - files are always returned in input order -
            so this trades memory for speed and never changes an answer. The default is 1 because
            every reader holds a whole file in memory, so ``threads=8`` can mean eight whole files
            at once.
        on_error: ``"fail"`` (the default) stops at the first file that cannot be read and raises,
            naming it; ``"skip"`` records the failure in that file's :attr:`FileReport.error` and
            reads the rest.
        timeout: Seconds to allow for the whole batch. ``None`` (the default) waits indefinitely.
        ms_order: Keep only scans at this MS level, in every file.
        peaks: Include the ``mz`` and ``intensity`` arrays. Off by default, and worth keeping off
            for a list: with peaks, write the batch to ``out``.

    Returns:
        A :class:`ReadBatch` with the columns of :class:`ScanRecords`, after ``source_index`` and
        ``source_path``.

    Raises:
        UsageError: ``paths`` is not a list, is empty, or holds a blank entry; a path is
            repeated; ``threads`` or ``on_error`` is not a value listed above; or, with
            ``on_error="fail"``, a file is missing or is not one this function reads - the
            message then starts ``Input <i> ('<path>')``.
        BridgeError: with ``on_error="fail"``, mzLib recognised a file and failed to parse it;
            the message names the input the same way.

    Examples:
        >>> # Not run in CI: the replay bridge answers single-path calls only.
        >>> runs = read_spectra_many(["run1.raw", "run2.raw"], ms_order=1, threads=2)   # doctest: +SKIP
        >>> [f.source.instrument_serial_number for f in runs.files]                   # doctest: +SKIP
        ['FSN20410', 'FSN20410']
    """
    return _read_many("read-spectra", paths, out=out, threads=threads, on_error=on_error, timeout=timeout,
                      extra=_ms_order_args(ms_order, peaks))

read_protein_groups

read_protein_groups(path: str | PathLike[str], *, limit: int | None = None, offset: int = 0, out: str | PathLike[str] | None = None, timeout: float | None = None) -> ProteinGroupRecords

Read a MetaMorpheus protein-group table as one row per group per sample group.

AllQuantifiedProteinGroups.tsv keeps each group's per-sample spectral counts and intensities in columns named after the samples, which mzLib 1.0.592 reads into a dictionary (mzLib #1347) that :func:read_records cannot project. This is that dictionary as a long table - sample_label, spectral_count, intensity - beside the fields you filter on. A wide matrix is one pivot away; the readers guide shows it.

The table is unfiltered, as MetaMorpheus writes it: decoys, contaminants and groups above 1% FDR are all rows. The group's other fields - coverage, masses, member counts - are in :func:read_records, joined on protein_group_name; PTM site occupancy is :func:read_occupancy.

Parameters:

Name Type Description Default
path str | PathLike[str]

Path to a MetaMorpheus protein-group table (:func:identify reports MetaMorpheusQuantifiedProteinGroups): AllQuantifiedProteinGroups.tsv, or, since mzLib 1.0.593 (#1365), AllProteinGroups.tsv and <file>_ProteinGroups.tsv from a search without quantification - whose rows have no intensity, named in absent_fields. An RNA search's AllQuantifiedTranscriptGroups.tsv (#1388) reads too, with the transcript group in protein_group_name.

required
limit int | None

Maximum groups to return; each group gives one row per sample group. None returns all of them.

None
offset int

Groups to skip.

0
out str | PathLike[str] | None

Write a tab-separated table here and return only a summary. The path must end in .tsv (any case); any other is a UsageError before a read.

None
timeout float | None

Seconds to allow.

None

Returns:

Name Type Description
A ProteinGroupRecords

class:ProteinGroupRecords.

Raises:

Type Description
UsageError

the path is blank or missing, or is not a protein-group table; or the bridge in use predates this function.

Examples:

>>> g = read_protein_groups("MetaMorpheus_1.1.11_AllQuantifiedProteinGroups.tsv", limit=1)
>>> g.record_count, g.returned_count, len(g.sample_labels), g.row_count
(6, 1, 18, 18)
>>> g.records[0]["sample_label"], g.records[0]["intensity"]
('QE-002106_GM1_a-calib', 1838317.1674502683)
Source code in pkg/python/src/pymzlib/readers.py
def read_protein_groups(
    path: str | os.PathLike[str],
    *,
    limit: int | None = None,
    offset: int = 0,
    out: str | os.PathLike[str] | None = None,
    timeout: float | None = None,
) -> ProteinGroupRecords:
    """Read a MetaMorpheus protein-group table as one row per group per sample group.

    ``AllQuantifiedProteinGroups.tsv`` keeps each group's per-sample spectral counts and
    intensities in columns named after the samples, which mzLib 1.0.592 reads into a dictionary
    (mzLib #1347) that :func:`read_records` cannot project. This is that dictionary as a **long**
    table - ``sample_label``, ``spectral_count``, ``intensity`` - beside the fields you filter on.
    A wide matrix is one ``pivot`` away; the readers guide shows it.

    **The table is unfiltered**, as MetaMorpheus writes it: decoys, contaminants and groups above
    1% FDR are all rows. The group's other fields - coverage, masses, member counts - are in
    :func:`read_records`, joined on ``protein_group_name``; PTM site occupancy is
    :func:`read_occupancy`.

    Args:
        path: Path to a MetaMorpheus protein-group table (:func:`identify` reports
            ``MetaMorpheusQuantifiedProteinGroups``): ``AllQuantifiedProteinGroups.tsv``, or, since
            mzLib 1.0.593 (#1365), ``AllProteinGroups.tsv`` and ``<file>_ProteinGroups.tsv`` from a
            search without quantification - whose rows have no ``intensity``, named in
            ``absent_fields``. An RNA search's ``AllQuantifiedTranscriptGroups.tsv`` (#1388) reads
            too, with the transcript group in ``protein_group_name``.
        limit: Maximum groups to return; each group gives one row per sample group. ``None``
            returns all of them.
        offset: Groups to skip.
        out: Write a tab-separated table here and return only a summary.
            The path must end in ``.tsv`` (any case); any other is a ``UsageError`` before a read.
        timeout: Seconds to allow.

    Returns:
        A :class:`ProteinGroupRecords`.

    Raises:
        UsageError: the path is blank or missing, or is not a protein-group table; or the bridge
            in use predates this function.

    Examples:
        >>> g = read_protein_groups("MetaMorpheus_1.1.11_AllQuantifiedProteinGroups.tsv", limit=1)
        >>> g.record_count, g.returned_count, len(g.sample_labels), g.row_count
        (6, 1, 18, 18)
        >>> g.records[0]["sample_label"], g.records[0]["intensity"]
        ('QE-002106_GM1_a-calib', 1838317.1674502683)
    """
    _bridge.require_verb("readers read-protein-groups", since=_QUANT_VIEWS_SINCE)
    args = _window("read-protein-groups", path, limit=limit, offset=offset, out=out)
    data = _bridge.invoke(*args, timeout=timeout)
    return ProteinGroupRecords._from_wire(data)

read_protein_groups_many

read_protein_groups_many(paths: PathList, *, out: str | PathLike[str] | None = None, threads: int = 1, on_error: str = 'fail', timeout: float | None = None) -> ReadBatch

Read many MetaMorpheus protein-group tables into one long table.

:func:read_protein_groups for a list - one search per condition, say. Each file's sample labels are in its :attr:FileReport.sample_labels.

Parameters:

Name Type Description Default
paths PathList

The files to read, as a list (or any iterable) of str or pathlib.Path. Read by ONE bridge process into ONE table, in this order; a file's position is its source_index. A repeated path is refused rather than read twice.

required
out str | PathLike[str] | None

Write the table to this path as tab-separated text and return only a summary (:attr:ReadBatch.output). Files are written one at a time, in order, so memory holds at most threads files however long the list is - the way to read hundreds. A batch that stops on an error removes its partial table. The path must end in .tsv (any case); any other is a UsageError before a read.

None
threads int

Files to read at once: 1 (the default), more, or -1 for one per core. The result is byte-identical at any value - files are always returned in input order - so this trades memory for speed and never changes an answer. The default is 1 because every reader holds a whole file in memory, so threads=8 can mean eight whole files at once.

1
on_error str

"fail" (the default) stops at the first file that cannot be read and raises, naming it; "skip" records the failure in that file's :attr:FileReport.error and reads the rest.

'fail'
timeout float | None

Seconds to allow for the whole batch. None (the default) waits indefinitely.

None

Returns:

Name Type Description
A ReadBatch

class:ReadBatch with the columns of :class:ProteinGroupRecords, after

ReadBatch

source_index and source_path.

Raises:

Type Description
UsageError

paths is not a list, is empty, or holds a blank entry; a path is repeated; threads or on_error is not a value listed above; or, with on_error="fail", a file is missing or is not one this function reads - the message then starts Input <i> ('<path>').

BridgeError

with on_error="fail", mzLib recognised a file and failed to parse it; the message names the input the same way.

Examples:

>>> # Not run in CI: the replay bridge answers single-path calls only.
>>> batch = read_protein_groups_many(["search1/AllQuantifiedProteinGroups.tsv",
...                                   "search2/AllQuantifiedProteinGroups.tsv"])
>>> batch.record_count, batch.row_count
(12, 216)
Source code in pkg/python/src/pymzlib/readers.py
def read_protein_groups_many(
    paths: PathList,
    *,
    out: str | os.PathLike[str] | None = None,
    threads: int = 1,
    on_error: str = "fail",
    timeout: float | None = None,
) -> ReadBatch:
    """Read many MetaMorpheus protein-group tables into one long table.

    :func:`read_protein_groups` for a list - one search per condition, say. Each file's sample
    labels are in its :attr:`FileReport.sample_labels`.

    Args:
        paths: The files to read, as a list (or any iterable) of ``str`` or ``pathlib.Path``.
            Read by ONE bridge process into ONE table, in this order; a file's position is its
            ``source_index``. A repeated path is refused rather than read twice.
        out: Write the table to this path as **tab-separated** text and return only a summary
            (:attr:`ReadBatch.output`). Files are written one at a time, in order, so memory holds
            at most ``threads`` files however long the list is - the way to read hundreds. A
            batch that stops on an error removes its partial table.
            The path must end in ``.tsv`` (any case); any other is a ``UsageError`` before a read.
        threads: Files to read at once: ``1`` (the default), more, or ``-1`` for one per core. The
            result is **byte-identical at any value** - files are always returned in input order -
            so this trades memory for speed and never changes an answer. The default is 1 because
            every reader holds a whole file in memory, so ``threads=8`` can mean eight whole files
            at once.
        on_error: ``"fail"`` (the default) stops at the first file that cannot be read and raises,
            naming it; ``"skip"`` records the failure in that file's :attr:`FileReport.error` and
            reads the rest.
        timeout: Seconds to allow for the whole batch. ``None`` (the default) waits indefinitely.

    Returns:
        A :class:`ReadBatch` with the columns of :class:`ProteinGroupRecords`, after
        ``source_index`` and ``source_path``.

    Raises:
        UsageError: ``paths`` is not a list, is empty, or holds a blank entry; a path is
            repeated; ``threads`` or ``on_error`` is not a value listed above; or, with
            ``on_error="fail"``, a file is missing or is not one this function reads - the
            message then starts ``Input <i> ('<path>')``.
        BridgeError: with ``on_error="fail"``, mzLib recognised a file and failed to parse it;
            the message names the input the same way.

    Examples:
        >>> # Not run in CI: the replay bridge answers single-path calls only.
        >>> batch = read_protein_groups_many(["search1/AllQuantifiedProteinGroups.tsv",
        ...                                   "search2/AllQuantifiedProteinGroups.tsv"])   # doctest: +SKIP
        >>> batch.record_count, batch.row_count                                          # doctest: +SKIP
        (12, 216)
    """
    _bridge.require_verb("readers read-protein-groups", since=_QUANT_VIEWS_SINCE)
    return _read_many("read-protein-groups", paths, out=out, threads=threads, on_error=on_error, timeout=timeout)

read_quantified_peptides

read_quantified_peptides(path: str | PathLike[str], *, limit: int | None = None, offset: int = 0, out: str | PathLike[str] | None = None, timeout: float | None = None) -> QuantifiedPeptideRecords

Read a FlashLFQ peptide table as one row per peptide per sample.

FlashLFQ's QuantifiedPeptides.tsv - and MetaMorpheus's AllQuantifiedPeptides.tsv, which is the same format - keeps per-sample intensities and detection types in columns named after the samples, which mzLib 1.0.592 reads into a dictionary (mzLib #1347). This is that dictionary as a long table: sample_label, intensity, detection_type and, for IsoTracker output, retention_time.

An intensity of 0 is not a measured zero. FlashLFQ writes 0 for a peptide it did not quantify in a sample; filter on detection_type before taking a mean or a log.

Parameters:

Name Type Description Default
path str | PathLike[str]

Path to a FlashLFQ QuantifiedPeptides.tsv or MetaMorpheus AllQuantifiedPeptides.tsv (:func:identify reports FlashLFQQuantifiedPeptide), or, since mzLib 1.0.593 (#1388), an RNA search's AllQuantifiedOligos.tsv, with the oligonucleotide in sequence.

required
limit int | None

Maximum peptides to return; each gives one row per sample. None returns all.

None
offset int

Peptides to skip.

0
out str | PathLike[str] | None

Write a tab-separated table here and return only a summary. The path must end in .tsv (any case); any other is a UsageError before a read.

None
timeout float | None

Seconds to allow.

None

Returns:

Name Type Description
A QuantifiedPeptideRecords

class:QuantifiedPeptideRecords.

Raises:

Type Description
UsageError

the path is blank or missing, or is not a peptide table; or the bridge in use predates this function.

Examples:

>>> p = read_quantified_peptides("MetaMorpheus_1.1.11_AllQuantifiedPeptides.tsv", limit=1)
>>> p.returned_count, p.row_count, p.absent_fields
(1, 18, ['peak_order', 'retention_time'])
>>> p.records[0]["intensity"], p.records[0]["detection_type"]
(0, 'NotDetected')
Source code in pkg/python/src/pymzlib/readers.py
def read_quantified_peptides(
    path: str | os.PathLike[str],
    *,
    limit: int | None = None,
    offset: int = 0,
    out: str | os.PathLike[str] | None = None,
    timeout: float | None = None,
) -> QuantifiedPeptideRecords:
    """Read a FlashLFQ peptide table as one row per peptide per sample.

    FlashLFQ's ``QuantifiedPeptides.tsv`` - and MetaMorpheus's ``AllQuantifiedPeptides.tsv``,
    which is the same format - keeps per-sample intensities and detection types in columns named
    after the samples, which mzLib 1.0.592 reads into a dictionary (mzLib #1347). This is that
    dictionary as a **long** table: ``sample_label``, ``intensity``, ``detection_type`` and, for
    IsoTracker output, ``retention_time``.

    **An intensity of 0 is not a measured zero.** FlashLFQ writes 0 for a peptide it did not
    quantify in a sample; filter on ``detection_type`` before taking a mean or a log.

    Args:
        path: Path to a FlashLFQ ``QuantifiedPeptides.tsv`` or MetaMorpheus
            ``AllQuantifiedPeptides.tsv`` (:func:`identify` reports ``FlashLFQQuantifiedPeptide``),
            or, since mzLib 1.0.593 (#1388), an RNA search's ``AllQuantifiedOligos.tsv``, with the
            oligonucleotide in ``sequence``.
        limit: Maximum peptides to return; each gives one row per sample. ``None`` returns all.
        offset: Peptides to skip.
        out: Write a tab-separated table here and return only a summary.
            The path must end in ``.tsv`` (any case); any other is a ``UsageError`` before a read.
        timeout: Seconds to allow.

    Returns:
        A :class:`QuantifiedPeptideRecords`.

    Raises:
        UsageError: the path is blank or missing, or is not a peptide table; or the bridge in use
            predates this function.

    Examples:
        >>> p = read_quantified_peptides("MetaMorpheus_1.1.11_AllQuantifiedPeptides.tsv", limit=1)
        >>> p.returned_count, p.row_count, p.absent_fields
        (1, 18, ['peak_order', 'retention_time'])
        >>> p.records[0]["intensity"], p.records[0]["detection_type"]
        (0, 'NotDetected')
    """
    _bridge.require_verb("readers read-quantified-peptides", since=_QUANT_VIEWS_SINCE)
    args = _window("read-quantified-peptides", path, limit=limit, offset=offset, out=out)
    data = _bridge.invoke(*args, timeout=timeout)
    return QuantifiedPeptideRecords._from_wire(data)

read_quantified_peptides_many

read_quantified_peptides_many(paths: PathList, *, out: str | PathLike[str] | None = None, threads: int = 1, on_error: str = 'fail', timeout: float | None = None) -> ReadBatch

Read many FlashLFQ peptide tables into one long table.

:func:read_quantified_peptides for a list.

Parameters:

Name Type Description Default
paths PathList

The files to read, as a list (or any iterable) of str or pathlib.Path. Read by ONE bridge process into ONE table, in this order; a file's position is its source_index. A repeated path is refused rather than read twice.

required
out str | PathLike[str] | None

Write the table to this path as tab-separated text and return only a summary (:attr:ReadBatch.output). Files are written one at a time, in order, so memory holds at most threads files however long the list is - the way to read hundreds. A batch that stops on an error removes its partial table. The path must end in .tsv (any case); any other is a UsageError before a read.

None
threads int

Files to read at once: 1 (the default), more, or -1 for one per core. The result is byte-identical at any value - files are always returned in input order - so this trades memory for speed and never changes an answer. The default is 1 because every reader holds a whole file in memory, so threads=8 can mean eight whole files at once.

1
on_error str

"fail" (the default) stops at the first file that cannot be read and raises, naming it; "skip" records the failure in that file's :attr:FileReport.error and reads the rest.

'fail'
timeout float | None

Seconds to allow for the whole batch. None (the default) waits indefinitely.

None

Returns:

Name Type Description
A ReadBatch

class:ReadBatch with the columns of :class:QuantifiedPeptideRecords, after

ReadBatch

source_index and source_path.

Raises:

Type Description
UsageError

paths is not a list, is empty, or holds a blank entry; a path is repeated; threads or on_error is not a value listed above; or, with on_error="fail", a file is missing or is not one this function reads - the message then starts Input <i> ('<path>').

BridgeError

with on_error="fail", mzLib recognised a file and failed to parse it; the message names the input the same way.

Examples:

>>> # Not run in CI: the replay bridge answers single-path calls only.
>>> batch = read_quantified_peptides_many(["a/QuantifiedPeptides.tsv",
...                                        "b/QuantifiedPeptides.tsv"])
>>> batch.read_count
2
Source code in pkg/python/src/pymzlib/readers.py
def read_quantified_peptides_many(
    paths: PathList,
    *,
    out: str | os.PathLike[str] | None = None,
    threads: int = 1,
    on_error: str = "fail",
    timeout: float | None = None,
) -> ReadBatch:
    """Read many FlashLFQ peptide tables into one long table.

    :func:`read_quantified_peptides` for a list.

    Args:
        paths: The files to read, as a list (or any iterable) of ``str`` or ``pathlib.Path``.
            Read by ONE bridge process into ONE table, in this order; a file's position is its
            ``source_index``. A repeated path is refused rather than read twice.
        out: Write the table to this path as **tab-separated** text and return only a summary
            (:attr:`ReadBatch.output`). Files are written one at a time, in order, so memory holds
            at most ``threads`` files however long the list is - the way to read hundreds. A
            batch that stops on an error removes its partial table.
            The path must end in ``.tsv`` (any case); any other is a ``UsageError`` before a read.
        threads: Files to read at once: ``1`` (the default), more, or ``-1`` for one per core. The
            result is **byte-identical at any value** - files are always returned in input order -
            so this trades memory for speed and never changes an answer. The default is 1 because
            every reader holds a whole file in memory, so ``threads=8`` can mean eight whole files
            at once.
        on_error: ``"fail"`` (the default) stops at the first file that cannot be read and raises,
            naming it; ``"skip"`` records the failure in that file's :attr:`FileReport.error` and
            reads the rest.
        timeout: Seconds to allow for the whole batch. ``None`` (the default) waits indefinitely.

    Returns:
        A :class:`ReadBatch` with the columns of :class:`QuantifiedPeptideRecords`, after
        ``source_index`` and ``source_path``.

    Raises:
        UsageError: ``paths`` is not a list, is empty, or holds a blank entry; a path is
            repeated; ``threads`` or ``on_error`` is not a value listed above; or, with
            ``on_error="fail"``, a file is missing or is not one this function reads - the
            message then starts ``Input <i> ('<path>')``.
        BridgeError: with ``on_error="fail"``, mzLib recognised a file and failed to parse it;
            the message names the input the same way.

    Examples:
        >>> # Not run in CI: the replay bridge answers single-path calls only.
        >>> batch = read_quantified_peptides_many(["a/QuantifiedPeptides.tsv",
        ...                                        "b/QuantifiedPeptides.tsv"])   # doctest: +SKIP
        >>> batch.read_count                                                     # doctest: +SKIP
        2
    """
    _bridge.require_verb("readers read-quantified-peptides", since=_QUANT_VIEWS_SINCE)
    return _read_many("read-quantified-peptides", paths, out=out, threads=threads, on_error=on_error,
                      timeout=timeout)

read_occupancy

read_occupancy(path: str | PathLike[str], *, limit: int | None = None, offset: int = 0, out: str | PathLike[str] | None = None, timeout: float | None = None) -> OccupancyRecords

Read the PTM site occupancy of a MetaMorpheus protein-group table, one row per site.

MetaMorpheus writes two occupancy cells per group per sample group - one from PSM counts (CountOccupancy_) and one from intensities (IntensityOccupancy_) - each a list of modified sites encoded as text. mzLib 1.0.592 parses them (ModificationOccupancyCell,

1347); this returns every site of every cell as a row, with basis saying which cell it

came from.

Parameters:

Name Type Description Default
path str | PathLike[str]

Path to any MetaMorpheus protein-group table :func:read_protein_groups reads, including an RNA search's AllQuantifiedTranscriptGroups.tsv, whose sites name RNA modifications.

required
limit int | None

Maximum groups to return. A group with no modified sites gives no rows.

None
offset int

Groups to skip.

0
out str | PathLike[str] | None

Write a tab-separated table here and return only a summary. The path must end in .tsv (any case); any other is a UsageError before a read.

None
timeout float | None

Seconds to allow.

None

Returns:

Name Type Description
An OccupancyRecords

class:OccupancyRecords. Check :attr:OccupancyRecords.truncated_cell_count and

OccupancyRecords

attr:OccupancyRecords.failed_fields - a cut or malformed cell shortens the table.

Raises:

Type Description
UsageError

the path is blank or missing, or is not a protein-group table; or the bridge in use predates this function.

Examples:

>>> o = read_occupancy("MetaMorpheus_1.1.11_AllQuantifiedProteinGroups.tsv")
>>> o.record_count, o.returned_count, o.row_count, o.truncated_cell_count
(6, 6, 95, 0)
>>> row = o.records[0]
>>> row["basis"], row["position"], row["modification"], row["numerator"], row["denominator"]
('count', 329, 'Deamidation on N', 1, 2)
Source code in pkg/python/src/pymzlib/readers.py
def read_occupancy(
    path: str | os.PathLike[str],
    *,
    limit: int | None = None,
    offset: int = 0,
    out: str | os.PathLike[str] | None = None,
    timeout: float | None = None,
) -> OccupancyRecords:
    """Read the PTM site occupancy of a MetaMorpheus protein-group table, one row per site.

    MetaMorpheus writes two occupancy cells per group per sample group - one from PSM counts
    (``CountOccupancy_``) and one from intensities (``IntensityOccupancy_``) - each a list of
    modified sites encoded as text. mzLib 1.0.592 parses them (``ModificationOccupancyCell``,
    #1347); this returns every site of every cell as a row, with ``basis`` saying which cell it
    came from.

    Args:
        path: Path to any MetaMorpheus protein-group table :func:`read_protein_groups` reads,
            including an RNA search's ``AllQuantifiedTranscriptGroups.tsv``, whose sites name RNA
            modifications.
        limit: Maximum groups to return. A group with no modified sites gives no rows.
        offset: Groups to skip.
        out: Write a tab-separated table here and return only a summary.
            The path must end in ``.tsv`` (any case); any other is a ``UsageError`` before a read.
        timeout: Seconds to allow.

    Returns:
        An :class:`OccupancyRecords`. Check :attr:`OccupancyRecords.truncated_cell_count` and
        :attr:`OccupancyRecords.failed_fields` - a cut or malformed cell shortens the table.

    Raises:
        UsageError: the path is blank or missing, or is not a protein-group table; or the bridge
            in use predates this function.

    Examples:
        >>> o = read_occupancy("MetaMorpheus_1.1.11_AllQuantifiedProteinGroups.tsv")
        >>> o.record_count, o.returned_count, o.row_count, o.truncated_cell_count
        (6, 6, 95, 0)
        >>> row = o.records[0]
        >>> row["basis"], row["position"], row["modification"], row["numerator"], row["denominator"]
        ('count', 329, 'Deamidation on N', 1, 2)
    """
    _bridge.require_verb("readers read-occupancy", since=_QUANT_VIEWS_SINCE)
    args = _window("read-occupancy", path, limit=limit, offset=offset, out=out)
    data = _bridge.invoke(*args, timeout=timeout)
    return OccupancyRecords._from_wire(data)

read_occupancy_many

read_occupancy_many(paths: PathList, *, out: str | PathLike[str] | None = None, threads: int = 1, on_error: str = 'fail', timeout: float | None = None) -> ReadBatch

Read the PTM site occupancy of many MetaMorpheus protein-group tables into one table.

:func:read_occupancy for a list. Each file's truncated_cell_count is in its :class:FileReport.

Parameters:

Name Type Description Default
paths PathList

The files to read, as a list (or any iterable) of str or pathlib.Path. Read by ONE bridge process into ONE table, in this order; a file's position is its source_index. A repeated path is refused rather than read twice.

required
out str | PathLike[str] | None

Write the table to this path as tab-separated text and return only a summary (:attr:ReadBatch.output). Files are written one at a time, in order, so memory holds at most threads files however long the list is - the way to read hundreds. A batch that stops on an error removes its partial table. The path must end in .tsv (any case); any other is a UsageError before a read.

None
threads int

Files to read at once: 1 (the default), more, or -1 for one per core. The result is byte-identical at any value - files are always returned in input order - so this trades memory for speed and never changes an answer. The default is 1 because every reader holds a whole file in memory, so threads=8 can mean eight whole files at once.

1
on_error str

"fail" (the default) stops at the first file that cannot be read and raises, naming it; "skip" records the failure in that file's :attr:FileReport.error and reads the rest.

'fail'
timeout float | None

Seconds to allow for the whole batch. None (the default) waits indefinitely.

None

Returns:

Name Type Description
A ReadBatch

class:ReadBatch with the columns of :class:OccupancyRecords, after source_index

ReadBatch

and source_path.

Raises:

Type Description
UsageError

paths is not a list, is empty, or holds a blank entry; a path is repeated; threads or on_error is not a value listed above; or, with on_error="fail", a file is missing or is not one this function reads - the message then starts Input <i> ('<path>').

BridgeError

with on_error="fail", mzLib recognised a file and failed to parse it; the message names the input the same way.

Examples:

>>> # Not run in CI: the replay bridge answers single-path calls only.
>>> sites = read_occupancy_many(["s1/AllQuantifiedProteinGroups.tsv",
...                              "s2/AllQuantifiedProteinGroups.tsv"])
>>> [f.truncated_cell_count for f in sites.files]
[0, 0]
Source code in pkg/python/src/pymzlib/readers.py
def read_occupancy_many(
    paths: PathList,
    *,
    out: str | os.PathLike[str] | None = None,
    threads: int = 1,
    on_error: str = "fail",
    timeout: float | None = None,
) -> ReadBatch:
    """Read the PTM site occupancy of many MetaMorpheus protein-group tables into one table.

    :func:`read_occupancy` for a list. Each file's ``truncated_cell_count`` is in its
    :class:`FileReport`.

    Args:
        paths: The files to read, as a list (or any iterable) of ``str`` or ``pathlib.Path``.
            Read by ONE bridge process into ONE table, in this order; a file's position is its
            ``source_index``. A repeated path is refused rather than read twice.
        out: Write the table to this path as **tab-separated** text and return only a summary
            (:attr:`ReadBatch.output`). Files are written one at a time, in order, so memory holds
            at most ``threads`` files however long the list is - the way to read hundreds. A
            batch that stops on an error removes its partial table.
            The path must end in ``.tsv`` (any case); any other is a ``UsageError`` before a read.
        threads: Files to read at once: ``1`` (the default), more, or ``-1`` for one per core. The
            result is **byte-identical at any value** - files are always returned in input order -
            so this trades memory for speed and never changes an answer. The default is 1 because
            every reader holds a whole file in memory, so ``threads=8`` can mean eight whole files
            at once.
        on_error: ``"fail"`` (the default) stops at the first file that cannot be read and raises,
            naming it; ``"skip"`` records the failure in that file's :attr:`FileReport.error` and
            reads the rest.
        timeout: Seconds to allow for the whole batch. ``None`` (the default) waits indefinitely.

    Returns:
        A :class:`ReadBatch` with the columns of :class:`OccupancyRecords`, after ``source_index``
        and ``source_path``.

    Raises:
        UsageError: ``paths`` is not a list, is empty, or holds a blank entry; a path is
            repeated; ``threads`` or ``on_error`` is not a value listed above; or, with
            ``on_error="fail"``, a file is missing or is not one this function reads - the
            message then starts ``Input <i> ('<path>')``.
        BridgeError: with ``on_error="fail"``, mzLib recognised a file and failed to parse it;
            the message names the input the same way.

    Examples:
        >>> # Not run in CI: the replay bridge answers single-path calls only.
        >>> sites = read_occupancy_many(["s1/AllQuantifiedProteinGroups.tsv",
        ...                              "s2/AllQuantifiedProteinGroups.tsv"])   # doctest: +SKIP
        >>> [f.truncated_cell_count for f in sites.files]                       # doctest: +SKIP
        [0, 0]
    """
    _bridge.require_verb("readers read-occupancy", since=_QUANT_VIEWS_SINCE)
    return _read_many("read-occupancy", paths, out=out, threads=threads, on_error=on_error, timeout=timeout)

pymzlib.sdrf

sdrf

Read SDRF-Proteomics experimental-design files, and pool several into one table.

Every other reader in pyMzLib answers what did the search find. SDRF answers what was searched - which sample, which organism part, which replicate, which instrument settings - and that is the half you need to group results across experiments::

>>> import pymzlib
>>> doc = pymzlib.sdrf.read("PXD000070.sdrf.tsv")
>>> doc.row_count, len(doc.columns)
(6, 31)
>>> doc.value("characteristics[organism]")               # doctest: +ELLIPSIS
['plasmodium falciparum', 'plasmodium falciparum', ...]

Pool several experiments into one analysis table, giving each a name you choose::

>>> pooled = pymzlib.sdrf.pool({
...     "PXD000070.sdrf.tsv": "malaria",
...     "PXD026824.sdrf.tsv": "colon",
... }, limit=4)
>>> pooled.document_count, pooled.row_count, pooled.labels
(2, 24, ['malaria', 'colon'])

This module is row-major, and every other reader here is columnar. That is not a style choice. :func:pymzlib.readers.read_records and friends hand back columns, a name-to-values dict, because their column names are a schema. SDRF's are data, and they repeat: 649 files in the curated corpus carry comment[modification parameters] more than once, up to eight times in one file, and one file repeats an empty name 23 times. A dict keyed by name would silently keep one occurrence and drop the rest. So :attr:SdrfDocument.columns is a list that may contain duplicates, :attr:SdrfDocument.rows is a list of cell lists, and position is what links them. Use :meth:SdrfDocument.value for the first cell under a name and :meth:SdrfDocument.all for every one.

Three things worth knowing before you index anything:

Rows are ragged. len(row) may be less than len(columns). Real files are like this - PXD059974 in mzLib's own fixtures has a 46-column header with 17 of its 23 rows carrying 42 cells - and mzLib preserves it rather than padding, so the file round-trips byte for byte. :meth:SdrfDocument.value returns None for a position a row does not reach.

Cells are raw strings, never interpreted. The SDRF key=value grammar ("NT=Oxidation;AC=UNIMOD:35") arrives exactly as written. It is not decoded, because it cannot be told apart from a cell that merely contains = and ; - comment[file uri] routinely carries pre-signed download URLs whose query strings contain Signature= and Expires=.

A reserved word is a real value. "not available" and "not applicable" mean the experiment stated an absence, which is not the same as a column the document does not have. None means the latter. Do not collapse the two.

Three questions this module answers about a document, and one about ages. Each is mzLib's own answer, projected once in the bridge for all three bindings:

  • :func:validate - is this file well-formed? Structural findings from mzLib's SdrfValidator: required columns, column order and casing, cell shape, row-key uniqueness.
  • :func:lint - do these files write the same thing the same way? Cross-document drift from SdrfDriftLint: one accession under two names, a column that is a CV term in one file and free text in another, "Homo sapiens" against "homo sapiens".
  • :func:assess - does this file describe its samples at all? SdrfSampleInformativeness's verdict - Informative, Partial or Skeleton - with the counts behind it.
  • :func:samples - what does it say about each sample? SdrfSampleBlock: one sample per source name, its characteristics and factor values, and ages read into years.
  • :func:parse_ages - SdrfAge.TryParse over any age cells, SDRF or not.
  • :func:design - can this file drive a quantification? SdrfLabelFreeDesign: the label-free experimental design MetaMorpheus and FlashLFQ take, or every reason it was refused.

The first three are blind in different places, which is why there are three. A file of "not available" validates cleanly and lints clean, and only :func:assess sees that it says nothing. Twenty individually valid files can still disagree, and only :func:lint looks across them. Each result's caveats names the blind spot it leaves.

:func:validate, :func:assess and :func:samples each have a *_many form that reads a whole corpus in one bridge call, with an explicit threads count - the batch is parallelised inside the bridge, never by a Python loop.

WrittenSdrf dataclass

Where :func:pool wrote the merged document, when asked to write one.

Attributes:

Name Type Description
path str

The path written.

row_count int

Rows written - the whole merged document, not the windowed slice. limit and offset shape what comes back over the wire; they never shorten the file.

Source code in pkg/python/src/pymzlib/sdrf.py
@dataclass(frozen=True)
class WrittenSdrf:
    """Where :func:`pool` wrote the merged document, when asked to write one.

    Attributes:
        path: The path written.
        row_count: Rows written - the **whole** merged document, not the windowed slice. ``limit``
            and ``offset`` shape what comes back over the wire; they never shorten the file.
    """

    path: str
    row_count: int

    @classmethod
    def _from_wire(cls, payload: Mapping[str, Any]) -> "WrittenSdrf":
        return cls(path=payload.get("path", ""), row_count=int(payload.get("row_count", 0)))

SdrfDocument dataclass

One SDRF-Proteomics document: an ordered header, and rows of raw cells.

Attributes:

Name Type Description
path str

The path that was read.

columns list[str]

The column names, verbatim and in document order. Names may repeat, and the order is part of the document, so this is a list rather than a set. Names are never case-normalised: the corpus contains "comment[MS min charge]" and "Material Type", and rewriting them would silently alter someone's file.

rows list[list[str]]

One list of cells per row. Ragged: a row may be shorter than columns.

row_count int

Rows in the whole document, regardless of limit or offset.

returned_count int

Rows actually carried back in :attr:rows.

offset int

The offset that was applied, in rows.

truncated bool

Whether rows were left behind, by either limit or offset. A short answer and a complete one must never look alike, so check this rather than assuming.

caveats list[str]

What this document's data cannot tell you about itself - raggedness, repeated names, reserved words. Worth printing the first time you read an unfamiliar file.

Source code in pkg/python/src/pymzlib/sdrf.py
@dataclass(frozen=True)
class SdrfDocument:
    """One SDRF-Proteomics document: an ordered header, and rows of raw cells.

    Attributes:
        path: The path that was read.
        columns: The column names, **verbatim and in document order**. Names may repeat, and the
            order is part of the document, so this is a list rather than a set. Names are never
            case-normalised: the corpus contains ``"comment[MS min charge]"`` and
            ``"Material Type"``, and rewriting them would silently alter someone's file.
        rows: One list of cells per row. **Ragged**: a row may be shorter than ``columns``.
        row_count: Rows in the **whole document**, regardless of ``limit`` or ``offset``.
        returned_count: Rows actually carried back in :attr:`rows`.
        offset: The offset that was applied, in rows.
        truncated: Whether rows were left behind, by either ``limit`` or ``offset``. A short answer
            and a complete one must never look alike, so check this rather than assuming.
        caveats: What this document's data cannot tell you about itself - raggedness, repeated
            names, reserved words. Worth printing the first time you read an unfamiliar file.
    """

    path: str
    columns: list[str]
    rows: list[list[str]]
    row_count: int
    returned_count: int
    offset: int
    truncated: bool
    caveats: list[str] = field(default_factory=list)

    def index_of(self, column: str) -> int:
        """The position of the first column with this name, or ``-1`` if absent.

        Comparison is exact and case-**sensitive**, matching the SDRF specification and mzLib.
        """
        try:
            return self.columns.index(column)
        except ValueError:
            return -1

    def indexes_of(self, column: str) -> list[int]:
        """Every position carrying this name, in document order. Empty when the column is absent."""
        return [i for i, name in enumerate(self.columns) if name == column]

    def value(self, column: str) -> list[str | None]:
        """The first cell under ``column``, one entry per returned row.

        ``None`` means **the document does not have this column, or the row is too short to reach
        it**. It does not mean "empty": the SDRF reserved words ``"not available"`` and
        ``"not applicable"`` are real values that an experiment chose to write, and they come back
        as themselves.

        Examples:
            >>> doc = read("PXD000070.sdrf.tsv")
            >>> doc.value("characteristics[disease]")     # doctest: +ELLIPSIS
            ['not applicable', 'not applicable', ...]
        """
        i = self.index_of(column)
        if i < 0:
            return [None] * len(self.rows)
        return [row[i] if i < len(row) else None for row in self.rows]

    def all(self, column: str) -> list[list[str]]:
        """Every cell under ``column``, one list per returned row.

        The accessor for a multi-cardinality column such as ``comment[modification parameters]``,
        which legitimately repeats - up to eight times in one corpus file. Positions a row is too
        short to reach are skipped rather than reported as ``None``, so each inner list holds only
        cells that exist.
        """
        indexes = self.indexes_of(column)
        return [[row[i] for i in indexes if i < len(row)] for row in self.rows]

    @property
    def records(self) -> list[dict[str, str]]:
        """The rows as dicts, for the common case of a document with no repeated column names.

        **Lossy when a name repeats** - later positions overwrite earlier ones - which is exactly
        why it is not the primary shape. :attr:`has_repeated_columns` says whether that applies to
        this document. For a repeating document use :meth:`all` instead.
        """
        return [
            {name: row[i] for i, name in enumerate(self.columns) if i < len(row)}
            for row in self.rows
        ]

    @property
    def has_repeated_columns(self) -> bool:
        """Whether any column name appears more than once - see :attr:`records`."""
        return len(self.columns) != len(set(self.columns))

    @property
    def ragged_row_count(self) -> int:
        """How many returned rows carry fewer cells than there are columns."""
        return sum(1 for row in self.rows if len(row) < len(self.columns))

    @classmethod
    def _from_wire(cls, data: Mapping[str, Any]) -> "SdrfDocument":
        return cls(
            path=data.get("path", ""),
            columns=list(data.get("column_names") or []),
            rows=[list(r) for r in (data.get("rows") or [])],
            row_count=int(data.get("row_count", 0)),
            returned_count=int(data.get("returned_count", 0)),
            offset=int(data.get("offset", 0)),
            truncated=bool(data.get("truncated", False)),
            caveats=list(data.get("caveats") or []),
        )
records property
records: list[dict[str, str]]

The rows as dicts, for the common case of a document with no repeated column names.

Lossy when a name repeats - later positions overwrite earlier ones - which is exactly why it is not the primary shape. :attr:has_repeated_columns says whether that applies to this document. For a repeating document use :meth:all instead.

has_repeated_columns property
has_repeated_columns: bool

Whether any column name appears more than once - see :attr:records.

ragged_row_count property
ragged_row_count: int

How many returned rows carry fewer cells than there are columns.

index_of
index_of(column: str) -> int

The position of the first column with this name, or -1 if absent.

Comparison is exact and case-sensitive, matching the SDRF specification and mzLib.

Source code in pkg/python/src/pymzlib/sdrf.py
def index_of(self, column: str) -> int:
    """The position of the first column with this name, or ``-1`` if absent.

    Comparison is exact and case-**sensitive**, matching the SDRF specification and mzLib.
    """
    try:
        return self.columns.index(column)
    except ValueError:
        return -1
indexes_of
indexes_of(column: str) -> list[int]

Every position carrying this name, in document order. Empty when the column is absent.

Source code in pkg/python/src/pymzlib/sdrf.py
def indexes_of(self, column: str) -> list[int]:
    """Every position carrying this name, in document order. Empty when the column is absent."""
    return [i for i, name in enumerate(self.columns) if name == column]
value
value(column: str) -> list[str | None]

The first cell under column, one entry per returned row.

None means the document does not have this column, or the row is too short to reach it. It does not mean "empty": the SDRF reserved words "not available" and "not applicable" are real values that an experiment chose to write, and they come back as themselves.

Examples:

>>> doc = read("PXD000070.sdrf.tsv")
>>> doc.value("characteristics[disease]")
['not applicable', 'not applicable', ...]
Source code in pkg/python/src/pymzlib/sdrf.py
def value(self, column: str) -> list[str | None]:
    """The first cell under ``column``, one entry per returned row.

    ``None`` means **the document does not have this column, or the row is too short to reach
    it**. It does not mean "empty": the SDRF reserved words ``"not available"`` and
    ``"not applicable"`` are real values that an experiment chose to write, and they come back
    as themselves.

    Examples:
        >>> doc = read("PXD000070.sdrf.tsv")
        >>> doc.value("characteristics[disease]")     # doctest: +ELLIPSIS
        ['not applicable', 'not applicable', ...]
    """
    i = self.index_of(column)
    if i < 0:
        return [None] * len(self.rows)
    return [row[i] if i < len(row) else None for row in self.rows]
all
all(column: str) -> list[list[str]]

Every cell under column, one list per returned row.

The accessor for a multi-cardinality column such as comment[modification parameters], which legitimately repeats - up to eight times in one corpus file. Positions a row is too short to reach are skipped rather than reported as None, so each inner list holds only cells that exist.

Source code in pkg/python/src/pymzlib/sdrf.py
def all(self, column: str) -> list[list[str]]:
    """Every cell under ``column``, one list per returned row.

    The accessor for a multi-cardinality column such as ``comment[modification parameters]``,
    which legitimately repeats - up to eight times in one corpus file. Positions a row is too
    short to reach are skipped rather than reported as ``None``, so each inner list holds only
    cells that exist.
    """
    indexes = self.indexes_of(column)
    return [[row[i] for i in indexes if i < len(row)] for row in self.rows]

PooledSdrf dataclass

Bases: SdrfDocument

Several SDRF documents merged into one table.

Everything :class:SdrfDocument offers, plus where the rows came from. path is empty: a pooled table is not a file that was read.

Attributes:

Name Type Description
columns list[str]

The merged header, a list that may repeat names: the union of every document's columns in SDRF block order, a repeated name at the highest multiplicity any one document used, and comment[source document] added.

rows list[list[str]]

One list of raw cells per returned row. A cell a document did not have is "not available".

row_count int

Merged rows in the whole pooled table, regardless of limit or offset.

returned_count int

Rows actually carried back in :attr:rows.

offset int

The offset that was applied, in rows.

truncated bool

Whether rows were left behind by limit or offset.

caveats list[str]

What a pooled table cannot tell you about itself - including, when no labels were given, that its provenance depends on where the files sat.

document_count int

How many documents were pooled.

paths list[str]

The paths pooled, in the order given.

labels list[str]

The provenance label used for each, in the same order - either what you supplied or mzLib's containing-folder/file-stem default.

written WrittenSdrf | None

Where the merged document was written, when out was given. None otherwise.

Source code in pkg/python/src/pymzlib/sdrf.py
@dataclass(frozen=True)
class PooledSdrf(SdrfDocument):
    """Several SDRF documents merged into one table.

    Everything :class:`SdrfDocument` offers, plus where the rows came from. ``path`` is empty:
    a pooled table is not a file that was read.

    Attributes:
        columns: The merged header, a list that may repeat names: the union of every document's
            columns in SDRF block order, a repeated name at the highest multiplicity any one
            document used, and ``comment[source document]`` added.
        rows: One list of raw cells per returned row. A cell a document did not have is
            ``"not available"``.
        row_count: Merged rows in the **whole** pooled table, regardless of ``limit`` or ``offset``.
        returned_count: Rows actually carried back in :attr:`rows`.
        offset: The offset that was applied, in rows.
        truncated: Whether rows were left behind by ``limit`` or ``offset``.
        caveats: What a pooled table cannot tell you about itself - including, when no labels
            were given, that its provenance depends on where the files sat.
        document_count: How many documents were pooled.
        paths: The paths pooled, in the order given.
        labels: The provenance label used for each, in the same order - either what you supplied
            or mzLib's ``containing-folder/file-stem`` default.
        written: Where the merged document was written, when ``out`` was given. ``None`` otherwise.
    """

    document_count: int = 0
    paths: list[str] = field(default_factory=list)
    labels: list[str] = field(default_factory=list)
    written: WrittenSdrf | None = None

    #: The column :meth:`pool` adds to record which document each row came from.
    SOURCE_DOCUMENT_COLUMN = "comment[source document]"

    def source_documents(self) -> list[str | None]:
        """The provenance label of each returned row - which document it came from."""
        return self.value(self.SOURCE_DOCUMENT_COLUMN)

    @classmethod
    def _from_wire(cls, data: Mapping[str, Any]) -> "PooledSdrf":
        written = data.get("written")
        return cls(
            path="",
            columns=list(data.get("column_names") or []),
            rows=[list(r) for r in (data.get("rows") or [])],
            row_count=int(data.get("row_count", 0)),
            returned_count=int(data.get("returned_count", 0)),
            offset=int(data.get("offset", 0)),
            truncated=bool(data.get("truncated", False)),
            caveats=list(data.get("caveats") or []),
            document_count=int(data.get("document_count", 0)),
            paths=list(data.get("paths") or []),
            labels=list(data.get("labels") or []),
            written=WrittenSdrf._from_wire(written) if written else None,
        )
source_documents
source_documents() -> list[str | None]

The provenance label of each returned row - which document it came from.

Source code in pkg/python/src/pymzlib/sdrf.py
def source_documents(self) -> list[str | None]:
    """The provenance label of each returned row - which document it came from."""
    return self.value(self.SOURCE_DOCUMENT_COLUMN)

FileError dataclass

Why one document in a *_many call produced no result, under on_error="skip".

Attributes:

Name Type Description
kind str

"usage" (the file is missing) or "correctness" (mzLib could not read it - message then starts with the .NET exception type, e.g. "MzLibException: ...").

message str

What went wrong, naming the file.

Source code in pkg/python/src/pymzlib/sdrf.py
@dataclass(frozen=True)
class FileError:
    """Why one document in a ``*_many`` call produced no result, under ``on_error="skip"``.

    Attributes:
        kind: ``"usage"`` (the file is missing) or ``"correctness"`` (mzLib could not read it -
            ``message`` then starts with the .NET exception type, e.g. ``"MzLibException: ..."``).
        message: What went wrong, naming the file.
    """

    kind: str
    message: str

    @classmethod
    def _from_wire(cls, payload: Mapping[str, Any] | None) -> "FileError | None":
        if not payload:
            return None
        return cls(kind=payload.get("kind", ""), message=payload.get("message", ""))

ValidationMessage dataclass

One finding from mzLib's SdrfValidator, located as precisely as its rule allows.

Attributes:

Name Type Description
severity str

"Error" (the document cannot be reliably consumed - a ragged row, a missing required column, two indistinguishable rows) or "Warning" (it deviates from the specification but can still be joined - casing, an empty cell, a missing recommended column).

rule str

The stable rule name, e.g. "RequiredColumn", "RowWidth", "RowKeyUniqueness", "ReservedWordCase". Stable so that you can count or suppress by it.

message str

A human-readable description including the offending value.

row_index int | None

0-based index into the document's data rows, or None for a finding about the whole document (a missing column, an empty header).

line_number int | None

The 1-based line in the file - row_index + 2, since the header is line 1. None exactly when row_index is.

column_name str | None

The column involved, or None when the finding is not about one column.

Source code in pkg/python/src/pymzlib/sdrf.py
@dataclass(frozen=True)
class ValidationMessage:
    """One finding from mzLib's ``SdrfValidator``, located as precisely as its rule allows.

    Attributes:
        severity: ``"Error"`` (the document cannot be reliably consumed - a ragged row, a missing
            required column, two indistinguishable rows) or ``"Warning"`` (it deviates from the
            specification but can still be joined - casing, an empty cell, a missing recommended
            column).
        rule: The stable rule name, e.g. ``"RequiredColumn"``, ``"RowWidth"``,
            ``"RowKeyUniqueness"``, ``"ReservedWordCase"``. Stable so that you can count or
            suppress by it.
        message: A human-readable description including the offending value.
        row_index: 0-based index into the document's data rows, or ``None`` for a finding about
            the whole document (a missing column, an empty header).
        line_number: The 1-based line in the file - ``row_index + 2``, since the header is line 1.
            ``None`` exactly when ``row_index`` is.
        column_name: The column involved, or ``None`` when the finding is not about one column.
    """

    severity: str
    rule: str
    message: str
    row_index: int | None
    line_number: int | None
    column_name: str | None

SdrfValidation dataclass

Bases: _Table

The structural findings for one SDRF document.

The findings are a table - columns maps severity, rule, message, row_index, line_number and column_name to one value per finding, ready for pandas.DataFrame(result.columns). :attr:messages gives the same rows as objects.

Attributes:

Name Type Description
path str

The absolute path validated.

is_valid bool

True when there is no Error. Warnings never make a document invalid: mzLib calibrated every severity against the 1,236-file curated corpus, and a rule that fired on most curated files was judged wrong rather than the files.

error_count int

Findings of severity Error.

warning_count int

Findings of severity Warning.

message_count int

All findings; the length of every column.

row_count int

Data rows in the document (the header is not a row).

column_names list[str]

The table's columns, in order.

columns dict[str, list[Any]]

Column name to one value per finding.

caveats list[str]

What a clean result does not mean - read these once.

Source code in pkg/python/src/pymzlib/sdrf.py
@dataclass(frozen=True)
class SdrfValidation(_Table):
    """The structural findings for one SDRF document.

    The findings are a table - ``columns`` maps ``severity``, ``rule``, ``message``, ``row_index``,
    ``line_number`` and ``column_name`` to one value per finding, ready for
    ``pandas.DataFrame(result.columns)``. :attr:`messages` gives the same rows as objects.

    Attributes:
        path: The absolute path validated.
        is_valid: ``True`` when there is no ``Error``. **Warnings never make a document invalid**:
            mzLib calibrated every severity against the 1,236-file curated corpus, and a rule that
            fired on most curated files was judged wrong rather than the files.
        error_count: Findings of severity ``Error``.
        warning_count: Findings of severity ``Warning``.
        message_count: All findings; the length of every column.
        row_count: Data rows in the document (the header is not a row).
        column_names: The table's columns, in order.
        columns: Column name to one value per finding.
        caveats: What a clean result does *not* mean - read these once.
    """

    path: str
    is_valid: bool
    error_count: int
    warning_count: int
    message_count: int
    row_count: int
    column_names: list[str]
    columns: dict[str, list[Any]]
    caveats: list[str] = field(default_factory=list)

    @property
    def messages(self) -> list[ValidationMessage]:
        """Every finding as a :class:`ValidationMessage`, in mzLib's order."""
        return _messages(self.columns)

    @property
    def errors(self) -> list[ValidationMessage]:
        """The findings that make the document invalid."""
        return [m for m in self.messages if m.severity == "Error"]

    @property
    def warnings(self) -> list[ValidationMessage]:
        """The findings worth fixing that do not make the document invalid."""
        return [m for m in self.messages if m.severity == "Warning"]

    @classmethod
    def _from_wire(cls, data: Mapping[str, Any]) -> "SdrfValidation":
        names, columns = _columns(data)
        return cls(
            path=data.get("path", ""),
            is_valid=bool(data.get("is_valid", False)),
            error_count=int(data.get("error_count", 0)),
            warning_count=int(data.get("warning_count", 0)),
            message_count=int(data.get("message_count", 0)),
            row_count=int(data.get("row_count", 0)),
            column_names=names,
            columns=columns,
            caveats=list(data.get("caveats") or []),
        )
messages property
messages: list[ValidationMessage]

Every finding as a :class:ValidationMessage, in mzLib's order.

errors property
errors: list[ValidationMessage]

The findings that make the document invalid.

warnings property
warnings: list[ValidationMessage]

The findings worth fixing that do not make the document invalid.

ValidatedFile dataclass

One document's summary in a :func:validate_many result.

Every count is None when the document could not be read, and :attr:error then says why.

Attributes:

Name Type Description
path str

The absolute path.

is_valid bool | None

No Error findings; None if unread.

error_count int | None

Error findings; None if unread.

warning_count int | None

Warning findings; None if unread.

message_count int | None

All findings; None if unread.

row_count int | None

Data rows in the document; None if unread.

error FileError | None

Why it was not read, or None when it was.

Source code in pkg/python/src/pymzlib/sdrf.py
@dataclass(frozen=True)
class ValidatedFile:
    """One document's summary in a :func:`validate_many` result.

    Every count is ``None`` when the document could not be read, and :attr:`error` then says why.

    Attributes:
        path: The absolute path.
        is_valid: No ``Error`` findings; ``None`` if unread.
        error_count: ``Error`` findings; ``None`` if unread.
        warning_count: ``Warning`` findings; ``None`` if unread.
        message_count: All findings; ``None`` if unread.
        row_count: Data rows in the document; ``None`` if unread.
        error: Why it was not read, or ``None`` when it was.
    """

    path: str
    is_valid: bool | None
    error_count: int | None
    warning_count: int | None
    message_count: int | None
    row_count: int | None
    error: FileError | None

    @classmethod
    def _from_wire(cls, f: Mapping[str, Any]) -> "ValidatedFile":
        return cls(
            path=f.get("path", ""),
            is_valid=f.get("is_valid"),
            error_count=f.get("error_count"),
            warning_count=f.get("warning_count"),
            message_count=f.get("message_count"),
            row_count=f.get("row_count"),
            error=FileError._from_wire(f.get("error")),
        )

SdrfValidationBatch dataclass

Bases: _Table

The findings for many SDRF documents, as one long table.

columns holds the same six columns as :class:SdrfValidation, preceded by source_index (the document's 0-based position in the list you passed) and source_path. Rows are in input order, then mzLib's order within a document, whatever threads was.

Attributes:

Name Type Description
file_count int

Documents given.

read_count int

Documents validated.

failed_count int

Documents that could not be read (only non-zero under on_error="skip").

record_count int

Rows in columns, over every document read - the same count under the name every bulk verb uses (BULK.md).

valid_count int

Documents read with no Error.

message_count int

Findings over every document read.

files list[ValidatedFile]

One :class:ValidatedFile per input, in input order.

column_names list[str]

The table's columns, source_index and source_path first.

columns dict[str, list[Any]]

Column name to one value per finding.

caveats list[str]

As for :class:SdrfValidation.

Source code in pkg/python/src/pymzlib/sdrf.py
@dataclass(frozen=True)
class SdrfValidationBatch(_Table):
    """The findings for many SDRF documents, as one long table.

    ``columns`` holds the same six columns as :class:`SdrfValidation`, preceded by
    ``source_index`` (the document's 0-based position in the list you passed) and ``source_path``.
    Rows are in input order, then mzLib's order within a document, whatever ``threads`` was.

    Attributes:
        file_count: Documents given.
        read_count: Documents validated.
        failed_count: Documents that could not be read (only non-zero under ``on_error="skip"``).
        record_count: Rows in ``columns``, over every document read - the same count under
            the name every bulk verb uses (BULK.md).
        valid_count: Documents read with no ``Error``.
        message_count: Findings over every document read.
        files: One :class:`ValidatedFile` per input, in input order.
        column_names: The table's columns, ``source_index`` and ``source_path`` first.
        columns: Column name to one value per finding.
        caveats: As for :class:`SdrfValidation`.
    """

    file_count: int
    read_count: int
    failed_count: int
    record_count: int
    valid_count: int
    message_count: int
    files: list[ValidatedFile]
    column_names: list[str]
    columns: dict[str, list[Any]]
    caveats: list[str] = field(default_factory=list)

    @property
    def messages(self) -> list[ValidationMessage]:
        """Every finding as a :class:`ValidationMessage`; use ``columns`` to keep the source."""
        return _messages(self.columns)

    @classmethod
    def _from_wire(cls, data: Mapping[str, Any]) -> "SdrfValidationBatch":
        names, columns = _columns(data)
        return cls(
            file_count=int(data.get("file_count", 0)),
            read_count=int(data.get("read_count", 0)),
            failed_count=int(data.get("failed_count", 0)),
            record_count=int(data.get("record_count", 0)),
            valid_count=int(data.get("valid_count", 0)),
            message_count=int(data.get("message_count", 0)),
            files=[ValidatedFile._from_wire(f) for f in (data.get("files") or [])],
            column_names=names,
            columns=columns,
            caveats=list(data.get("caveats") or []),
        )
messages property
messages: list[ValidationMessage]

Every finding as a :class:ValidationMessage; use columns to keep the source.

SdrfDrift dataclass

Bases: _Table

Concepts a set of SDRF documents wrote inconsistently, from mzLib's SdrfDriftLint.

columns is a long table, one row per finding x variant: a finding with three spellings is three rows sharing a finding_index. Its columns:

  • finding_index - which finding the row belongs to, 0-based, most impactful first.
  • kind - AccessionNameConflict (one accession, several names), NameAccessionConflict (one name, several accessions - the serious one), MixedTermAndFreeText, ColumnNameVariant (names differing only by case or spacing), ValueCaseVariant.
  • concept - what was written inconsistently: an accession, a name, a normalised value, or a normalised column name, depending on kind.
  • column_name - the column involved; None for ColumnNameVariant, whose finding is about a name rather than a column's values.
  • variant_rank - 0 for the majority spelling, then by frequency.
  • value - this spelling exactly as written.
  • occurrences - how widely it was used (a count of documents, at mzLib 1.0.592).
  • documents - the labels of the documents that used it, as a list.

Attributes:

Name Type Description
document_count int

Documents linted.

paths list[str]

The paths, in the order given.

labels list[str]

The label each document is named by in documents - yours, or mzLib's containing-folder/file-stem default.

finding_count int

Distinct findings (not rows).

column_names list[str]

The table's columns.

columns dict[str, list[Any]]

Column name to one value per finding x variant.

caveats list[str]

Including why the majority spelling is not advice.

Source code in pkg/python/src/pymzlib/sdrf.py
@dataclass(frozen=True)
class SdrfDrift(_Table):
    """Concepts a set of SDRF documents wrote inconsistently, from mzLib's ``SdrfDriftLint``.

    ``columns`` is a long table, **one row per finding x variant**: a finding with three spellings
    is three rows sharing a ``finding_index``. Its columns:

    - ``finding_index`` - which finding the row belongs to, 0-based, most impactful first.
    - ``kind`` - ``AccessionNameConflict`` (one accession, several names), ``NameAccessionConflict``
      (one name, several accessions - the serious one), ``MixedTermAndFreeText``,
      ``ColumnNameVariant`` (names differing only by case or spacing), ``ValueCaseVariant``.
    - ``concept`` - what was written inconsistently: an accession, a name, a normalised value, or a
      normalised column name, depending on ``kind``.
    - ``column_name`` - the column involved; ``None`` for ``ColumnNameVariant``, whose finding is
      about a name rather than a column's values.
    - ``variant_rank`` - 0 for the majority spelling, then by frequency.
    - ``value`` - this spelling exactly as written.
    - ``occurrences`` - how widely it was used (a count of documents, at mzLib 1.0.592).
    - ``documents`` - the labels of the documents that used it, as a list.

    Attributes:
        document_count: Documents linted.
        paths: The paths, in the order given.
        labels: The label each document is named by in ``documents`` - yours, or mzLib's
            ``containing-folder/file-stem`` default.
        finding_count: Distinct findings (not rows).
        column_names: The table's columns.
        columns: Column name to one value per finding x variant.
        caveats: Including why the majority spelling is not advice.
    """

    document_count: int
    paths: list[str]
    labels: list[str]
    finding_count: int
    column_names: list[str]
    columns: dict[str, list[Any]]
    caveats: list[str] = field(default_factory=list)

    def findings(self) -> list[list[dict[str, Any]]]:
        """The rows grouped by finding: one list of variant dicts per finding, majority first."""
        grouped: dict[int, list[dict[str, Any]]] = {}
        for record in self.records:
            grouped.setdefault(record["finding_index"], []).append(record)
        return [grouped[i] for i in sorted(grouped)]

    @classmethod
    def _from_wire(cls, data: Mapping[str, Any]) -> "SdrfDrift":
        names, columns = _columns(data)
        return cls(
            document_count=int(data.get("document_count", 0)),
            paths=list(data.get("paths") or []),
            labels=list(data.get("labels") or []),
            finding_count=int(data.get("finding_count", 0)),
            column_names=names,
            columns=columns,
            caveats=list(data.get("caveats") or []),
        )
findings
findings() -> list[list[dict[str, Any]]]

The rows grouped by finding: one list of variant dicts per finding, majority first.

Source code in pkg/python/src/pymzlib/sdrf.py
def findings(self) -> list[list[dict[str, Any]]]:
    """The rows grouped by finding: one list of variant dicts per finding, majority first."""
    grouped: dict[int, list[dict[str, Any]]] = {}
    for record in self.records:
        grouped.setdefault(record["finding_index"], []).append(record)
    return [grouped[i] for i in sorted(grouped)]

SdrfAssessment dataclass

Bases: _Table

Whether one SDRF document's sample half describes an experimental design.

mzLib's SdrfSampleInformativeness asks three questions, and the verdict is how many pass: all three is Informative, none is Skeleton, anything between is Partial.

columns is the evidence: one row per column a check read, with role (factor_value, sample_characteristic or biological_replicate), column_name, rows, filled (rows with a real answer), absent (empty or a reserved word), distinct_values (compared ignoring case and surrounding space) and fill_rate (filled / rows, a fraction from 0 to 1).

Attributes:

Name Type Description
path str

The absolute path assessed.

verdict str

"Informative", "Partial" or "Skeleton" - see :data:VERDICTS.

factor_value_varies bool

Some factor value[...] column holds two or more real answers.

sample_is_described bool

Some characteristics[...] column other than organism and biological replicate holds a real answer. Organism is left out because a search can fill it without a human.

biological_replicate_varies bool

characteristics[biological replicate] holds two or more real answers. False also when the column is missing (there is then no biological_replicate row in columns).

row_count int

Data rows in the document.

column_names list[str]

The evidence table's columns.

columns dict[str, list[Any]]

Column name to one value per evidence row.

caveats list[str]

Including when Partial is perfectly legitimate.

Source code in pkg/python/src/pymzlib/sdrf.py
@dataclass(frozen=True)
class SdrfAssessment(_Table):
    """Whether one SDRF document's sample half describes an experimental design.

    mzLib's ``SdrfSampleInformativeness`` asks three questions, and the verdict is how many pass:
    all three is ``Informative``, none is ``Skeleton``, anything between is ``Partial``.

    ``columns`` is the evidence: one row per column a check read, with ``role``
    (``factor_value``, ``sample_characteristic`` or ``biological_replicate``), ``column_name``,
    ``rows``, ``filled`` (rows with a real answer), ``absent`` (empty or a reserved word),
    ``distinct_values`` (compared ignoring case and surrounding space) and ``fill_rate``
    (``filled / rows``, a fraction from 0 to 1).

    Attributes:
        path: The absolute path assessed.
        verdict: ``"Informative"``, ``"Partial"`` or ``"Skeleton"`` - see :data:`VERDICTS`.
        factor_value_varies: Some ``factor value[...]`` column holds two or more real answers.
        sample_is_described: Some ``characteristics[...]`` column other than organism and
            biological replicate holds a real answer. Organism is left out because a search can
            fill it without a human.
        biological_replicate_varies: ``characteristics[biological replicate]`` holds two or more
            real answers. ``False`` also when the column is missing (there is then no
            ``biological_replicate`` row in ``columns``).
        row_count: Data rows in the document.
        column_names: The evidence table's columns.
        columns: Column name to one value per evidence row.
        caveats: Including when ``Partial`` is perfectly legitimate.
    """

    path: str
    verdict: str
    factor_value_varies: bool
    sample_is_described: bool
    biological_replicate_varies: bool
    row_count: int
    column_names: list[str]
    columns: dict[str, list[Any]]
    caveats: list[str] = field(default_factory=list)

    @classmethod
    def _from_wire(cls, data: Mapping[str, Any]) -> "SdrfAssessment":
        names, columns = _columns(data)
        return cls(
            path=data.get("path", ""),
            verdict=data.get("verdict", ""),
            factor_value_varies=bool(data.get("factor_value_varies", False)),
            sample_is_described=bool(data.get("sample_is_described", False)),
            biological_replicate_varies=bool(data.get("biological_replicate_varies", False)),
            row_count=int(data.get("row_count", 0)),
            column_names=names,
            columns=columns,
            caveats=list(data.get("caveats") or []),
        )

AssessedFile dataclass

One document's verdict in an :func:assess_many result; None fields mean unread.

Attributes:

Name Type Description
path str

The absolute path.

verdict str | None

"Informative", "Partial" or "Skeleton"; None if unread.

factor_value_varies bool | None

As :attr:SdrfAssessment.factor_value_varies; None if unread.

sample_is_described bool | None

As :attr:SdrfAssessment.sample_is_described; None if unread.

biological_replicate_varies bool | None

As :attr:SdrfAssessment.biological_replicate_varies; None if unread.

row_count int | None

Data rows; None if unread.

error FileError | None

Why it was not read, or None.

Source code in pkg/python/src/pymzlib/sdrf.py
@dataclass(frozen=True)
class AssessedFile:
    """One document's verdict in an :func:`assess_many` result; ``None`` fields mean unread.

    Attributes:
        path: The absolute path.
        verdict: ``"Informative"``, ``"Partial"`` or ``"Skeleton"``; ``None`` if unread.
        factor_value_varies: As :attr:`SdrfAssessment.factor_value_varies`; ``None`` if unread.
        sample_is_described: As :attr:`SdrfAssessment.sample_is_described`; ``None`` if unread.
        biological_replicate_varies: As :attr:`SdrfAssessment.biological_replicate_varies`;
            ``None`` if unread.
        row_count: Data rows; ``None`` if unread.
        error: Why it was not read, or ``None``.
    """

    path: str
    verdict: str | None
    factor_value_varies: bool | None
    sample_is_described: bool | None
    biological_replicate_varies: bool | None
    row_count: int | None
    error: FileError | None

    @classmethod
    def _from_wire(cls, f: Mapping[str, Any]) -> "AssessedFile":
        return cls(
            path=f.get("path", ""),
            verdict=f.get("verdict"),
            factor_value_varies=f.get("factor_value_varies"),
            sample_is_described=f.get("sample_is_described"),
            biological_replicate_varies=f.get("biological_replicate_varies"),
            row_count=f.get("row_count"),
            error=FileError._from_wire(f.get("error")),
        )

SdrfAssessmentBatch dataclass

Bases: _Table

Verdicts for many SDRF documents, with the evidence as one long table.

Attributes:

Name Type Description
file_count int

Documents given.

read_count int

Documents assessed.

failed_count int

Documents that could not be read (only under on_error="skip").

record_count int

Rows in columns, over every document read - the same count under the name every bulk verb uses (BULK.md).

verdict_counts dict[str, int]

{"informative": n, "partial": n, "skeleton": n} over the documents read.

files list[AssessedFile]

One :class:AssessedFile per input, in input order.

column_names list[str]

The evidence columns of :class:SdrfAssessment, with source_index and source_path first.

columns dict[str, list[Any]]

Column name to one value per evidence row.

caveats list[str]

As for :class:SdrfAssessment.

Source code in pkg/python/src/pymzlib/sdrf.py
@dataclass(frozen=True)
class SdrfAssessmentBatch(_Table):
    """Verdicts for many SDRF documents, with the evidence as one long table.

    Attributes:
        file_count: Documents given.
        read_count: Documents assessed.
        failed_count: Documents that could not be read (only under ``on_error="skip"``).
        record_count: Rows in ``columns``, over every document read - the same count under
            the name every bulk verb uses (BULK.md).
        verdict_counts: ``{"informative": n, "partial": n, "skeleton": n}`` over the documents read.
        files: One :class:`AssessedFile` per input, in input order.
        column_names: The evidence columns of :class:`SdrfAssessment`, with ``source_index`` and
            ``source_path`` first.
        columns: Column name to one value per evidence row.
        caveats: As for :class:`SdrfAssessment`.
    """

    file_count: int
    read_count: int
    failed_count: int
    record_count: int
    verdict_counts: dict[str, int]
    files: list[AssessedFile]
    column_names: list[str]
    columns: dict[str, list[Any]]
    caveats: list[str] = field(default_factory=list)

    def paths_with(self, *verdicts: str) -> list[str]:
        """The paths whose verdict is one of ``verdicts``, in input order.

        Raises:
            UsageError: a verdict that is not one of :data:`VERDICTS` - a typo would otherwise
                silently select nothing.

        Examples:
            >>> batch = assess_many(
            ...     ["sdrf_cohort.sdrf.tsv", "sdrf_skeleton.sdrf.tsv", "PXD000070.sdrf.tsv"]
            ... )
            >>> [os.path.basename(p) for p in batch.paths_with("Informative", "Partial")]
            ['sdrf_cohort.sdrf.tsv', 'PXD000070.sdrf.tsv']
        """
        unknown = [v for v in verdicts if v not in VERDICTS]
        if unknown:
            raise _bridge.UsageError(f"Unknown verdict(s) {unknown}; expected some of {VERDICTS}.")
        return [f.path for f in self.files if f.verdict in verdicts]

    @classmethod
    def _from_wire(cls, data: Mapping[str, Any]) -> "SdrfAssessmentBatch":
        names, columns = _columns(data)
        return cls(
            file_count=int(data.get("file_count", 0)),
            read_count=int(data.get("read_count", 0)),
            failed_count=int(data.get("failed_count", 0)),
            record_count=int(data.get("record_count", 0)),
            verdict_counts={k: int(v) for k, v in (data.get("verdict_counts") or {}).items()},
            files=[AssessedFile._from_wire(f) for f in (data.get("files") or [])],
            column_names=names,
            columns=columns,
            caveats=list(data.get("caveats") or []),
        )
paths_with
paths_with(*verdicts: str) -> list[str]

The paths whose verdict is one of verdicts, in input order.

Raises:

Type Description
UsageError

a verdict that is not one of :data:VERDICTS - a typo would otherwise silently select nothing.

Examples:

>>> batch = assess_many(
...     ["sdrf_cohort.sdrf.tsv", "sdrf_skeleton.sdrf.tsv", "PXD000070.sdrf.tsv"]
... )
>>> [os.path.basename(p) for p in batch.paths_with("Informative", "Partial")]
['sdrf_cohort.sdrf.tsv', 'PXD000070.sdrf.tsv']
Source code in pkg/python/src/pymzlib/sdrf.py
def paths_with(self, *verdicts: str) -> list[str]:
    """The paths whose verdict is one of ``verdicts``, in input order.

    Raises:
        UsageError: a verdict that is not one of :data:`VERDICTS` - a typo would otherwise
            silently select nothing.

    Examples:
        >>> batch = assess_many(
        ...     ["sdrf_cohort.sdrf.tsv", "sdrf_skeleton.sdrf.tsv", "PXD000070.sdrf.tsv"]
        ... )
        >>> [os.path.basename(p) for p in batch.paths_with("Informative", "Partial")]
        ['sdrf_cohort.sdrf.tsv', 'PXD000070.sdrf.tsv']
    """
    unknown = [v for v in verdicts if v not in VERDICTS]
    if unknown:
        raise _bridge.UsageError(f"Unknown verdict(s) {unknown}; expected some of {VERDICTS}.")
    return [f.path for f in self.files if f.verdict in verdicts]

SdrfSamples dataclass

Bases: _Table

Every sample in one SDRF document and what its rows agree on, as a long table.

A sample is a source name, matched ignoring case and surrounding space, exactly as mzLib's SdrfSampleBlock.BySourceName keys it: one sample measured in three fractions is one sample. columns has one row per sample x column x position, samples in document order:

  • source_name - the sample, as its first row spelled it.
  • sample_row_count - how many rows of the document carry it.
  • column_name - the header's own spelling (characteristics[Age] stays itself).
  • column_kind - source_name, characteristic or factor_value.
  • position - 0 for the first occurrence of a repeated column, 1 for the second, and so on; None on a conflicting row.
  • status - "agreed", or "conflicting" when the sample's rows disagree about the column: mzLib then withholds it rather than guess, and value is None.
  • value - the cell, verbatim. Reserved words are real answers and come back as themselves.
  • age_years, age_min_years, age_max_years, age_precision, age_follows_specification, age_refusal - filled only on characteristics[age] rows, with exactly the meanings :class:ParsedAges documents. On any other row all six are None.

Attributes:

Name Type Description
path str

The absolute path read.

sample_count int

Distinct samples (source names).

row_count int

Data rows in the document.

conflict_count int

Sample x column pairs withheld as conflicting.

problems list[str]

Rows mzLib could not place - no source name column, or a blank source name. Empty for a well-formed document.

column_names list[str]

The table's columns.

columns dict[str, list[Any]]

Column name to one value per row of the table.

caveats list[str]

Including how to key samples across documents.

Source code in pkg/python/src/pymzlib/sdrf.py
@dataclass(frozen=True)
class SdrfSamples(_Table):
    """Every sample in one SDRF document and what its rows agree on, as a long table.

    A sample is a ``source name``, matched ignoring case and surrounding space, exactly as mzLib's
    ``SdrfSampleBlock.BySourceName`` keys it: one sample measured in three fractions is one sample.
    ``columns`` has one row per sample x column x position, samples in document order:

    - ``source_name`` - the sample, as its first row spelled it.
    - ``sample_row_count`` - how many rows of the document carry it.
    - ``column_name`` - the header's own spelling (``characteristics[Age]`` stays itself).
    - ``column_kind`` - ``source_name``, ``characteristic`` or ``factor_value``.
    - ``position`` - 0 for the first occurrence of a repeated column, 1 for the second, and so on;
      ``None`` on a conflicting row.
    - ``status`` - ``"agreed"``, or ``"conflicting"`` when the sample's rows disagree about the
      column: mzLib then **withholds** it rather than guess, and ``value`` is ``None``.
    - ``value`` - the cell, verbatim. Reserved words are real answers and come back as themselves.
    - ``age_years``, ``age_min_years``, ``age_max_years``, ``age_precision``,
      ``age_follows_specification``, ``age_refusal`` - filled only on ``characteristics[age]``
      rows, with exactly the meanings :class:`ParsedAges` documents. On any other row all six are
      ``None``.

    Attributes:
        path: The absolute path read.
        sample_count: Distinct samples (source names).
        row_count: Data rows in the document.
        conflict_count: Sample x column pairs withheld as conflicting.
        problems: Rows mzLib could not place - no ``source name`` column, or a blank source name.
            Empty for a well-formed document.
        column_names: The table's columns.
        columns: Column name to one value per row of the table.
        caveats: Including how to key samples across documents.
    """

    path: str
    sample_count: int
    row_count: int
    conflict_count: int
    problems: list[str]
    column_names: list[str]
    columns: dict[str, list[Any]]
    caveats: list[str] = field(default_factory=list)

    def ages(self) -> list[dict[str, Any]]:
        """Only the parsed-age rows: one per sample whose ``characteristics[age]`` was agreed."""
        return [
            r for r in self.records if r["age_refusal"] is not None or r["age_years"] is not None
        ]

    def conflicts(self) -> list[tuple[str, str]]:
        """``(source_name, column_name)`` for every column mzLib withheld as conflicting."""
        return [
            (r["source_name"], r["column_name"])
            for r in self.records
            if r["status"] == "conflicting"
        ]

    @classmethod
    def _from_wire(cls, data: Mapping[str, Any]) -> "SdrfSamples":
        names, columns = _columns(data)
        return cls(
            path=data.get("path", ""),
            sample_count=int(data.get("sample_count", 0)),
            row_count=int(data.get("row_count", 0)),
            conflict_count=int(data.get("conflict_count", 0)),
            problems=list(data.get("problems") or []),
            column_names=names,
            columns=columns,
            caveats=list(data.get("caveats") or []),
        )
ages
ages() -> list[dict[str, Any]]

Only the parsed-age rows: one per sample whose characteristics[age] was agreed.

Source code in pkg/python/src/pymzlib/sdrf.py
def ages(self) -> list[dict[str, Any]]:
    """Only the parsed-age rows: one per sample whose ``characteristics[age]`` was agreed."""
    return [
        r for r in self.records if r["age_refusal"] is not None or r["age_years"] is not None
    ]
conflicts
conflicts() -> list[tuple[str, str]]

(source_name, column_name) for every column mzLib withheld as conflicting.

Source code in pkg/python/src/pymzlib/sdrf.py
def conflicts(self) -> list[tuple[str, str]]:
    """``(source_name, column_name)`` for every column mzLib withheld as conflicting."""
    return [
        (r["source_name"], r["column_name"])
        for r in self.records
        if r["status"] == "conflicting"
    ]

SampledFile dataclass

One document's summary in a :func:samples_many result; None fields mean unread.

Attributes:

Name Type Description
path str

The absolute path.

sample_count int | None

Distinct samples; None if unread.

row_count int | None

Data rows; None if unread.

conflict_count int | None

Columns withheld as conflicting; None if unread.

problems list[str] | None

Rows mzLib could not place; None if unread.

error FileError | None

Why it was not read, or None.

Source code in pkg/python/src/pymzlib/sdrf.py
@dataclass(frozen=True)
class SampledFile:
    """One document's summary in a :func:`samples_many` result; ``None`` fields mean unread.

    Attributes:
        path: The absolute path.
        sample_count: Distinct samples; ``None`` if unread.
        row_count: Data rows; ``None`` if unread.
        conflict_count: Columns withheld as conflicting; ``None`` if unread.
        problems: Rows mzLib could not place; ``None`` if unread.
        error: Why it was not read, or ``None``.
    """

    path: str
    sample_count: int | None
    row_count: int | None
    conflict_count: int | None
    problems: list[str] | None
    error: FileError | None

    @classmethod
    def _from_wire(cls, f: Mapping[str, Any]) -> "SampledFile":
        problems = f.get("problems")
        return cls(
            path=f.get("path", ""),
            sample_count=f.get("sample_count"),
            row_count=f.get("row_count"),
            conflict_count=f.get("conflict_count"),
            problems=list(problems) if problems is not None else None,
            error=FileError._from_wire(f.get("error")),
        )

SdrfSamplesBatch dataclass

Bases: _Table

The samples of many SDRF documents, as one long table.

columns has the columns of :class:SdrfSamples, preceded by source_index and source_path. A source name is only unique within one document - two studies may both have a "Sample 1" - so key a sample on (source_index, source_name).

Attributes:

Name Type Description
file_count int

Documents given.

read_count int

Documents read.

failed_count int

Documents that could not be read (only under on_error="skip").

record_count int

Rows in columns, over every document read - the same count under the name every bulk verb uses (BULK.md).

sample_count int

Samples over every document read.

files list[SampledFile]

One :class:SampledFile per input, in input order.

column_names list[str]

The table's columns.

columns dict[str, list[Any]]

Column name to one value per row.

caveats list[str]

As for :class:SdrfSamples.

Source code in pkg/python/src/pymzlib/sdrf.py
@dataclass(frozen=True)
class SdrfSamplesBatch(_Table):
    """The samples of many SDRF documents, as one long table.

    ``columns`` has the columns of :class:`SdrfSamples`, preceded by ``source_index`` and
    ``source_path``. **A source name is only unique within one document** - two studies may both
    have a ``"Sample 1"`` - so key a sample on ``(source_index, source_name)``.

    Attributes:
        file_count: Documents given.
        read_count: Documents read.
        failed_count: Documents that could not be read (only under ``on_error="skip"``).
        record_count: Rows in ``columns``, over every document read - the same count under
            the name every bulk verb uses (BULK.md).
        sample_count: Samples over every document read.
        files: One :class:`SampledFile` per input, in input order.
        column_names: The table's columns.
        columns: Column name to one value per row.
        caveats: As for :class:`SdrfSamples`.
    """

    file_count: int
    read_count: int
    failed_count: int
    record_count: int
    sample_count: int
    files: list[SampledFile]
    column_names: list[str]
    columns: dict[str, list[Any]]
    caveats: list[str] = field(default_factory=list)

    @classmethod
    def _from_wire(cls, data: Mapping[str, Any]) -> "SdrfSamplesBatch":
        names, columns = _columns(data)
        return cls(
            file_count=int(data.get("file_count", 0)),
            read_count=int(data.get("read_count", 0)),
            failed_count=int(data.get("failed_count", 0)),
            record_count=int(data.get("record_count", 0)),
            sample_count=int(data.get("sample_count", 0)),
            files=[SampledFile._from_wire(f) for f in (data.get("files") or [])],
            column_names=names,
            columns=columns,
            caveats=list(data.get("caveats") or []),
        )

ParsedAges dataclass

Bases: _Table

Age cells read into years by mzLib's SdrfAge.TryParse, one row per cell given.

columns has one row per input cell, in input order (blank cells included), with:

  • cell - the cell exactly as you gave it.
  • years - the single figure to place the sample on an age axis, in years: the age, a range's midpoint, or a bound. None when refused.
  • min_years - the youngest the cell allows, in years; 0 for an upper bound. None when refused.
  • max_years - the oldest the cell allows, in years. None when refused or when there is no upper bound (precision == "LowerBound", e.g. >=90Y): mzLib's value is +infinity, which JSON cannot carry. precision tells the two apart.
  • precision - one of :data:AGE_PRECISIONS. None when refused.
  • follows_specification - True for the specification's own nYnMnD / nW grammar, False for unambiguous words ("3 year", "6-8 weeks"). None when refused.
  • refusal - None when the cell was read, else one of :data:AGE_REFUSALS.

A month is 1/12 year, a week 7/365.25, a day 1/365.25, an hour 1/(365.25 x 24).

Attributes:

Name Type Description
cell_count int

Cells given; the length of every column.

parsed_count int

Cells read as an age.

column_names list[str]

The table's columns.

columns dict[str, list[Any]]

Column name to one value per cell.

caveats list[str]

The unit conventions and refusal rules.

Source code in pkg/python/src/pymzlib/sdrf.py
@dataclass(frozen=True)
class ParsedAges(_Table):
    """Age cells read into years by mzLib's ``SdrfAge.TryParse``, one row per cell given.

    ``columns`` has one row per input cell, **in input order** (blank cells included), with:

    - ``cell`` - the cell exactly as you gave it.
    - ``years`` - the single figure to place the sample on an age axis, in **years**: the age,
      a range's **midpoint**, or a bound. ``None`` when refused.
    - ``min_years`` - the youngest the cell allows, in years; ``0`` for an upper bound.
      ``None`` when refused.
    - ``max_years`` - the oldest the cell allows, in years. ``None`` when refused **or** when there
      is no upper bound (``precision == "LowerBound"``, e.g. ``>=90Y``): mzLib's value is +infinity,
      which JSON cannot carry. ``precision`` tells the two apart.
    - ``precision`` - one of :data:`AGE_PRECISIONS`. ``None`` when refused.
    - ``follows_specification`` - ``True`` for the specification's own ``nYnMnD`` / ``nW`` grammar,
      ``False`` for unambiguous words (``"3 year"``, ``"6-8 weeks"``). ``None`` when refused.
    - ``refusal`` - ``None`` when the cell was read, else one of :data:`AGE_REFUSALS`.

    A month is 1/12 year, a week 7/365.25, a day 1/365.25, an hour 1/(365.25 x 24).

    Attributes:
        cell_count: Cells given; the length of every column.
        parsed_count: Cells read as an age.
        column_names: The table's columns.
        columns: Column name to one value per cell.
        caveats: The unit conventions and refusal rules.
    """

    cell_count: int
    parsed_count: int
    column_names: list[str]
    columns: dict[str, list[Any]]
    caveats: list[str] = field(default_factory=list)

    @classmethod
    def _from_wire(cls, data: Mapping[str, Any]) -> "ParsedAges":
        names, columns = _columns(data)
        return cls(
            cell_count=int(data.get("cell_count", 0)),
            parsed_count=int(data.get("parsed_count", 0)),
            column_names=names,
            columns=columns,
            caveats=list(data.get("caveats") or []),
        )

DesignedFile dataclass

One run in a label-free design, in mzLib's SpectraFileInfo coordinates.

The design coordinates are 0-based, exactly as :func:pymzlib.flashlfq.quantify and :func:pymzlib.flashlfq.median_polish take them. ExperimentalDesign.tsv writes the same numbers plus one.

Attributes:

Name Type Description
full_path str

The file as the SDRF names it, or the searched path you passed for it.

file_name str

full_path's file name without its extension - the run key FlashLFQ uses (Intensity_<file_name>).

condition str

The condition columns' values joined with _.

biological_replicate int

0-based, ranked within the condition.

technical_replicate int

0-based.

fraction int

0-based.

Source code in pkg/python/src/pymzlib/sdrf.py
@dataclass(frozen=True)
class DesignedFile:
    """One run in a label-free design, in mzLib's ``SpectraFileInfo`` coordinates.

    The design coordinates are **0-based**, exactly as :func:`pymzlib.flashlfq.quantify` and
    :func:`pymzlib.flashlfq.median_polish` take them. ``ExperimentalDesign.tsv`` writes the same
    numbers plus one.

    Attributes:
        full_path: The file as the SDRF names it, or the searched path you passed for it.
        file_name: ``full_path``'s file name without its extension - the run key FlashLFQ uses
            (``Intensity_<file_name>``).
        condition: The condition columns' values joined with ``_``.
        biological_replicate: 0-based, ranked within the condition.
        technical_replicate: 0-based.
        fraction: 0-based.
    """

    full_path: str
    file_name: str
    condition: str
    biological_replicate: int
    technical_replicate: int
    fraction: int

SdrfDesign dataclass

Bases: _Table

A label-free experimental design read from an SDRF, or the reasons it was refused.

A refusal is an answer, not an exception. When is_valid is False, refusals lists every reason at once and the table is empty, so a design that MetaMorpheus would reject can never be used by mistake. mzLib refuses rather than repairs because MetaMorpheus skips quantification with only a warning when its design file is invalid.

columns maps full_path, file_name, condition, biological_replicate, technical_replicate and fraction to one value per run, in SDRF row order; :attr:files gives the same rows as :class:DesignedFile objects.

Attributes:

Name Type Description
path str

The SDRF path as given.

is_valid bool

True when there are no refusals.

file_key_column str | None

The column runs were keyed on - comment[searched data file] when the SDRF has it, else comment[data file] - or None when it has neither (which is itself a refusal).

condition_columns list[str]

The factor value[...] columns the condition was built from, in join order. Empty when none could be chosen.

condition_columns_declared bool

Whether you passed condition_columns.

searched_files_given bool

Whether you passed searched_files.

file_count int

Runs (files) in the design; 0 when refused.

refusals list[str]

Every reason the design was refused, in mzLib's words. Empty when valid.

notes list[str]

Every relabelling on the way to a valid design: biological replicates ranked within a condition (with the old -> new mapping), and rows dropped because the search does not read their file. The only record of a renumbering - read it.

report str

mzLib's human-readable account of all of the above. Print it.

column_names list[str]

The table's columns, in order.

columns dict[str, list[Any]]

Column name to one value per run.

written dict[str, Any] | None

{"path", "file_count"} of the ExperimentalDesign.tsv written, or None when out was not given or the design was refused.

caveats list[str]

What the design does and does not cover - read these once.

Source code in pkg/python/src/pymzlib/sdrf.py
@dataclass(frozen=True)
class SdrfDesign(_Table):
    """A label-free experimental design read from an SDRF, or the reasons it was refused.

    **A refusal is an answer, not an exception.** When ``is_valid`` is ``False``, ``refusals`` lists
    every reason at once and the table is empty, so a design that MetaMorpheus would reject can
    never be used by mistake. mzLib refuses rather than repairs because MetaMorpheus skips
    quantification with only a warning when its design file is invalid.

    ``columns`` maps ``full_path``, ``file_name``, ``condition``, ``biological_replicate``,
    ``technical_replicate`` and ``fraction`` to one value per run, in SDRF row order;
    :attr:`files` gives the same rows as :class:`DesignedFile` objects.

    Attributes:
        path: The SDRF path as given.
        is_valid: ``True`` when there are no refusals.
        file_key_column: The column runs were keyed on - ``comment[searched data file]`` when the
            SDRF has it, else ``comment[data file]`` - or ``None`` when it has neither (which is
            itself a refusal).
        condition_columns: The ``factor value[...]`` columns the condition was built from, in join
            order. Empty when none could be chosen.
        condition_columns_declared: Whether you passed ``condition_columns``.
        searched_files_given: Whether you passed ``searched_files``.
        file_count: Runs (files) in the design; ``0`` when refused.
        refusals: Every reason the design was refused, in mzLib's words. Empty when valid.
        notes: Every relabelling on the way to a valid design: biological replicates ranked within
            a condition (with the ``old -> new`` mapping), and rows dropped because the search does
            not read their file. **The only record of a renumbering - read it.**
        report: mzLib's human-readable account of all of the above. Print it.
        column_names: The table's columns, in order.
        columns: Column name to one value per run.
        written: ``{"path", "file_count"}`` of the ``ExperimentalDesign.tsv`` written, or ``None``
            when ``out`` was not given or the design was refused.
        caveats: What the design does and does not cover - read these once.
    """

    path: str
    is_valid: bool
    file_key_column: str | None
    condition_columns: list[str]
    condition_columns_declared: bool
    searched_files_given: bool
    file_count: int
    refusals: list[str]
    notes: list[str]
    report: str
    column_names: list[str]
    columns: dict[str, list[Any]]
    written: dict[str, Any] | None = None
    caveats: list[str] = field(default_factory=list)

    @property
    def files(self) -> list[DesignedFile]:
        """Every run as a :class:`DesignedFile`, in SDRF row order. Empty when refused."""
        return [DesignedFile(**row) for row in self.records]

    def _require_valid(self, what: str) -> None:
        if not self.is_valid:
            raise _bridge.UsageError(
                f"The design was refused, so there is no {what} to give. mzLib's reasons:\n  "
                + "\n  ".join(self.refusals)
            )

    def spectra(self) -> list[dict[str, Any]]:
        """The design as :func:`pymzlib.flashlfq.quantify` takes its ``spectra`` argument.

        One mapping per run - ``path`` plus the four design fields - so the SDRF drives FlashLFQ
        with no hand-written design. Each ``path`` is ``full_path``; pass ``searched_files`` to
        :func:`design` when the SDRF names files without the directory you keep them in.

        Raises:
            UsageError: the design was refused (mzLib's ``ToExperimentalDesign`` refuses too).
        """
        self._require_valid("spectra list")
        return [
            {
                "path": f.full_path,
                "condition": f.condition,
                "biological_replicate": f.biological_replicate,
                "technical_replicate": f.technical_replicate,
                "fraction": f.fraction,
            }
            for f in self.files
        ]

    def run_design(self) -> list[dict[str, Any]]:
        """The design as :func:`pymzlib.flashlfq.median_polish` takes its ``design`` argument.

        One mapping per run, keyed by ``file_name`` (the ``Intensity_<file_name>`` column of a
        ``QuantifiedPeptides.tsv``).

        Raises:
            UsageError: the design was refused.
        """
        self._require_valid("run design")
        return [
            {
                "file_name": f.file_name,
                "condition": f.condition,
                "biological_replicate": f.biological_replicate,
                "technical_replicate": f.technical_replicate,
                "fraction": f.fraction,
            }
            for f in self.files
        ]

    @classmethod
    def _from_wire(cls, data: Mapping[str, Any]) -> "SdrfDesign":
        names, columns = _columns(data)
        return cls(
            path=data.get("path", ""),
            is_valid=bool(data.get("is_valid", False)),
            file_key_column=data.get("file_key_column"),
            condition_columns=list(data.get("condition_columns") or []),
            condition_columns_declared=bool(data.get("condition_columns_declared", False)),
            searched_files_given=bool(data.get("searched_files_given", False)),
            file_count=int(data.get("file_count", 0)),
            refusals=list(data.get("refusals") or []),
            notes=list(data.get("notes") or []),
            report=data.get("report", ""),
            column_names=names,
            columns=columns,
            written=data.get("written"),
            caveats=list(data.get("caveats") or []),
        )
files property
files: list[DesignedFile]

Every run as a :class:DesignedFile, in SDRF row order. Empty when refused.

spectra
spectra() -> list[dict[str, Any]]

The design as :func:pymzlib.flashlfq.quantify takes its spectra argument.

One mapping per run - path plus the four design fields - so the SDRF drives FlashLFQ with no hand-written design. Each path is full_path; pass searched_files to :func:design when the SDRF names files without the directory you keep them in.

Raises:

Type Description
UsageError

the design was refused (mzLib's ToExperimentalDesign refuses too).

Source code in pkg/python/src/pymzlib/sdrf.py
def spectra(self) -> list[dict[str, Any]]:
    """The design as :func:`pymzlib.flashlfq.quantify` takes its ``spectra`` argument.

    One mapping per run - ``path`` plus the four design fields - so the SDRF drives FlashLFQ
    with no hand-written design. Each ``path`` is ``full_path``; pass ``searched_files`` to
    :func:`design` when the SDRF names files without the directory you keep them in.

    Raises:
        UsageError: the design was refused (mzLib's ``ToExperimentalDesign`` refuses too).
    """
    self._require_valid("spectra list")
    return [
        {
            "path": f.full_path,
            "condition": f.condition,
            "biological_replicate": f.biological_replicate,
            "technical_replicate": f.technical_replicate,
            "fraction": f.fraction,
        }
        for f in self.files
    ]
run_design
run_design() -> list[dict[str, Any]]

The design as :func:pymzlib.flashlfq.median_polish takes its design argument.

One mapping per run, keyed by file_name (the Intensity_<file_name> column of a QuantifiedPeptides.tsv).

Raises:

Type Description
UsageError

the design was refused.

Source code in pkg/python/src/pymzlib/sdrf.py
def run_design(self) -> list[dict[str, Any]]:
    """The design as :func:`pymzlib.flashlfq.median_polish` takes its ``design`` argument.

    One mapping per run, keyed by ``file_name`` (the ``Intensity_<file_name>`` column of a
    ``QuantifiedPeptides.tsv``).

    Raises:
        UsageError: the design was refused.
    """
    self._require_valid("run design")
    return [
        {
            "file_name": f.file_name,
            "condition": f.condition,
            "biological_replicate": f.biological_replicate,
            "technical_replicate": f.technical_replicate,
            "fraction": f.fraction,
        }
        for f in self.files
    ]

read

read(path: str | PathLike[str], *, limit: int | None = None, offset: int = 0, timeout: float | None = 60) -> SdrfDocument

Read one SDRF-Proteomics file.

Parameters:

Name Type Description Default
path str | PathLike[str]

Path to a .sdrf.tsv file.

required
limit int | None

Maximum rows to return. None (the default) returns all of them.

None
offset int

Rows to skip.

0
timeout float | None

Seconds to allow. None waits indefinitely.

60

Returns:

Name Type Description
An SdrfDocument

class:SdrfDocument.

Raises:

Type Description
UsageError

the path is blank, or the file is missing or unreadable as SDRF.

Examples:

>>> doc = read("PXD000070.sdrf.tsv")
>>> doc.value("characteristics[organism part]")[0]
'human erythrocytes'
>>> doc.all("comment[modification parameters]")[0]
['NT=Carbamidomethyl;AC=UNIMOD:4;TA=C;MT=Fixed', 'NT=Oxidation;AC=UNIMOD:35;...]
Source code in pkg/python/src/pymzlib/sdrf.py
def read(
    path: str | os.PathLike[str],
    *,
    limit: int | None = None,
    offset: int = 0,
    timeout: float | None = 60,
) -> SdrfDocument:
    """Read one SDRF-Proteomics file.

    Args:
        path: Path to a ``.sdrf.tsv`` file.
        limit: Maximum rows to return. ``None`` (the default) returns all of them.
        offset: Rows to skip.
        timeout: Seconds to allow. ``None`` waits indefinitely.

    Returns:
        An :class:`SdrfDocument`.

    Raises:
        UsageError: the path is blank, or the file is missing or unreadable as SDRF.

    Examples:
        >>> doc = read("PXD000070.sdrf.tsv")
        >>> doc.value("characteristics[organism part]")[0]
        'human erythrocytes'
        >>> doc.all("comment[modification parameters]")[0]     # doctest: +ELLIPSIS
        ['NT=Carbamidomethyl;AC=UNIMOD:4;TA=C;MT=Fixed', 'NT=Oxidation;AC=UNIMOD:35;...]
    """
    path = _bridge.path_text(path)
    if not path:
        raise _bridge.UsageError("A file path is required, e.g. 'PXD000070.sdrf.tsv'.")

    args = ["sdrf", "read", "--path", path, *_window(limit, offset)]
    data = _bridge.invoke(*args, timeout=timeout)
    return SdrfDocument._from_wire(data)

pool

pool(documents: Sequence[str] | Mapping[str, str], *, out: str | PathLike[str] | None = None, limit: int | None = None, offset: int = 0, timeout: float | None = 60) -> PooledSdrf

Merge several SDRF documents into one analysis table.

Columns are the union of every document's, ordered by SDRF's own block structure, and a name that repeats is carried at the highest multiplicity any single document used, so nothing is dropped. A cell a document did not have is filled with the reserved word "not available", and a comment[source document] column records which document each row came from.

Give your documents names. Pass a mapping to choose them. With a plain sequence, mzLib falls back to containing-folder/file-stem, which depends on where the files happen to sit - so the same two documents pooled from a different directory produce a different table. The returned :attr:PooledSdrf.caveats says so when that fallback was used.

The result is an analysis table, not something to deposit. source name + assay name + comment[label] is unique within one document, but two experiments may both have a "Sample 1", so a pooled table will usually violate SDRF's uniqueness rule. Use the source-document column as part of any key.

Parameters:

Name Type Description Default
documents Sequence[str] | Mapping[str, str]

The files to pool - a sequence of paths, or a {path: label} mapping.

required
out str | PathLike[str] | None

Write the merged document here as SDRF. The whole document is written regardless of limit and offset.

None
limit int | None

Maximum rows to return over the wire. None (the default) returns all of them.

None
offset int

Rows to skip.

0
timeout float | None

Seconds to allow. None waits indefinitely.

60

Returns:

Name Type Description
A PooledSdrf

class:PooledSdrf.

Raises:

Type Description
UsageError

no documents were given, a path is missing, or a label is blank.

Examples:

>>> pooled = pool({"PXD000070.sdrf.tsv": "malaria", "PXD026824.sdrf.tsv": "colon"}, limit=4)
>>> pooled.returned_count, pooled.truncated, set(pooled.source_documents())
(4, True, {'malaria'})
Source code in pkg/python/src/pymzlib/sdrf.py
def pool(
    documents: Sequence[str] | Mapping[str, str],
    *,
    out: str | os.PathLike[str] | None = None,
    limit: int | None = None,
    offset: int = 0,
    timeout: float | None = 60,
) -> PooledSdrf:
    """Merge several SDRF documents into one analysis table.

    Columns are the union of every document's, ordered by SDRF's own block structure, and a name
    that repeats is carried at the highest multiplicity any single document used, so nothing is
    dropped. A cell a document did not have is filled with the reserved word ``"not available"``,
    and a ``comment[source document]`` column records which document each row came from.

    **Give your documents names.** Pass a mapping to choose them. With a plain sequence, mzLib
    falls back to ``containing-folder/file-stem``, which depends on where the files happen to sit -
    so the same two documents pooled from a different directory produce a different table. The
    returned :attr:`PooledSdrf.caveats` says so when that fallback was used.

    **The result is an analysis table, not something to deposit.** ``source name`` + ``assay
    name`` + ``comment[label]`` is unique within one document, but two experiments may both have a
    ``"Sample 1"``, so a pooled table will usually violate SDRF's uniqueness rule. Use the
    source-document column as part of any key.

    Args:
        documents: The files to pool - a sequence of paths, or a ``{path: label}`` mapping.
        out: Write the merged document here as SDRF. The **whole** document is written regardless
            of ``limit`` and ``offset``.
        limit: Maximum rows to return over the wire. ``None`` (the default) returns all of them.
        offset: Rows to skip.
        timeout: Seconds to allow. ``None`` waits indefinitely.

    Returns:
        A :class:`PooledSdrf`.

    Raises:
        UsageError: no documents were given, a path is missing, or a label is blank.

    Examples:
        >>> pooled = pool({"PXD000070.sdrf.tsv": "malaria", "PXD026824.sdrf.tsv": "colon"}, limit=4)
        >>> pooled.returned_count, pooled.truncated, set(pooled.source_documents())
        (4, True, {'malaria'})
    """
    lines = _document_lines(documents, verb="pool")

    args = ["sdrf", "pool", *_window(limit, offset)]
    if out is not None:
        out = _bridge.path_text(out)
        if not out:
            raise _bridge.UsageError("out must be a non-empty path or None.")
        args += ["--out", out]

    data = _bridge.invoke(*args, stdin="\n".join(lines), timeout=timeout)
    return PooledSdrf._from_wire(data)

validate

validate(path: str | PathLike[str], *, timeout: float | None = 60) -> SdrfValidation

Check one SDRF file against the SDRF-Proteomics specification's structural rules.

Calls mzLib's SdrfValidator.Validate. Structure only - required and recommended columns, column order, casing, malformed names, ragged rows, integer replicate and fraction columns, reserved-word casing, and the one hard row rule: source name + assay name + comment[label] must be unique. Controlled-vocabulary accessions are not resolved.

Parameters:

Name Type Description Default
path str | PathLike[str]

Path to a .sdrf.tsv file.

required
timeout float | None

Seconds to allow. None waits indefinitely.

60

Returns:

Name Type Description
An SdrfValidation

class:SdrfValidation: is_valid, the counts, and one row per finding.

Raises:

Type Description
UsageError

the path is blank or the file does not exist.

BridgeError

mzLib could not read the file at all (for example, it is empty).

Examples:

>>> result = validate("sdrf_cohort.sdrf.tsv")
>>> result.is_valid, result.error_count, result.warning_count
(True, 0, 2)
>>> for m in result.warnings:
...     print(m.line_number, m.rule, m.column_name)
10 ReservedWordCase characteristics[age]
11 ReservedWordCase characteristics[age]
>>> skeleton = validate("sdrf_skeleton.sdrf.tsv")
>>> skeleton.is_valid, skeleton.errors[0].rule, skeleton.errors[0].line_number
(False, 'RequiredColumn', None)
Source code in pkg/python/src/pymzlib/sdrf.py
def validate(path: str | os.PathLike[str], *, timeout: float | None = 60) -> SdrfValidation:
    """Check one SDRF file against the SDRF-Proteomics specification's structural rules.

    Calls mzLib's ``SdrfValidator.Validate``. Structure only - required and recommended columns,
    column order, casing, malformed names, ragged rows, integer replicate and fraction columns,
    reserved-word casing, and the one hard row rule: ``source name`` + ``assay name`` +
    ``comment[label]`` must be unique. Controlled-vocabulary accessions are **not** resolved.

    Args:
        path: Path to a ``.sdrf.tsv`` file.
        timeout: Seconds to allow. ``None`` waits indefinitely.

    Returns:
        An :class:`SdrfValidation`: ``is_valid``, the counts, and one row per finding.

    Raises:
        UsageError: the path is blank or the file does not exist.
        BridgeError: mzLib could not read the file at all (for example, it is empty).

    Examples:
        >>> result = validate("sdrf_cohort.sdrf.tsv")
        >>> result.is_valid, result.error_count, result.warning_count
        (True, 0, 2)
        >>> for m in result.warnings:
        ...     print(m.line_number, m.rule, m.column_name)
        10 ReservedWordCase characteristics[age]
        11 ReservedWordCase characteristics[age]
        >>> skeleton = validate("sdrf_skeleton.sdrf.tsv")
        >>> skeleton.is_valid, skeleton.errors[0].rule, skeleton.errors[0].line_number
        (False, 'RequiredColumn', None)
    """
    data = _bridge.invoke("sdrf", "validate", "--path", _one_path(path), timeout=timeout)
    return SdrfValidation._from_wire(data)

validate_many

validate_many(paths: Sequence[str | PathLike[str]], *, threads: int = 1, on_error: str = 'fail', timeout: float | None = None) -> SdrfValidationBatch

Validate many SDRF files in one bridge call.

One process for the whole corpus instead of one per file - roughly 120 ms saved per document - with the parallelism inside the bridge, where it belongs. The result does not depend on threads: rows are always in input order.

Parameters:

Name Type Description Default
paths Sequence[str | PathLike[str]]

The files, in the order you want them reported. Each may be given once.

required
threads int

Documents validated at once. 1 (the default) keeps one document in memory at a time; -1 uses every core.

1
on_error str

"fail" (the default) raises on the first unreadable document in input order. "skip" records the failure in that document's :attr:ValidatedFile.error and carries on.

'fail'
timeout float | None

Seconds to allow for the whole batch. None (the default) waits indefinitely.

None

Returns:

Name Type Description
An SdrfValidationBatch

class:SdrfValidationBatch.

Raises:

Type Description
UsageError

no paths, a blank or repeated path, a bad threads or on_error, or - under "fail" - a file that does not exist.

BridgeError

under "fail", mzLib could not read one of the files.

Examples:

>>> batch = validate_many(
...     ["sdrf_skeleton.sdrf.tsv", "sdrf_cohort.sdrf.tsv", "missing.sdrf.tsv"],
...     on_error="skip",
... )
>>> batch.file_count, batch.read_count, batch.valid_count
(3, 2, 1)
>>> [f.is_valid for f in batch.files]
[False, True, None]
>>> batch.files[2].error.kind
'usage'
Source code in pkg/python/src/pymzlib/sdrf.py
def validate_many(
    paths: Sequence[str | os.PathLike[str]],
    *,
    threads: int = 1,
    on_error: str = "fail",
    timeout: float | None = None,
) -> SdrfValidationBatch:
    """Validate many SDRF files in one bridge call.

    One process for the whole corpus instead of one per file - roughly 120 ms saved per document -
    with the parallelism inside the bridge, where it belongs. The result does not depend on
    ``threads``: rows are always in input order.

    Args:
        paths: The files, in the order you want them reported. Each may be given once.
        threads: Documents validated at once. ``1`` (the default) keeps one document in memory at
            a time; ``-1`` uses every core.
        on_error: ``"fail"`` (the default) raises on the first unreadable document in input order.
            ``"skip"`` records the failure in that document's :attr:`ValidatedFile.error` and
            carries on.
        timeout: Seconds to allow for the whole batch. ``None`` (the default) waits indefinitely.

    Returns:
        An :class:`SdrfValidationBatch`.

    Raises:
        UsageError: no paths, a blank or repeated path, a bad ``threads`` or ``on_error``, or -
            under ``"fail"`` - a file that does not exist.
        BridgeError: under ``"fail"``, mzLib could not read one of the files.

    Examples:
        >>> batch = validate_many(
        ...     ["sdrf_skeleton.sdrf.tsv", "sdrf_cohort.sdrf.tsv", "missing.sdrf.tsv"],
        ...     on_error="skip",
        ... )
        >>> batch.file_count, batch.read_count, batch.valid_count
        (3, 2, 1)
        >>> [f.is_valid for f in batch.files]
        [False, True, None]
        >>> batch.files[2].error.kind
        'usage'
    """
    args, stdin = _bulk_args(paths, threads, on_error, verb="validate")
    data = _bridge.invoke("sdrf", "validate", *args, stdin=stdin, timeout=timeout)
    return SdrfValidationBatch._from_wire(data)

lint

lint(documents: Sequence[str] | Mapping[str, str], *, timeout: float | None = 60) -> SdrfDrift

Find the concepts a set of SDRF documents annotated inconsistently.

Validity is a property of one file; comparability is a property of the relationship between files. Every document can pass :func:validate and the pooled table still be unusable because one file writes "Homo sapiens" and another "homo sapiens". This is mzLib's check for that.

Columns that are unique per row by construction are never compared - source name, assay name, comment[data file], comment[searched data file] (since mzLib 1.0.592), comment[file uri] and comment[source document] - so differing file names are not reported as drift. Reserved words and empty cells are skipped too, so a set of documents that say nothing lints clean: use :func:assess for that.

Parameters:

Name Type Description Default
documents Sequence[str] | Mapping[str, str]

The files - a sequence of paths, or a {path: label} mapping, exactly as for :func:pool. Label them: labels are how documents names each file in the result, and mzLib's default depends on where the files sit.

required
timeout float | None

Seconds to allow. None waits indefinitely.

60

Returns:

Name Type Description
An SdrfDrift

class:SdrfDrift, one row per finding x variant.

Raises:

Type Description
UsageError

no documents, a missing file, or a blank label.

Examples:

>>> drift = lint({"sdrf_cohort.sdrf.tsv": "cohort", "sdrf_cohort_partner.sdrf.tsv": "partner"})
>>> drift.finding_count
4
>>> for variants in drift.findings():
...     print(variants[0]["kind"], [v["value"] for v in variants])
AccessionNameConflict ['Exploris 480', 'Orbitrap Exploris 480']
MixedTermAndFreeText ['controlled vocabulary term', 'free text']
ColumnNameVariant ['Characteristics[sex]', 'characteristics[sex]']
ValueCaseVariant ['Homo sapiens', 'homo sapiens']
Source code in pkg/python/src/pymzlib/sdrf.py
def lint(
    documents: Sequence[str] | Mapping[str, str], *, timeout: float | None = 60
) -> SdrfDrift:
    """Find the concepts a set of SDRF documents annotated inconsistently.

    Validity is a property of one file; comparability is a property of the relationship between
    files. Every document can pass :func:`validate` and the pooled table still be unusable because
    one file writes ``"Homo sapiens"`` and another ``"homo sapiens"``. This is mzLib's check for that.

    Columns that are unique per row by construction are never compared - ``source name``,
    ``assay name``, ``comment[data file]``, ``comment[searched data file]`` (since mzLib 1.0.592),
    ``comment[file uri]`` and ``comment[source document]`` - so differing file names are not
    reported as drift. Reserved words and empty cells are skipped too, so a set of documents that
    say nothing lints clean: use :func:`assess` for that.

    Args:
        documents: The files - a sequence of paths, or a ``{path: label}`` mapping, exactly as for
            :func:`pool`. **Label them**: labels are how ``documents`` names each file in the
            result, and mzLib's default depends on where the files sit.
        timeout: Seconds to allow. ``None`` waits indefinitely.

    Returns:
        An :class:`SdrfDrift`, one row per finding x variant.

    Raises:
        UsageError: no documents, a missing file, or a blank label.

    Examples:
        >>> drift = lint({"sdrf_cohort.sdrf.tsv": "cohort", "sdrf_cohort_partner.sdrf.tsv": "partner"})
        >>> drift.finding_count
        4
        >>> for variants in drift.findings():
        ...     print(variants[0]["kind"], [v["value"] for v in variants])
        AccessionNameConflict ['Exploris 480', 'Orbitrap Exploris 480']
        MixedTermAndFreeText ['controlled vocabulary term', 'free text']
        ColumnNameVariant ['Characteristics[sex]', 'characteristics[sex]']
        ValueCaseVariant ['Homo sapiens', 'homo sapiens']
    """
    lines = _document_lines(documents, verb="lint")
    data = _bridge.invoke("sdrf", "lint", stdin="\n".join(lines), timeout=timeout)
    return SdrfDrift._from_wire(data)

assess

assess(path: str | PathLike[str], *, timeout: float | None = 60) -> SdrfAssessment

Decide whether an SDRF file actually describes its samples, or is a valid skeleton.

A file generated from a list of data files - reserved words in every sample column, one replicate number, no factor - passes :func:validate, because reserved words are the specification's correct way to say nothing. For grouping results by biology it is the same as having no SDRF at all. mzLib's SdrfSampleInformativeness.Assess is the gate for that.

Parameters:

Name Type Description Default
path str | PathLike[str]

Path to a .sdrf.tsv file.

required
timeout float | None

Seconds to allow. None waits indefinitely.

60

Returns:

Name Type Description
An SdrfAssessment

class:SdrfAssessment: the verdict, the three checks, and the per-column counts.

Raises:

Type Description
UsageError

the path is blank or the file does not exist.

BridgeError

mzLib could not read the file.

Examples:

>>> a = assess("sdrf_cohort.sdrf.tsv")
>>> a.verdict, a.factor_value_varies, a.sample_is_described, a.biological_replicate_varies
('Informative', True, True, True)
>>> a.columns["column_name"][0], a.columns["distinct_values"][0]
('factor value[disease]', 2)
Source code in pkg/python/src/pymzlib/sdrf.py
def assess(path: str | os.PathLike[str], *, timeout: float | None = 60) -> SdrfAssessment:
    """Decide whether an SDRF file actually describes its samples, or is a valid skeleton.

    A file generated from a list of data files - reserved words in every sample column, one
    replicate number, no factor - passes :func:`validate`, because reserved words are the
    specification's correct way to say nothing. For grouping results by biology it is the same as
    having no SDRF at all. mzLib's ``SdrfSampleInformativeness.Assess`` is the gate for that.

    Args:
        path: Path to a ``.sdrf.tsv`` file.
        timeout: Seconds to allow. ``None`` waits indefinitely.

    Returns:
        An :class:`SdrfAssessment`: the verdict, the three checks, and the per-column counts.

    Raises:
        UsageError: the path is blank or the file does not exist.
        BridgeError: mzLib could not read the file.

    Examples:
        >>> a = assess("sdrf_cohort.sdrf.tsv")
        >>> a.verdict, a.factor_value_varies, a.sample_is_described, a.biological_replicate_varies
        ('Informative', True, True, True)
        >>> a.columns["column_name"][0], a.columns["distinct_values"][0]
        ('factor value[disease]', 2)
    """
    data = _bridge.invoke("sdrf", "assess", "--path", _one_path(path), timeout=timeout)
    return SdrfAssessment._from_wire(data)

assess_many

assess_many(paths: Sequence[str | PathLike[str]], *, threads: int = 1, on_error: str = 'fail', timeout: float | None = None) -> SdrfAssessmentBatch

Assess many SDRF files in one bridge call - the gate for a corpus.

Parameters:

Name Type Description Default
paths Sequence[str | PathLike[str]]

The files, in the order you want them reported. Each may be given once.

required
threads int

Documents assessed at once. 1 by default; -1 uses every core. The result is the same at any value.

1
on_error str

"fail" (default) or "skip", as for :func:validate_many.

'fail'
timeout float | None

Seconds for the whole batch. None (the default) waits indefinitely.

None

Returns:

Name Type Description
An SdrfAssessmentBatch

class:SdrfAssessmentBatch.

Raises:

Type Description
UsageError

as for :func:validate_many.

BridgeError

under "fail", mzLib could not read one of the files.

Examples:

>>> batch = assess_many(
...     ["sdrf_cohort.sdrf.tsv", "sdrf_skeleton.sdrf.tsv", "PXD000070.sdrf.tsv"]
... )
>>> batch.verdict_counts
{'informative': 1, 'partial': 1, 'skeleton': 1}
>>> [f.verdict for f in batch.files]
['Informative', 'Skeleton', 'Partial']
Source code in pkg/python/src/pymzlib/sdrf.py
def assess_many(
    paths: Sequence[str | os.PathLike[str]],
    *,
    threads: int = 1,
    on_error: str = "fail",
    timeout: float | None = None,
) -> SdrfAssessmentBatch:
    """Assess many SDRF files in one bridge call - the gate for a corpus.

    Args:
        paths: The files, in the order you want them reported. Each may be given once.
        threads: Documents assessed at once. ``1`` by default; ``-1`` uses every core. The result
            is the same at any value.
        on_error: ``"fail"`` (default) or ``"skip"``, as for :func:`validate_many`.
        timeout: Seconds for the whole batch. ``None`` (the default) waits indefinitely.

    Returns:
        An :class:`SdrfAssessmentBatch`.

    Raises:
        UsageError: as for :func:`validate_many`.
        BridgeError: under ``"fail"``, mzLib could not read one of the files.

    Examples:
        >>> batch = assess_many(
        ...     ["sdrf_cohort.sdrf.tsv", "sdrf_skeleton.sdrf.tsv", "PXD000070.sdrf.tsv"]
        ... )
        >>> batch.verdict_counts
        {'informative': 1, 'partial': 1, 'skeleton': 1}
        >>> [f.verdict for f in batch.files]
        ['Informative', 'Skeleton', 'Partial']
    """
    args, stdin = _bulk_args(paths, threads, on_error, verb="assess")
    data = _bridge.invoke("sdrf", "assess", *args, stdin=stdin, timeout=timeout)
    return SdrfAssessmentBatch._from_wire(data)

samples

samples(path: str | PathLike[str], *, timeout: float | None = 60) -> SdrfSamples

Lift each sample's characteristics and factor values out of an SDRF file, ages parsed.

Calls mzLib's SdrfSampleBlock.BySourceName: the sample half of the document (source name, every characteristics[...], every factor value[...]) per sample, merged over the sample's rows. Where those rows disagree - one fraction says normal and another COVID-19 - mzLib names the column and withholds it rather than let the first row win.

Parameters:

Name Type Description Default
path str | PathLike[str]

Path to a .sdrf.tsv file.

required
timeout float | None

Seconds to allow. None waits indefinitely.

60

Returns:

Name Type Description
An SdrfSamples

class:SdrfSamples long table.

Raises:

Type Description
UsageError

the path is blank or the file does not exist.

BridgeError

mzLib could not read the file.

Examples:

>>> s = samples("sdrf_cohort.sdrf.tsv")
>>> s.sample_count, s.conflicts()
(6, [('S6', 'characteristics[disease]')])
>>> for a in s.ages():
...     print(a["source_name"], a["value"], a["age_years"], a["age_precision"], a["age_refusal"])
S1 58Y 58 Exact None
S2 40Y-85Y 62.5 Range None
S3 >=90Y 90 LowerBound None
S4 63 None None no_unit
S5 Not available None None reserved_word
S6 40Y-40Y 40 Exact None
Source code in pkg/python/src/pymzlib/sdrf.py
def samples(path: str | os.PathLike[str], *, timeout: float | None = 60) -> SdrfSamples:
    """Lift each sample's characteristics and factor values out of an SDRF file, ages parsed.

    Calls mzLib's ``SdrfSampleBlock.BySourceName``: the sample half of the document (``source
    name``, every ``characteristics[...]``, every ``factor value[...]``) per sample, merged over
    the sample's rows. Where those rows **disagree** - one fraction says ``normal`` and another
    ``COVID-19`` - mzLib names the column and withholds it rather than let the first row win.

    Args:
        path: Path to a ``.sdrf.tsv`` file.
        timeout: Seconds to allow. ``None`` waits indefinitely.

    Returns:
        An :class:`SdrfSamples` long table.

    Raises:
        UsageError: the path is blank or the file does not exist.
        BridgeError: mzLib could not read the file.

    Examples:
        >>> s = samples("sdrf_cohort.sdrf.tsv")
        >>> s.sample_count, s.conflicts()
        (6, [('S6', 'characteristics[disease]')])
        >>> for a in s.ages():
        ...     print(a["source_name"], a["value"], a["age_years"], a["age_precision"], a["age_refusal"])
        S1 58Y 58 Exact None
        S2 40Y-85Y 62.5 Range None
        S3 >=90Y 90 LowerBound None
        S4 63 None None no_unit
        S5 Not available None None reserved_word
        S6 40Y-40Y 40 Exact None
    """
    data = _bridge.invoke("sdrf", "samples", "--path", _one_path(path), timeout=timeout)
    return SdrfSamples._from_wire(data)

samples_many

samples_many(paths: Sequence[str | PathLike[str]], *, threads: int = 1, on_error: str = 'fail', timeout: float | None = None) -> SdrfSamplesBatch

Lift the samples of many SDRF files in one bridge call.

Parameters:

Name Type Description Default
paths Sequence[str | PathLike[str]]

The files, in the order you want them reported. Each may be given once.

required
threads int

Documents read at once. 1 by default; -1 uses every core. The result is the same at any value.

1
on_error str

"fail" (default) or "skip", as for :func:validate_many.

'fail'
timeout float | None

Seconds for the whole batch. None (the default) waits indefinitely.

None

Returns:

Name Type Description
An SdrfSamplesBatch

class:SdrfSamplesBatch.

Raises:

Type Description
UsageError

as for :func:validate_many.

BridgeError

under "fail", mzLib could not read one of the files.

Examples:

>>> batch = samples_many(["sdrf_cohort.sdrf.tsv", "sdrf_cohort_partner.sdrf.tsv"])
>>> batch.sample_count, [f.sample_count for f in batch.files]
(8, [6, 2])
Source code in pkg/python/src/pymzlib/sdrf.py
def samples_many(
    paths: Sequence[str | os.PathLike[str]],
    *,
    threads: int = 1,
    on_error: str = "fail",
    timeout: float | None = None,
) -> SdrfSamplesBatch:
    """Lift the samples of many SDRF files in one bridge call.

    Args:
        paths: The files, in the order you want them reported. Each may be given once.
        threads: Documents read at once. ``1`` by default; ``-1`` uses every core. The result is
            the same at any value.
        on_error: ``"fail"`` (default) or ``"skip"``, as for :func:`validate_many`.
        timeout: Seconds for the whole batch. ``None`` (the default) waits indefinitely.

    Returns:
        An :class:`SdrfSamplesBatch`.

    Raises:
        UsageError: as for :func:`validate_many`.
        BridgeError: under ``"fail"``, mzLib could not read one of the files.

    Examples:
        >>> batch = samples_many(["sdrf_cohort.sdrf.tsv", "sdrf_cohort_partner.sdrf.tsv"])
        >>> batch.sample_count, [f.sample_count for f in batch.files]
        (8, [6, 2])
    """
    args, stdin = _bulk_args(paths, threads, on_error, verb="samples")
    data = _bridge.invoke("sdrf", "samples", *args, stdin=stdin, timeout=timeout)
    return SdrfSamplesBatch._from_wire(data)

parse_ages

parse_ages(cells: Sequence[str | None], *, timeout: float | None = 60) -> ParsedAges

Read characteristics[age] cells into years, refusing anything that would need a guess.

Calls mzLib's SdrfAge.TryParse on each cell. It reads the specification's grammar (58Y, 30Y6M, 16W), ranges (40Y-85Y, 6-8 weeks), bounds (>=90Y, <1Y) and unambiguous words (3 year, 4 hour). It refuses a bare number - 63 is 11% of real age cells, and 63 years and 63 days are both plausible in one study - as well as reserved words and free text. A refused cell has no age; refusal says why.

One bridge call for any number of cells, so pass them all at once rather than looping.

Parameters:

Name Type Description Default
cells Sequence[str | None]

The cells, e.g. doc.value("characteristics[age]"). None is sent as an empty cell and comes back refused as "empty", so the result stays aligned with the input.

required
timeout float | None

Seconds to allow. None waits indefinitely.

60

Returns:

Name Type Description
A ParsedAges

class:ParsedAges table, row i for cell i.

Raises:

Type Description
UsageError

cells is a bare string or empty, or a cell is not a string or contains a newline.

Examples:

>>> ages = parse_ages(["58Y", "30Y6M", "40Y-85Y", "40Y-40Y", ">=90Y", "<1Y", "6-8 weeks",
...                    "63", "not available", "", "about forty"])
>>> ages.parsed_count, ages.cell_count
(7, 11)
>>> for row in ages.records[:5]:
...     print(row["cell"], row["years"], row["min_years"], row["max_years"], row["precision"])
58Y 58 58 58 Exact
30Y6M 30.5 30.5 30.5 Exact
40Y-85Y 62.5 40 85 Range
40Y-40Y 40 40 40 Exact
>=90Y 90 90 None LowerBound
>>> ages.columns["refusal"][7:]
['no_unit', 'reserved_word', 'empty', 'unreadable']
Source code in pkg/python/src/pymzlib/sdrf.py
def parse_ages(cells: Sequence[str | None], *, timeout: float | None = 60) -> ParsedAges:
    """Read ``characteristics[age]`` cells into years, refusing anything that would need a guess.

    Calls mzLib's ``SdrfAge.TryParse`` on each cell. It reads the specification's grammar
    (``58Y``, ``30Y6M``, ``16W``), ranges (``40Y-85Y``, ``6-8 weeks``), bounds (``>=90Y``,
    ``<1Y``) and unambiguous words (``3 year``, ``4 hour``). It **refuses** a bare number - ``63``
    is 11% of real age cells, and 63 years and 63 days are both plausible in one study - as well as
    reserved words and free text. A refused cell has no age; ``refusal`` says why.

    One bridge call for any number of cells, so pass them all at once rather than looping.

    Args:
        cells: The cells, e.g. ``doc.value("characteristics[age]")``. ``None`` is sent as an empty
            cell and comes back refused as ``"empty"``, so the result stays aligned with the input.
        timeout: Seconds to allow. ``None`` waits indefinitely.

    Returns:
        A :class:`ParsedAges` table, row ``i`` for cell ``i``.

    Raises:
        UsageError: ``cells`` is a bare string or empty, or a cell is not a string or contains a
            newline.

    Examples:
        >>> ages = parse_ages(["58Y", "30Y6M", "40Y-85Y", "40Y-40Y", ">=90Y", "<1Y", "6-8 weeks",
        ...                    "63", "not available", "", "about forty"])
        >>> ages.parsed_count, ages.cell_count
        (7, 11)
        >>> for row in ages.records[:5]:
        ...     print(row["cell"], row["years"], row["min_years"], row["max_years"], row["precision"])
        58Y 58 58 58 Exact
        30Y6M 30.5 30.5 30.5 Exact
        40Y-85Y 62.5 40 85 Range
        40Y-40Y 40 40 40 Exact
        >=90Y 90 90 None LowerBound
        >>> ages.columns["refusal"][7:]
        ['no_unit', 'reserved_word', 'empty', 'unreadable']
    """
    if isinstance(cells, str):
        raise _bridge.UsageError(
            f"parse_ages() takes a list of cells; for one cell use parse_ages([{cells!r}])."
        )
    if not isinstance(cells, Sequence) or not cells:
        raise _bridge.UsageError("parse_ages() needs at least one cell, as a list.")
    lines = []
    for c in cells:
        text = "" if c is None else c
        if not isinstance(text, str):
            raise _bridge.UsageError(f"Every cell must be a str or None; got {c!r}.")
        if "\n" in text or "\r" in text:
            raise _bridge.UsageError(f"A cell contains a newline, which separates cells: {text!r}.")
        lines.append(text)
    # A trailing newline ends the last cell, so a final blank cell survives the trip.
    data = _bridge.invoke("sdrf", "parse-age", stdin="\n".join(lines) + "\n", timeout=timeout)
    return ParsedAges._from_wire(data)

design

design(path: str | PathLike[str], *, condition_columns: Sequence[str] | None = None, searched_files: Sequence[str | PathLike[str]] | None = None, out: str | PathLike[str] | None = None, timeout: float | None = 60) -> SdrfDesign

Read a label-free experimental design out of an SDRF, or every reason it cannot be read.

Calls mzLib's SdrfLabelFreeDesign.Read. It runs every check MetaMorpheus's own design validator runs and refuses rather than repairs: a factor written not available, two files claiming the same replicate and fraction, a document with several factor columns and no declaration - each is a refusal, and all of them are reported at once. It relabels in only two ways, both recorded in notes: study-wide biological replicate numbers are ranked within each condition (22 -> 1), and rows for files the search does not read are dropped.

Parameters:

Name Type Description Default
path str | PathLike[str]

Path to a .sdrf.tsv file.

required
condition_columns Sequence[str] | None

The factor value[...] columns that make up the condition, by exact (case-sensitive) name, in the order to join them with _. None uses the document's only factor column; a document with several is then refused, because joining all of them could split a condition on a nuisance factor.

None
searched_files Sequence[str | PathLike[str]] | None

The files the search will read, as paths or bare names. Each must be named exactly (case and extension) by one SDRF row; rows for other files are dropped and reported in notes, and the paths you give become full_path. None takes the SDRF's own file names.

None
out str | PathLike[str] | None

Also write MetaMorpheus's ExperimentalDesign.tsv (1-based) here. Must end in .tsv. Written only when the design is valid. MetaMorpheus finds the file only when it is named ExperimentalDesign.tsv and sits beside the spectra.

None
timeout float | None

Seconds to allow. None waits indefinitely.

60

Returns:

Name Type Description
An SdrfDesign

class:SdrfDesign: is_valid, the refusals or the runs, and mzLib's notes.

Raises:

Type Description
UsageError

the path is blank or the file does not exist; out does not end in .tsv (checked before the SDRF is read); a condition column or searched file is blank or contains a tab or newline.

BridgeError

mzLib could not parse the SDRF at all, or out's directory cannot be written.

Examples:

A valid two-factor design - 24 runs, 8 conditions of 3 biological replicates:

>>> both = ["factor value[genotype]", "factor value[treatment]"]
>>> d = design("PXD067622.sdrf.tsv", condition_columns=both)
>>> d.is_valid, d.file_count, len(set(d.columns["condition"]))
(True, 24, 8)
>>> run = d.files[0]
>>> run.file_name, run.condition, run.biological_replicate
('20240830_HF_LC3_MAA_RK_12032_CA_DMSO4', 'SPRTN-TurboID CA_DMSO (vehicle)', 0)

A refusal lists every reason at once - here one per run, because the deposit's treatment is not available:

>>> refused = design("PXD049018.sdrf.tsv", condition_columns=both)
>>> refused.is_valid, len(refused.refusals), refused.file_count
(False, 20, 0)
>>> print(refused.refusals[0])
Line 2 (MSB67868ABand_01.raw): 'factor value[treatment]' is 'not available'. A condition
cannot be built from an unknown factor; fill it in, or leave the column out of the
declared condition columns to pool these rows.

Study-wide replicate numbers are ranked within each condition, and the mapping is kept:

>>> ranked = design("PXD067622_studywide.sdrf.tsv", condition_columns=both)
>>> ranked.is_valid, len(ranked.notes)
(True, 7)
>>> print(ranked.notes[1])
Condition 'SPRTN-TurboID CA_formaldehyde 1 mM, 1 h': biological replicates renumbered
22 -> 1, 23 -> 2, 24 -> 3.
Source code in pkg/python/src/pymzlib/sdrf.py
def design(
    path: str | os.PathLike[str],
    *,
    condition_columns: Sequence[str] | None = None,
    searched_files: Sequence[str | os.PathLike[str]] | None = None,
    out: str | os.PathLike[str] | None = None,
    timeout: float | None = 60,
) -> SdrfDesign:
    """Read a label-free experimental design out of an SDRF, or every reason it cannot be read.

    Calls mzLib's ``SdrfLabelFreeDesign.Read``. It runs every check MetaMorpheus's own design
    validator runs and **refuses rather than repairs**: a factor written ``not available``, two
    files claiming the same replicate and fraction, a document with several factor columns and no
    declaration - each is a refusal, and all of them are reported at once. It relabels in only two
    ways, both recorded in ``notes``: study-wide biological replicate numbers are ranked within each
    condition (``22 -> 1``), and rows for files the search does not read are dropped.

    Args:
        path: Path to a ``.sdrf.tsv`` file.
        condition_columns: The ``factor value[...]`` columns that make up the condition, by exact
            (case-sensitive) name, in the order to join them with ``_``. ``None`` uses the
            document's only factor column; a document with several is then refused, because
            joining all of them could split a condition on a nuisance factor.
        searched_files: The files the search will read, as paths or bare names. Each must be named
            **exactly** (case and extension) by one SDRF row; rows for other files are dropped and
            reported in ``notes``, and the paths you give become ``full_path``. ``None`` takes the
            SDRF's own file names.
        out: Also write MetaMorpheus's ``ExperimentalDesign.tsv`` (1-based) here. Must end in
            ``.tsv``. Written only when the design is valid. MetaMorpheus finds the file only when
            it is named ``ExperimentalDesign.tsv`` and sits beside the spectra.
        timeout: Seconds to allow. ``None`` waits indefinitely.

    Returns:
        An :class:`SdrfDesign`: ``is_valid``, the refusals or the runs, and mzLib's notes.

    Raises:
        UsageError: the path is blank or the file does not exist; ``out`` does not end in ``.tsv``
            (checked before the SDRF is read); a condition column or searched file is blank or
            contains a tab or newline.
        BridgeError: mzLib could not parse the SDRF at all, or ``out``'s directory cannot be
            written.

    Examples:
        A valid two-factor design - 24 runs, 8 conditions of 3 biological replicates:

        >>> both = ["factor value[genotype]", "factor value[treatment]"]
        >>> d = design("PXD067622.sdrf.tsv", condition_columns=both)
        >>> d.is_valid, d.file_count, len(set(d.columns["condition"]))
        (True, 24, 8)
        >>> run = d.files[0]
        >>> run.file_name, run.condition, run.biological_replicate
        ('20240830_HF_LC3_MAA_RK_12032_CA_DMSO4', 'SPRTN-TurboID CA_DMSO (vehicle)', 0)

        A refusal lists every reason at once - here one per run, because the deposit's treatment
        is ``not available``:

        >>> refused = design("PXD049018.sdrf.tsv", condition_columns=both)
        >>> refused.is_valid, len(refused.refusals), refused.file_count
        (False, 20, 0)
        >>> print(refused.refusals[0])  # doctest: +NORMALIZE_WHITESPACE
        Line 2 (MSB67868ABand_01.raw): 'factor value[treatment]' is 'not available'. A condition
        cannot be built from an unknown factor; fill it in, or leave the column out of the
        declared condition columns to pool these rows.

        Study-wide replicate numbers are ranked within each condition, and the mapping is kept:

        >>> ranked = design("PXD067622_studywide.sdrf.tsv", condition_columns=both)
        >>> ranked.is_valid, len(ranked.notes)
        (True, 7)
        >>> print(ranked.notes[1])  # doctest: +NORMALIZE_WHITESPACE
        Condition 'SPRTN-TurboID CA_formaldehyde 1 mM, 1 h': biological replicates renumbered
        22 -> 1, 23 -> 2, 24 -> 3.
    """
    args = ["sdrf", "design", "--path", _one_path(path)]

    if condition_columns is not None:
        if isinstance(condition_columns, str):
            raise _bridge.UsageError(
                "condition_columns takes a list of column names, e.g. ['factor value[treatment]']."
            )
        names = []
        for name in condition_columns:
            if not isinstance(name, str) or not name.strip():
                raise _bridge.UsageError(
                    f"Every condition column must be a non-blank str; got {name!r}."
                )
            if "\t" in name or "\n" in name or "\r" in name:
                raise _bridge.UsageError(
                    f"A condition column may not contain a tab or newline: {name!r}."
                )
            names.append(name)
        if not names:
            raise _bridge.UsageError(
                "condition_columns is empty; pass None to use the document's only factor column."
            )
        args += ["--condition-columns", "\t".join(names)]

    stdin = None
    if searched_files is not None:
        if isinstance(searched_files, (str, os.PathLike)):
            raise _bridge.UsageError(
                "searched_files takes a list of paths, e.g. ['run1.raw', 'run2.raw']."
            )
        lines = []
        for f in searched_files:
            text = _bridge.path_text(f)
            if not text:
                raise _bridge.UsageError(
                    f"Every searched file must be a non-blank str or PathLike; got {f!r}."
                )
            if "\n" in text or "\r" in text:
                raise _bridge.UsageError(f"A searched file contains a newline: {text!r}.")
            lines.append(text)
        args.append("--searched-files-stdin")
        stdin = "\n".join(lines) + "\n"

    if out is not None:
        out_text = _bridge.path_text(out)
        if not out_text:
            raise _bridge.UsageError(
                "out must be a file path ending in .tsv, e.g. 'ExperimentalDesign.tsv'."
            )
        args += ["--out", out_text]

    data = _bridge.invoke(*args, stdin=stdin, timeout=timeout)
    return SdrfDesign._from_wire(data)

pymzlib.isobaric

isobaric

Isobaric labelling kits - TMT, TMTpro, iTRAQ, DiLeu - with every channel's reporter-ion m/z.

An isobaric experiment is read out of the low-m/z reporter ions: one ion per channel, a few millidaltons apart. Getting a channel's label or its m/z wrong mislabels every sample after it, so pyMzLib does not keep its own table. :func:kits returns mzLib's::

>>> import pymzlib
>>> tmtpro = pymzlib.isobaric.kits("TMT18")
>>> kit = tmtpro.kits[0]
>>> kit.name, kit.channel_count
('TMT18', 18)
>>> for channel in kit.channels[:3]:
...     print(channel.label, round(channel.reporter_ion_mz, 5))
126 126.12773
127N 127.12476
127C 127.13108

Nothing in that table is typed in. mzLib holds only the channel labels. Each m/z is a DI HCD diagnostic-ion line of the kit's Multiplex Label modification in mzLib's embedded TMT.txt, plus one proton, sorted ascending and paired with the labels by position. The m/z are therefore the same ones MetaMorpheus uses to quantify a TMT search.

The kit is looked up the way MetaMorpheus looks it up: by its whole name, case-insensitively, optionally followed by " on <motif>" - "TMT10", "tmt10", "TMT6-plex", "iTRAQ-4plex on K". Never by substring, so "TMT10plex" is refused rather than mistaken for a kit whose name it contains.

ReporterChannel dataclass

One channel of a kit and the theoretical m/z of its reporter ion.

Attributes:

Name Type Description
kit str

mzLib's kit name (an IsobaricMassTagType member, e.g. "TMT18").

index int

0-based position within the kit, ascending with reporter_ion_mz.

label str

The channel's name as MetaMorpheus spells it - "126", "127N", "115a".

reporter_ion_mz float

Theoretical m/z at charge 1, in m/z (Th): the diagnostic-ion mass from mzLib's TMT.txt plus one proton. Not a calibrated or observed value.

mz_min float

reporter_ion_mz minus the matching tolerance, in m/z.

mz_max float

reporter_ion_mz plus the matching tolerance, in m/z.

Source code in pkg/python/src/pymzlib/isobaric.py
@dataclass(frozen=True)
class ReporterChannel:
    """One channel of a kit and the theoretical m/z of its reporter ion.

    Attributes:
        kit: mzLib's kit name (an ``IsobaricMassTagType`` member, e.g. ``"TMT18"``).
        index: 0-based position within the kit, ascending with ``reporter_ion_mz``.
        label: The channel's name as MetaMorpheus spells it - ``"126"``, ``"127N"``, ``"115a"``.
        reporter_ion_mz: Theoretical m/z at charge 1, in **m/z** (Th): the diagnostic-ion mass
            from mzLib's ``TMT.txt`` plus one proton. Not a calibrated or observed value.
        mz_min: ``reporter_ion_mz`` minus the matching tolerance, in m/z.
        mz_max: ``reporter_ion_mz`` plus the matching tolerance, in m/z.
    """

    kit: str
    index: int
    label: str
    reporter_ion_mz: float
    mz_min: float
    mz_max: float

IsobaricKit dataclass

One kit and its channels, in ascending reporter-ion m/z.

Attributes:

Name Type Description
name str

mzLib's kit name, an IsobaricMassTagType member. TMT16 and TMT18 are TMTpro; kits().kits lists every name.

channel_count int

How many channels the kit has.

channels list[ReporterChannel]

Every channel as a :class:ReporterChannel.

Source code in pkg/python/src/pymzlib/isobaric.py
@dataclass(frozen=True)
class IsobaricKit:
    """One kit and its channels, in ascending reporter-ion m/z.

    Attributes:
        name: mzLib's kit name, an ``IsobaricMassTagType`` member. ``TMT16`` and ``TMT18`` are
            TMTpro; ``kits().kits`` lists every name.
        channel_count: How many channels the kit has.
        channels: Every channel as a :class:`ReporterChannel`.
    """

    name: str
    channel_count: int
    channels: list[ReporterChannel]

    @property
    def labels(self) -> list[str]:
        """The channel labels, in ascending reporter-ion m/z."""
        return [c.label for c in self.channels]

    @property
    def reporter_ion_mzs(self) -> list[float]:
        """The reporter-ion m/z values, ascending, in m/z."""
        return [c.reporter_ion_mz for c in self.channels]
labels property
labels: list[str]

The channel labels, in ascending reporter-ion m/z.

reporter_ion_mzs property
reporter_ion_mzs: list[float]

The reporter-ion m/z values, ascending, in m/z.

IsobaricKits dataclass

Bases: _Table

The kits :func:kits returned, as a long table and as objects.

columns maps kit, channel_index, channel_label, reporter_ion_mz, mz_min and mz_max to one value per (kit, channel) - ready for pandas.DataFrame(result.columns). :attr:kits groups the same rows by kit.

Attributes:

Name Type Description
kit str | None

The name you asked for, exactly as given, or None when every kit was listed.

kit_count int

Kits listed.

record_count int

Channels listed, over every kit; the length of every column.

absolute_tolerance float

The half-width, in Da, of each channel's matching window. mzLib reads a reporter intensity as the most intense peak within it.

column_names list[str]

The table's columns, in order.

columns dict[str, list[Any]]

Column name to one value per channel.

kit_summaries list[dict[str, Any]]

The wire's per-kit summaries, {"kit", "channel_count"}, in mzLib's order. :attr:kits regroups the table under them as :class:IsobaricKit objects.

caveats list[str]

What the m/z are and are not - read these once.

Source code in pkg/python/src/pymzlib/isobaric.py
@dataclass(frozen=True)
class IsobaricKits(_Table):
    """The kits :func:`kits` returned, as a long table and as objects.

    ``columns`` maps ``kit``, ``channel_index``, ``channel_label``, ``reporter_ion_mz``, ``mz_min``
    and ``mz_max`` to one value per (kit, channel) - ready for ``pandas.DataFrame(result.columns)``.
    :attr:`kits` groups the same rows by kit.

    Attributes:
        kit: The name you asked for, exactly as given, or ``None`` when every kit was listed.
        kit_count: Kits listed.
        record_count: Channels listed, over every kit; the length of every column.
        absolute_tolerance: The half-width, in **Da**, of each channel's matching window. mzLib
            reads a reporter intensity as the most intense peak within it.
        column_names: The table's columns, in order.
        columns: Column name to one value per channel.
        kit_summaries: The wire's per-kit summaries, ``{"kit", "channel_count"}``, in mzLib's
            order. :attr:`kits` regroups the table under them as :class:`IsobaricKit` objects.
        caveats: What the m/z are and are not - read these once.
    """

    kit: str | None
    kit_count: int
    record_count: int
    absolute_tolerance: float
    column_names: list[str]
    columns: dict[str, list[Any]]
    kit_summaries: list[dict[str, Any]] = field(default_factory=list, repr=False)
    caveats: list[str] = field(default_factory=list)

    @property
    def channels(self) -> list[ReporterChannel]:
        """Every channel as a :class:`ReporterChannel`, kits in mzLib's order."""
        return [
            ReporterChannel(
                kit=row["kit"],
                index=row["channel_index"],
                label=row["channel_label"],
                reporter_ion_mz=row["reporter_ion_mz"],
                mz_min=row["mz_min"],
                mz_max=row["mz_max"],
            )
            for row in self.records
        ]

    @property
    def kits(self) -> list[IsobaricKit]:
        """Every kit as an :class:`IsobaricKit`, in mzLib's order."""
        channels = self.channels
        return [
            IsobaricKit(
                name=summary["kit"],
                channel_count=summary["channel_count"],
                channels=[c for c in channels if c.kit == summary["kit"]],
            )
            for summary in self.kit_summaries
        ]

    def kit_named(self, name: str) -> IsobaricKit:
        """The listed kit whose :attr:`IsobaricKit.name` is exactly ``name``.

        Raises:
            UsageError: no listed kit has that name.
        """
        for kit in self.kits:
            if kit.name == name:
                return kit
        raise _bridge.UsageError(
            f"No kit named {name!r} in this result; it lists {[k.name for k in self.kits]}."
        )

    @classmethod
    def _from_wire(cls, data: Mapping[str, Any]) -> "IsobaricKits":
        return cls(
            kit=data.get("kit"),
            kit_count=int(data.get("kit_count", 0)),
            record_count=int(data.get("record_count", 0)),
            absolute_tolerance=float(data.get("absolute_tolerance", 0.0)),
            column_names=list(data.get("column_names") or []),
            columns={k: list(v) for k, v in (data.get("columns") or {}).items()},
            kit_summaries=[dict(k) for k in (data.get("kits") or [])],
            caveats=list(data.get("caveats") or []),
        )
channels property
channels: list[ReporterChannel]

Every channel as a :class:ReporterChannel, kits in mzLib's order.

kits property
kits: list[IsobaricKit]

Every kit as an :class:IsobaricKit, in mzLib's order.

kit_named
kit_named(name: str) -> IsobaricKit

The listed kit whose :attr:IsobaricKit.name is exactly name.

Raises:

Type Description
UsageError

no listed kit has that name.

Source code in pkg/python/src/pymzlib/isobaric.py
def kit_named(self, name: str) -> IsobaricKit:
    """The listed kit whose :attr:`IsobaricKit.name` is exactly ``name``.

    Raises:
        UsageError: no listed kit has that name.
    """
    for kit in self.kits:
        if kit.name == name:
            return kit
    raise _bridge.UsageError(
        f"No kit named {name!r} in this result; it lists {[k.name for k in self.kits]}."
    )

kits

kits(kit: str | None = None, *, timeout: float | None = 60) -> IsobaricKits

The isobaric kits mzLib can name, with every channel's label and reporter-ion m/z.

Calls mzLib's IsobaricMassTag.TryGetIsobaricMassTag for each IsobaricMassTagType, or for the one kit resolves to through IsobaricMassTag.TryGetTagType. No file, no network.

Parameters:

Name Type Description Default
kit str | None

One kit, matched by mzLib's whole-name rule: case-insensitive, optionally followed by " on <motif>" ("TMT10", "TMT6-plex", "iTRAQ-4plex on K"). None lists every kit.

None
timeout float | None

Seconds to allow. None waits indefinitely.

60

Returns:

Name Type Description
An IsobaricKits

class:IsobaricKits table, also grouped as :attr:IsobaricKits.kits.

Raises:

Type Description
UsageError

kit is blank or not a name mzLib knows. The message lists the kits.

Examples:

Every kit mzLib knows:

>>> catalogue = kits()
>>> [(k.name, k.channel_count) for k in catalogue.kits]
[('TMT6', 6), ('TMT10', 10), ('TMT11', 11), ('TMT16', 16), ('TMT18', 18),
 ('iTRAQ4', 4), ('iTRAQ8', 8), ('diLeu4', 4), ('diLeu12', 12)]
>>> catalogue.absolute_tolerance
0.003
>>> catalogue.kit_named("iTRAQ8").labels
['113', '114', '115', '116', '117', '118', '119', '121']

TMTpro 18-plex, whose top channel sits at 135.15:

>>> tmtpro = kits("TMT18")
>>> last = tmtpro.kits[0].channels[-1]
>>> last.label, round(last.reporter_ion_mz, 4), round(last.mz_max - last.mz_min, 6)
('135N', 135.1516, 0.006)
Source code in pkg/python/src/pymzlib/isobaric.py
def kits(kit: str | None = None, *, timeout: float | None = 60) -> IsobaricKits:
    """The isobaric kits mzLib can name, with every channel's label and reporter-ion m/z.

    Calls mzLib's ``IsobaricMassTag.TryGetIsobaricMassTag`` for each ``IsobaricMassTagType``, or for
    the one ``kit`` resolves to through ``IsobaricMassTag.TryGetTagType``. No file, no network.

    Args:
        kit: One kit, matched by mzLib's whole-name rule: case-insensitive, optionally followed by
            ``" on <motif>"`` (``"TMT10"``, ``"TMT6-plex"``, ``"iTRAQ-4plex on K"``). ``None`` lists
            every kit.
        timeout: Seconds to allow. ``None`` waits indefinitely.

    Returns:
        An :class:`IsobaricKits` table, also grouped as :attr:`IsobaricKits.kits`.

    Raises:
        UsageError: ``kit`` is blank or not a name mzLib knows. The message lists the kits.

    Examples:
        Every kit mzLib knows:

        >>> catalogue = kits()
        >>> [(k.name, k.channel_count) for k in catalogue.kits]  # doctest: +NORMALIZE_WHITESPACE
        [('TMT6', 6), ('TMT10', 10), ('TMT11', 11), ('TMT16', 16), ('TMT18', 18),
         ('iTRAQ4', 4), ('iTRAQ8', 8), ('diLeu4', 4), ('diLeu12', 12)]
        >>> catalogue.absolute_tolerance
        0.003
        >>> catalogue.kit_named("iTRAQ8").labels
        ['113', '114', '115', '116', '117', '118', '119', '121']

        TMTpro 18-plex, whose top channel sits at 135.15:

        >>> tmtpro = kits("TMT18")
        >>> last = tmtpro.kits[0].channels[-1]
        >>> last.label, round(last.reporter_ion_mz, 4), round(last.mz_max - last.mz_min, 6)
        ('135N', 135.1516, 0.006)
    """
    args = ["isobaric", "kits"]
    if kit is not None:
        if not isinstance(kit, str) or not kit.strip():
            raise _bridge.UsageError(
                f"kit must be a kit name such as 'TMT10' or 'iTRAQ-4plex', or None; got {kit!r}."
            )
        args += ["--kit", kit]
    data = _bridge.invoke(*args, timeout=timeout)
    return IsobaricKits._from_wire(data)

pymzlib.proteins

proteins

Protein databases: what each protein is, which gene it belongs to, and whether a peptide identifies it.

Questions a search result cannot answer on its own, each answered from the protein database you searched, by mzLib:

What is this accession? :func:read loads a UniProt XML or a FASTA and returns one row per protein - organism, NCBI taxonomy id, gene names, length, monoisotopic mass - and, on request, its Gene Ontology terms and Ensembl gene links as long tables::

>>> db = pymzlib.proteins.read(
...     ["human_subset.xml", "human_extra.fasta", "mouse_aifm1.fasta"],
...     contaminants=["contaminants.fasta"], tables=pymzlib.proteins.TABLES)
>>> db.taxonomy()["Q9Z0X1"], db.organisms()["P02769"]
('10090', 'Bos taurus')
>>> db.go_terms.columns["term_name"][:3]
['cytoplasm', 'cytosol', 'extracellular exosome']

Which gene is it, reproducibly? :func:resolve_genes resolves every protein to a stable Ensembl gene id, counted against a gene set you supply and pin - an Ensembl GTF for one release - and says, per protein, how it resolved (resolved, multi_gene, off_primary_only, not_in_source, ...). Every row carries the sha256 of the database, the GTF and the optional cross-reference table it was computed from::

>>> genes = pymzlib.proteins.resolve_genes(
...     ["human_subset.xml", "human_extra.fasta"], contaminants=["contaminants.fasta"],
...     gtf="Homo_sapiens.GRCh38.116.gtf", xref="Homo_sapiens.GRCh38.116.uniprot.tsv")
>>> genes.gene_set.release, genes.gene_set.genome_build, genes.outcome_counts["resolved"]
('116', 'GRCh38.p14', 1)

Does this peptide identify one protein? :func:classify_peptides classifies each peptide as Unique, SharedWithinGene, SharedAcrossGenes or NotInDatabase, treating I and L as the same residue, because a mass spectrometer cannot tell them apart::

>>> calls = pymzlib.proteins.classify_peptides(
...     ["VGVNGFGR", "LVLNGNPLTLFQER", "ALSEQINIFFDYSGR", "YLYEIAR", "AEFVEVTK", "PEPTIDEK"],
...     ["human_subset.xml", "human_extra.fasta"], contaminants=["contaminants.fasta"])
>>> calls.columns["sharing"]
['Unique', 'Unique', 'SharedWithinGene', 'SharedAcrossGenes', 'Unique', 'NotInDatabase']

What does each protein group do, every member kept? :func:annotate_go reads the protein-group table MetaMorpheus wrote, the UniProt XML it searched and a go.obo you pinned, and returns one row per (group, GO term) that any member holds - directly or through an ancestor in the ontology - naming the members that carry it. Consensus and direct-only views are filters on the rows. :func:update_go is the one function that fetches a go.obo; :func:annotate_go never does::

>>> go = pymzlib.proteins.annotate_go(
...     "PXD036557_AllQuantifiedProteinGroups.tsv", "pxd036557_proteins.xml",
...     go_obo="go-pxd036557.obo", category_map="organelle_map.tsv")
>>> go.group_count, go.row_count, go.header["status_contaminant"]
(5, 563, '1')

These examples are the calls that recorded the payloads in pyMzLib's test fixtures, over the small databases in pkg/python/tests/fixtures/proteins/. The GO example's table is a real MetaMorpheus 1.1.11 search (PXD036557).

:func:read, :func:resolve_genes and :func:classify_peptides take one database or many. Pass a list and they are read in one bridge call, in order; threads says how many are read at once (default 1), and the answer is identical at any value. Mark contaminant databases with contaminants= - it changes answers: a contaminant is never mapped to a gene, and a peptide it shares with a target is shared.

What these functions will not tell you, and say so instead:

  • A FASTA knows no GO terms and no Ensembl genes. Its headers carry organism (OS=), taxonomy (OX=) and gene name (GN=) and nothing else, so it contributes no go_terms or ensembl_genes rows and :func:resolve_genes calls every FASTA protein not_in_source. That is the format's silence, not a biological absence, and each result says so in :attr:DatabaseFile.absent_fields and caveats. Read the UniProt XML of the same proteome.
  • No decoys are generated and no variants are expanded. A database is read as written. A decoy already in the file (an accession starting DECOY) is kept and flagged is_decoy. A UniProt XML that records a genotype still has those variants applied by mzLib, which renames the accession (P38936_C117Y); the file's caveats say how many.
  • Masses are of the unmodified sequence as written - the precursor, initiator methionine and signal peptide included - so they are not the mass of the mature protein.

Wire verbs: proteins read, genes resolve, proteins classify-peptides, proteins annotate-go and proteins update-go; their language-neutral specs live in the bridge repository under design/verbs/.

FileError dataclass

Why one database could not be read, under on_error="skip".

Attributes:

Name Type Description
kind str

"usage" (the file is missing, or not a database this reads) or "correctness" (mzLib could not parse it).

type str

The error type as the bridge classified it - "usage", or the .NET exception name.

message str

What went wrong, naming the file.

Source code in pkg/python/src/pymzlib/proteins.py
@dataclass(frozen=True)
class FileError:
    """Why one database could not be read, under ``on_error="skip"``.

    Attributes:
        kind: ``"usage"`` (the file is missing, or not a database this reads) or ``"correctness"``
            (mzLib could not parse it).
        type: The error type as the bridge classified it - ``"usage"``, or the .NET exception name.
        message: What went wrong, naming the file.
    """

    kind: str
    type: str
    message: str

    @classmethod
    def _from_wire(cls, data: Mapping[str, Any]) -> FileError:
        return cls(kind=data.get("kind", ""), type=data.get("type", ""), message=data.get("message", ""))

DatabaseFile dataclass

What happened to one input database.

One per input, in input order, whether it was read or not - so a result for many databases always says which of them contributed.

Attributes:

Name Type Description
source_index int

Position in the input: databases first, then contaminants. The source_index column of every table points back here.

path str

The absolute path.

file_type str | None

"UniProtXml" or "Fasta", chosen from the extension (.xml, or .fasta/.fa/.faa/.fas, each optionally .gz). None if the file failed before its type was known.

reader str | None

The mzLib loader used, e.g. "ProteinDbLoader.LoadProteinXML".

contaminant bool

Whether this database was loaded as contaminants.

protein_count int

Proteins mzLib loaded from it, before any accession filter.

decoy_count int

Of those, how many the file itself marks as decoys (accession starts DECOY). None are generated.

record_count int

Rows this database contributed to the main table. For :func:read, proteins that passed the accession filter; for :func:resolve_genes, resolution rows; for :func:classify_peptides, target proteins searched.

search_database_sha256 str | None

For :func:resolve_genes only: the lower-case hex sha256 of the database's decompressed bytes, the key mzLib's resolver pins every row to. None from the other two functions.

caveats list[str]

What this file cannot tell you, or what mzLib did to it: a FASTA has no GO or Ensembl source; genotype variants were applied; FASTA lines the loader skipped.

absent_fields list[str]

Tables and columns this format has no source for, so that an empty value reads as "this file cannot say" and never as "there is none". For a FASTA: ["go_terms", "ensembl_genes", "ensembl_gene_ids"].

error FileError | None

None, or why the file could not be read (on_error="skip" only).

Source code in pkg/python/src/pymzlib/proteins.py
@dataclass(frozen=True)
class DatabaseFile:
    """What happened to one input database.

    One per input, in input order, whether it was read or not - so a result for many databases
    always says which of them contributed.

    Attributes:
        source_index: Position in the input: databases first, then ``contaminants``. The
            ``source_index`` column of every table points back here.
        path: The absolute path.
        file_type: ``"UniProtXml"`` or ``"Fasta"``, chosen from the extension (``.xml``, or
            ``.fasta``/``.fa``/``.faa``/``.fas``, each optionally ``.gz``). ``None`` if the file
            failed before its type was known.
        reader: The mzLib loader used, e.g. ``"ProteinDbLoader.LoadProteinXML"``.
        contaminant: Whether this database was loaded as contaminants.
        protein_count: Proteins mzLib loaded from it, before any accession filter.
        decoy_count: Of those, how many the file itself marks as decoys (accession starts
            ``DECOY``). None are generated.
        record_count: Rows this database contributed to the main table. For :func:`read`, proteins
            that passed the accession filter; for :func:`resolve_genes`, resolution rows; for
            :func:`classify_peptides`, target proteins searched.
        search_database_sha256: For :func:`resolve_genes` only: the lower-case hex sha256 of the
            database's **decompressed** bytes, the key mzLib's resolver pins every row to. ``None``
            from the other two functions.
        caveats: What this file cannot tell you, or what mzLib did to it: a FASTA has no GO or
            Ensembl source; genotype variants were applied; FASTA lines the loader skipped.
        absent_fields: Tables and columns this **format** has no source for, so that an empty value
            reads as "this file cannot say" and never as "there is none". For a FASTA:
            ``["go_terms", "ensembl_genes", "ensembl_gene_ids"]``.
        error: ``None``, or why the file could not be read (``on_error="skip"`` only).
    """

    source_index: int
    path: str
    file_type: str | None
    reader: str | None
    contaminant: bool
    protein_count: int
    decoy_count: int
    record_count: int
    search_database_sha256: str | None = None
    caveats: list[str] = field(default_factory=list)
    absent_fields: list[str] = field(default_factory=list)
    error: FileError | None = None

    @classmethod
    def _from_wire(cls, data: Mapping[str, Any]) -> DatabaseFile:
        error = data.get("error")
        return cls(
            source_index=int(data.get("source_index", 0)),
            path=data.get("path", ""),
            file_type=data.get("file_type"),
            reader=data.get("reader"),
            contaminant=bool(data.get("contaminant", False)),
            protein_count=int(data.get("protein_count", 0)),
            decoy_count=int(data.get("decoy_count", 0)),
            record_count=int(data.get("record_count", 0)),
            search_database_sha256=data.get("search_database_sha256"),
            caveats=list(data.get("caveats") or []),
            absent_fields=list(data.get("absent_fields") or []),
            error=FileError._from_wire(error) if error else None,
        )

Table dataclass

Bases: _Table

A long table: one row per record, column-major.

Attributes:

Name Type Description
row_count int

Rows in the table.

column_names list[str]

Column order.

columns dict[str, list[Any]]

Column name -> list of values, all the same length. Hand it straight to pandas.DataFrame(table.columns), or use :attr:records for a list of dicts.

Source code in pkg/python/src/pymzlib/proteins.py
@dataclass(frozen=True)
class Table(_Table):
    """A long table: one row per record, column-major.

    Attributes:
        row_count: Rows in the table.
        column_names: Column order.
        columns: Column name -> list of values, all the same length. Hand it straight to
            ``pandas.DataFrame(table.columns)``, or use :attr:`records` for a list of dicts.
    """

    row_count: int
    column_names: list[str] = field(default_factory=list)
    columns: dict[str, list[Any]] = field(default_factory=dict)

    @classmethod
    def _from_wire(cls, data: Mapping[str, Any] | None) -> Table | None:
        if data is None:
            return None
        return cls(
            row_count=int(data.get("row_count", 0)),
            column_names=list(data.get("column_names") or []),
            columns=dict(data.get("columns") or {}),
        )

ProteinDatabase dataclass

Bases: _Table

What :func:read returns: the proteins, and the long tables asked for.

The proteins table (:attr:columns) has one row per protein, in database order:

====================== ============= ====================================================== column type / unit meaning; None means ====================== ============= ====================================================== source_index int which input (:attr:files) the row came from source_path str that input's absolute path accession str as the database wrote it (P04406, Q13409-2) name str entry name (G3P_HUMAN); None: not recorded full_name str protein name; None: not recorded organism str OS= / <organism> scientific name; None: not recorded ncbi_taxonomy_id str NCBI taxon ("9606") from XML or FASTA OX=; None: the entry records none. Kept a string: it is an identifier, not a quantity. primary_gene_name str the primary gene name (GN=); None: none given gene_names list[str] every gene name the entry gives, primary first, synonyms, ORF and locus names after; may be empty length int, residues sequence length monoisotopic_mass float, Da unmodified sequence plus one water; None: the sequence holds a letter with no defined mass (X, B, Z, J) is_contaminant bool loaded from a contaminants= database is_decoy bool the file marks it a decoy (DECOY prefix) is_entrapment bool mzLib's entrapment flag (accession contains Random) ensembl_gene_ids list[str] distinct stable Ensembl gene ids, ordinal order; empty for FASTA (see absent_fields) sequence str only with sequences=True ====================== ============= ======================================================

Attributes:

Name Type Description
file_count int

Database files given.

read_count int

Database files read.

failed_count int

Database files that failed (on_error="skip" only; otherwise the call raises).

record_count int

Proteins that passed the accession filter - the rows of the proteins table when it was requested.

tables list[str]

The tables returned, in :data:TABLES order.

accession_filter_count int | None

Distinct accessions in the filter, or None with no filter.

accessions_not_found list[str] | None

Filter accessions no database contained, in the order given; None with no filter. Check this: matching is exact, so P04406-1 does not find P04406.

caveats list[str]

Traps in this result as a whole, e.g. proteins with no computable mass.

files list[DatabaseFile]

One :class:DatabaseFile per input, in input order.

column_names list[str] | None

The proteins table's column order; None if it was not requested.

columns dict[str, list[Any]] | None

The proteins table; None if it was not requested.

go_terms Table | None

One row per (protein, GO term), or None if not requested. Columns: source_index, source_path, accession, go_id ("GO:0005737"), aspect ("BiologicalProcess", "CellularComponent", "MolecularFunction", or "Unknown" when UniProt gave no C:/F:/P: prefix - never guessed), term_name (prefix removed; None when the reference carried no name), evidence_codes (ECO ids, sorted; may be empty) and projects (annotating sources such as UniProtKB, sorted). A GO id repeated with several lines of evidence is one row with the evidence unioned.

ensembl_genes Table | None

One row per (protein, Ensembl transcript), or None if not requested. Columns: source_index, source_path, accession, transcript_id (as UniProt wrote it, versioned), protein_id (None when UniProt gave none), gene_id (stable - join on this), versioned_gene_id (as written, e.g. ENSG00000111640.15) and gene_version (int; None when unversioned, which is not version 0).

Source code in pkg/python/src/pymzlib/proteins.py
@dataclass(frozen=True)
class ProteinDatabase(_Table):
    """What :func:`read` returns: the proteins, and the long tables asked for.

    The ``proteins`` table (:attr:`columns`) has one row per protein, in database order:

    ======================  =============  ======================================================
    column                  type / unit    meaning; ``None`` means
    ======================  =============  ======================================================
    ``source_index``        int            which input (:attr:`files`) the row came from
    ``source_path``         str            that input's absolute path
    ``accession``           str            as the database wrote it (``P04406``, ``Q13409-2``)
    ``name``                str            entry name (``G3P_HUMAN``); ``None``: not recorded
    ``full_name``           str            protein name; ``None``: not recorded
    ``organism``            str            ``OS=`` / ``<organism>`` scientific name;
                                           ``None``: not recorded
    ``ncbi_taxonomy_id``    str            NCBI taxon (``"9606"``) from XML or FASTA ``OX=``;
                                           ``None``: the entry records none. Kept a string: it
                                           is an identifier, not a quantity.
    ``primary_gene_name``   str            the primary gene name (``GN=``); ``None``: none given
    ``gene_names``          list[str]      every gene name the entry gives, primary first,
                                           synonyms, ORF and locus names after; may be empty
    ``length``              int, residues  sequence length
    ``monoisotopic_mass``   float, Da      unmodified sequence plus one water; ``None``: the
                                           sequence holds a letter with no defined mass (X, B,
                                           Z, J)
    ``is_contaminant``      bool           loaded from a ``contaminants=`` database
    ``is_decoy``            bool           the file marks it a decoy (``DECOY`` prefix)
    ``is_entrapment``       bool           mzLib's entrapment flag (accession contains
                                           ``Random``)
    ``ensembl_gene_ids``    list[str]      distinct stable Ensembl gene ids, ordinal order;
                                           empty for FASTA (see ``absent_fields``)
    ``sequence``            str            only with ``sequences=True``
    ======================  =============  ======================================================

    Attributes:
        file_count: Database files given.
        read_count: Database files read.
        failed_count: Database files that failed (``on_error="skip"`` only; otherwise the call
            raises).
        record_count: Proteins that passed the accession filter - the rows of the ``proteins``
            table when it was requested.
        tables: The tables returned, in :data:`TABLES` order.
        accession_filter_count: Distinct accessions in the filter, or ``None`` with no filter.
        accessions_not_found: Filter accessions no database contained, in the order given; ``None``
            with no filter. **Check this**: matching is exact, so ``P04406-1`` does not find
            ``P04406``.
        caveats: Traps in this result as a whole, e.g. proteins with no computable mass.
        files: One :class:`DatabaseFile` per input, in input order.
        column_names: The ``proteins`` table's column order; ``None`` if it was not requested.
        columns: The ``proteins`` table; ``None`` if it was not requested.
        go_terms: One row per (protein, GO term), or ``None`` if not requested. Columns:
            ``source_index``, ``source_path``, ``accession``, ``go_id`` (``"GO:0005737"``),
            ``aspect`` (``"BiologicalProcess"``, ``"CellularComponent"``,
            ``"MolecularFunction"``, or ``"Unknown"`` when UniProt gave no ``C:``/``F:``/``P:``
            prefix - never guessed), ``term_name`` (prefix removed; ``None`` when the reference
            carried no name), ``evidence_codes`` (ECO ids, sorted; may be empty) and ``projects``
            (annotating sources such as ``UniProtKB``, sorted). A GO id repeated with several lines
            of evidence is **one** row with the evidence unioned.
        ensembl_genes: One row per (protein, Ensembl transcript), or ``None`` if not requested.
            Columns: ``source_index``, ``source_path``, ``accession``, ``transcript_id`` (as UniProt
            wrote it, versioned), ``protein_id`` (``None`` when UniProt gave none), ``gene_id``
            (stable - **join on this**), ``versioned_gene_id`` (as written, e.g.
            ``ENSG00000111640.15``) and ``gene_version`` (int; ``None`` when unversioned, which is
            not version 0).
    """

    file_count: int
    read_count: int
    failed_count: int
    record_count: int
    tables: list[str] = field(default_factory=list)
    accession_filter_count: int | None = None
    accessions_not_found: list[str] | None = None
    caveats: list[str] = field(default_factory=list)
    files: list[DatabaseFile] = field(default_factory=list)
    column_names: list[str] | None = None
    columns: dict[str, list[Any]] | None = None
    go_terms: Table | None = None
    ensembl_genes: Table | None = None

    def taxonomy(self) -> dict[str, str | None]:
        """Accession -> NCBI taxonomy id, for every protein in the table.

        Raises:
            UsageError: the ``proteins`` table was not requested.

        Examples:
            >>> db = read(["human_subset.xml", "human_extra.fasta", "mouse_aifm1.fasta"],
            ...           contaminants=["contaminants.fasta"], tables=TABLES)
            >>> db.taxonomy()["Q9Z0X1"]
            '10090'
        """
        columns = self._require_proteins()
        return dict(zip(columns["accession"], columns["ncbi_taxonomy_id"]))

    def organisms(self) -> dict[str, str | None]:
        """Accession -> organism name, for every protein in the table.

        Raises:
            UsageError: the ``proteins`` table was not requested.
        """
        columns = self._require_proteins()
        return dict(zip(columns["accession"], columns["organism"]))

    def _require_proteins(self) -> dict[str, list[Any]]:
        if self.columns is None:
            raise _bridge.UsageError(
                "The proteins table was not requested. Call read(..., tables=(\"proteins\", ...))."
            )
        return self.columns

    @classmethod
    def _from_wire(cls, data: Mapping[str, Any]) -> ProteinDatabase:
        return cls(
            file_count=int(data.get("file_count", 0)),
            read_count=int(data.get("read_count", 0)),
            failed_count=int(data.get("failed_count", 0)),
            record_count=int(data.get("record_count", 0)),
            tables=list(data.get("tables") or []),
            accession_filter_count=data.get("accession_filter_count"),
            accessions_not_found=(
                None if data.get("accessions_not_found") is None
                else list(data["accessions_not_found"])
            ),
            caveats=list(data.get("caveats") or []),
            files=_files(data),
            column_names=None if data.get("column_names") is None else list(data["column_names"]),
            columns=data.get("columns"),
            go_terms=Table._from_wire(data.get("go_terms")),
            ensembl_genes=Table._from_wire(data.get("ensembl_genes")),
        )
taxonomy
taxonomy() -> dict[str, str | None]

Accession -> NCBI taxonomy id, for every protein in the table.

Raises:

Type Description
UsageError

the proteins table was not requested.

Examples:

>>> db = read(["human_subset.xml", "human_extra.fasta", "mouse_aifm1.fasta"],
...           contaminants=["contaminants.fasta"], tables=TABLES)
>>> db.taxonomy()["Q9Z0X1"]
'10090'
Source code in pkg/python/src/pymzlib/proteins.py
def taxonomy(self) -> dict[str, str | None]:
    """Accession -> NCBI taxonomy id, for every protein in the table.

    Raises:
        UsageError: the ``proteins`` table was not requested.

    Examples:
        >>> db = read(["human_subset.xml", "human_extra.fasta", "mouse_aifm1.fasta"],
        ...           contaminants=["contaminants.fasta"], tables=TABLES)
        >>> db.taxonomy()["Q9Z0X1"]
        '10090'
    """
    columns = self._require_proteins()
    return dict(zip(columns["accession"], columns["ncbi_taxonomy_id"]))
organisms
organisms() -> dict[str, str | None]

Accession -> organism name, for every protein in the table.

Raises:

Type Description
UsageError

the proteins table was not requested.

Source code in pkg/python/src/pymzlib/proteins.py
def organisms(self) -> dict[str, str | None]:
    """Accession -> organism name, for every protein in the table.

    Raises:
        UsageError: the ``proteins`` table was not requested.
    """
    columns = self._require_proteins()
    return dict(zip(columns["accession"], columns["organism"]))

GeneSet dataclass

The Ensembl gene set every resolution was counted against, and where it came from.

Attributes:

Name Type Description
source_file_name str

The GTF's file name. With gene_set=, still the GTF's name: a compact table carries the provenance of the GTF it was made from.

sha256 str

Lower-case hex sha256 of the GTF's bytes as read - the compressed bytes for a .gz, which is what Ensembl publishes and checksums.

release str | None

The Ensembl release parsed from the file name ("116" from Homo_sapiens.GRCh38.116.gtf.gz); None when the name carries none. Keep Ensembl's file name.

genome_build str | None

The #!genome-build header, e.g. "GRCh38.p14"; None if absent.

genebuild_last_updated str | None

The #!genebuild-last-updated header; None if absent.

gene_count int

Genes in the set.

Source code in pkg/python/src/pymzlib/proteins.py
@dataclass(frozen=True)
class GeneSet:
    """The Ensembl gene set every resolution was counted against, and where it came from.

    Attributes:
        source_file_name: The GTF's file name. With ``gene_set=``, still the **GTF's** name: a
            compact table carries the provenance of the GTF it was made from.
        sha256: Lower-case hex sha256 of the GTF's bytes as read - the compressed bytes for a
            ``.gz``, which is what Ensembl publishes and checksums.
        release: The Ensembl release parsed from the file name (``"116"`` from
            ``Homo_sapiens.GRCh38.116.gtf.gz``); ``None`` when the name carries none. Keep
            Ensembl's file name.
        genome_build: The ``#!genome-build`` header, e.g. ``"GRCh38.p14"``; ``None`` if absent.
        genebuild_last_updated: The ``#!genebuild-last-updated`` header; ``None`` if absent.
        gene_count: Genes in the set.
    """

    source_file_name: str
    sha256: str
    release: str | None
    genome_build: str | None
    genebuild_last_updated: str | None
    gene_count: int

    @classmethod
    def _from_wire(cls, data: Mapping[str, Any]) -> GeneSet:
        return cls(
            source_file_name=data.get("source_file_name", ""),
            sha256=data.get("sha256", ""),
            release=data.get("release"),
            genome_build=data.get("genome_build"),
            genebuild_last_updated=data.get("genebuild_last_updated"),
            gene_count=int(data.get("gene_count", 0)),
        )

XrefTable dataclass

Ensembl's own accession-to-gene cross-references, when xref= was given.

Attributes:

Name Type Description
source_file_name str

The file's name (e.g. Homo_sapiens.GRCh38.116.uniprot.tsv.gz).

sha256 str

Lower-case hex sha256 of the file's bytes as read.

release str | None

The Ensembl release from the file name; None if absent.

accession_count int

Distinct accessions the table links.

Source code in pkg/python/src/pymzlib/proteins.py
@dataclass(frozen=True)
class XrefTable:
    """Ensembl's own accession-to-gene cross-references, when ``xref=`` was given.

    Attributes:
        source_file_name: The file's name (e.g. ``Homo_sapiens.GRCh38.116.uniprot.tsv.gz``).
        sha256: Lower-case hex sha256 of the file's bytes as read.
        release: The Ensembl release from the file name; ``None`` if absent.
        accession_count: Distinct accessions the table links.
    """

    source_file_name: str
    sha256: str
    release: str | None
    accession_count: int

    @classmethod
    def _from_wire(cls, data: Mapping[str, Any] | None) -> XrefTable | None:
        if data is None:
            return None
        return cls(
            source_file_name=data.get("source_file_name", ""),
            sha256=data.get("sha256", ""),
            release=data.get("release"),
            accession_count=int(data.get("accession_count", 0)),
        )

GeneResolutions dataclass

Bases: _Table

What :func:resolve_genes returns: one row per (protein, gene), or one outcome row per protein with no gene. Never a |-joined cell, and never a pick among several genes.

The column names after source_index and source_path are mzLib's own GeneResolutionTsv schema, so this table and one written by mzLib's TSV writer mean the same:

========================== ========= =========================================================== column type meaning; None means ========================== ========= =========================================================== accession str as the search reported it, variant suffix and all entry_accession str the UniProt entry / unversioned RefSeq; verbatim otherwise isoform int UniProt isoform number; None: no -N suffix namespace str uniprot, refseq or unrecognized outcome str one of :data:OUTCOMES n_genes int genes on the gene set's assembly (0, 1, or more) gene_id str stable id (ENSG...); None: no gene on the assembly versioned_gene_id str the id as the database wrote it; None: no gene gene_symbol str the release's symbol (display only); None: no gene, or the GTF gives it no name gene_biotype str the release's biotype; None: no gene off_primary_genes int linked genes outside the set (ALT haplotypes, patches): dropped from n_genes, but counted here uniprot_gene_name str the database's own primary gene name, a label; None: none source str search_database_dbreference, or ensembl_xref for a gene only Ensembl links (adds an answer, never changes the outcome) search_database_sha256 str sha256 of the decompressed database gene_set_release str Ensembl release; None: the GTF name carries none gene_set_sha256 str sha256 of the GTF ensembl_xref_agrees bool whether Ensembl's xref links this pair; None: no gene on the row, or no xref= given (unknown, not false) ensembl_xref_info_type str Ensembl's evidence (DIRECT, SEQUENCE_MATCH, INFERRED_PAIR); None: no xref link ensembl_xref_sha256 str sha256 of the xref file; None: no xref= ========================== ========= ===========================================================

Attributes:

Name Type Description
file_count int

Database files given.

read_count int

Database files read.

failed_count int

Database files that failed (on_error="skip" only).

protein_count int

Proteins resolved.

record_count int

Rows in the table: at least one per protein, one per gene for multi_gene, plus any ensembl_xref rows.

gene_set GeneSet

The gene set and its provenance.

xref XrefTable | None

The xref table and its provenance, or None.

outcome_counts dict[str, int]

Outcome -> number of proteins with it. Every key in :data:OUTCOMES is present, zeros included.

caveats list[str]

Traps in this result: FASTA proteins, a GTF with no release in its name, no xref.

files list[DatabaseFile]

One :class:DatabaseFile per input, each with its search_database_sha256.

column_names list[str]

Column order.

columns dict[str, list[Any]]

Column name -> list of values.

Source code in pkg/python/src/pymzlib/proteins.py
@dataclass(frozen=True)
class GeneResolutions(_Table):
    """What :func:`resolve_genes` returns: one row per (protein, gene), or one outcome row per
    protein with no gene. Never a ``|``-joined cell, and never a pick among several genes.

    The column names after ``source_index`` and ``source_path`` are **mzLib's own**
    ``GeneResolutionTsv`` schema, so this table and one written by mzLib's TSV writer mean the same:

    ==========================  =========  ===========================================================
    column                      type       meaning; ``None`` means
    ==========================  =========  ===========================================================
    ``accession``               str        as the search reported it, variant suffix and all
    ``entry_accession``         str        the UniProt entry / unversioned RefSeq; verbatim otherwise
    ``isoform``                 int        UniProt isoform number; ``None``: no ``-N`` suffix
    ``namespace``               str        ``uniprot``, ``refseq`` or ``unrecognized``
    ``outcome``                 str        one of :data:`OUTCOMES`
    ``n_genes``                 int        genes on the gene set's assembly (0, 1, or more)
    ``gene_id``                 str        stable id (``ENSG...``); ``None``: no gene on the assembly
    ``versioned_gene_id``       str        the id as the database wrote it; ``None``: no gene
    ``gene_symbol``             str        the **release's** symbol (display only); ``None``: no gene,
                                           or the GTF gives it no name
    ``gene_biotype``            str        the release's biotype; ``None``: no gene
    ``off_primary_genes``       int        linked genes **outside** the set (ALT haplotypes, patches):
                                           dropped from ``n_genes``, but counted here
    ``uniprot_gene_name``       str        the database's own primary gene name, a label;
                                           ``None``: none
    ``source``                  str        ``search_database_dbreference``, or ``ensembl_xref`` for a
                                           gene only Ensembl links (adds an answer, never changes the
                                           outcome)
    ``search_database_sha256``  str        sha256 of the decompressed database
    ``gene_set_release``        str        Ensembl release; ``None``: the GTF name carries none
    ``gene_set_sha256``         str        sha256 of the GTF
    ``ensembl_xref_agrees``     bool       whether Ensembl's xref links this pair; ``None``: no gene on
                                           the row, or no ``xref=`` given (**unknown, not false**)
    ``ensembl_xref_info_type``  str        Ensembl's evidence (``DIRECT``, ``SEQUENCE_MATCH``,
                                           ``INFERRED_PAIR``); ``None``: no xref link
    ``ensembl_xref_sha256``     str        sha256 of the xref file; ``None``: no ``xref=``
    ==========================  =========  ===========================================================

    Attributes:
        file_count: Database files given.
        read_count: Database files read.
        failed_count: Database files that failed (``on_error="skip"`` only).
        protein_count: Proteins resolved.
        record_count: Rows in the table: at least one per protein, one per gene for
            ``multi_gene``, plus any ``ensembl_xref`` rows.
        gene_set: The gene set and its provenance.
        xref: The xref table and its provenance, or ``None``.
        outcome_counts: Outcome -> number of **proteins** with it. Every key in :data:`OUTCOMES` is
            present, zeros included.
        caveats: Traps in this result: FASTA proteins, a GTF with no release in its name, no xref.
        files: One :class:`DatabaseFile` per input, each with its ``search_database_sha256``.
        column_names: Column order.
        columns: Column name -> list of values.
    """

    file_count: int
    read_count: int
    failed_count: int
    protein_count: int
    record_count: int
    gene_set: GeneSet
    xref: XrefTable | None = None
    outcome_counts: dict[str, int] = field(default_factory=dict)
    caveats: list[str] = field(default_factory=list)
    files: list[DatabaseFile] = field(default_factory=list)
    column_names: list[str] = field(default_factory=list)
    columns: dict[str, list[Any]] = field(default_factory=dict)

    @classmethod
    def _from_wire(cls, data: Mapping[str, Any]) -> GeneResolutions:
        return cls(
            file_count=int(data.get("file_count", 0)),
            read_count=int(data.get("read_count", 0)),
            failed_count=int(data.get("failed_count", 0)),
            protein_count=int(data.get("protein_count", 0)),
            record_count=int(data.get("record_count", 0)),
            gene_set=GeneSet._from_wire(data.get("gene_set") or {}),
            xref=XrefTable._from_wire(data.get("xref")),
            outcome_counts=dict(data.get("outcome_counts") or {}),
            caveats=list(data.get("caveats") or []),
            files=_files(data),
            column_names=list(data.get("column_names") or []),
            columns=dict(data.get("columns") or {}),
        )

PeptideClassification dataclass

Bases: _Table

What :func:classify_peptides returns: one row per peptide, in the order given.

==================== ========= =============================================================== column type meaning ==================== ========= =============================================================== peptide str exactly as given - not I/L-folded sharing str :data:UNIQUE, :data:SHARED_WITHIN_GENE, :data:SHARED_ACROSS_GENES or :data:NOT_IN_DATABASE accession_count int target proteins containing it accessions list[str] those proteins, distinct, ordinal order; empty when not found shared_gene_keys list[str] the gene keys common to all of them, e.g. "ensembl:ENSG00000111640", "gene:Homo sapiens:GAPDH", "entry:P04406". Empty for NotInDatabase and SharedAcrossGenes ==================== ========= ===============================================================

Attributes:

Name Type Description
file_count int

Database files searched.

peptide_count int

Rows: peptides, one per input peptide, duplicates included.

target_protein_count int

Proteins searched. Contaminants count: they are real sequences in the search space.

decoy_proteins_ignored int

Decoy proteins in the databases, which are never searched.

i_and_l_equivalent bool

Always True: I and L are one residue for matching. On the wire so that the rule travels with every result rather than living only in documentation.

sharing_counts dict[str, int]

Class -> peptides in it; every class present, zeros included.

files list[DatabaseFile]

One :class:DatabaseFile per input.

column_names list[str]

Column order.

columns dict[str, list[Any]]

Column name -> list of values.

Source code in pkg/python/src/pymzlib/proteins.py
@dataclass(frozen=True)
class PeptideClassification(_Table):
    """What :func:`classify_peptides` returns: one row per peptide, in the order given.

    ====================  =========  ===============================================================
    column                type       meaning
    ====================  =========  ===============================================================
    ``peptide``           str        exactly as given - **not** I/L-folded
    ``sharing``           str        :data:`UNIQUE`, :data:`SHARED_WITHIN_GENE`,
                                     :data:`SHARED_ACROSS_GENES` or :data:`NOT_IN_DATABASE`
    ``accession_count``   int        target proteins containing it
    ``accessions``        list[str]  those proteins, distinct, ordinal order; empty when not found
    ``shared_gene_keys``  list[str]  the gene keys common to all of them, e.g.
                                     ``"ensembl:ENSG00000111640"``, ``"gene:Homo sapiens:GAPDH"``,
                                     ``"entry:P04406"``. Empty for ``NotInDatabase`` and
                                     ``SharedAcrossGenes``
    ====================  =========  ===============================================================

    Attributes:
        file_count: Database files searched.
        peptide_count: Rows: peptides, one per input peptide, duplicates included.
        target_protein_count: Proteins searched. Contaminants count: they are real sequences in
            the search space.
        decoy_proteins_ignored: Decoy proteins in the databases, which are never searched.
        i_and_l_equivalent: Always ``True``: I and L are one residue for matching. On the wire so
            that the rule travels with every result rather than living only in documentation.
        sharing_counts: Class -> peptides in it; every class present, zeros included.
        files: One :class:`DatabaseFile` per input.
        column_names: Column order.
        columns: Column name -> list of values.
    """

    file_count: int
    peptide_count: int
    target_protein_count: int
    decoy_proteins_ignored: int
    i_and_l_equivalent: bool
    sharing_counts: dict[str, int] = field(default_factory=dict)
    files: list[DatabaseFile] = field(default_factory=list)
    column_names: list[str] = field(default_factory=list)
    columns: dict[str, list[Any]] = field(default_factory=dict)

    def sharing_of(self) -> dict[str, str]:
        """Peptide -> sharing class. A peptide given twice appears once (it classifies the same).

        Examples:
            >>> calls = classify_peptides(
            ...     ["VGVNGFGR", "LVLNGNPLTLFQER", "ALSEQINIFFDYSGR", "YLYEIAR", "AEFVEVTK", "PEPTIDEK"],
            ...     ["human_subset.xml", "human_extra.fasta"], contaminants=["contaminants.fasta"])
            >>> calls.sharing_of()["YLYEIAR"]
            'SharedAcrossGenes'
        """
        return dict(zip(self.columns.get("peptide", []), self.columns.get("sharing", [])))

    @classmethod
    def _from_wire(cls, data: Mapping[str, Any]) -> PeptideClassification:
        return cls(
            file_count=int(data.get("file_count", 0)),
            peptide_count=int(data.get("peptide_count", 0)),
            target_protein_count=int(data.get("target_protein_count", 0)),
            decoy_proteins_ignored=int(data.get("decoy_proteins_ignored", 0)),
            i_and_l_equivalent=bool(data.get("i_and_l_equivalent", True)),
            sharing_counts=dict(data.get("sharing_counts") or {}),
            files=_files(data),
            column_names=list(data.get("column_names") or []),
            columns=dict(data.get("columns") or {}),
        )
sharing_of
sharing_of() -> dict[str, str]

Peptide -> sharing class. A peptide given twice appears once (it classifies the same).

Examples:

>>> calls = classify_peptides(
...     ["VGVNGFGR", "LVLNGNPLTLFQER", "ALSEQINIFFDYSGR", "YLYEIAR", "AEFVEVTK", "PEPTIDEK"],
...     ["human_subset.xml", "human_extra.fasta"], contaminants=["contaminants.fasta"])
>>> calls.sharing_of()["YLYEIAR"]
'SharedAcrossGenes'
Source code in pkg/python/src/pymzlib/proteins.py
def sharing_of(self) -> dict[str, str]:
    """Peptide -> sharing class. A peptide given twice appears once (it classifies the same).

    Examples:
        >>> calls = classify_peptides(
        ...     ["VGVNGFGR", "LVLNGNPLTLFQER", "ALSEQINIFFDYSGR", "YLYEIAR", "AEFVEVTK", "PEPTIDEK"],
        ...     ["human_subset.xml", "human_extra.fasta"], contaminants=["contaminants.fasta"])
        >>> calls.sharing_of()["YLYEIAR"]
        'SharedAcrossGenes'
    """
    return dict(zip(self.columns.get("peptide", []), self.columns.get("sharing", [])))

GoRelease dataclass

The go.obo a result was computed against. Record it with your results.

Attributes:

Name Type Description
source_file_name str

The file name read.

sha256 str

Lower-case hex sha256 of the file's bytes. This, not the release, is what proves two runs used the same ontology.

release str | None

The file's data-version, e.g. "releases/2026-07-26". None: the file has no data-version header.

term_count int

Terms in the file, obsolete terms included (alternative ids are not counted).

Source code in pkg/python/src/pymzlib/proteins.py
@dataclass(frozen=True)
class GoRelease:
    """The go.obo a result was computed against. Record it with your results.

    Attributes:
        source_file_name: The file name read.
        sha256: Lower-case hex sha256 of the file's bytes. This, not the release, is what proves two
            runs used the same ontology.
        release: The file's ``data-version``, e.g. ``"releases/2026-07-26"``. ``None``: the file
            has no ``data-version`` header.
        term_count: Terms in the file, obsolete terms included (alternative ids are not counted).
    """

    source_file_name: str
    sha256: str
    release: str | None
    term_count: int

    @classmethod
    def _from_wire(cls, data: Mapping[str, Any]) -> GoRelease:
        return cls(
            source_file_name=data.get("source_file_name", ""),
            sha256=data.get("sha256", ""),
            release=data.get("release"),
            term_count=int(data.get("term_count", 0)),
        )

AnnotationDatabase dataclass

The protein database whose GO terms annotated the groups.

Attributes:

Name Type Description
path str

The absolute path.

file_type str | None

"UniProtXml", the only type :func:annotate_go accepts.

reader str | None

The mzLib loader used, "ProteinDbLoader.LoadProteinXML".

protein_count int

Entries loaded, as written (no decoys generated, no variants applied).

sha256 str

Lower-case hex sha256 of the decompressed database, so .xml and .xml.gz agree. Every row's annotation_db_sha256 is this.

caveats list[str]

What mzLib did to the file while loading it, e.g. genotype variants applied.

Source code in pkg/python/src/pymzlib/proteins.py
@dataclass(frozen=True)
class AnnotationDatabase:
    """The protein database whose GO terms annotated the groups.

    Attributes:
        path: The absolute path.
        file_type: ``"UniProtXml"``, the only type :func:`annotate_go` accepts.
        reader: The mzLib loader used, ``"ProteinDbLoader.LoadProteinXML"``.
        protein_count: Entries loaded, as written (no decoys generated, no variants applied).
        sha256: Lower-case hex sha256 of the **decompressed** database, so ``.xml`` and
            ``.xml.gz`` agree. Every row's ``annotation_db_sha256`` is this.
        caveats: What mzLib did to the file while loading it, e.g. genotype variants applied.
    """

    path: str
    file_type: str | None
    reader: str | None
    protein_count: int
    sha256: str
    caveats: list[str] = field(default_factory=list)

    @classmethod
    def _from_wire(cls, data: Mapping[str, Any]) -> AnnotationDatabase:
        return cls(
            path=data.get("path", ""),
            file_type=data.get("file_type"),
            reader=data.get("reader"),
            protein_count=int(data.get("protein_count", 0)),
            sha256=data.get("sha256", ""),
            caveats=list(data.get("caveats") or []),
        )

WrittenTable dataclass

A file :func:annotate_go wrote, when asked to.

Attributes:

Name Type Description
path str

The absolute path written.

row_count int | None

Rows written: the whole table, never the limit/offset window. None for the category table, whose rows are counted in :attr:GoCategories.row_count.

Source code in pkg/python/src/pymzlib/proteins.py
@dataclass(frozen=True)
class WrittenTable:
    """A file :func:`annotate_go` wrote, when asked to.

    Attributes:
        path: The absolute path written.
        row_count: Rows written: the **whole** table, never the ``limit``/``offset`` window.
            ``None`` for the category table, whose rows are counted in :attr:`GoCategories.row_count`.
    """

    path: str
    row_count: int | None = None

    @classmethod
    def _from_wire(cls, data: Mapping[str, Any] | None) -> WrittenTable | None:
        if not data:
            return None
        row_count = data.get("row_count")
        return cls(path=data.get("path", ""), row_count=None if row_count is None else int(row_count))

GoCategories dataclass

Bases: _Table

Your category map applied to the annotated terms: one row per (term, category, subcategory).

A term belongs to a category when one of that category's anchors is the term itself or one of its ancestors. A term under no anchor has no row, so absence means "outside your map". Join to :class:GoAnnotations on go_id: both tables come from the same go.obo release.

=============== ===== =================================================================== column type meaning; None means =============== ===== =================================================================== go_id str an annotated term at or below one of the map's anchors category str the category label, as your map writes it subcategory str "category:subcategory", the most specific anchor within the category; None: the term reaches only the category's own anchor =============== ===== ===================================================================

Attributes:

Name Type Description
map_name str

The map's declared map_name.

map_version str

The map's declared map_version. Yours to bump; mzLib only records it.

source_file_name str

The map file read.

sha256 str

Lower-case hex sha256 of the map file. A version is a claim; the hash is proof.

anchor_count int

Rows (anchors) in the map.

row_count int

Rows in the table.

column_names list[str]

Column order.

columns dict[str, list[Any]]

Column name -> list of values.

Source code in pkg/python/src/pymzlib/proteins.py
@dataclass(frozen=True)
class GoCategories(_Table):
    """Your category map applied to the annotated terms: one row per (term, category, subcategory).

    A term belongs to a category when one of that category's anchors is the term itself or one of
    its ancestors. A term under no anchor has **no row**, so absence means "outside your map". Join
    to :class:`GoAnnotations` on ``go_id``: both tables come from the same go.obo release.

    ===============  =====  ===================================================================
    column           type   meaning; ``None`` means
    ===============  =====  ===================================================================
    ``go_id``        str    an annotated term at or below one of the map's anchors
    ``category``     str    the category label, as your map writes it
    ``subcategory``  str    ``"category:subcategory"``, the most specific anchor within the
                            category; ``None``: the term reaches only the category's own anchor
    ===============  =====  ===================================================================

    Attributes:
        map_name: The map's declared ``map_name``.
        map_version: The map's declared ``map_version``. Yours to bump; mzLib only records it.
        source_file_name: The map file read.
        sha256: Lower-case hex sha256 of the map file. A version is a claim; the hash is proof.
        anchor_count: Rows (anchors) in the map.
        row_count: Rows in the table.
        column_names: Column order.
        columns: Column name -> list of values.
    """

    map_name: str
    map_version: str
    source_file_name: str
    sha256: str
    anchor_count: int
    row_count: int
    column_names: list[str] = field(default_factory=list)
    columns: dict[str, list[Any]] = field(default_factory=dict)

    @classmethod
    def _from_wire(cls, data: Mapping[str, Any] | None) -> GoCategories | None:
        if data is None:
            return None
        return cls(
            map_name=data.get("map_name", ""),
            map_version=data.get("map_version", ""),
            source_file_name=data.get("source_file_name", ""),
            sha256=data.get("sha256", ""),
            anchor_count=int(data.get("anchor_count", 0)),
            row_count=int(data.get("row_count", 0)),
            column_names=list(data.get("column_names") or []),
            columns=dict(data.get("columns") or {}),
        )

GoAnnotations dataclass

Bases: _Table

What :func:annotate_go returns: one row per (protein group, GO term) that any member of the group holds, directly or through an ancestor.

No member of a group is privileged - MetaMorpheus never picks a leading protein - so the table is the union, and every row says which members carry its term. The views people usually want are filters you apply: consensus is n_with == n_members; direct annotations only is propagated is False; leave out isoform inheritance is inherited is False.

The column names are mzLib's own GoAnnotationTsv schema, in its order, so this table and the file written by out= (or by mzLib inside a search) mean the same thing:

========================= ========== =========================================================== column type meaning; None means ========================= ========== =========================================================== protein_group str the group, as MetaMorpheus named it (P0C0S5|Q71UI9) accession_used list[str] members carrying the term, directly or by propagation accession_direct list[str] of those, members annotated to this exact term accession_inherited list[str] of those, members whose terms were borrowed: an isoform (P04406-2) or sequence variant (P04406_A20T) absent from the database takes its entry's terms go_id str the term's primary id; None: a term-less row go_name str the term's name; None: a term-less row aspect str biological_process, cellular_component, molecular_function or unknown; None: term-less evidence list[str] evidence codes (ECO) pooled over the carrying members evidence_by_member dict member -> its own evidence codes for this term inherited bool every carrying member's terms were borrowed; None: a term-less row propagated bool no member is annotated to this exact term - it is implied by a more specific one; None: a term-less row n_members int members in the group n_with int members carrying the term (0 on a term-less row) entrapment_members list[str] the group's entrapment members, on every row of the group annotation_status str one of :data:ANNOTATION_STATUSES q_value float the group's q-value from the table (never filtered on) go_release str the go.obo data-version; None: the file has none go_obo_sha256 str sha256 of the go.obo annotation_db_sha256 str sha256 of the decompressed annotation database ========================= ========== ===========================================================

Attributes:

Name Type Description
groups_file str

The absolute path of the protein-group table read.

groups_file_sha256 str

Lower-case hex sha256 of that table, also in :attr:header as source_file_sha256.

table_row_count int

Rows in the table, decoys included.

decoy_group_count int

Of those, decoy groups, which are skipped: decoys carry no GO.

group_count int

Groups annotated: every non-decoy group, contaminants included, each once even if MetaMorpheus wrote it twice.

annotation_database AnnotationDatabase

The database the terms came from.

go GoRelease

The ontology release the terms were propagated against.

skip_unknown_go_ids bool

Whether skip_unknown_go_ids=True was given.

unresolved_go_ids list[str]

GO ids the database cites that the ontology release lacks, dropped because skip_unknown_go_ids=True. Always empty otherwise (the call fails instead).

header dict[str, str]

mzLib's value header, as its writer produced it: the format version, the mzLib build, the release and every sha256, and five counters - n_multi_member_groups and one status_<status> per status. The counters count groups, not rows, and only groups at q <= counter_q_value_max (0.01); the rows themselves are not filtered. Every value is a string, as in the file.

row_count int

Rows in the whole table.

returned_count int

Rows returned here (the limit/offset window).

offset int

Rows skipped before the window: the offset applied.

truncated bool

True when rows were left out of :attr:columns by limit or offset.

caveats list[str]

What this result cannot tell you: ids dropped from the database, a go.obo with no release, what mzLib did loading the database.

written WrittenTable | None

Where the whole table was written, when out= was given.

categories GoCategories | None

Your category map applied to these terms, when category_map= was given.

categories_written WrittenTable | None

Where the category table was written, when categories_out= was given.

column_names list[str]

Column order: mzLib's schema.

columns dict[str, list[Any]]

Column name -> list of values, for the returned window.

Source code in pkg/python/src/pymzlib/proteins.py
@dataclass(frozen=True)
class GoAnnotations(_Table):
    """What :func:`annotate_go` returns: one row per (protein group, GO term) that **any** member of
    the group holds, directly or through an ancestor.

    No member of a group is privileged - MetaMorpheus never picks a leading protein - so the table
    is the union, and every row says which members carry its term. The views people usually want
    are filters you apply: **consensus** is ``n_with == n_members``; **direct annotations only** is
    ``propagated is False``; **leave out isoform inheritance** is ``inherited is False``.

    The column names are **mzLib's own** ``GoAnnotationTsv`` schema, in its order, so this table and
    the file written by ``out=`` (or by mzLib inside a search) mean the same thing:

    =========================  ==========  ===========================================================
    column                     type        meaning; ``None`` means
    =========================  ==========  ===========================================================
    ``protein_group``          str         the group, as MetaMorpheus named it (``P0C0S5|Q71UI9``)
    ``accession_used``         list[str]   members carrying the term, directly or by propagation
    ``accession_direct``       list[str]   of those, members annotated to this exact term
    ``accession_inherited``    list[str]   of those, members whose terms were borrowed: an isoform
                                           (``P04406-2``) or sequence variant (``P04406_A20T``) absent
                                           from the database takes its entry's terms
    ``go_id``                  str         the term's primary id; ``None``: a term-less row
    ``go_name``                str         the term's name; ``None``: a term-less row
    ``aspect``                 str         ``biological_process``, ``cellular_component``,
                                           ``molecular_function`` or ``unknown``; ``None``: term-less
    ``evidence``               list[str]   evidence codes (ECO) pooled over the carrying members
    ``evidence_by_member``     dict        member -> its own evidence codes for this term
    ``inherited``              bool        every carrying member's terms were borrowed;
                                           ``None``: a term-less row
    ``propagated``             bool        no member is annotated to this exact term - it is implied
                                           by a more specific one; ``None``: a term-less row
    ``n_members``              int         members in the group
    ``n_with``                 int         members carrying the term (``0`` on a term-less row)
    ``entrapment_members``     list[str]   the group's entrapment members, on every row of the group
    ``annotation_status``      str         one of :data:`ANNOTATION_STATUSES`
    ``q_value``                float       the group's q-value from the table (never filtered on)
    ``go_release``             str         the go.obo ``data-version``; ``None``: the file has none
    ``go_obo_sha256``          str         sha256 of the go.obo
    ``annotation_db_sha256``   str         sha256 of the decompressed annotation database
    =========================  ==========  ===========================================================

    Attributes:
        groups_file: The absolute path of the protein-group table read.
        groups_file_sha256: Lower-case hex sha256 of that table, also in :attr:`header` as
            ``source_file_sha256``.
        table_row_count: Rows in the table, decoys included.
        decoy_group_count: Of those, decoy groups, which are skipped: decoys carry no GO.
        group_count: Groups annotated: every non-decoy group, contaminants included, each once even
            if MetaMorpheus wrote it twice.
        annotation_database: The database the terms came from.
        go: The ontology release the terms were propagated against.
        skip_unknown_go_ids: Whether ``skip_unknown_go_ids=True`` was given.
        unresolved_go_ids: GO ids the database cites that the ontology release lacks, dropped
            because ``skip_unknown_go_ids=True``. Always empty otherwise (the call fails instead).
        header: mzLib's ``#!key value`` header, as its writer produced it: the format version, the
            mzLib build, the release and every sha256, and five counters - ``n_multi_member_groups``
            and one ``status_<status>`` per status. **The counters count groups, not rows, and only
            groups at q <= ``counter_q_value_max`` (0.01)**; the rows themselves are not filtered.
            Every value is a string, as in the file.
        row_count: Rows in the whole table.
        returned_count: Rows returned here (the ``limit``/``offset`` window).
        offset: Rows skipped before the window: the ``offset`` applied.
        truncated: True when rows were left out of :attr:`columns` by ``limit`` or ``offset``.
        caveats: What this result cannot tell you: ids dropped from the database, a go.obo with no
            release, what mzLib did loading the database.
        written: Where the whole table was written, when ``out=`` was given.
        categories: Your category map applied to these terms, when ``category_map=`` was given.
        categories_written: Where the category table was written, when ``categories_out=`` was given.
        column_names: Column order: mzLib's schema.
        columns: Column name -> list of values, for the returned window.
    """

    groups_file: str
    groups_file_sha256: str
    table_row_count: int
    decoy_group_count: int
    group_count: int
    annotation_database: AnnotationDatabase
    go: GoRelease
    skip_unknown_go_ids: bool
    row_count: int
    returned_count: int
    offset: int = 0
    truncated: bool = False
    unresolved_go_ids: list[str] = field(default_factory=list)
    header: dict[str, str] = field(default_factory=dict)
    caveats: list[str] = field(default_factory=list)
    written: WrittenTable | None = None
    categories: GoCategories | None = None
    categories_written: WrittenTable | None = None
    column_names: list[str] = field(default_factory=list)
    columns: dict[str, list[Any]] = field(default_factory=dict)

    @classmethod
    def _from_wire(cls, data: Mapping[str, Any]) -> GoAnnotations:
        return cls(
            groups_file=data.get("groups_file", ""),
            groups_file_sha256=data.get("groups_file_sha256", ""),
            table_row_count=int(data.get("table_row_count", 0)),
            decoy_group_count=int(data.get("decoy_group_count", 0)),
            group_count=int(data.get("group_count", 0)),
            annotation_database=AnnotationDatabase._from_wire(data.get("annotation_database") or {}),
            go=GoRelease._from_wire(data.get("go") or {}),
            skip_unknown_go_ids=bool(data.get("skip_unknown_go_ids", False)),
            row_count=int(data.get("row_count", 0)),
            returned_count=int(data.get("returned_count", 0)),
            offset=int(data.get("offset", 0)),
            truncated=bool(data.get("truncated", False)),
            unresolved_go_ids=list(data.get("unresolved_go_ids") or []),
            header=dict(data.get("header") or {}),
            caveats=list(data.get("caveats") or []),
            written=WrittenTable._from_wire(data.get("written")),
            categories=GoCategories._from_wire(data.get("categories")),
            categories_written=WrittenTable._from_wire(data.get("categories_written")),
            column_names=list(data.get("column_names") or []),
            columns=dict(data.get("columns") or {}),
        )

GoUpdate dataclass

What :func:update_go returns: which go.obo is now on disk, and whether it changed.

Attributes:

Name Type Description
go_obo_file str

The absolute path written.

url str

Where it was fetched from: GO's PURL, which always serves the current release.

existed_before bool

Whether a file was already at the path.

previous_sha256 str | None

That file's sha256; None: there was none.

changed bool

Whether the file on disk is now different. When it is and a file existed, the old one was kept beside it as go.obo.<yyyyMMdd-HHmmss-fff> (see :attr:caveats).

go GoRelease

The release now at the path.

caveats list[str]

The backup kept, or a file with no data-version.

Source code in pkg/python/src/pymzlib/proteins.py
@dataclass(frozen=True)
class GoUpdate:
    """What :func:`update_go` returns: which go.obo is now on disk, and whether it changed.

    Attributes:
        go_obo_file: The absolute path written.
        url: Where it was fetched from: GO's PURL, which always serves the **current** release.
        existed_before: Whether a file was already at the path.
        previous_sha256: That file's sha256; ``None``: there was none.
        changed: Whether the file on disk is now different. When it is and a file existed, the old
            one was kept beside it as ``go.obo.<yyyyMMdd-HHmmss-fff>`` (see :attr:`caveats`).
        go: The release now at the path.
        caveats: The backup kept, or a file with no ``data-version``.
    """

    go_obo_file: str
    url: str
    existed_before: bool
    previous_sha256: str | None
    changed: bool
    go: GoRelease
    caveats: list[str] = field(default_factory=list)

    @classmethod
    def _from_wire(cls, data: Mapping[str, Any]) -> GoUpdate:
        return cls(
            go_obo_file=data.get("go_obo_file", ""),
            url=data.get("url", ""),
            existed_before=bool(data.get("existed_before", False)),
            previous_sha256=data.get("previous_sha256"),
            changed=bool(data.get("changed", False)),
            go=GoRelease._from_wire(data.get("go") or {}),
            caveats=list(data.get("caveats") or []),
        )

read

read(databases: _PathLike | Iterable[_PathLike], *, tables: Sequence[str] = ('proteins',), accessions: Iterable[str] | None = None, contaminants: _PathLike | Iterable[_PathLike] | None = None, sequences: bool = False, threads: int = 1, on_error: str = 'fail', timeout: float | None = None) -> ProteinDatabase

Read protein databases: one row per protein, and GO and Ensembl tables on request.

Loads each database with mzLib's ProteinDbLoader, exactly as a search would but with no decoys generated. A UniProt XML carries everything; a FASTA carries organism, taxon (OX=) and gene name (GN=) only - see :attr:DatabaseFile.absent_fields.

Parameters:

Name Type Description Default
databases _PathLike | Iterable[_PathLike]

A UniProt XML (.xml) or FASTA (.fasta, .fa, .faa, .fas), each optionally gzipped, or a list of them. Read in the order given.

required
tables Sequence[str]

Which tables to return, from :data:TABLES. Default ("proteins",). Ask for "go_terms" and "ensembl_genes" when you want them: a whole human proteome has about twenty GO rows per protein.

('proteins',)
accessions Iterable[str] | None

Keep only proteins whose accession is one of these, exact match. Applies to every table. Misses are listed in :attr:ProteinDatabase.accessions_not_found. Sent on stdin, so tens of thousands are fine.

None
contaminants _PathLike | Iterable[_PathLike] | None

Databases to load as contaminants; is_contaminant is then True on their rows. They come after databases in source_index order.

None
sequences bool

Add a sequence column to the proteins table.

False
threads int

Databases read at once. Default 1; -1 means every core. A resource choice only: the result is identical at any value.

1
on_error str

"fail" (default) raises on the first unreadable database, in input order; "skip" records the failure in that file's :attr:DatabaseFile.error and reads the rest.

'fail'
timeout float | None

Seconds to allow. None (default) waits: a proteome XML takes a while.

None

Returns:

Name Type Description
A ProteinDatabase

class:ProteinDatabase.

Raises:

Type Description
UsageError

no database, a blank or repeated path, an unknown table, a bad threads or on_error, an empty accessions list; or (under on_error="fail") a missing file or one that is neither XML nor FASTA by name.

BridgeError

mzLib could not parse a database (under on_error="fail").

Examples:

>>> db = read(["human_subset.xml", "human_extra.fasta", "mouse_aifm1.fasta"],
...           contaminants=["contaminants.fasta"], tables=TABLES)
>>> db.record_count, db.go_terms.row_count, db.ensembl_genes.row_count
(8, 47, 6)
>>> db.files[1].file_type, db.files[1].absent_fields
('Fasta', ['go_terms', 'ensembl_genes', 'ensembl_gene_ids'])
Source code in pkg/python/src/pymzlib/proteins.py
def read(
    databases: _PathLike | Iterable[_PathLike],
    *,
    tables: Sequence[str] = ("proteins",),
    accessions: Iterable[str] | None = None,
    contaminants: _PathLike | Iterable[_PathLike] | None = None,
    sequences: bool = False,
    threads: int = 1,
    on_error: str = "fail",
    timeout: float | None = None,
) -> ProteinDatabase:
    """Read protein databases: one row per protein, and GO and Ensembl tables on request.

    Loads each database with mzLib's ``ProteinDbLoader``, exactly as a search would but with no
    decoys generated. A UniProt XML carries everything; a FASTA carries organism, taxon (``OX=``)
    and gene name (``GN=``) only - see :attr:`DatabaseFile.absent_fields`.

    Args:
        databases: A UniProt XML (``.xml``) or FASTA (``.fasta``, ``.fa``, ``.faa``, ``.fas``), each
            optionally gzipped, or a list of them. Read in the order given.
        tables: Which tables to return, from :data:`TABLES`. Default ``("proteins",)``. Ask for
            ``"go_terms"`` and ``"ensembl_genes"`` when you want them: a whole human proteome has
            about twenty GO rows per protein.
        accessions: Keep only proteins whose accession is one of these, **exact match**. Applies to
            every table. Misses are listed in :attr:`ProteinDatabase.accessions_not_found`. Sent on
            stdin, so tens of thousands are fine.
        contaminants: Databases to load as contaminants; ``is_contaminant`` is then ``True`` on
            their rows. They come after ``databases`` in ``source_index`` order.
        sequences: Add a ``sequence`` column to the proteins table.
        threads: Databases read at once. Default 1; ``-1`` means every core. A resource choice
            only: the result is identical at any value.
        on_error: ``"fail"`` (default) raises on the first unreadable database, in input order;
            ``"skip"`` records the failure in that file's :attr:`DatabaseFile.error` and reads the
            rest.
        timeout: Seconds to allow. ``None`` (default) waits: a proteome XML takes a while.

    Returns:
        A :class:`ProteinDatabase`.

    Raises:
        UsageError: no database, a blank or repeated path, an unknown table, a bad ``threads`` or
            ``on_error``, an empty ``accessions`` list; or (under ``on_error="fail"``) a missing
            file or one that is neither XML nor FASTA by name.
        BridgeError: mzLib could not parse a database (under ``on_error="fail"``).

    Examples:
        >>> db = read(["human_subset.xml", "human_extra.fasta", "mouse_aifm1.fasta"],
        ...           contaminants=["contaminants.fasta"], tables=TABLES)
        >>> db.record_count, db.go_terms.row_count, db.ensembl_genes.row_count
        (8, 47, 6)
        >>> db.files[1].file_type, db.files[1].absent_fields
        ('Fasta', ['go_terms', 'ensembl_genes', 'ensembl_gene_ids'])
    """
    if isinstance(tables, str):
        tables = (tables,)
    tables = list(tables)
    unknown = [t for t in tables if t not in TABLES]
    if not tables or unknown:
        raise _bridge.UsageError(f"tables must be a non-empty selection from {TABLES}; got {tables!r}.")

    db_args, path_lines = _database_args(databases, contaminants)
    args = ["proteins", "read", *db_args, "--tables", ",".join(tables), *_threads(threads), *_on_error(on_error)]
    if sequences:
        args.append("--sequences")

    filter_lines = None
    if accessions is not None:
        filter_lines = _lines(accessions, "accessions")
        if not filter_lines:
            raise _bridge.UsageError("accessions is empty; pass None to read every protein.")
        args.append("--accessions-stdin")

    data = _bridge.invoke(*args, stdin=_stdin(path_lines, filter_lines), timeout=timeout)
    return ProteinDatabase._from_wire(data)

resolve_genes

resolve_genes(databases: _PathLike | Iterable[_PathLike], *, gtf: _PathLike | None = None, gene_set: _PathLike | None = None, xref: _PathLike | None = None, contaminants: _PathLike | Iterable[_PathLike] | None = None, threads: int = 1, on_error: str = 'fail', timeout: float | None = None) -> GeneResolutions

Resolve every protein to stable Ensembl gene ids, against a gene set you pin.

Wraps mzLib's EnsemblGeneResolver. The gene links come from the database itself (UniProt's <dbReference type="Ensembl">), and each is counted against the gene set you pass: a gene in the set counts; a gene outside it (an ALT haplotype, a patch) is dropped from n_genes but counted in off_primary_genes, and a protein with only such genes is off_primary_only.

You supply the gene set, and you should pin it. Gene ids, versions, symbols and the very set of genes on the primary assembly change between Ensembl releases, so a resolution means something only relative to one release. Use Ensembl's primary-assembly GTF ( Species.Assembly.Release.gtf.gz, not the chr_patch_hapl_scaff one): counting ALT haplotypes as separate genes made human release 116 look 6.99% multi-gene when it is 0.36%. Keep Ensembl's file name so the release is recorded, and record :attr:GeneSet.sha256 with your results. Nothing is downloaded or defaulted here.

Parameters:

Name Type Description Default
databases _PathLike | Iterable[_PathLike]

The search database(s), UniProt XML for real answers. A FASTA carries no gene links, so every FASTA protein is not_in_source - a caveat says so.

required
gtf _PathLike | None

An Ensembl GTF, plain or .gz. Only its gene rows are read.

None
gene_set _PathLike | None

Instead of gtf, a compact gene table written by mzLib's EnsemblGeneSetWriter (about 0.5 MB for human, against a 141 MB GTF). It carries the GTF's provenance, so rows are keyed exactly as against the GTF. Give exactly one.

None
xref _PathLike | None

Optional. Ensembl's Species.Assembly.Release.uniprot.tsv.gz, for a second opinion: each gene row then says whether Ensembl agrees, and a gene only Ensembl links gets its own row (source == "ensembl_xref"). Without it agreement is None - unknown, not false.

None
contaminants _PathLike | Iterable[_PathLike] | None

Databases to load as contaminants. Their proteins are contaminant_not_mapped - never mapped, and never silently dropped.

None
threads int

Databases read at once. Default 1; -1 means every core. Same answer at any value.

1
on_error str

"fail" (default) or "skip", as for :func:read.

'fail'
timeout float | None

Seconds to allow. None (default) waits: a gzipped GTF takes tens of seconds.

None

Returns:

Name Type Description
A GeneResolutions

class:GeneResolutions.

Raises:

Type Description
UsageError

neither or both of gtf and gene_set; a missing GTF, gene set, xref or database; any argument :func:read would refuse.

BridgeError

a GTF gene row without gene_id, an xref table with unexpected columns, or a database mzLib cannot parse.

Examples:

>>> genes = resolve_genes(
...     ["human_subset.xml", "human_extra.fasta"], contaminants=["contaminants.fasta"],
...     gtf="Homo_sapiens.GRCh38.116.gtf", xref="Homo_sapiens.GRCh38.116.uniprot.tsv")
>>> genes.gene_set.release, genes.gene_set.genome_build
('116', 'GRCh38.p14')
>>> genes.outcome_counts["contaminant_not_mapped"]
1
Source code in pkg/python/src/pymzlib/proteins.py
def resolve_genes(
    databases: _PathLike | Iterable[_PathLike],
    *,
    gtf: _PathLike | None = None,
    gene_set: _PathLike | None = None,
    xref: _PathLike | None = None,
    contaminants: _PathLike | Iterable[_PathLike] | None = None,
    threads: int = 1,
    on_error: str = "fail",
    timeout: float | None = None,
) -> GeneResolutions:
    """Resolve every protein to stable Ensembl gene ids, against a gene set you pin.

    Wraps mzLib's ``EnsemblGeneResolver``. The gene links come from the database itself (UniProt's
    ``<dbReference type="Ensembl">``), and each is **counted against the gene set you pass**: a gene
    in the set counts; a gene outside it (an ALT haplotype, a patch) is dropped from ``n_genes`` but
    counted in ``off_primary_genes``, and a protein with only such genes is ``off_primary_only``.

    **You supply the gene set, and you should pin it.** Gene ids, versions, symbols and the very
    set of genes on the primary assembly change between Ensembl releases, so a resolution means
    something only relative to one release. Use Ensembl's **primary-assembly** GTF (
    ``Species.Assembly.Release.gtf.gz``, not the ``chr_patch_hapl_scaff`` one): counting ALT
    haplotypes as separate genes made human release 116 look 6.99% multi-gene when it is 0.36%.
    Keep Ensembl's file name so the release is recorded, and record :attr:`GeneSet.sha256` with your
    results. Nothing is downloaded or defaulted here.

    Args:
        databases: The search database(s), UniProt XML for real answers. A FASTA carries no gene
            links, so every FASTA protein is ``not_in_source`` - a caveat says so.
        gtf: An Ensembl GTF, plain or ``.gz``. Only its ``gene`` rows are read.
        gene_set: Instead of ``gtf``, a compact gene table written by mzLib's
            ``EnsemblGeneSetWriter`` (about 0.5 MB for human, against a 141 MB GTF). It carries the
            GTF's provenance, so rows are keyed exactly as against the GTF. Give exactly one.
        xref: Optional. Ensembl's ``Species.Assembly.Release.uniprot.tsv.gz``, for a second
            opinion: each gene row then says whether Ensembl agrees, and a gene only Ensembl links
            gets its own row (``source == "ensembl_xref"``). Without it agreement is ``None`` -
            unknown, not false.
        contaminants: Databases to load as contaminants. Their proteins are
            ``contaminant_not_mapped`` - never mapped, and never silently dropped.
        threads: Databases read at once. Default 1; ``-1`` means every core. Same answer at any
            value.
        on_error: ``"fail"`` (default) or ``"skip"``, as for :func:`read`.
        timeout: Seconds to allow. ``None`` (default) waits: a gzipped GTF takes tens of seconds.

    Returns:
        A :class:`GeneResolutions`.

    Raises:
        UsageError: neither or both of ``gtf`` and ``gene_set``; a missing GTF, gene set, xref or
            database; any argument :func:`read` would refuse.
        BridgeError: a GTF gene row without ``gene_id``, an xref table with unexpected columns, or a
            database mzLib cannot parse.

    Examples:
        >>> genes = resolve_genes(
        ...     ["human_subset.xml", "human_extra.fasta"], contaminants=["contaminants.fasta"],
        ...     gtf="Homo_sapiens.GRCh38.116.gtf", xref="Homo_sapiens.GRCh38.116.uniprot.tsv")
        >>> genes.gene_set.release, genes.gene_set.genome_build
        ('116', 'GRCh38.p14')
        >>> genes.outcome_counts["contaminant_not_mapped"]
        1
    """
    gtf_text = _bridge.path_text(gtf) if gtf is not None else None
    set_text = _bridge.path_text(gene_set) if gene_set is not None else None
    if (gtf is None) == (gene_set is None) or gtf_text == "" or set_text == "":
        raise _bridge.UsageError(
            "Give exactly one of gtf= (an Ensembl GTF, e.g. 'Homo_sapiens.GRCh38.116.gtf.gz') or "
            "gene_set= (a compact table made from one). There is no default gene set: the release "
            "it pins is part of the answer."
        )

    db_args, path_lines = _database_args(databases, contaminants)
    args = ["genes", "resolve", *db_args, *_threads(threads), *_on_error(on_error)]
    args += ["--gtf", gtf_text] if gtf_text else ["--gene-set", set_text or ""]
    if xref is not None:
        xref_text = _bridge.path_text(xref)
        if not xref_text:
            raise _bridge.UsageError("xref must be a non-empty path or None.")
        args += ["--xref", xref_text]

    data = _bridge.invoke(*args, stdin=_stdin(path_lines, None), timeout=timeout)
    return GeneResolutions._from_wire(data)

classify_peptides

classify_peptides(peptides: Iterable[str], databases: _PathLike | Iterable[_PathLike], *, contaminants: _PathLike | Iterable[_PathLike] | None = None, threads: int = 1, timeout: float | None = None) -> PeptideClassification

Classify peptides by how widely they are shared across the databases, with I = L.

Wraps mzLib's PeptideUniquenessClassifier.Classify. A protein contains a peptide when its sequence contains it anywhere, whatever the protease - deliberately conservative, so a peptide called Unique cannot be explained by another entry at a site the search's cleavage rules happened to skip. I and L are the same residue throughout: LVLNGNPLTLFQER finds GAPDH's LVINGNPITIFQER.

  • Unique: every protein containing it has the same sequence (identical entries under two accessions count as one; both are listed).
  • SharedWithinGene: several distinct sequences, all sharing a gene - isoforms. The peptide supports the gene, not an isoform.
  • SharedAcrossGenes: sequences with no gene in common.
  • NotInDatabase: no target protein contains it.

Two proteins share a gene when they share any key: an Ensembl gene id, organism plus primary gene name (so human and bovine ALB stay apart), or the UniProt entry (so P12345-2 meets P12345). Decoys are ignored; contaminants are included, because they are real sequences in the search space.

Every database is part of one search space, so there is no on_error="skip": dropping a database that failed to load would report the peptides it contains as unique.

Parameters:

Name Type Description Default
peptides Iterable[str]

Unmodified base sequences, upper case ("PEPTIDEK"), one per result row in the same order, duplicates included. Sent on stdin. Strip modifications first: mzLib refuses "PEPT[Phospho]IDEK" and "peptidek" rather than guess.

required
databases _PathLike | Iterable[_PathLike]

The search database(s): UniProt XML or FASTA, optionally gzipped.

required
contaminants _PathLike | Iterable[_PathLike] | None

Contaminant databases searched alongside.

None
threads int

Databases read at once (the classification itself is one pass). Default 1; -1 means every core. Same answer at any value.

1
timeout float | None

Seconds to allow. None (default) waits.

None

Returns:

Name Type Description
A PeptideClassification

class:PeptideClassification, one row per peptide.

Raises:

Type Description
UsageError

no peptides, a blank one, one that is not upper-case A-Z (from mzLib, naming it), or any database problem :func:read would refuse.

BridgeError

mzLib could not parse a database.

Examples:

>>> calls = classify_peptides(
...     ["VGVNGFGR", "LVLNGNPLTLFQER", "ALSEQINIFFDYSGR", "YLYEIAR", "AEFVEVTK", "PEPTIDEK"],
...     ["human_subset.xml", "human_extra.fasta"], contaminants=["contaminants.fasta"])
>>> calls.records[3]["peptide"], calls.records[3]["sharing"], calls.records[3]["accessions"]
('YLYEIAR', 'SharedAcrossGenes', ['P02768', 'P02769'])
Source code in pkg/python/src/pymzlib/proteins.py
def classify_peptides(
    peptides: Iterable[str],
    databases: _PathLike | Iterable[_PathLike],
    *,
    contaminants: _PathLike | Iterable[_PathLike] | None = None,
    threads: int = 1,
    timeout: float | None = None,
) -> PeptideClassification:
    """Classify peptides by how widely they are shared across the databases, with I = L.

    Wraps mzLib's ``PeptideUniquenessClassifier.Classify``. A protein contains a peptide when its
    sequence contains it **anywhere, whatever the protease** - deliberately conservative, so a
    peptide called ``Unique`` cannot be explained by another entry at a site the search's cleavage
    rules happened to skip. **I and L are the same residue** throughout: ``LVLNGNPLTLFQER`` finds
    GAPDH's ``LVINGNPITIFQER``.

    - ``Unique``: every protein containing it has the **same sequence** (identical entries under two
      accessions count as one; both are listed).
    - ``SharedWithinGene``: several distinct sequences, all sharing a gene - isoforms. The peptide
      supports the gene, not an isoform.
    - ``SharedAcrossGenes``: sequences with no gene in common.
    - ``NotInDatabase``: no target protein contains it.

    Two proteins share a gene when they share any key: an Ensembl gene id, organism plus primary
    gene name (so human and bovine ALB stay apart), or the UniProt entry (so ``P12345-2`` meets
    ``P12345``). Decoys are ignored; contaminants are included, because they are real sequences in
    the search space.

    Every database is part of one search space, so there is no ``on_error="skip"``: dropping a
    database that failed to load would report the peptides it contains as unique.

    Args:
        peptides: Unmodified base sequences, upper case (``"PEPTIDEK"``), one per result row in the
            same order, duplicates included. Sent on stdin. Strip modifications first: mzLib refuses
            ``"PEPT[Phospho]IDEK"`` and ``"peptidek"`` rather than guess.
        databases: The search database(s): UniProt XML or FASTA, optionally gzipped.
        contaminants: Contaminant databases searched alongside.
        threads: Databases read at once (the classification itself is one pass). Default 1;
            ``-1`` means every core. Same answer at any value.
        timeout: Seconds to allow. ``None`` (default) waits.

    Returns:
        A :class:`PeptideClassification`, one row per peptide.

    Raises:
        UsageError: no peptides, a blank one, one that is not upper-case A-Z (from mzLib, naming
            it), or any database problem :func:`read` would refuse.
        BridgeError: mzLib could not parse a database.

    Examples:
        >>> calls = classify_peptides(
        ...     ["VGVNGFGR", "LVLNGNPLTLFQER", "ALSEQINIFFDYSGR", "YLYEIAR", "AEFVEVTK", "PEPTIDEK"],
        ...     ["human_subset.xml", "human_extra.fasta"], contaminants=["contaminants.fasta"])
        >>> calls.records[3]["peptide"], calls.records[3]["sharing"], calls.records[3]["accessions"]
        ('YLYEIAR', 'SharedAcrossGenes', ['P02768', 'P02769'])
    """
    peptide_lines = _lines(peptides, "peptides")
    if not peptide_lines:
        raise _bridge.UsageError("At least one peptide is required, e.g. ['PEPTIDEK'].")

    db_args, path_lines = _database_args(databases, contaminants)
    args = ["proteins", "classify-peptides", *db_args, *_threads(threads)]
    data = _bridge.invoke(*args, stdin=_stdin(path_lines, peptide_lines), timeout=timeout)
    return PeptideClassification._from_wire(data)

annotate_go

annotate_go(groups: _PathLike, database: _PathLike, *, go_obo: _PathLike, category_map: _PathLike | None = None, skip_unknown_go_ids: bool = False, out: _PathLike | None = None, categories_out: _PathLike | None = None, limit: int | None = None, offset: int = 0, timeout: float | None = None) -> GoAnnotations

Annotate MetaMorpheus protein groups with Gene Ontology terms, keeping every member.

Wraps mzLib's GoGroupAnnotator over a stored protein-group table (ProteinGroupFromTsv.ToGoAnnotationGroups). For each group it returns one row per GO term that any member holds - directly, or by propagation up is_a and part_of - and each row names the members that carry it. Nothing is collapsed: the union, the consensus and the direct-only views are filters on the rows (see :class:GoAnnotations).

Every non-decoy group gets at least one row. A group with no term gets a single row whose annotation_status says why: no_go_terms (its members have none), no_entry (a member is not in the database - annotate against the database the search used), or contaminant.

Pin the ontology. Terms and their ancestors change between GO releases, so a result means something only relative to one go.obo. This reads the file you name and never downloads one; fetch a release on purpose with :func:update_go, keep the file, and record :attr:GoAnnotations.go (its sha256) with your results.

Parameters:

Name Type Description Default
groups _PathLike

A MetaMorpheus protein-group table: AllQuantifiedProteinGroups.tsv, AllProteinGroups.tsv (a search without quantification), or one file's <file>_ProteinGroups.tsv.

required
database _PathLike

The UniProt XML (.xml or .xml.gz) whose GO terms annotate the members: the proteome the search used. A FASTA is refused, because it carries no GO and every group would read no_go_terms whatever the proteins are.

required
go_obo _PathLike

A go.obo file. It must exist; nothing is fetched.

required
category_map _PathLike | None

Optional. Your own term-to-category map in mzLib's format (1, #!map_name, #!map_version, then category, subcategory and anchor_go_id columns). mzLib ships no vocabulary. Adds :attr:GoAnnotations.categories.

None
skip_unknown_go_ids bool

False (default) fails when the database cites a GO id the ontology release lacks - usually a UniProt release newer than the go.obo - and names every missing id. True drops each such id and lists it in :attr:GoAnnotations.unresolved_go_ids, so one new term does not cost the run.

False
out _PathLike | None

Write the whole table here with mzLib's own GoAnnotationTsv writer, provenance header included. Must end in .tsv. For a large run, pair it with limit=0 so only the summary comes back.

None
categories_out _PathLike | None

Write the category table here with mzLib's GoCategoryTsv writer. Must end in .tsv, and needs category_map.

None
limit int | None

At most this many rows (rows, not groups) in :attr:GoAnnotations.columns. None (default) returns them all. Never shortens out.

None
offset int

Rows to skip before the window. Default 0.

0
timeout float | None

Seconds to allow. None (default) waits: a whole proteome XML takes a while.

None

Returns:

Name Type Description
A GoAnnotations

class:GoAnnotations.

Raises:

Type Description
UsageError

a missing groups table, database, go.obo or category map; a FASTA database; an out or categories_out that is not .tsv or names an input; categories_out without category_map; a negative limit or offset. All of these are raised before any file is read.

BridgeError

the database cites GO ids the release lacks (InvalidDataException, every id named) unless skip_unknown_go_ids=True; a go.obo, category map or table mzLib cannot parse; a group MetaMorpheus wrote twice with different members.

Examples:

>>> go = annotate_go("PXD036557_AllQuantifiedProteinGroups.tsv", "pxd036557_proteins.xml",
...                  go_obo="go-pxd036557.obo", category_map="organelle_map.tsv")
>>> go.group_count, go.row_count, go.go.release
(5, 563, 'releases/2026-07-26')
>>> go.header["status_annotated"], go.header["status_contaminant"]
('4', '1')
>>> histones = [r for r in go.records if r["protein_group"] == "P0C0S5|Q71UI9"]
>>> len(histones), sum(r["n_with"] == r["n_members"] for r in histones)
(106, 57)
Source code in pkg/python/src/pymzlib/proteins.py
def annotate_go(
    groups: _PathLike,
    database: _PathLike,
    *,
    go_obo: _PathLike,
    category_map: _PathLike | None = None,
    skip_unknown_go_ids: bool = False,
    out: _PathLike | None = None,
    categories_out: _PathLike | None = None,
    limit: int | None = None,
    offset: int = 0,
    timeout: float | None = None,
) -> GoAnnotations:
    """Annotate MetaMorpheus protein groups with Gene Ontology terms, keeping every member.

    Wraps mzLib's ``GoGroupAnnotator`` over a stored protein-group table
    (``ProteinGroupFromTsv.ToGoAnnotationGroups``). For each group it returns one row per GO term
    that **any** member holds - directly, or by propagation up ``is_a`` and ``part_of`` - and each
    row names the members that carry it. Nothing is collapsed: the union, the consensus and the
    direct-only views are filters on the rows (see :class:`GoAnnotations`).

    **Every non-decoy group gets at least one row.** A group with no term gets a single row whose
    ``annotation_status`` says why: ``no_go_terms`` (its members have none), ``no_entry`` (a member
    is not in the database - annotate against the database the search used), or ``contaminant``.

    **Pin the ontology.** Terms and their ancestors change between GO releases, so a result means
    something only relative to one go.obo. This reads the file you name and never downloads one;
    fetch a release on purpose with :func:`update_go`, keep the file, and record
    :attr:`GoAnnotations.go` (its ``sha256``) with your results.

    Args:
        groups: A MetaMorpheus protein-group table: ``AllQuantifiedProteinGroups.tsv``,
            ``AllProteinGroups.tsv`` (a search without quantification), or one file's
            ``<file>_ProteinGroups.tsv``.
        database: The UniProt XML (``.xml`` or ``.xml.gz``) whose GO terms annotate the members:
            the proteome the search used. A FASTA is refused, because it carries no GO and every
            group would read ``no_go_terms`` whatever the proteins are.
        go_obo: A go.obo file. It must exist; nothing is fetched.
        category_map: Optional. Your own term-to-category map in mzLib's format
            (``#!category_map_format 1``, ``#!map_name``, ``#!map_version``, then ``category``,
            ``subcategory`` and ``anchor_go_id`` columns). mzLib ships no vocabulary. Adds
            :attr:`GoAnnotations.categories`.
        skip_unknown_go_ids: ``False`` (default) fails when the database cites a GO id the
            ontology release lacks - usually a UniProt release newer than the go.obo - and names
            every missing id. ``True`` drops each such id and lists it in
            :attr:`GoAnnotations.unresolved_go_ids`, so one new term does not cost the run.
        out: Write the **whole** table here with mzLib's own ``GoAnnotationTsv`` writer, provenance
            header included. Must end in ``.tsv``. For a large run, pair it with ``limit=0`` so only
            the summary comes back.
        categories_out: Write the category table here with mzLib's ``GoCategoryTsv`` writer. Must
            end in ``.tsv``, and needs ``category_map``.
        limit: At most this many rows (rows, not groups) in :attr:`GoAnnotations.columns`.
            ``None`` (default) returns them all. Never shortens ``out``.
        offset: Rows to skip before the window. Default 0.
        timeout: Seconds to allow. ``None`` (default) waits: a whole proteome XML takes a while.

    Returns:
        A :class:`GoAnnotations`.

    Raises:
        UsageError: a missing groups table, database, go.obo or category map; a FASTA database;
            an ``out`` or ``categories_out`` that is not ``.tsv`` or names an input;
            ``categories_out`` without ``category_map``; a negative ``limit`` or ``offset``. All of
            these are raised before any file is read.
        BridgeError: the database cites GO ids the release lacks (``InvalidDataException``, every id
            named) unless ``skip_unknown_go_ids=True``; a go.obo, category map or table mzLib
            cannot parse; a group MetaMorpheus wrote twice with different members.

    Examples:
        >>> go = annotate_go("PXD036557_AllQuantifiedProteinGroups.tsv", "pxd036557_proteins.xml",
        ...                  go_obo="go-pxd036557.obo", category_map="organelle_map.tsv")
        >>> go.group_count, go.row_count, go.go.release
        (5, 563, 'releases/2026-07-26')
        >>> go.header["status_annotated"], go.header["status_contaminant"]
        ('4', '1')
        >>> histones = [r for r in go.records if r["protein_group"] == "P0C0S5|Q71UI9"]
        >>> len(histones), sum(r["n_with"] == r["n_members"] for r in histones)
        (106, 57)
    """
    args = [
        "proteins", "annotate-go",
        "--groups", _one_path(groups, "groups", required=True) or "",
        "--database", _one_path(database, "database", required=True) or "",
        "--go-obo", _one_path(go_obo, "go_obo", required=True) or "",
    ]
    for option, value in (("--category-map", category_map), ("--out", out), ("--categories-out", categories_out)):
        text = _one_path(value, option[2:].replace("-", "_"), required=False)
        if text is not None:
            args += [option, text]
    if skip_unknown_go_ids:
        args.append("--skip-unknown-go-ids")
    for name, value in (("limit", limit), ("offset", offset)):
        if value is None:
            continue
        if isinstance(value, bool) or not isinstance(value, int) or value < 0:
            raise _bridge.UsageError(f"{name} must be an int, zero or greater; got {value!r}.")
        args += [f"--{name}", str(value)]

    _bridge.require_verb("proteins annotate-go", since=_GO_SINCE)
    return GoAnnotations._from_wire(_bridge.invoke(*args, timeout=timeout))

update_go

update_go(go_obo: _PathLike, *, timeout: float | None = None) -> GoUpdate

Download the current Gene Ontology release to go_obo, on purpose.

Wraps mzLib's Loaders.UpdateGeneOntology. The whole go.obo (about 37 MB) is streamed from GO's PURL, which always serves the current release. When a file is already at the path, it is kept beside the new one as go.obo.<yyyyMMdd-HHmmss-fff> if the download differs, and left alone if it is the same, so earlier runs stay reproducible. A failed download leaves any existing file untouched.

This is the only function in pyMzLib that fetches go.obo. :func:annotate_go never does, so the release a result was computed against is always a file you chose to keep.

Parameters:

Name Type Description Default
go_obo _PathLike

Where to write, e.g. "go.obo". Its folder must exist.

required
timeout float | None

Seconds to allow. None (default) waits; mzLib itself gives up after two minutes without data.

None

Returns:

Name Type Description
A GoUpdate

class:GoUpdate.

Raises:

Type Description
UsageError

the folder does not exist.

BridgeError

GO's server is unreachable or answered with an error. type is "ServiceUnavailable" for an outage, where retrying later makes sense.

Examples:

>>> update = update_go("go.obo")
>>> update.go.release, update.go.term_count, update.changed
('releases/2026-07-26', 48340, True)
Source code in pkg/python/src/pymzlib/proteins.py
def update_go(go_obo: _PathLike, *, timeout: float | None = None) -> GoUpdate:
    """Download the current Gene Ontology release to ``go_obo``, on purpose.

    Wraps mzLib's ``Loaders.UpdateGeneOntology``. The whole go.obo (about 37 MB) is streamed from
    GO's PURL, which always serves the **current** release. When a file is already at the path, it
    is kept beside the new one as ``go.obo.<yyyyMMdd-HHmmss-fff>`` if the download differs, and left
    alone if it is the same, so earlier runs stay reproducible. A failed download leaves any
    existing file untouched.

    This is the only function in pyMzLib that fetches go.obo. :func:`annotate_go` never does, so
    the release a result was computed against is always a file you chose to keep.

    Args:
        go_obo: Where to write, e.g. ``"go.obo"``. Its folder must exist.
        timeout: Seconds to allow. ``None`` (default) waits; mzLib itself gives up after two
            minutes without data.

    Returns:
        A :class:`GoUpdate`.

    Raises:
        UsageError: the folder does not exist.
        BridgeError: GO's server is unreachable or answered with an error. ``type`` is
            ``"ServiceUnavailable"`` for an outage, where retrying later makes sense.

    Examples:
        >>> update = update_go("go.obo")
        >>> update.go.release, update.go.term_count, update.changed
        ('releases/2026-07-26', 48340, True)
    """
    path = _one_path(go_obo, "go_obo", required=True) or ""
    _bridge.require_verb("proteins update-go", since=_GO_SINCE)
    return GoUpdate._from_wire(_bridge.invoke("proteins", "update-go", "--go-obo", path, timeout=timeout))

pymzlib.stats

stats

Differential abundance with no R: limma's moderated t-test, Benjamini-Hochberg, and random-effects meta-analysis, computed by mzLib.

Three questions, each answered by mzLib's StatisticalModels (mzLib #1341 and #1357):

========================================================== ===================== ============================================= You want to know Call mzLib computes it with ========================================================== ===================== ============================================= Which features change with a coefficient, and how surely? :func:fit LinearModel.Fit + EmpiricalBayes.Moderate Which of these p-values survive a false discovery rate? :func:adjust MultipleTesting.BenjaminiHochberg What is one feature's effect, pooled across studies? :func:meta RandomEffectsMeta.Pool (DerSimonian-Laird) ========================================================== ===================== =============================================

It is limma, checked. :func:fit is limma's lmFit followed by eBayes(legacy = TRUE) (Smyth 2004), and mzLib holds it to limma 3.68.5 to 1e-8 relative on limma's own reference output. pyMzLib ships that reference - limma's input, and limma's own eBayes output for it - so you can run the comparison yourself::

>>> fit = pymzlib.stats.fit("limma_reference_responses.tsv", "limma_reference_design.tsv",
...                         ["age_decades"])
>>> import csv
>>> with open("limma_ebayes_notrend.tsv", newline="") as fh:
...     limma = list(csv.DictReader(fh, delimiter="\t"))
>>> def worst(ours, theirs):
...     return max(abs(a - float(b)) / abs(float(b)) for a, b in zip(ours, theirs))
>>> worst(fit.columns["t"], [row["t_age"] for row in limma]) < 1e-8
True
>>> worst(fit.columns["bh_adjusted"], [row["bh_age"] for row in limma]) < 1e-8
True
>>> fit.feature_count, fit.residual_df_differ
(400, True)

Every one of the 400 moderated t-statistics and adjusted p-values agrees with limma to better than one part in 10^8.

What it is not. It is limma's legacy estimator. When features have different residual degrees of freedom - which omitting missing values causes, so in most label-free data - limma 3.61 and later default to a different prior estimator, and :attr:ModeratedFit.residual_df_differ says when your input is such a case. robust = TRUE, contrasts, the B-statistic and observation weights are not implemented; write a contrast as its own design column instead.

Your data, as files. :func:fit reads two tab-separated files - a feature-by-sample table of (already log-transformed) responses, and a sample-by-coefficient design - because that is the shape a table library writes (df.to_csv(path, sep="\t")) and it keeps a 10,000-protein table off the command line. A blank, NA or NaN cell is missing and is left out of that feature's fit, never imputed. A 0 is an observation, as in mzLib, so blank out any 0 that means "not measured".

Wire verbs: stats fit, stats adjust and stats meta; their language-neutral specs live in the bridge repository under design/verbs/.

VariancePrior dataclass

The fitted prior of the per-feature residual variances, s²_g ~ s0² · χ²(d0)/d0.

It depends only on the fit, never on which coefficient is tested, so one prior serves every coefficient in a :class:ModeratedFit.

Attributes:

Name Type Description
df float | None

Prior degrees of freedom d0, or None when it is infinite (see df_infinite). Larger means the features' variances agree, so each is shrunk harder toward scale.

df_infinite bool

True when the residual variances are no more dispersed than sampling alone explains, so every feature takes the prior variance.

trended bool

Whether s0² varies with each feature's average response (trend=True).

spline_basis_count int

Basis functions the trend spline used, intercept included; 1 without a trend.

scale float | None

s0², the prior variance, in squared response units; None when trended, in which case it is per feature, in the prior_variance column.

Source code in pkg/python/src/pymzlib/stats.py
@dataclass(frozen=True)
class VariancePrior:
    """The fitted prior of the per-feature residual variances, s²_g ~ s0² · χ²(d0)/d0.

    It depends only on the fit, never on which coefficient is tested, so one prior serves every
    coefficient in a :class:`ModeratedFit`.

    Attributes:
        df: Prior degrees of freedom d0, or ``None`` when it is infinite (see ``df_infinite``).
            Larger means the features' variances agree, so each is shrunk harder toward ``scale``.
        df_infinite: ``True`` when the residual variances are no more dispersed than sampling alone
            explains, so every feature takes the prior variance.
        trended: Whether s0² varies with each feature's average response (``trend=True``).
        spline_basis_count: Basis functions the trend spline used, intercept included; 1 without a
            trend.
        scale: s0², the prior variance, in squared response units; ``None`` when trended, in which
            case it is per feature, in the ``prior_variance`` column.
    """

    df: float | None
    df_infinite: bool
    trended: bool
    spline_basis_count: int
    scale: float | None

    @classmethod
    def _from_wire(cls, data: Mapping[str, Any]) -> VariancePrior:
        return cls(
            df=data.get("df"),
            df_infinite=bool(data.get("df_infinite", False)),
            trended=bool(data.get("trended", False)),
            spline_basis_count=int(data.get("spline_basis_count", 1)),
            scale=data.get("scale"),
        )

ModeratedFit dataclass

Bases: _Table

What :func:fit returns: one row per (tested coefficient, feature), with limma's moderated t.

columns holds one block of feature_count rows per tested coefficient, in the order you named them, each block in the responses file's feature order:

====================== ============================== ==================================================== column unit meaning; None means ====================== ============================== ==================================================== feature - the id in the responses file's first column coefficient - the coefficient this row tests status - one of :data:FIT_STATUSES estimate response units per coefficient least-squares coefficient: a log2 fold change unit for a 0/1 group column on log2 intensities; None = not fitted standard_error response units per coefficient moderated standard error; None = not fitted unit t standard errors moderated t; None = not fitted df_total degrees of freedom df_residual + prior df, capped at the pooled df p_value fraction (0 to 1) two-sided; None = not fitted bh_adjusted fraction (0 to 1) Benjamini-Hochberg over this coefficient's fitted features; not a target-decoy q-value posterior_variance squared response units the moderated residual variance prior_variance squared response units s0² for this feature sigma response units residual standard deviation before moderation df_residual - observed samples minus coefficients observed samples samples with a finite response average_response response units the covariate trend=True fits on ====================== ============================== ====================================================

Attributes:

Name Type Description
responses_file str

Absolute path of the responses table.

design_file str

Absolute path of the design table.

feature_count int

Feature rows read (features).

sample_count int

Sample columns read (samples).

sample_names list[str]

In the responses header's order.

coefficient_names list[str]

Every design column, in order: the model always fits all of them.

tested_coefficients list[str]

The coefficients you asked to test, in order.

trend bool

Whether the prior variance was allowed to trend with average response.

threads int

The thread count used (threads); the answer is identical at any count.

missing_count int

Response cells that were blank, NA or NaN (cells), left out of their fits.

zero_count int

Response cells that are exactly 0 (cells), fitted as observations.

status_counts dict[str, int]

Features per status (features), keyed by :data:FIT_STATUSES.

residual_df_differ bool

True when the fitted features do not all have the same residual degrees of freedom. Then default limma (3.61 and later) would use a different prior estimator and report different moderated statistics; these are legacy = TRUE.

prior VariancePrior

The fitted :class:VariancePrior.

row_count int

Rows in the table (rows): feature_count x the number of tested coefficients.

column_names list[str]

The table's columns, in order.

columns dict[str, list[Any]]

Column name -> list of values - the shape pandas.DataFrame takes as is.

caveats list[str]

What the numbers are and are not, including any that apply to this input (unfitted features, zeros, residual_df_differ).

Source code in pkg/python/src/pymzlib/stats.py
@dataclass(frozen=True)
class ModeratedFit(_Table):
    """What :func:`fit` returns: one row per (tested coefficient, feature), with limma's moderated t.

    ``columns`` holds one block of ``feature_count`` rows per tested coefficient, in the order you
    named them, each block in the responses file's feature order:

    ======================  ==============================  ====================================================
    column                  unit                            meaning; ``None`` means
    ======================  ==============================  ====================================================
    ``feature``             -                               the id in the responses file's first column
    ``coefficient``         -                               the coefficient this row tests
    ``status``              -                               one of :data:`FIT_STATUSES`
    ``estimate``            response units per coefficient  least-squares coefficient: a **log2 fold change**
                            unit                            for a 0/1 group column on log2 intensities;
                                                            ``None`` = not fitted
    ``standard_error``      response units per coefficient  moderated standard error; ``None`` = not fitted
                            unit
    ``t``                   standard errors                 moderated t; ``None`` = not fitted
    ``df_total``            degrees of freedom              df_residual + prior df, capped at the pooled df
    ``p_value``             fraction (0 to 1)               two-sided; ``None`` = not fitted
    ``bh_adjusted``         fraction (0 to 1)               Benjamini-Hochberg over this coefficient's fitted
                                                            features; **not a target-decoy q-value**
    ``posterior_variance``  squared response units          the moderated residual variance
    ``prior_variance``      squared response units          s0² for this feature
    ``sigma``               response units                  residual standard deviation before moderation
    ``df_residual``         -                               observed samples minus coefficients
    ``observed``            samples                         samples with a finite response
    ``average_response``    response units                  the covariate ``trend=True`` fits on
    ======================  ==============================  ====================================================

    Attributes:
        responses_file: Absolute path of the responses table.
        design_file: Absolute path of the design table.
        feature_count: Feature rows read (features).
        sample_count: Sample columns read (samples).
        sample_names: In the responses header's order.
        coefficient_names: Every design column, in order: the model always fits all of them.
        tested_coefficients: The coefficients you asked to test, in order.
        trend: Whether the prior variance was allowed to trend with average response.
        threads: The thread count used (threads); the answer is identical at any count.
        missing_count: Response cells that were blank, NA or NaN (cells), left out of their fits.
        zero_count: Response cells that are exactly 0 (cells), fitted as observations.
        status_counts: Features per status (features), keyed by :data:`FIT_STATUSES`.
        residual_df_differ: ``True`` when the fitted features do not all have the same residual
            degrees of freedom. Then default limma (3.61 and later) would use a different prior
            estimator and report different moderated statistics; these are ``legacy = TRUE``.
        prior: The fitted :class:`VariancePrior`.
        row_count: Rows in the table (rows): ``feature_count`` x the number of tested coefficients.
        column_names: The table's columns, in order.
        columns: Column name -> list of values - the shape ``pandas.DataFrame`` takes as is.
        caveats: What the numbers are and are not, including any that apply to this input
            (unfitted features, zeros, ``residual_df_differ``).
    """

    responses_file: str
    design_file: str
    feature_count: int
    sample_count: int
    sample_names: list[str]
    coefficient_names: list[str]
    tested_coefficients: list[str]
    trend: bool
    threads: int
    missing_count: int
    zero_count: int
    status_counts: dict[str, int]
    residual_df_differ: bool
    prior: VariancePrior
    row_count: int
    column_names: list[str] = field(default_factory=list)
    columns: dict[str, list[Any]] = field(default_factory=dict)
    caveats: list[str] = field(default_factory=list)

    def rows(self, coefficient: str) -> list[dict[str, Any]]:
        """The rows testing one coefficient, as dicts, in feature order.

        Args:
            coefficient: One of :attr:`tested_coefficients`.

        Raises:
            UsageError: ``coefficient`` was not tested in this fit.
        """
        if coefficient not in self.tested_coefficients:
            raise _bridge.UsageError(
                f"'{coefficient}' was not tested in this fit; it tested: "
                + ", ".join(self.tested_coefficients)
                + "."
            )
        return [r for r in self.records if r["coefficient"] == coefficient]

    @classmethod
    def _from_wire(cls, data: Mapping[str, Any]) -> ModeratedFit:
        names, columns = _columns(data)
        return cls(
            responses_file=str(data.get("responses_file", "")),
            design_file=str(data.get("design_file", "")),
            feature_count=int(data.get("feature_count", 0)),
            sample_count=int(data.get("sample_count", 0)),
            sample_names=list(data.get("sample_names") or []),
            coefficient_names=list(data.get("coefficient_names") or []),
            tested_coefficients=list(data.get("tested_coefficients") or []),
            trend=bool(data.get("trend", False)),
            threads=int(data.get("threads", 1)),
            missing_count=int(data.get("missing_count", 0)),
            zero_count=int(data.get("zero_count", 0)),
            status_counts=dict(data.get("status_counts") or {}),
            residual_df_differ=bool(data.get("residual_df_differ", False)),
            prior=VariancePrior._from_wire(data.get("prior") or {}),
            row_count=int(data.get("row_count", 0)),
            column_names=names,
            columns=columns,
            caveats=list(data.get("caveats") or []),
        )
rows
rows(coefficient: str) -> list[dict[str, Any]]

The rows testing one coefficient, as dicts, in feature order.

Parameters:

Name Type Description Default
coefficient str

One of :attr:tested_coefficients.

required

Raises:

Type Description
UsageError

coefficient was not tested in this fit.

Source code in pkg/python/src/pymzlib/stats.py
def rows(self, coefficient: str) -> list[dict[str, Any]]:
    """The rows testing one coefficient, as dicts, in feature order.

    Args:
        coefficient: One of :attr:`tested_coefficients`.

    Raises:
        UsageError: ``coefficient`` was not tested in this fit.
    """
    if coefficient not in self.tested_coefficients:
        raise _bridge.UsageError(
            f"'{coefficient}' was not tested in this fit; it tested: "
            + ", ".join(self.tested_coefficients)
            + "."
        )
    return [r for r in self.records if r["coefficient"] == coefficient]

Adjusted dataclass

Bases: _Table

What :func:adjust returns: one row per p-value given, in input order.

columns["p_value"] is each p-value as read (a fraction, 0 to 1), None where you passed None or NaN; columns["bh_adjusted"] is its Benjamini-Hochberg adjusted value (a fraction, 0 to 1), None for an untested entry.

Attributes:

Name Type Description
line_count int

Values given (lines); the length of every column.

tested_count int

m, the finite p-values the adjustment is over (p-values).

row_count int

Rows in the table (rows), equal to line_count.

column_names list[str]

["p_value", "bh_adjusted"].

columns dict[str, list[Any]]

Column name -> one value per input.

caveats list[str]

What the adjustment is and is not.

Source code in pkg/python/src/pymzlib/stats.py
@dataclass(frozen=True)
class Adjusted(_Table):
    """What :func:`adjust` returns: one row per p-value given, in input order.

    ``columns["p_value"]`` is each p-value as read (a fraction, 0 to 1), ``None`` where you passed
    ``None`` or NaN; ``columns["bh_adjusted"]`` is its Benjamini-Hochberg adjusted value (a
    fraction, 0 to 1), ``None`` for an untested entry.

    Attributes:
        line_count: Values given (lines); the length of every column.
        tested_count: m, the finite p-values the adjustment is over (p-values).
        row_count: Rows in the table (rows), equal to ``line_count``.
        column_names: ``["p_value", "bh_adjusted"]``.
        columns: Column name -> one value per input.
        caveats: What the adjustment is and is not.
    """

    line_count: int
    tested_count: int
    row_count: int
    column_names: list[str] = field(default_factory=list)
    columns: dict[str, list[Any]] = field(default_factory=dict)
    caveats: list[str] = field(default_factory=list)

    @property
    def bh_adjusted(self) -> list[float | None]:
        """The adjusted values alone, aligned with the input (``None`` = not tested)."""
        return list(self.columns.get("bh_adjusted") or [])

    @classmethod
    def _from_wire(cls, data: Mapping[str, Any]) -> Adjusted:
        names, columns = _columns(data)
        return cls(
            line_count=int(data.get("line_count", 0)),
            tested_count=int(data.get("tested_count", 0)),
            row_count=int(data.get("row_count", 0)),
            column_names=names,
            columns=columns,
            caveats=list(data.get("caveats") or []),
        )
bh_adjusted property
bh_adjusted: list[float | None]

The adjusted values alone, aligned with the input (None = not tested).

MetaAnalysis dataclass

Bases: _Table

What :func:meta returns: one pooled row per feature, in order of first appearance.

=========================== ==================================== =========================================== column unit meaning; None means =========================== ==================================== =========================================== feature - as given studies studies studies pooled estimate units of the study estimates random-effects pooled estimate standard_error units of the study estimates its standard error confidence_low / _high units of the study estimates the interval at :attr:confidence p_value fraction (0 to 1) two-sided, normal reference; not adjusted across features tau2 squared units of the study estimates between-study variance; 0 for one study q chi-square statistic Cochran's Q; None = one study i_squared fraction (0 to 1) share of variation from heterogeneity; None = one study direction_agree studies studies with the pooled estimate's sign direction_disagree studies studies with the opposite sign leave_one_out_max_delta units of the study estimates largest change from dropping one study; None = one study =========================== ==================================== ===========================================

Attributes:

Name Type Description
study_count int

Studies read, over every feature (studies).

feature_count int

Distinct features (features); the table's row count.

confidence float

The interval's coverage, as a fraction (0 to 1).

row_count int

Rows in the table (rows), equal to feature_count.

column_names list[str]

The table's columns, in order.

columns dict[str, list[Any]]

Column name -> one value per feature.

caveats list[str]

What the pooling assumes.

Source code in pkg/python/src/pymzlib/stats.py
@dataclass(frozen=True)
class MetaAnalysis(_Table):
    """What :func:`meta` returns: one pooled row per feature, in order of first appearance.

    ===========================  ====================================  ===========================================
    column                       unit                                  meaning; ``None`` means
    ===========================  ====================================  ===========================================
    ``feature``                  -                                     as given
    ``studies``                  studies                               studies pooled
    ``estimate``                 units of the study estimates          random-effects pooled estimate
    ``standard_error``           units of the study estimates          its standard error
    ``confidence_low`` / _high   units of the study estimates          the interval at :attr:`confidence`
    ``p_value``                  fraction (0 to 1)                     two-sided, normal reference; not adjusted
                                                                       across features
    ``tau2``                     squared units of the study estimates  between-study variance; 0 for one study
    ``q``                        chi-square statistic                  Cochran's Q; ``None`` = one study
    ``i_squared``                fraction (0 to 1)                     share of variation from heterogeneity;
                                                                       ``None`` = one study
    ``direction_agree``          studies                               studies with the pooled estimate's sign
    ``direction_disagree``       studies                               studies with the opposite sign
    ``leave_one_out_max_delta``  units of the study estimates          largest change from dropping one study;
                                                                       ``None`` = one study
    ===========================  ====================================  ===========================================

    Attributes:
        study_count: Studies read, over every feature (studies).
        feature_count: Distinct features (features); the table's row count.
        confidence: The interval's coverage, as a fraction (0 to 1).
        row_count: Rows in the table (rows), equal to ``feature_count``.
        column_names: The table's columns, in order.
        columns: Column name -> one value per feature.
        caveats: What the pooling assumes.
    """

    study_count: int
    feature_count: int
    confidence: float
    row_count: int
    column_names: list[str] = field(default_factory=list)
    columns: dict[str, list[Any]] = field(default_factory=dict)
    caveats: list[str] = field(default_factory=list)

    @classmethod
    def _from_wire(cls, data: Mapping[str, Any]) -> MetaAnalysis:
        names, columns = _columns(data)
        return cls(
            study_count=int(data.get("study_count", 0)),
            feature_count=int(data.get("feature_count", 0)),
            confidence=float(data.get("confidence", 0.95)),
            row_count=int(data.get("row_count", 0)),
            column_names=names,
            columns=columns,
            caveats=list(data.get("caveats") or []),
        )

fit

fit(responses: _PathLike, design: _PathLike, coefficients: Sequence[str], *, trend: bool = False, spline_basis: int | None = None, threads: int = 1, timeout: float | None = 600) -> ModeratedFit

Fit one linear model per feature and test coefficients with limma's moderated t.

Runs mzLib's LinearModel.Fit once - every design column is fitted - and then EmpiricalBayes.Moderate for each coefficient you name, which also applies Benjamini-Hochberg across that coefficient's fitted features. This is limma's lmFit(...) then eBayes(..., legacy = TRUE), reproduced by mzLib to 1e-8 relative.

A feature is fitted on the samples where it was observed, and is reported - never dropped - when it cannot be: too_few_observations when it has no more observed samples than the design has coefficients, rank_deficient when its observed samples cannot separate the coefficients (a feature seen in only one group cannot have a group effect).

Parameters:

Name Type Description Default
responses _PathLike

A TSV with a header row. First column the feature id; every other column one sample, named by its header. Use the values as you want them modelled - log2 intensities, usually. Blank, NA or NaN is missing; 0 is an observation.

required
design _PathLike

A TSV with a header row and one row per sample: first column the sample name (matching the responses header exactly, in any order), then one numeric column per coefficient. Include an intercept column yourself if you want one.

required
coefficients Sequence[str]

Design column names to test, each with its own moderated test and its own Benjamini-Hochberg family.

required
trend bool

Let the prior variance follow average response (limma's trend = TRUE), for data whose low-abundance features are noisier.

False
spline_basis int | None

Basis functions of the trend spline, intercept included (default 4). Only with trend=True; mzLib may use fewer, and :attr:VariancePrior.spline_basis_count reports what it used.

None
threads int

Features fitted at once (threads), or -1 for every core. The answer is identical at any value.

1
timeout float | None

Seconds to allow. None waits indefinitely.

600

Returns:

Name Type Description
A ModeratedFit

class:ModeratedFit.

Raises:

Type Description
UsageError

a file is missing or malformed; the design's samples are not exactly the responses' samples; a coefficient is not in the design or is named twice; the design is not of full rank; or fewer than two features can be fitted, so no prior exists.

Examples:

A dilution series from mzLib's own RNA test data: MALAT1 loaded at 500, 250 and 125 ng against a constant 500 ng FLuc spike, log2 intensities normalised to FLuc in each run (see tests/fixtures/stats/README.md). Halving MALAT1 should read as about -1:

>>> fit = pymzlib.stats.fit("malat_dilution_log2_vs_fluc.tsv", "malat_dilution_design.tsv",
...                         ["malat_250ng", "malat_125ng"])
>>> fit.feature_count, fit.sample_count, fit.status_counts
(492, 27, {'fitted': 212, 'too_few_observations': 214, 'rank_deficient': 66})
>>> fit.residual_df_differ, round(fit.prior.df, 1)
(True, 19.6)
>>> import statistics
>>> malat = [r["estimate"] for r in fit.rows("malat_250ng")
...          if r["status"] == "fitted" and r["feature"].startswith("MALAT1:")]
>>> len(malat), round(statistics.median(malat), 2)
(116, -0.89)
Source code in pkg/python/src/pymzlib/stats.py
def fit(
    responses: _PathLike,
    design: _PathLike,
    coefficients: Sequence[str],
    *,
    trend: bool = False,
    spline_basis: int | None = None,
    threads: int = 1,
    timeout: float | None = 600,
) -> ModeratedFit:
    """Fit one linear model per feature and test coefficients with limma's moderated t.

    Runs mzLib's ``LinearModel.Fit`` once - every design column is fitted - and then
    ``EmpiricalBayes.Moderate`` for each coefficient you name, which also applies
    Benjamini-Hochberg across that coefficient's fitted features. This is limma's
    ``lmFit(...)`` then ``eBayes(..., legacy = TRUE)``, reproduced by mzLib to 1e-8 relative.

    A feature is fitted on the samples where it was observed, and is reported - never dropped -
    when it cannot be: ``too_few_observations`` when it has no more observed samples than the
    design has coefficients, ``rank_deficient`` when its observed samples cannot separate the
    coefficients (a feature seen in only one group cannot have a group effect).

    Args:
        responses: A TSV with a header row. First column the feature id; every other column one
            sample, named by its header. Use the values as you want them modelled - log2
            intensities, usually. Blank, ``NA`` or ``NaN`` is missing; **0 is an observation**.
        design: A TSV with a header row and one row per sample: first column the sample name
            (matching the responses header exactly, in any order), then one numeric column per
            coefficient. Include an intercept column yourself if you want one.
        coefficients: Design column names to test, each with its own moderated test and its own
            Benjamini-Hochberg family.
        trend: Let the prior variance follow average response (limma's ``trend = TRUE``), for data
            whose low-abundance features are noisier.
        spline_basis: Basis functions of the trend spline, intercept included (default 4). Only
            with ``trend=True``; mzLib may use fewer, and :attr:`VariancePrior.spline_basis_count`
            reports what it used.
        threads: Features fitted at once (threads), or ``-1`` for every core. The answer is
            identical at any value.
        timeout: Seconds to allow. ``None`` waits indefinitely.

    Returns:
        A :class:`ModeratedFit`.

    Raises:
        UsageError: a file is missing or malformed; the design's samples are not exactly the
            responses' samples; a coefficient is not in the design or is named twice; the design
            is not of full rank; or fewer than two features can be fitted, so no prior exists.

    Examples:
        A dilution series from mzLib's own RNA test data: MALAT1 loaded at 500, 250 and 125 ng
        against a constant 500 ng FLuc spike, log2 intensities normalised to FLuc in each run (see
        ``tests/fixtures/stats/README.md``). Halving MALAT1 should read as about -1:

        >>> fit = pymzlib.stats.fit("malat_dilution_log2_vs_fluc.tsv", "malat_dilution_design.tsv",
        ...                         ["malat_250ng", "malat_125ng"])
        >>> fit.feature_count, fit.sample_count, fit.status_counts
        (492, 27, {'fitted': 212, 'too_few_observations': 214, 'rank_deficient': 66})
        >>> fit.residual_df_differ, round(fit.prior.df, 1)
        (True, 19.6)
        >>> import statistics
        >>> malat = [r["estimate"] for r in fit.rows("malat_250ng")
        ...          if r["status"] == "fitted" and r["feature"].startswith("MALAT1:")]
        >>> len(malat), round(statistics.median(malat), 2)
        (116, -0.89)
    """
    if isinstance(coefficients, str):
        raise _bridge.UsageError(
            f"coefficients takes a list of design column names; for one, use [{coefficients!r}]."
        )
    if not isinstance(coefficients, Sequence) or not coefficients:
        raise _bridge.UsageError("fit() needs at least one coefficient to test, as a list.")
    for name in coefficients:
        if not isinstance(name, str) or not name.strip():
            raise _bridge.UsageError(f"Every coefficient must be a non-empty str; got {name!r}.")
        _one_line(name, "A coefficient name")
    if not isinstance(trend, bool):
        raise _bridge.UsageError(f"trend must be True or False; got {trend!r}.")
    if isinstance(threads, bool) or not isinstance(threads, int):
        raise _bridge.UsageError(f"threads must be an int; got {threads!r}.")

    args = ["stats", "fit", "--responses", _bridge.path_text(responses),
            "--design", _bridge.path_text(design), "--threads", str(threads)]
    if trend:
        args.append("--trend")
    if spline_basis is not None:
        if isinstance(spline_basis, bool) or not isinstance(spline_basis, int):
            raise _bridge.UsageError(f"spline_basis must be an int; got {spline_basis!r}.")
        args += ["--spline-basis", str(spline_basis)]

    _bridge.require_verb("stats fit", since=_SINCE)
    data = _bridge.invoke(*args, stdin="\n".join(coefficients) + "\n", timeout=timeout)
    return ModeratedFit._from_wire(data)

adjust

adjust(p_values: Sequence[float | None], *, timeout: float | None = 60) -> Adjusted

Benjamini-Hochberg adjust a list of p-values, keeping every position.

For the i-th smallest of m p-values the adjusted value is min over j >= i of p_(j) m / j, capped at 1 (Benjamini and Hochberg 1995), computed by mzLib's MultipleTesting.BenjaminiHochberg. None or NaN marks a feature that was not tested: it stays None and is not counted in m, which is the honest family. Do not write 1 for an untested feature; that enlarges m.

:func:fit already reports bh_adjusted; this is for p-values from anywhere else.

Parameters:

Name Type Description Default
p_values Sequence[float | None]

The p-values, each a fraction from 0 to 1, or None/NaN for untested.

required
timeout float | None

Seconds to allow. None waits indefinitely.

60

Returns:

Name Type Description
An Adjusted

class:Adjusted table, row i for p_values[i].

Raises:

Type Description
UsageError

p_values is empty or not a list, an entry is not a number, or a p-value is outside [0, 1].

Examples:

>>> adjusted = pymzlib.stats.adjust([0.0002, 0.004, 0.019, None, 0.031, 0.2, float("nan"), 0.74])
>>> adjusted.tested_count
6
>>> [None if v is None else round(v, 4) for v in adjusted.bh_adjusted]
[0.0012, 0.012, 0.038, None, 0.0465, 0.24, None, 0.74]
Source code in pkg/python/src/pymzlib/stats.py
def adjust(p_values: Sequence[float | None], *, timeout: float | None = 60) -> Adjusted:
    """Benjamini-Hochberg adjust a list of p-values, keeping every position.

    For the i-th smallest of m p-values the adjusted value is min over j >= i of p_(j) m / j, capped
    at 1 (Benjamini and Hochberg 1995), computed by mzLib's ``MultipleTesting.BenjaminiHochberg``.
    ``None`` or NaN marks a feature that was not tested: it stays ``None`` and is **not counted in
    m**, which is the honest family. Do not write 1 for an untested feature; that enlarges m.

    :func:`fit` already reports ``bh_adjusted``; this is for p-values from anywhere else.

    Args:
        p_values: The p-values, each a fraction from 0 to 1, or ``None``/NaN for untested.
        timeout: Seconds to allow. ``None`` waits indefinitely.

    Returns:
        An :class:`Adjusted` table, row ``i`` for ``p_values[i]``.

    Raises:
        UsageError: ``p_values`` is empty or not a list, an entry is not a number, or a p-value is
            outside [0, 1].

    Examples:
        >>> adjusted = pymzlib.stats.adjust([0.0002, 0.004, 0.019, None, 0.031, 0.2, float("nan"), 0.74])
        >>> adjusted.tested_count
        6
        >>> [None if v is None else round(v, 4) for v in adjusted.bh_adjusted]
        [0.0012, 0.012, 0.038, None, 0.0465, 0.24, None, 0.74]
    """
    if isinstance(p_values, (str, bytes)) or not isinstance(p_values, Sequence) or not p_values:
        raise _bridge.UsageError("adjust() needs at least one p-value, as a list.")
    lines = []
    for i, value in enumerate(p_values):
        if value is None:
            lines.append("")
            continue
        number = _number(value, f"p_values[{i}]")
        lines.append("" if math.isnan(number) else repr(number))
    _bridge.require_verb("stats adjust", since=_SINCE)
    # A trailing newline ends the last line, so a final untested entry survives the trip.
    data = _bridge.invoke("stats", "adjust", stdin="\n".join(lines) + "\n", timeout=timeout)
    return Adjusted._from_wire(data)

meta

meta(studies: Iterable[Sequence[Any] | Mapping[str, Any]], *, confidence: float = 0.95, timeout: float | None = 60) -> MetaAnalysis

Pool one effect size per study into a random-effects estimate per feature.

Each feature is pooled on its own by mzLib's RandomEffectsMeta.Pool, with the DerSimonian-Laird (1986) moment estimator of the between-study variance, as metafor's rma(method = "DL") does (mzLib matches metafor 5.2.1 to 1e-8 relative). Every row also says how many studies agree in direction and how far the estimate moves when any one is dropped - the two checks a reader of a pooled result asks for first.

Parameters:

Name Type Description Default
studies Iterable[Sequence[Any] | Mapping[str, Any]]

One entry per study: (feature, estimate, standard_error), or a mapping with those three keys. A feature's studies need not be adjacent. The standard error must be positive.

required
confidence float

Coverage of the reported interval, as a fraction (0 to 1).

0.95
timeout float | None

Seconds to allow. None waits indefinitely.

60

Returns:

Name Type Description
A MetaAnalysis

class:MetaAnalysis table, one row per feature in order of first appearance.

Raises:

Type Description
UsageError

no studies; an entry without the three values; a feature name that is empty or holds a tab or newline; an estimate or standard error that is not a finite number, or a standard error that is not positive; confidence not strictly between 0 and 1.

Examples:

metafor's own DerSimonian-Laird reference cases, from mzLib's test data:

>>> import csv
>>> with open("metafor_dl_inputs.tsv", newline="") as fh:
...     studies = [(r["case"], float(r["yi"]), float(r["sei"]))
...                for r in csv.DictReader(fh, delimiter="\t")]
>>> pooled = pymzlib.stats.meta(studies)
>>> for r in pooled.records:
...     print(r["feature"], r["studies"], round(r["estimate"], 4), round(r["tau2"], 4),
...           r["direction_agree"], r["direction_disagree"])
homogeneous 5 0.3097 0 5 0
heterogeneous 6 0.2513 0.0752 5 1
two_studies 2 -0.2 0.06 1 1
random_eight 8 0.107 0.0911 6 2
Source code in pkg/python/src/pymzlib/stats.py
def meta(
    studies: Iterable[Sequence[Any] | Mapping[str, Any]],
    *,
    confidence: float = 0.95,
    timeout: float | None = 60,
) -> MetaAnalysis:
    """Pool one effect size per study into a random-effects estimate per feature.

    Each feature is pooled on its own by mzLib's ``RandomEffectsMeta.Pool``, with the
    DerSimonian-Laird (1986) moment estimator of the between-study variance, as metafor's
    ``rma(method = "DL")`` does (mzLib matches metafor 5.2.1 to 1e-8 relative). Every row also says
    how many studies agree in direction and how far the estimate moves when any one is dropped -
    the two checks a reader of a pooled result asks for first.

    Args:
        studies: One entry per study: ``(feature, estimate, standard_error)``, or a mapping with
            those three keys. A feature's studies need not be adjacent. The standard error must be
            positive.
        confidence: Coverage of the reported interval, as a fraction (0 to 1).
        timeout: Seconds to allow. ``None`` waits indefinitely.

    Returns:
        A :class:`MetaAnalysis` table, one row per feature in order of first appearance.

    Raises:
        UsageError: no studies; an entry without the three values; a feature name that is empty
            or holds a tab or newline; an estimate or standard error that is not a finite number,
            or a standard error that is not positive; ``confidence`` not strictly between 0 and 1.

    Examples:
        metafor's own DerSimonian-Laird reference cases, from mzLib's test data:

        >>> import csv
        >>> with open("metafor_dl_inputs.tsv", newline="") as fh:
        ...     studies = [(r["case"], float(r["yi"]), float(r["sei"]))
        ...                for r in csv.DictReader(fh, delimiter="\\t")]
        >>> pooled = pymzlib.stats.meta(studies)
        >>> for r in pooled.records:
        ...     print(r["feature"], r["studies"], round(r["estimate"], 4), round(r["tau2"], 4),
        ...           r["direction_agree"], r["direction_disagree"])
        homogeneous 5 0.3097 0 5 0
        heterogeneous 6 0.2513 0.0752 5 1
        two_studies 2 -0.2 0.06 1 1
        random_eight 8 0.107 0.0911 6 2
    """
    if isinstance(confidence, bool) or not isinstance(confidence, numbers.Real):
        raise _bridge.UsageError(f"confidence must be a number; got {confidence!r}.")
    if isinstance(studies, (str, bytes, Mapping)):
        raise _bridge.UsageError(
            "meta() takes a list of studies, each (feature, estimate, standard_error)."
        )
    lines = []
    for i, study in enumerate(studies):
        if isinstance(study, Mapping):
            try:
                feature, estimate, error = study["feature"], study["estimate"], study["standard_error"]
            except KeyError as missing:
                raise _bridge.UsageError(
                    f"studies[{i}] has no {missing.args[0]!r} key; a mapping needs feature, "
                    "estimate and standard_error."
                ) from None
        elif isinstance(study, Sequence) and not isinstance(study, (str, bytes)) and len(study) == 3:
            feature, estimate, error = study
        else:
            raise _bridge.UsageError(
                f"studies[{i}] must be (feature, estimate, standard_error); got {study!r}."
            )
        if not isinstance(feature, str) or not feature.strip():
            raise _bridge.UsageError(f"studies[{i}] needs a non-empty feature name; got {feature!r}.")
        if "\t" in feature:
            raise _bridge.UsageError(f"studies[{i}]'s feature name contains a tab: {feature!r}.")
        _one_line(feature, f"studies[{i}]'s feature name")
        lines.append(
            f"{feature}\t{_number(estimate, f'studies[{i}] estimate')!r}"
            f"\t{_number(error, f'studies[{i}] standard_error')!r}"
        )
    if not lines:
        raise _bridge.UsageError("meta() needs at least one study.")
    _bridge.require_verb("stats meta", since=_SINCE)
    data = _bridge.invoke(
        "stats", "meta", "--confidence", repr(float(confidence)),
        stdin="\n".join(lines) + "\n", timeout=timeout,
    )
    return MetaAnalysis._from_wire(data)

Errors and diagnostics

Everything pyMzLib raises inherits from PyMzLibError, so a single except catches all of it.

_bridge

Locating and invoking the bundled mzLib bridge executable.

This module is the only place in pyMzLib that knows the bridge exists. Everything above it sees ordinary Python functions and objects. That boundary is deliberate: the transport (today, a self-contained .NET executable invoked per call) can be replaced by an in-process binding or a long-lived local server without any public API changing.

Nothing here imports a third-party package. pyMzLib declares no runtime dependencies, so it cannot participate in a dependency conflict inside anyone's environment.

PyMzLibError

Bases: Exception

Base class for every error pyMzLib raises.

Source code in pkg/python/src/pymzlib/_bridge.py
class PyMzLibError(Exception):
    """Base class for every error pyMzLib raises."""

UsageError

Bases: PyMzLibError, ValueError

A call was malformed — a missing or invalid argument. Raised before any work happens.

Source code in pkg/python/src/pymzlib/_bridge.py
class UsageError(PyMzLibError, ValueError):
    """A call was malformed — a missing or invalid argument. Raised before any work happens."""

BridgeError

Bases: PyMzLibError

mzLib reported a failure.

Attributes:

Name Type Description
error_type

The .NET exception type name, e.g. HttpRequestException. Useful for distinguishing a network failure from a bad accession without parsing prose.

Source code in pkg/python/src/pymzlib/_bridge.py
class BridgeError(PyMzLibError):
    """mzLib reported a failure.

    Attributes:
        error_type: The .NET exception type name, e.g. ``HttpRequestException``. Useful for
            distinguishing a network failure from a bad accession without parsing prose.
    """

    def __init__(self, error_type: str, message: str) -> None:
        super().__init__(message)
        self.error_type = error_type

ServiceUnavailableError

Bases: BridgeError

An external service is unavailable — down, rate-limited, timing out, or unreachable.

This is deliberately a distinct type, because the difference between "the repository is having a bad morning" and "something is broken" is the difference between retrying later and filing a bug. Catch it to back off and retry::

try:
    files = pymzlib.pride.list_files("PXD000001")
except pymzlib.ServiceUnavailableError:
    ...   # EBI's problem; try again later
except pymzlib.BridgeError:
    ...   # ours

The classification happens in the bridge rather than here, so every consumer of the wire format gets it and not only Python. HTTP 408, 429, and 5xx count as unavailable; 404 and 400 do not, because a wrong URL or a malformed request is our problem and excusing it as an outage would hide a real bug.

Source code in pkg/python/src/pymzlib/_bridge.py
class ServiceUnavailableError(BridgeError):
    """An external service is unavailable — down, rate-limited, timing out, or unreachable.

    This is deliberately a distinct type, because the difference between "the repository is
    having a bad morning" and "something is broken" is the difference between retrying later and
    filing a bug. Catch it to back off and retry::

        try:
            files = pymzlib.pride.list_files("PXD000001")
        except pymzlib.ServiceUnavailableError:
            ...   # EBI's problem; try again later
        except pymzlib.BridgeError:
            ...   # ours

    The classification happens in the bridge rather than here, so every consumer of the wire
    format gets it and not only Python. HTTP 408, 429, and 5xx count as unavailable; 404 and 400
    do not, because a wrong URL or a malformed request is our problem and excusing it as an
    outage would hide a real bug.
    """

BridgeTimeoutError

Bases: PyMzLibError

The bridge process did not finish within the timeout.

Deliberately not a :class:ServiceUnavailableError, and the distinction is the whole point. A subprocess timeout has several possible causes and only one of them is a slow service: the bridge may be wedged, the executable may be corrupt, antivirus may be holding it, or the caller may simply have passed a timeout that was too short. Reporting all of that as "the repository is down" is how a real bug gets skipped by every test suite and never seen again.

If the caller wants a slow network to be treated as an outage, they can catch this explicitly — but the library will not guess on their behalf.

Source code in pkg/python/src/pymzlib/_bridge.py
class BridgeTimeoutError(PyMzLibError):
    """The bridge process did not finish within the timeout.

    Deliberately **not** a :class:`ServiceUnavailableError`, and the distinction is the whole
    point. A subprocess timeout has several possible causes and only one of them is a slow
    service: the bridge may be wedged, the executable may be corrupt, antivirus may be holding
    it, or the caller may simply have passed a timeout that was too short. Reporting all of that
    as "the repository is down" is how a real bug gets skipped by every test suite and never
    seen again.

    If the caller wants a slow network to be treated as an outage, they can catch this
    explicitly — but the library will not guess on their behalf.
    """

BridgeNotFoundError

Bases: PyMzLibError

The bridge executable could not be located.

In a released wheel this should be impossible: the executable ships inside the package. It normally means pyMzLib is being run from a source checkout where the bridge has not been built yet (see pkg/build/publish-bridge.ps1).

Source code in pkg/python/src/pymzlib/_bridge.py
class BridgeNotFoundError(PyMzLibError):
    """The bridge executable could not be located.

    In a released wheel this should be impossible: the executable ships inside the package.
    It normally means pyMzLib is being run from a source checkout where the bridge has not
    been built yet (see ``pkg/build/publish-bridge.ps1``).
    """

BridgeVersion

Bases: TypedDict

What :func:bridge_version returns: a plain dict with these keys.

A TypedDict only so its keys are documented and type-checkable; at runtime it is the dict it always was. mzlib and verbs are absent from bridges built before they were added, so read them with .get("mzlib") and .get("verbs") rather than indexing.

Attributes:

Name Type Description
bridge str

The bridge assembly's own version string.

protocol int

The wire-format version. This - not the mzLib version - is the compatibility contract: a binding is compatible with a bridge by protocol.

runtime str

The bundled .NET runtime's version string.

mzlib 'str | None'

Which mzLib this bridge was built against, as 1.0.0+<commit>; absent (or None) when the build recorded no commit. It answers "which mzLib am I actually running?" for someone holding a wheel with no access to the repository's pin file. It is deliberately not a version to compare against: use protocol for that.

verbs 'list[str]'

Every command this bridge dispatches, e.g. "readers read-protein-groups", generated from the bridge's own dispatch table when it was built, so it cannot list one it lacks. pyMzLib checks it before calling a command newer than the bridge it may be paired with (see :func:require_verb), so an old bridge is reported as old instead of answering "Unknown command". Absent from bridges before pyMzLib 0.2.0.

Source code in pkg/python/src/pymzlib/_bridge.py
class BridgeVersion(TypedDict, total=False):
    """What :func:`bridge_version` returns: a plain ``dict`` with these keys.

    A ``TypedDict`` only so its keys are documented and type-checkable; at runtime it is the
    ``dict`` it always was. ``mzlib`` and ``verbs`` are absent from bridges built before they were
    added, so read them with ``.get("mzlib")`` and ``.get("verbs")`` rather than indexing.

    Attributes:
        bridge: The bridge assembly's own version string.
        protocol: The wire-format version. This - not the mzLib version - is the compatibility
            contract: a binding is compatible with a *bridge* by protocol.
        runtime: The bundled .NET runtime's version string.
        mzlib: Which mzLib this bridge was built against, as ``1.0.0+<commit>``; absent (or
            ``None``) when the build recorded no commit. It answers "which mzLib am I actually
            running?" for someone holding a wheel with no access to the repository's pin file. It
            is deliberately *not* a version to compare against: use ``protocol`` for that.
        verbs: Every command this bridge dispatches, e.g. ``"readers read-protein-groups"``,
            generated from the bridge's own dispatch table when it was built, so it cannot list
            one it lacks. pyMzLib checks it before calling a command newer than the bridge it may
            be paired with (see :func:`require_verb`), so an old bridge is reported as old instead
            of answering "Unknown command". Absent from bridges before pyMzLib 0.2.0.
    """

    bridge: str
    protocol: int
    runtime: str
    mzlib: "str | None"
    verbs: "list[str]"

bridge_path

bridge_path() -> Path

Return the path of the bridge executable that will be used.

Resolution order: the PYMZLIB_BRIDGE environment variable, then the copy staged inside this package for the current platform.

Raises:

Type Description
BridgeNotFoundError

if neither exists.

Source code in pkg/python/src/pymzlib/_bridge.py
def bridge_path() -> Path:
    """Return the path of the bridge executable that will be used.

    Resolution order: the ``PYMZLIB_BRIDGE`` environment variable, then the copy staged
    inside this package for the current platform.

    Raises:
        BridgeNotFoundError: if neither exists.
    """
    override = os.environ.get(BRIDGE_ENV_VAR)
    if override:
        candidate = Path(override)
        if not candidate.is_file():
            raise BridgeNotFoundError(f"{BRIDGE_ENV_VAR} points at '{override}', which is not a file.")
        return candidate

    executable = "mzlib-bridge.exe" if sys.platform == "win32" else "mzlib-bridge"
    candidate = Path(__file__).parent / "_dotnet" / _platform_tag() / executable
    if not candidate.is_file():
        raise BridgeNotFoundError(
            f"No mzLib bridge for this platform at '{candidate}'. "
            f"In a source checkout, build one and set {BRIDGE_ENV_VAR} to its path."
        )
    return candidate

bridge_version

bridge_version() -> BridgeVersion

Return the bridge's own version information, and check protocol compatibility.

Returns:

Name Type Description
A BridgeVersion

class:BridgeVersion - a dict with bridge, protocol, runtime,

BridgeVersion

mzlib and verbs.

Raises:

Type Description
PyMzLibError

if the bridge speaks a different wire format than this package.

Examples:

>>> info = pymzlib.bridge_version()
>>> info["protocol"], "readers read-protein-groups" in info["verbs"]
(1, True)
Source code in pkg/python/src/pymzlib/_bridge.py
def bridge_version() -> BridgeVersion:
    """Return the bridge's own version information, and check protocol compatibility.

    Returns:
        A :class:`BridgeVersion` - a ``dict`` with ``bridge``, ``protocol``, ``runtime``,
        ``mzlib`` and ``verbs``.

    Raises:
        PyMzLibError: if the bridge speaks a different wire format than this package.

    Examples:
        >>> info = pymzlib.bridge_version()
        >>> info["protocol"], "readers read-protein-groups" in info["verbs"]
        (1, True)
    """
    info = invoke("version", timeout=60)
    reported = info.get("protocol")
    if reported != PROTOCOL_VERSION:
        raise PyMzLibError(
            f"mzLib bridge speaks protocol {reported}, but this pyMzLib expects {PROTOCOL_VERSION}. "
            "The Python package and the bridge were built from different sources."
        )
    return info