firedrill
You don't have backups. You have hopes.
firedrill takes a Postgres backup and actually restores it into a disposable, version-matched container, then reports whether it worked and how long each stage took. It is built to fail your build the day a backup stops being restorable — not the day you need it.
A verification that could not run never reports as passing.
https://maximo000.github.io/firedrill/ — what it checks, and the measurements behind each check.
That rule is the whole design. No Docker, an unreadable archive, a container that never came up — every one of those produces COULD NOT VERIFY and a non-zero exit, never a green tick. A backup tool that says "OK" because it silently skipped the restore is worse than no tool, because it manufactures confidence.
Status
Fetch (local, presigned URL, or S3, checksum-verified when you supply a digest — and reported as unverified when you do not), restore into an ephemeral version-matched container, then the ladder: structure, volume, semantics, integrity. Three tiers, JUnit and JSON output, a history file that turns RTO into a measured trend, a GitHub Action, and point-in-time recovery.
What works today:
$ firedrill run backups/production-2026-08-23.dump
firedrill backups/production-2026-08-23.dump
archive custom v1.15.0 13.3KB from 'postgres'
source PostgreSQL 16.15 (Debian 16.15-1.pgdg13+2) -> restored into postgres:16
[ok ] inspect 0.00s 16.15 (Debian 16.15-1.pgdg13+2) (custom)
[ok ] target 0.83s postgres:16
[ok ] restore 0.09s exit 0
[ok ] smoke 0.15s 1 user table(s)
total 1.42s
PASS -- restored and answered queries.
And when it isn't fine:
[ok ] inspect 0.00s 16.15 (Debian 16.15-1.pgdg13+2) (custom)
[ok ] target 0.83s postgres:16
[FAIL] restore 0.09s exit 1
[ok ] smoke 0.16s 1 user table(s)
1 finding(s):
CRITICAL ARCHIVE_TRUNCATED could not read from input file: end of file
The archive is incomplete. The backup job most likely ran out of
disk or was killed. Check the writer's exit status and free space
at write time -- pg_dump can exit 0 having written a short file.
FAIL -- the restore ran and produced findings above.
Note the schema restored and the table exists. Only the rows are missing. That is what a truncated backup looks like from the outside.
Install
pip install firedrill
Python 3.10+, and a working Docker. No Postgres client is required on the host — the archive header is parsed in pure Python and every database operation happens inside the target container, which is also what makes the version matching real.
Use
firedrill run path/to/dump.dump # restore and report
firedrill run s3://backups/pg/daily/ # the newest object under a prefix
firedrill run --live DATABASE_URL --keep backups/ # dump a running database now, prove it restores
firedrill run dump.dump --image 'pgvector/pgvector:pg{major}' # extensions the stock image lacks
firedrill physical pgbackrest --repo DIR --target "..." # pgBackRest / WAL-G, with PITR -- see below
firedrill run dump.dump --json report.json # machine-readable
firedrill run dump.dump --rto 45m # exceeding the budget is a finding
firedrill run dump.dump --tier fast # schema only, for every commit
firedrill clean # remove containers left by a crash
firedrill doctor # check Docker, disk and config before trusting a run
Exit code is 0 only when the restore genuinely ran and produced no finding at
or above --fail-on (default high).
Where the backup lives
A path works, and so does the place the backup actually sits:
version: 1
source:
type: s3
bucket: acme-backups
prefix: postgres/daily/
select: newest # or an explicit `key:`
sha256: "007168050a7570c6a9c93230992de425f81316bf688ceb277546e45265aae9d5"
type: local (a path), type: https (a presigned URL — S3, GCS and Azure all
issue them), and type: s3 (pip install firedrill[s3]). Give a sha256: or
a size: and the artefact is checked against it before anything tries to
restore it: a dump that arrives truncated but plausible never reaches a
container, and the run reports rather than passes. Quote the digest — YAML
reads a bare all-digit value as a number.
Or skip the config: firedrill run s3://bucket/key, and a trailing slash
(s3://bucket/prefix/) takes the newest object under it.
No backup yet? Take one and prove it in the same command
$ DATABASE_URL=postgresql://app@db.internal/shop firedrill run --live DATABASE_URL --keep backups/
[ok ] fetch 26.59s 4,601,447 bytes from live fresh pg_dump
[ok ] target 8.48s postgres:18
[ok ] restore 5.94s exit 0
[ok ] smoke 0.55s 70 user table(s)
(Real output, against pagila on
PostgreSQL 18.) --live asks the server its version, runs pg_dump -Fc
from a container of that same major -- nothing but Docker on the host --
and drills the result; --keep DIR leaves the verified dump there as
<db>-<UTC timestamp>.dump. The argument is the name of the variable
holding the URL, never the URL, for the same reason there is no
--password; the password is registered for redaction and passed to the
container by name. A localhost database is reached as the Docker host.
pg_dump only reads, so this is read-only too. In firedrill.yml:
source: {type: live, url_env: DATABASE_URL, keep: backups/}.
Three properties worth stating plainly:
- Read-only by construction.
sources.pycontains no verb that writes, deletes, copies or tags anything at the origin. "Never writes to the source" is a property of the file, not a promise in a README. - Credentials from the environment only. There is no
--access-keyand no place for one infiredrill.yml, because/proc/*/cmdlineis world-readable and CI logs echo command lines. boto3's default chain already does the right thing. - A presigned URL's signature is a credential, so it is stripped from
every report, log line and finding, and plain
httpto a non-local host is refused outright rather than putting a working one on the wire.
Which dump formats
| format | flag | restored by | supported |
|---|---|---|---|
| custom | -Fc |
pg_restore |
yes |
| directory | -Fd |
pg_restore |
yes — the one that restores in parallel, so the one large databases use |
| tar | -Ft |
pg_restore |
yes |
| plain SQL | -Fp, or pg_dump db > backup.sql |
psql |
yes, and .sql.gz too — full tier only |
--jobs N (or jobs: N in firedrill.yml) passes -j N to pg_restore, which is what makes the directory format's parallel restore actually parallel. Custom and tar archives accept it too, restoring serially where pg_restore cannot split the work. Default is 1.
Custom, directory and tar all carry a PGDMP header — directory and tar keep
theirs in a toc.dat member — so the major version is read out of the artefact
itself, with no PostgreSQL client on the host. That is what makes the
version-matching real.
Plain SQL has no binary header, but pg_dump starts every plain dump with
-- Dumped from database version 16.15 (...), and that's where the version
comes from (read straight out of a .gz too). It's a comment, so it can be
stripped; when it's gone, firedrill refuses with VERSION_UNKNOWN and asks
for --postgres MAJOR rather than guessing.
Plain dumps need one check the others don't, measured on PostgreSQL 16:
psql restores a plain dump that was cut off mid-table with exit 0 and no
error at all — it takes end-of-file as the end of the data, and the table
comes back with 9,710 of its 20,000 rows. Nothing in the restore reports it.
The only witness is the file: pg_dump always ends a plain dump with
-- PostgreSQL database dump complete, so a dump without that line is
reported ARCHIVE_TRUNCATED (critical) before anything is restored. psql
also exits 0 when a statement fails (a missing role, measured), so its
stderr is classified line by line with the same rules as pg_restore's,
and the exit code firedrill reports comes from those findings.
A plain dump's header is only a comment, and people edit these files.
pagila's published pagila-schema.sql says "Dumped from database version
12.11" and uses uuidv7(), which arrived in 18. Restored into 12 as the
header says, 140 statements fail -- and psql, reading from a pipe, prints
them without its usual psql:file:line: prefix. 0.3.0 matched only the
prefixed form and called that restore clean; 0.4.0 reads both and runs
psql with -f - so the prefix is there anyway. --postgres 18 then
restores all 70 tables, and the one remaining failure is real: pagila's
film_embedding needs pgvector, which --image 'pgvector/pgvector:pg{major}'
supplies.
A plain dump can only be restored whole, so the fast and sample tiers
refuse it (TIER_UNSUPPORTED) instead of quietly running a full restore.
MySQL and MariaDB
firedrill run backup.sql recognises a mysqldump or mariadb-dump file
(plain or .gz) by its own header and takes the same path Postgres does:
the server version comes from the dump, the restore runs in mysql:<version>
or mariadb:<version>, and then it runs smoke, your semantics: checks, and
CHECK TABLE on every restored table.
- Truncation. Both tools end a finished dump with
-- Dump completed on .... Themysqlclient restores a dump cut at a statement boundary with exit 0, so a missing trailer isARCHIVE_TRUNCATED(critical) before anything is restored. - Errors. The client stops at the first failing statement; its
ERROR nnnn (state) at line Nbecomes a finding naming the line, since everything after it was never restored. - MariaDB 10.5.25+/11.x dumps open with a
/*M!999999\- enable the sandbox mode */line before the header -- the reason they won't load into MySQL. firedrill reads past it and restores them into MariaDB. - The password reaches the client through
MYSQL_PWDinside the container, never argv.
Tested on datacharmer/test_db's employees database (300,024 employees, 2,844,047 salary rows), dumped by each engine's own tool:
| dump | result |
|---|---|
| MySQL 8.4, 168 MB | PASS: 8 tables, both exact counts, CHECK TABLE clean (restore 63 s) |
| MariaDB 11.4, 172 MB, with the sandbox-mode line | PASS (restore 25 s) |
| the MySQL dump gzipped, 36 MB | PASS |
| the MySQL dump cut off at 80 MB | FAIL: ARCHIVE_TRUNCATED, the client's ERROR 1064 at line 265, 7 of 8 tables, both counts wrong |
Which PostgreSQL versions
Tested end to end against 13, 14, 15, 16, 17 and 18 — dump on any of them, restored into a container of that same major, read out of the archive header. Archive formats 1.14 (13–15), 1.15 (16) and 1.16 (17–18) each have a committed header fixture, so the parser stays covered even where containers cannot run.
One honest limit: datcollversion arrived in PostgreSQL 15, so on 13 and 14 no
collation version exists to compare. firedrill reports COLLATION_UNVERIFIABLE
there rather than pretending — nothing, not this tool and not PostgreSQL, can
tell you whether text indexes on those servers sort like production's.
A structure reference is portable across all six: taken on 14, it is clean against a restore on 18. Upgrading Postgres must not turn every check red, because that is how a team learns to ignore the tool.
Tiers: how much to restore
A 2 TB restore cannot run on every commit.
| tier | restores | what does NOT run |
|---|---|---|
full |
everything | — |
fast |
schema only | row counts, smoke queries, sequence checks |
sample |
schema + rows for named tables | smoke queries, sequence checks |
version: 1
tier: sample
sample:
tables: [orders, customer]
The report always says which tier ran, in capitals when it is not full, and
a partial pass gets its own sentence — PASS (fast tier) — the schema restored. Whether the DATA is there was not checked. A pass from a
schema-only run must never look like a pass from a full one.
The rungs a tier cannot honour report NOT RUN, never a tick, and the
config refuses combinations that would produce a misleading finding rather
than running them: tier: sample with a volume rule on an unsampled table,
or with semantics at all — a smoke query is arbitrary SQL, so there is no
knowing whether it reads a table whose rows came back.
The ladder, and firedrill.yml
A bare firedrill run proves the backup restores and answers queries. To prove
it restored the right data, put a firedrill.yml next to it — it is picked up
automatically, or named with --config.
version: 1
rto_budget: 45m
structure:
reference: schema/production.txt # committed, reviewable, diffable
volume:
tables:
orders: {min_rows: 1}
semantics:
- name: recent orders exist
sql: SELECT count(*) FROM orders WHERE created_at > now() - interval '7 days'
expect: "> 0"
ignore:
- check: COLLATION_UNVERIFIABLE
reason: "restoring on alpine in CI; tracked in DR-114"
Generate the structure reference once and commit it:
firedrill run dump.dump --write-reference schema/production.txt
It is one line per catalog object, sorted, so a schema change shows up as a
readable diff in review rather than a wall of pg_dump output.
Three things the loader does that are worth knowing, because each one is a failure it refuses to let pass quietly:
- An unknown key is an error, not a warning. A typo'd
tolerence:that loaded silently would mean a check you believe is running is not running, and the run would still go green. - Every
ignoreneeds a written reason. An unexplained suppression is a config error. It is the only process this tool imposes, and it is what keeps a green run meaningful. expectmust be a comparison against a number. There is deliberately no way to write a check whose result is printed, so a smoke query returns a shape and never a row.
Rungs that nothing configured report n/a — not configured — rather than a
tick. "Nothing asked for this" and "this passed" are different facts.
What it checks today
| Rule | Meaning |
|---|---|
ARCHIVE_UNREADABLE |
the header will not parse — truncated, empty, or not a custom-format dump |
ARCHIVE_TRUNCATED |
the restore hit end-of-file partway through |
ROLE_ABSENT |
OWNER TO names a role that does not exist on the target |
EXTENSION_ABSENT |
an extension's binaries are missing from the restore image |
RESTORE_ERROR / RESTORE_WARNING |
anything else pg_restore said |
RESTORE_FAILED |
non-zero exit with nothing classifiable — never treated as success |
EXIT_CODE_LIED |
exit 0 with errors on stderr |
EMPTY_RESTORE |
restored cleanly and contains no user tables |
TARGET_UNAVAILABLE |
the restore could not be attempted |
RTO_EXCEEDED |
slower than the stated budget |
FETCH_FAILED |
the artefact could not be obtained, or is not the bytes that were claimed |
SOURCE_AMBIGUOUS |
a path and a configured source — which backup did you mean? |
VOLUME_DRIFT |
a table lost more rows than the tolerance allows, against the last known-good run |
SEQUENCE_UNCHECKED |
the database has sequences and none could be tied to a column, so none were checked |
ARTEFACT_UNVERIFIED |
a remote artefact was fetched with neither a digest nor a size to check it against |
PITR_TARGET_UNREACHED |
the WAL archive ends before the moment you asked to recover to |
PITR_UNASSERTED |
recovery reached the target and nothing checked what the database then held |
VERSION_MISMATCH |
--postgres pinned a major the archive did not come from |
STRUCTURE_MISSING |
an object in the committed reference did not come back |
STRUCTURE_UNEXPECTED |
the database has drifted from the reference |
VOLUME_BELOW_MINIMUM |
a table restored with fewer rows than the config requires |
VOLUME_TABLE_MISSING |
the config expects a table the restore does not have |
SEMANTICS_FAILED |
a smoke query restored cleanly and answered the wrong thing |
SEQUENCE_BEHIND |
a sequence is below max(id); the first insert will collide |
MATVIEW_UNPOPULATED |
a materialized view came back holding nothing; every query against it raises |
COLLATION_MISMATCH |
the target's libc differs from the reference's — text indexes sort differently |
COLLATION_UNVERIFIABLE |
the target reports no collation version, so sort order cannot be checked at all |
Each of these is proved against a deliberately broken backup that pg_restore
itself is perfectly happy with — that is the point of the whole ladder — and
each is also asserted not to fire on a healthy one. A false positive costs
exactly what a false negative costs: a DR tool that cries wolf gets muted, and
a muted DR tool is worse than none because it still looks like coverage.
Why the exit code is not enough
A missing role or extension is often described as a pg_restore warning
with a zero exit code. Measured on PostgreSQL 16 and 18 with custom-format
archives, that is not what happens: the exit code was 1 in every broken case,
accompanied by warning: errors ignored on restore: N.
The exit code is still not sufficient:
it is one bit, so it says something broke but never what, and "role absent"
and "archive truncated" need different findings and different fixes. firedrill
therefore uses both signals and reports which one fired — with EXIT_CODE_LIED
kept as a guard, because the tool must not depend on that measurement staying
true in a future release.
In CI, in ten lines
- uses: MaXiMo000/firedrill@v0
with:
config: firedrill.yml
rto: 45m
history: firedrill-history.json
The action fails the build when the drill fails and when it could not run
at all — "we did not verify" must never be quieter than "we verified and it
was fine". It publishes the report before failing, so a red build is an
actionable one, and exposes ok, verified, findings, seconds and a
summary like restored in 4m12s, 0 findings.
Copy-paste templates live in examples/: a nightly full drill
with read-only AWS credentials via OIDC, and a PR comment that runs the fast
tier and edits its own comment instead of posting a new one each push.
--junit report.xml writes JUnit XML so CI shows each rung separately. A rung
that could not run is <skipped>, never a silent pass, because most
dashboards colour those differently — which is exactly the distinction worth
preserving.
Trends: RTO you have measured, not claimed
firedrill run dump.dump --history firedrill-history.json
Each run appends its durations, row counts and versions, and is measured
against the last known-good run of the same tier. The report then says
19% slower than the last good full run (2.3s on 2026-08-24T…).
That history is also what makes volume.tolerance meaningful — a tolerance
needs something to be tolerant of:
version: 1
volume:
tolerance: 10%
tables:
audit_log: {tolerance: 50%} # append-only, grows fast
Only a drop past the tolerance is a finding. Tables grow; a rule that fired on growth would go off every week until somebody muted it, taking the real findings with it.
The history file holds counts, durations and versions — aggregates and catalog facts. It has no field that could hold a row, which is pinned by a test, because it is the artefact of this tool most likely to be committed to a repo by accident.
pgBackRest and WAL-G
Most production Postgres isn't backed up with pg_dump at all. It's
backed up with pgBackRest or
WAL-G: a base backup plus a WAL archive.
firedrill restores those with the tool itself, replays the WAL, and puts
the recovered server through the same checks:
$ firedrill physical pgbackrest --repo /backups/pgbackrest --target "2026-09-26 20:28:59+00" --config boundary.yml
backup pgBackRest 20260926-202837F stanza demo finished 2026-09-26T20:28:56+00:00
source PostgreSQL 16 -> recovered to 2026-09-26 20:28:59+00 on postgres:16
[ok ] recover 23.81s 20260926-202837F -> 2026-09-26 20:28:59+00
[ok ] smoke 0.84s shop: 2 table(s)
[ok ] semantics 0.72s 2 check(s) in shop
[ok ] integrity 2.39s 1 sequence(s)
PASS -- recovered to the target, and the boundary is where it should be.
firedrill physical pgbackrest --repo DIR # newest backup, all archived WAL
firedrill physical walg --repo DIR --target "2026-09-26 20:30:23+00"
firedrill physical pgbackrest --stanza main --postgres 16 # a remote (S3/GCS/Azure) repository
firedrill physical walg --repo DIR --max-age 26h # and the newest backup must be recent
- The major version comes from the repository (
backup.info, the WAL-G stop sentinel), and the restore runs inpostgres:<major>with the tool added: pgBackRest from the image's own PGDG repository, WAL-G's release binary pinned by sha256.--imagetakes a{major}template for extension images. - A remote repository is configured the way the tools are always
configured --
PGBACKREST_REPO1_TYPE=s3,WALG_S3_PREFIX,AWS_*, and so on. Those variables are passed to the container by name, never value. - It never archives into the repository it checks. A restored data
directory carries production's own
archive_command; started as it is, the drill would push WAL into production's archive. The server is started with-c archive_mode=off(which outranks every file), and then asked: if it reports anything butoff, the drill stops withARCHIVING_ENABLED. Tested with a backup that carriesarchive_mode = 'on'fromALTER SYSTEM: the repository held the same WAL files before and after. A local repository is also mounted read-only.
Measured against repositories made by pgBackRest 2.59.1 and WAL-G v3.0.9
themselves (built by tests/make_physical.py, in CI):
| case | result |
|---|---|
| replay all archived WAL | PASS: all 3 tables back, including the 2 written after the backup |
recover to the timestamp between before_target and after_target |
PASS: exactly one of them |
| a target later than the archive | FAIL PITR_TARGET_UNREACHED |
| a target before every backup (retention already pruned what you'd need) | FAIL PITR_TARGET_BEFORE_BACKUP |
| 64 bytes zeroed in one pgBackRest file | FAIL RESTORE_FAILED: zlib threw error: [-3] data error |
newest backup older than --max-age |
FAIL BACKUP_STALE |
A restore that ran and broke says FAIL: the backup was tested, and it is broken. COULD NOT VERIFY stays reserved for a check that could not happen.
Alerts, and running it on a schedule
export BACKUP_ALERTS=https://hooks.slack.com/services/...
firedrill physical pgbackrest --repo /backups --max-age 26h --notify BACKUP_ALERTS
--notify ENV_VAR POSTs one JSON message when a drill fails or could not
run. It carries text (Slack, Mattermost) and content (Discord) with a
one-line verdict, plus ok, verified, worst and the finding rules for
anything else. --notify-on always reports passes too. The argument is the
variable's name: a webhook URL is a credential. A webhook that refuses the
message fails the run rather than being skipped quietly, and its URL never
appears in the error.
Without GitHub Actions, a systemd timer on any host with Docker does it:
# /etc/systemd/system/firedrill.service
[Service]
Type=oneshot
EnvironmentFile=/etc/firedrill.env # BACKUP_ALERTS=..., PGBACKREST_REPO1_* ...
ExecStart=/usr/local/bin/firedrill physical pgbackrest --stanza main --postgres 16 --max-age 26h --notify BACKUP_ALERTS
# /etc/systemd/system/firedrill.timer
[Timer]
OnCalendar=*-*-* 04:17:00
Persistent=true
[Install]
WantedBy=timers.target
Point-in-time recovery
A dump proves you can get a database back. PITR proves you can get it back to
a chosen moment — which is what you need after a DELETE without a WHERE
at 14:02. Almost nobody tests it, because testing it means performing it.
firedrill pitr \
--base /backups/base --wal /backups/wal \
--target '2026-08-25 14:01:00' --config firedrill.yml
The assertion has two halves, and it needs both:
version: 1
semantics:
- name: the row written before the target survived
sql: select count(*) from events where label = 'before'
expect: "== 1"
- name: the row written after the target did not
sql: select count(*) from events where label = 'after'
expect: "== 0"
Restoring everything satisfies the first. Restoring nothing satisfies the
second. Only together do they say recovery stopped where it was told. Configure
neither and firedrill reports PITR_UNASSERTED rather than calling it a pass —
a server that came up proves a server came up.
Two things measured on PostgreSQL 16 that shaped this:
- An unreachable target does not silently promote. Recovery exits with
FATAL: recovery ended before configured recovery target was reached, so "did it get there" needs no heuristic. recovery_target_timecounts as reached only when a commit with a later timestamp exists in the WAL. A target after the final commit is unreached, not "satisfied at end of WAL".
Prior art, honestly
| Thing | What it does | The gap |
|---|---|---|
| pgBackRest / WAL-G / Barman | take backups; --check validates archives |
validates the archive, not a restored database |
| Cloud snapshot restore | restores | manual, unscheduled, unverified, unmeasured |
pg_verifybackup |
checksums a pg_basebackup |
file integrity, not usability |
| Enterprise DR products | do this, well | expensive, closed, agent-based |
| Cron scripts | what most teams have | unversioned, unreported, silently rotted |
firedrill never takes backups. A tool that both takes and verifies its own backups is grading its own homework.
Safety
This tool restores databases, so the interlocks are the price of admission:
- It only ever uses a target it created itself. There is no code path that connects to a user-supplied DSN.
- The backup is bind-mounted read-only.
- No
--dsnand no--passwordflags. The container password is generated per run, passed to Docker by variable name so it never enters argv, and never written to disk. - No port is published; the target is unreachable from the host.
- Teardown runs in a
finally, andfiredrill cleanremoves anything a crash orphaned.
See SECURITY.md.
Development
python tests/make_corpus.py # build the broken-backup corpus
python tests/test_firedrill.py --require-integration
The corpus is generated from real containers, never hand-edited. Findings are asserted in both directions — a false positive fails the build exactly as hard as a false negative, because a DR tool that cries wolf gets muted, and a muted DR tool still looks like coverage.
--require-integration makes a skipped container test a build failure. Without
it (Windows, where Linux containers cannot run) those tests skip by name and
the count is printed, so "it passed" and "it did not run" never look alike.
python tests/field_test.py restores pagila, chinook and northwind — real
databases this project did not write. Worth running after changing any check:
the corpus is synthetic, so it can only find the cases its author thought of.
Cutting a release
# pyproject.toml is the single source of truth; release.yml fails if the
# tag and the declared version disagree.
python tests/test_firedrill.py --require-integration
git commit -am "Release vX.Y.Z" && git tag -a vX.Y.Z -m "firedrill vX.Y.Z"
git push origin main --follow-tags
git tag -f v0 vX.Y.Z && git push -f origin v0 # the moving major tag
The tag runs verify → github-release → image (GHCR, with provenance) →
pypi (Trusted Publishing over OIDC — there is no API token in this repo and
there must never be one). The pypi job waits for a reviewer, and a published
version number can never be reused, so let the gate do its job.
Licence
MIT.
Metadata
Release files for firedrill 0.5.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| firedrill-0.5.1.tar.gz | 134.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| firedrill-0.5.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 229.9 kB
Release files / firedrill-0.5.1.tar.gz
| Download URL | firedrill-0.5.1.tar.gz |
|---|---|
| Size | 134.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
8a7ce60e039a41567ad9fe600f5fefb469398c146be32f88c8f71f39cdd24285
|
|
BLAKE2b-256 checksum How to use checksums |
e4dcb484bca9cad65d593ecfadbe5f0cccb489ec62c229777a427a3e91938a49
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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 Oct 16, 2026.
Transparency logRelease files / firedrill-0.5.1-py3-none-any.whl
| Download URL | firedrill-0.5.1-py3-none-any.whl |
|---|---|
| Size | 95.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
1002fee1d3622826b72efb71522845caf10f35455dd3a668c38b039806930c66
|
|
BLAKE2b-256 checksum How to use checksums |
12ad14e0a1f756a7011b98880c2acaae5c8d5cb3662516cd90ee49b2b4717c9d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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 Oct 16, 2026.
Transparency log