Skip to content

Architecture

You don't need any of this to use pyMzLib. It's here because the design has consequences worth knowing about, and because the reasoning should survive the people who made it.

The shape of it

your code
    │   ordinary Python objects
pymzlib.pride                    public API — knows nothing about how the work gets done
pymzlib._bridge                  the ONLY module aware a subprocess exists
    │   JSON over stdout:  {"ok": true, "data": …}  |  {"ok": false, "error": {…}}
mzlib-bridge(.exe)               self-contained .NET executable, shipped inside the wheel
mzLib                            the actual proteomics library, in C#

When you call pymzlib.pride.list_files("PXD000001"), pyMzLib runs the bundled executable with arguments, that executable calls mzLib's PrideArchiveClient, serializes the result as JSON to standard output, and exits. Python parses it into dataclasses and hands them back.

That's it. It is not clever, and the lack of cleverness is the point.

Why a subprocess

The obvious alternative is pythonnet, which loads a real CLR into the Python process and makes every C# class directly available with no wrapper code at all. It is genuinely impressive, and it was the wrong choice here.

pythonnet needs a .NET runtime installed on the machine, and since version 3.0 it needs Runtime.PythonDLL or PYTHONNET_PYDLL pointed at the exact python3XX.dll in use. That is precisely the class of problem this project exists to eliminate. A wrapper whose install instructions begin "first, install the .NET runtime, then set an environment variable to the path of your Python shared library" has already lost, no matter how complete its API surface.

A self-contained single-file executable has none of that. It carries its own runtime, consults no system installation, and needs no configuration. Costs it does have:

  • Startup. About 120 ms per call warm, and roughly a second on the very first call of a session while the bundle extracts (measured, not estimated). Irrelevant next to an HTTP request; unacceptable in a per-spectrum loop.
  • Serialization. Everything crossing the boundary must be JSON-representable. Fine for manifests and file paths; wrong for a million-point spectrum.
  • Size. The wheel is about 115 MB because it contains a .NET runtime and mzLib's dependencies. See design decisions.

These costs bound where the design applies, and that boundary is honest: pyMzLib is for coarse-grained operations. When a fine-grained numeric surface is added, the transport underneath will change — which is why it's confined to one module.

The two load-bearing properties

The transport is hidden

_bridge.py is the only file that knows a subprocess is involved. Everything above it deals in Python objects. A future in-process binding, or a long-lived local server for stateful work, can replace it with no change to any public API. Nothing outside _bridge.py may import subprocess, and no public function may leak a process detail — a CalledProcessError reaching a caller would be a bug.

The wire format is language-neutral

Nothing in the JSON envelope assumes Python. It has no Python-shaped names, no Python types, no assumptions about the caller. A Rust crate, an R package, or a shell script would use the identical contract with no change on the .NET side:

$ mzlib-bridge pride files --accession PXD000001
{"ok":true,"data":{"accession":"PXD000001","file_count":8,…}}

This costs nothing to maintain and preserves an option worth having. See D6.

The envelope

Every invocation writes exactly one JSON object to standard output and nothing else — diagnostics go to stderr, so a caller can parse stdout without stripping log lines.

{"ok": true,  "data": {  }, "error": null}
{"ok": false, "data": null,  "error": {"type": "HttpRequestException", "message": "…"}}

Exit codes: 0 success, 1 handled failure, 2 malformed command.

Failures cross the boundary as structured data, never a stack trace. A .NET exception dump is unparseable noise to a caller in another language; {"type": "HttpRequestException"} lets that caller distinguish a network blip from a bad accession programmatically. The type field carries through to Python as BridgeError.error_type.

The envelope is versioned. mzlib-bridge version reports a protocol number, and pyMzLib checks it, so a Python package and an executable built from different sources fail loudly rather than producing subtly wrong results.

Dependencies, or the lack of them

pyMzLib declares zero third-party runtime dependencies. Not few — zero.

Python permits exactly one version of any package per environment. There is no side-by-side loading and no binding redirect, so two packages with incompatible requirements simply cannot coexist. Every dependency a library declares is a constraint it imposes on everyone who installs it. A scientific package that requires numpy<2 can lock a user out of half the ecosystem.

By requiring nothing, pyMzLib cannot participate in that failure mode at all. This is worth protecting: a convenience that would cost a runtime dependency is not worth it, and if a future capability genuinely needs one, it belongs behind an optional extra (pip install pymzlib[something]), never in the base requirements.

Where the .NET payload lives

Inside the installed package:

site-packages/pymzlib/
    __init__.py
    pride.py
    _bridge.py
    _dotnet/
        win-x64/
            mzlib-bridge.exe        ← the whole .NET runtime + mzLib, in one file

bridge_path() resolves it by platform, and PYMZLIB_BRIDGE overrides it for development. Since the payload is a binary, each wheel is specific to an operating system — but because there is no compiled Python code, one wheel serves every Python version. pyMzLib ships one wheel per OS, not one per OS × Python version, which is why the supported Python range can be generous without maintenance cost.