Spynach
A Python reimplementation of the Spinach spin
dynamics kernel (MIT licence, see LICENSE), verified stage by stage against the MATLAB
original. Start with the Hilbert-space guide; if you use
Spinach, also read Coming from Spinach. Each
guide has a runnable Jupyter notebook in docs/notebooks/. The project
page at ajerschow.github.io/spynach links the
guides, the notebooks (rendered by nbviewer) and the Spinach comparison reports, and
has an "Open in Google Colab" button that runs the quick-start example without a local
install (docs/colab/quick-start.ipynb), and a page that
runs Spynach, easyspyn and ctps in the browser through Pyodide
(try.html, contributed by Moritz Zaiss). See HANDOFF.md for goals and design decisions, and
oracle/README.md for the reference-data ("fixture") harness.
Quick start
Nothing to install: run it in the browser at
ajerschow.github.io/spynach/try.html
(Pyodide; NMR, EPR and pathway examples you can edit), or open a notebook in the cloud:
Install (Python 3.11 or newer):
pip install spynach # NMR: Spinach's kernel, from PyPI (installs Numba; also provides ctps)
pip install easyspyn # EPR: EasySpin's simulators, from PyPI (its own distribution, installs Numba)
pip install "spynach[epr]" # both
import easyspyn as es
nitroxide = {"g": [2.0088, 2.0061, 2.0027], "Nucs": "14N", "A": [16, 16, 86], "lwpp": 0.3}
powder = es.pepper(nitroxide, {"mwFreq": 9.5, "Range": [330, 350], "nPoints": 1024})
print(powder.x.shape, powder.spec.shape)
Status
Ported and verified against Spinach 2.13 fixtures (tests/slice/): the
zeeman-hilb pulse-acquire chain for liquid-state NMR with isotropic
interactions. The sphten-liouv Liouville-space formalism (basis with state-space
restriction, superoperators, states, Hamiltonian and kinetics
superoperators, analytical filters, propagation with zero track
elimination, path tracing and destination state screening) runs
pulse-acquire and chemical exchange end to end; its setup is verified
against Spinach 2.13 fixtures (tests/slice/test_liouv_setup_parity.py)
and its propagation against Spinach 2.13 fixtures as well
(tests/slice/test_liouv_pa_parity.py), Hilbert space, and analytic
exchange line shapes. Anisotropic interactions (shielding and coupling
tensors, dipolar couplings from coordinates, quadrupolar couplings) with
the crystal and powder contexts are ported and verified against
Spinach 2.13 fixtures as well (tests/slice/test_aniso_parity.py).
Relaxation (Redfield theory, T1/T2 and damping models, thermalisation)
is ported and verified against Spinach 2.13 fixtures
(tests/slice/test_relax_parity.py) and the Solomon equations; scalar
relaxation of the first and second kind (SRFK, SRSK) is ported and
verified against Spinach 2.13 fixtures (tests/slice/test_scalar_parity.py),
Abragam's expressions and the explicit integral.
Magic angle spinning in Fokker-Planck form (singlerot) is ported and
verified against Spinach 2.13 fixtures (tests/slice/test_mas_parity.py),
rotor echoes and the fast-spinning limit.
The pulse sequence library (COSY, NOESY, HSQC with isotopomer dilution,
analytical decoupling, apodisation and 2D plotting) is ported and
verified against Spinach 2.13 fixtures (tests/slice/test_seq_parity.py).
import numpy as np
import spynach
spin_system = spynach.create(
{"isotopes": ["1H", "1H"], "magnet": 14.1},
{"zeeman": {"scalar": [1.00, 1.10]}, "coupling": {"scalar": [[0, 12.0], [0, 0]]}},
)
spin_system = spynach.basis(spin_system, {"formalism": "zeeman-hilb", "approximation": "none"})
pars = {
"spins": ["1H"],
"offset": 630,
"sweep": 200,
"npoints": 1024,
"rho0": spynach.state(spin_system, "L+", "1H"),
"coil": spynach.state(spin_system, "L+", "1H"),
}
fid = spynach.liquid(spin_system, spynach.acquire, pars, "nmr")
fid = spynach.apodisation(fid, [("exp", 6)]) # halve the first point, exponential window
spectrum = np.fft.fftshift(np.fft.fft(fid, 4096))
| Spinach | Spynach | ported |
|---|---|---|
create.m, dipolar.m |
spynach.create |
isotopes, magnet, tolerances, Zeeman and coupling tensors as scalars, matrices, or eigenvalues with Euler angles, coordinates with point dipolar couplings and the proximity graph, chemical subsystems (inter.chem parts, concentrations, exchange rates) |
basis.m |
spynach.basis |
zeeman-hilb; sphten-liouv with none, IK-0, IK-1, IK-2, the projections/longitudinal/zero_quantum/manual filters, and chemical subsystems (no symmetry, no coordinates) |
operator.m, state.m |
spynach.operator, spynach.state |
Hilbert space and sphten-liouv superoperators (left, right, comm, acomm); states by the exact, cheap and chem methods; E, Lx, Ly, Lz, L+, L-, Tl,m |
assume.m |
spynach.assume |
nmr, esr, deer, deer-zz, labframe, with zeeman/couplings retention |
hamiltonian.m, orientation.m, frqoffset.m |
spynach.hamiltonian, spynach.orientation, spynach.frqoffset |
isotropic part and the rank-1/rank-2 irreducible components Q (CSA, anisotropic couplings, quadrupolar); in sphten-liouv as left/right/comm/acomm superoperators |
euler2dcm.m, mat2sphten.m, wigner.m, eeqq2nqi.m |
spynach.conventions |
the same conventions |
kinetics.m, equilibrate.m |
spynach.kinetics, spynach.equilibrate |
first-order exchange between chemical subsystems; equilibrium concentrations |
trajan.m, stateinfo.m |
spynach.trajan, spynach.stateinfo, spynach.plotting.plot_trajan |
correlation-order, coherence-order and per-spin amplitudes along a trajectory; the largest coefficients of a state (no level_populations) |
correlation.m, coherence.m |
spynach.correlation, spynach.coherence |
sphten-liouv |
propagator.m, step.m, krylov.m |
spynach.propagator, spynach.step, spynach.krylov |
dense Pade below small_matrix, scaled-squared Taylor above; Taylor action on vectors with 1-, 2-, 3-point Lie quadratures |
evolution.m |
spynach.evolution |
zeeman-hilb: observable; sphten-liouv: observable, final, trajectory, multichannel, refocus, destination state screening, Krylov path |
reduce.m, zte.m, path_trace.m |
spynach.reduce, spynach.zte, spynach.path_trace |
zero track elimination, path tracing with subspace merging; no symmetry |
relaxation.m, equilibrium.m, thermalize.m |
spynach.relaxation, spynach.equilibrium, spynach.thermalize |
Redfield (isotropic, axial, rhombic diffusion; diagonalisation-free integral), t1_t2, damp, SRFK (rlx_scalar.m), SRSK; rlx_keep, rlx_dfs; zero/IME/dibari thermalisation |
liquid.m, crystal.m, powder.m, singlerot.m, fourdif.m, acquire.m |
spynach.liquid, spynach.crystal, spynach.powder, spynach.singlerot, spynach.contexts.fourdif, spynach.acquire |
both formalisms (MAS in Liouville space); relaxation and exchange kinetics; powder averages over Spinach's grids (shipped as spynach/grids); MAS in Fokker-Planck form with the Fourier spectral derivative; no decoupling |
ft_axis.m, axis_1d.m, plot_1d.m, plot_2d.m, contspacing.m, apodisation.m |
spynach.plotting, spynach.apodisation |
all axis units; interactive Plotly 1D and 2D contour figures; window functions except bad-z*; no derivative |
etc/molecules/strychnine.m |
spynach.molecules.strychnine |
the 22-proton system (shifts, couplings, coordinates); 13C/15N not ported |
cosy.m, noesy.m, hsqc.m, decouple.m, homospoil.m, dilute.m, kill_spin.m |
spynach.cosy, spynach.noesy, spynach.hsqc, spynach.decouple, spynach.homospoil, spynach.dilute, spynach.kill_spin |
COSY, States-TPPI NOESY, echo-antiecho HSQC; analytical decoupling; isotopomer sums |
Anything not ported raises NotImplementedError; inputs are never silently
ignored.
Differences from Spinach
- Spin indices are 0-based (
operator(spin_system, ["Lz", "L+"], [0, 1])is Spinach'soperator(spin_system, {'Lz','L+'}, {1,2})). Isotope names and'all'/'nuclei'/'electrons'work as in Spinach. - Product vs. sum operators are distinguished by the type of the first
argument: a string (
"Lz") gives a sum over spins, a list (["Lz", "L+"]) a product operator. - Spin system objects are frozen dataclasses;
basis()andassume()return modified copies, as Spinach returns a modified struct. - Time series are 1-D arrays (MATLAB returns
n x 1columns). evolution()runs serially; Spinach's Kuprov-Edwards parallel split computes the same observable.
Beyond the port
Add-ons originating here, with no Spinach counterpart, each with an example in the guide Beyond the port:
ctps, a separate module (import ctps, no dependency onspynach) that ports CCCP.m and CCCP++, the coherence transfer pathway programs: every pathway a sequence allows, weighted by phase cycle, gradients, diffusion and pulses, with an interactive pathway explorer (docs/ctps.md,src/ctps/README.mdfor how to remove it without touching Spynach).- Non-ideal pulses (perfect 180°, imperfect 180°, small flip angle) after
J. Chem. Phys. 113, 979 (2000), and a cogwheel cycle search by exact
integer arithmetic (
find_cycle,coverage,hcl_cycle) plus the DOTCOPS crusher objective (crusher_scheme), inctps. - Pulse programs, gradients, phase cycling (beta) (
spynach.program): gradients without a spatial grid. A sequence written as blocks with its phase lists and gradients runs as one branch of the density matrix per coherence pathway that survives the phase cycle (pathways and weights fromctps), each branch carrying its gradient k-vector and diffusion integral, so gradient selection, crushers, read-out gradients and PGSE diffusion are exact per-pathway factors with no voxels and no Nyquist limit; per-pathway FIDs and echo times, and the explicit cycle as the reference. The phase-cycle and gradient selection are new and in beta: the API may still change (docs/pulse-programs.md; browser examples ex-dqf-cosy, ex-phase-cycle, ex-gcosy, ex-pathway-echoes, ex-pgse, and a 23Na triple-quantum filter by phase cycling and by gradients, checked against each other: ex-na23-tqf-cycle, ex-na23-tqf-gradients). - Relaxation pathways by mechanism (
spynach.relaxation_pathways): the Redfield relaxation matrix between product operators split into dipolar, CSA, quadrupolar and anisotropic-J contributions and all their cross-correlations, each with an exact expression in the coupling strengths and spectral densities, drawn as a chord diagram byplot_pathways(docs/relaxation-pathways.md), also in the browser playground (try.html#ex-pathways). - Radiation damping (
spynach.radiation_damping,rd_rate,rd_phase): the feedback of the probe on any spin system as a state-dependent generator, with isochromat ensembles for inhomogeneous lines, propagated byiserstep(a port of Spinach's Lie-group and Runge-Kutta-Munthe-Kaas integrators) and checked against the Bloembergen-Pound closed form and the classical Bloch-Maxwell limit (docs/radiation-damping.md), also in the browser playground (try.html#ex-rd). - Interactive Plotly figures from
spynach.plotting,plot_fid,plot_time_seriesandplot_trajanwith no Spinach equivalent, andfigure_pngfor static copies. - Credits: the Spinach authors of every translated function, by name, in its docstring
(
help(spynach.propagator)) and indocs/spinach-credits.md; for EasySpyn, the EasySpin contributors to each original file (docs/easyspin-credits.md). - Verification tooling:
spynach.oracle.comparefor checking any result against a Spinach run, the generated parity pagedocs/parity.md, and the timed, profiled comparison protocol incomparison/. - Executable documentation: guides whose code runs in the tests, notebooks
generated with outputs, a Colab quick start, and contributed notebooks in
user_py/. - Browser-based execution: try.html
runs Spynach,
easyspynandctpsin the browser through Pyodide, with nothing installed, and plots spectra, FIDs and 2D contours from an editor preloaded with NMR, EPR and pathway examples (contributed by Moritz Zaiss). Its Parameters view (a "Prefer GUI" / "Prefer code" toggle next to Run) shows the main settings as a form with units and descriptions, kept in step with the code. easyspyn, a separate package (import easyspyn, depends onspynach, never the reverse) that ports EasySpin to Python with EasySpin's interface: the foundation (spin systems,sop,stev,hamwith the field split,levels, rotations, units, nuclear data),resfieldsand the line shapes, the cw simulatorspepper(powders and crystals) andgarlic(solutions), least-squares fitting withesfit, ENDOR withsalt, magnetometry withcurry, slow-motion spectra withchiliand pulse EPR (ESEEM, HYSCORE, Mims ENDOR) withsaffron, verified against EasySpin 6.0.15 (docs/easyspyn.md, docs/coming-from-easyspin.md,src/easyspyn/README.mdfor how to remove it without touching Spynach).
Accuracy notes
Every parity comparison is relative with zero absolute tolerance, at 1e-14
for operators and states, 1e-12 for Hamiltonians and 1e-9 for anything
through a propagator or an FID; docs/parity.md, regenerated from a run
of the suite by oracle/parity_summary.py, lists every fixture case,
tolerance and measured difference. To check a workflow of your own against
a Spinach run, see the section
Checking your own workflow against Spinach.
- Spin operators match Spinach to 1 ulp: MATLAB evaluates
2^(-1/2)one ulp below the correctly rounded value, so Spinach'sLzis0.4999999999999999. - Spinach rounds Hamiltonian elements to a 1e-10 rad/s grid (
clean_up) after summing terms in random order, so Spinach itself reproduces only to about one grid step per element; FIDs therefore agree to ~1e-10 x acquisition time. - The Taylor propagator's error scales with
tols.prop_chopand the number of squarings: about 7e-8 relative at the default 1e-10 with 7 squarings.
Development
From a clone of the repository, an editable install with the test and notebook tools runs the
test suite and the guides' notebooks (jupyter lab docs/notebooks/):
python -m pip install -e ".[test,docs]"
python -m pytest -q
Without the install, PYTHONPATH=src python -m pytest -q runs the tests from the source tree.
Parity tests skip when fixtures are absent ($SPYNACH_FIXTURES, default
/fixtures). A caveat for containers: when the CPU quota is below the
visible core count (the development container sees 12 cores under a 4-CPU
quota), OpenBLAS oversubscribes the quota and dense LAPACK calls run ~1000x
slower (a 256x256 eigh takes 10 s instead of 10 ms). Set
OPENBLAS_NUM_THREADS to the quota there, for tests and especially for
benchmarks; on an ordinary machine nothing needs to be set.
To test on a macOS host outside the container, install
uv (brew install uv) and run
scripts/mac-test.sh (extra arguments go to pytest). The container's .venv
does not work on the host, so the script keeps a separate .venv-mac and
syncs it with pyproject.toml before every run. Source edits need no
reinstall. Point VS Code's interpreter at .venv-mac/bin/python.
scripts/mac-compare.sh runs the Spynach side of a comparison case in
the same environment (with the compare extra), so that both sides of a
report are timed on the same machine.
Releases are tagged vX.Y.Z and listed in CHANGELOG.md;
spynach.__version__ gives the installed version, and
pip install spynach==0.7.0 installs a tagged one from PyPI (before 0.5.0:
pip install "spynach @ git+https://github.com/ajerschow/spynach.git@v0.3.0"). To release: set the version in pyproject.toml,
src/spynach/__init__.py and CITATION.cff (a test keeps them equal), add
the CHANGELOG.md entry, refresh uv.lock, run the checks above plus
python docs/build_notebooks.py --check and
python oracle/parity_summary.py --check, commit, tag with
git tag -a vX.Y.Z -m "Spynach X.Y.Z", push with --follow-tags, and draft
the GitHub release from the tag with the changelog entry as its notes.
Publishing that release runs .github/workflows/publish-spynach.yml, which tests and builds
the commit and, once the reviewer of the pypi environment approves, uploads it to PyPI as
spynach (Trusted Publishing, no token; the tag must equal the version in pyproject.toml).
The distribution holds spynach and ctps; the epr extra pins the matching easyspyn, so
release that first (easyspyn-vX.Y.Z, see src/easyspyn/README.md). A version can be
uploaded only once.
Code style is enforced by ruff (pip install -e ".[dev]"), configured in pyproject.toml: 120-character lines, import
sorting, pyupgrade and bugbear checks. Before committing, run
ruff format && ruff check
Spinach's own matrix names (I, Q, L, A, B, ...) are kept where a
function mirrors a Spinach function, so the ambiguous-name check is off.
Every user-facing change comes with user documentation in the same
commit; see docs/README.md for the conventions. After
editing a guide, regenerate its notebook with
python docs/build_notebooks.py; tests/docs/ fails if it is stale.
The project site is hand-written HTML at the repository root, served by
GitHub Pages from the root of main (Settings > Pages > Deploy from a
branch > main, / (root); .nojekyll keeps Pages from preprocessing the
tree): index.html (landing page), spinach.html, easyspin.html,
extras.html, docs.html, and try.html, the playground that runs
Spynach, easyspyn and ctps in the browser (worker.js loads Pyodide in a
web worker and fetches the sources). They share assets/site.css. The parts
that are data rather than prose sit between <!-- site:... --> markers and
are written by scripts/build_site.py: the navigation, the guide tables of
docs.html (from the table in docs/README.md, filed by package in the
script's groups), the timing tables of spinach.html (from
comparison/runs/*/summary.json, which comparison/report.py writes beside
each report) and easyspin.html (from comparison/easyspin/timings.json,
written by scripts/easyspin-timings.py from the EasySpin fixtures), and
the SOURCE list of worker.js; tests/unit/test_site.py fails when a
span is stale. The parity paragraph of spinach.html comes from
oracle/parity_summary.py. Reports are linked relatively, so they are
served by Pages; guides and notebooks link to GitHub and nbviewer. The
playground's examples are written into try.html and run natively by
tests/unit/test_playground_examples.py and through the worker's Python
runner by tests/unit/test_playground_runner.py; scripts/pyodide-examples.mjs
runs them in Pyodide itself under Node (npm install pyodide in a scratch
directory), which is the only way to catch what differs in 32-bit WebAssembly:
NumPy's index type is int32 there, so integer arrays used as repeat counts or
bincount indices must be np.intp, never np.int64. The Parameters view
reads # GUI: label | unit | description | range comments on the assignments at
the top of an example (assets/playground-params.js, documented in its header);
tests/unit/test_playground_params.py checks that each parameter is documented
and that the example runs with it at both ends of its range. The hero figure is
assets/Spynoza.jpg; assets/liouvillian-strychnine*.png are sparsity
patterns of the strychnine NOESY Liouvillian rendered by
scripts/landing-figure.py (needs Pillow and Matplotlib, which the
compare extra brings in), kept as alternatives.
src/spynach/_isotope_data.py is generated from Spinach's spin.m by
porting/tools/gen_isotopes.py; do not edit it by hand.
src/spynach/grids/*.npz are generated from Spinach's kernel/grids by
porting/tools/gen_grids.py. examples/ holds full-size translations of
Spinach example scripts for side-by-side comparison with MATLAB (see
docs/README.md); comparison/ holds the
protocol for timed, profiled Spinach-vs-spynach runs with an HTML report
(comparison/README.md). porting/tools/bench_strychnine.py times the
kernel on the 22-proton strychnine system (12 316 states at IK-2,
proximity level 3): basis 0.04 s, Hamiltonian 0.1 s, Redfield
relaxation 2.4 s, pulse-acquire 2.1 s, COSY 32 x 64 3.3 s in this
container.
Contributors
- Florin Teleanu: Spinach scripts in
user_ml/(two-site 7Li exchange at equilibrium, zero- to ultralow-field PF6⁻ spectrum), translated inexamples/user_ml/and used as comparison cases, and the project page's hero figure (assets/Spynoza.jpg); and the relaxation pathways module (spynach.relaxation_pathways,docs/relaxation-pathways.md, thetry.html#ex-pathwaysexample; pull request #2). - Moritz Zaiss (MRsources): the in-browser playground
(
try.html,worker.js, Spynach in Pyodide) and the orientationprogresscallback ofpowderandsinglerot(pull request #1). - The port was a collaboration between Claude (Anthropic) and
Alexej Jerschow, who also contributed
several new scripts and functions (for example the port of the
coherence pathway calculations,
ctps).
New contributors are added here and in the "With thanks" section of
index.html when their first contribution is merged; the CHANGELOG names
them in the release entry.
Licence and attribution
Spynach is released under the MIT licence (LICENSE). It is derived from
Spinach by Ilya Kuprov and co-workers, also MIT
licensed: the kernel, molecule data, isotope table, spherical grids and
example scripts were translated from it, and Spinach's copyright and
permission notice is reproduced in LICENSE. Keep that notice with any
copy or substantial portion of this software.
The translation from MATLAB to Python was carried out largely by an AI
coding agent (Claude, Anthropic) under the direction of
Alexej Jerschow,
who reviewed the results; commits made by the agent are marked as such in
the git history. Correctness rests on the test suite's comparison with
Spinach reference data, not on the method of authorship (see LICENSE).
Results obtained with Spynach should cite Spinach: H. J. Hogben, M. Krzystyniak, G. T. P. Charnock, P. J. Hore, I. Kuprov, Spinach: a software library for simulation of spin dynamics in large spin systems, J. Magn. Reson. 208, 179 (2011), together with the method papers that the Spinach documentation lists for the features used (restricted state spaces, relaxation theories, Fokker-Planck contexts).
Results obtained with EasySpyn should credit the EasySpin development effort: cite the papers on the EasySpin publication list, at least S. Stoll, A. Schweiger, EasySpin, a comprehensive software package for spectral simulation and analysis in EPR, J. Magn. Reson. 178, 42 (2006), and for pulse EPR S. Stoll, R. D. Britt, Phys. Chem. Chem. Phys. 11, 6614 (2009).
The Spynach and EasySpyn ports should be attributed to this repository,
https://github.com/ajerschow/spynach (CITATION.cff gives the citation).
The ctps package is a Python port of earlier MATLAB (CCCP.m) and C++
(CCCP++) implementations of coherence pathway selection and is included in
the spynach distribution. Results obtained with it should cite
A. Jerschow, N. Müller, J. Magn. Reson. 134, 17 (1998); A. Jerschow,
R. Kumar, J. Magn. Reson. 160, 59 (2003); for non-ideal pulses
A. Jerschow, J. Chem. Phys. 113, 979 (2000); and this repository.
Scripts contributed by users in user_ml/ (Spinach) and user_py/
(Python notebooks and scripts) remain the copyright of their authors
and are redistributed, with their agreement, under the same MIT licence
(see the README in each directory).
Metadata
Release files for spynach 0.7.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| spynach-0.7.0.tar.gz | 5.2 MB | With provenance | Details for spynach-0.7.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| spynach-0.7.0-py3-none-any.whl | Python 3 | none | any | Details for spynach-0.7.0-py3-none-any.whl |
Total release size: 10.5 MB
Release files / spynach-0.7.0.tar.gz
| Download URL | spynach-0.7.0.tar.gz |
|---|---|
| Size | 5.2 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
4ac18fcda586d2b8e063dfe14acd9e039f6bf650285b877a80be42d436acf22e
|
|
BLAKE2b-256 checksum How to use checksums |
218bb86f1bf12ab8316263b0edbf5fbb11ec9755784d5d10335d044c6ea01b50
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 12, 2026.
Transparency logRelease files / spynach-0.7.0-py3-none-any.whl
| Download URL | spynach-0.7.0-py3-none-any.whl |
|---|---|
| Size | 5.2 MB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
066012271059bd18d20f901a14dbe0e1b39cb40506c2b6dde9996ef9c7e757b9
|
|
BLAKE2b-256 checksum How to use checksums |
dee0f0798d3d4702bf8799dcd7538de0f907bf583fb0ddce58581a60ca6c2a53
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 12, 2026.
Transparency logSecurity for spynach 0.7.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
Both files in this release have been uploaded with provenance metadata.
Attestation for spynach-0.7.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 12, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.