π§ Work in progress. Under active development; interfaces and defaults may change. Issues and contributions welcome.
A throttled Internet Archive (Wayback Machine) harvester for NYC / NYS government web properties. Given a government entity's domains, it enumerates that domain's historical Wayback captures over time and β only when explicitly told to β downloads them to a local cache, writing a provenance manifest either way.
This is the middle layer of a three-layer architecture:
ny-gov-web-registry β ny-gov-web-archiver β downstream applications
(what exists: entities (this repo: given domains, (e.g. the NYC mayoral
& their domains) enumerate/fetch their executive-orders corpus)
Wayback captures)
It is the general tool. It knows nothing about executive orders or any
specific document type β it harvests captures for whatever domains it's given.
Application-specific URL patterns (e.g. the old nyc.gov/html/records/... EO
PDF paths, or a mayor's-office listing API) belong to the downstream
application, not here.
The Wayback access is EDGI's wayback
library (docs: https://wayback.readthedocs.io/). We deliberately do not
reimplement CDX querying, retry/backoff, or 429 handling β the library already
does all of that. This repo is a thin, registry-fed orchestrator on top of it:
wayback_client.pyβ builds aWaybackClientconfigured for a conservative go-slow rate (see "Rate posture").harvest.pyβ enumerates captures per domain viaWaybackClient.search()(CDX), applies MIME / status / year filters, and optionally fetches viaWaybackClient.get_memento(). Also the CLI.manifest.pyβ writesmanifest.csv+manifest.jsonl(one row per capture) and agaps.mderrors/empties report.registry_input.pyβ loads the siblingny-gov-web-registryand yields(betanyc_id, domain, role)targets.
Internet Archive is a nonprofit on constrained infrastructure and is
consolidating its endpoints behind a shared ~30 requests/minute budget
(EDGI issue #205). BetaNYC's standing etiquette lives in the workspace at
team/reference/external-data-source-access.md. This tool encodes it:
- Go slow.
config.SEARCH_CALLS_PER_SECOND = 0.40(1 CDX query / 2.5s) andconfig.MEMENTO_CALLS_PER_SECOND = 0.33(1 download / ~3s). Both sit well under the shared 30/min ceiling. - One CDX query per domain, then throttle downloads; the library backs off
and honors
Retry-Afteron a 429. - Cache locally, never re-fetch. A capture already on disk is not re-downloaded (idempotent re-runs).
- Descriptive User-Agent identifying BetaNYC + this project.
The entire test suite is offline β it mocks the wayback client and asserts
that a dry-run issues zero fetch calls. A guard fixture makes any accidental
real call fail the test. No agent and no CI job hits the live Internet Archive.
Live access happens in exactly one place: scripts/smoke_test_live.py, a
supervised, human-run script (see below). It refuses to run without an
explicit acknowledgement flag.
# From the repo root:
uv pip install -e . # or: pip install -e .Requires Python >=3.11,<3.15. The only runtime dependency is wayback.
Dry-run (enumerate only β the default, never fetches):
python -m ny_gov_web_archiver.harvest --domain mta.info --from 2010 --to 2012 --dry-runRegistry-wide dry-run (every domain in ny-gov-web-registry):
python -m ny_gov_web_archiver.harvest --registry-all --from 2010 --to 2012 --dry-runFilter by MIME type and status, and actually download (explicit --fetch):
python -m ny_gov_web_archiver.harvest \
--domain nycedc.com --from 2008 --to 2014 \
--mime application/pdf --status 200 \
--fetchKey flags:
| Flag | Default | Meaning |
|---|---|---|
--domain D (repeatable) / --registry-all |
β (one required) | Targets: ad-hoc domains, or the whole registry |
--registry PATH |
../ny-gov-web-registry/data/registry.json |
Registry file for --registry-all |
--from / --to YEAR |
none | Inclusive capture-year window |
--match-type |
host |
CDX granularity: host (safe, exact host), domain, prefix, exact |
--mime M (repeatable) |
none | Keep only these MIME types |
--status C (repeatable) |
none | Keep only these HTTP status codes |
--collapse |
digest |
CDX collapse field (dedupe identical content) |
--limit |
5000 | Max CDX rows per domain |
--fetch / --dry-run |
dry-run | --fetch downloads; dry-run never fetches |
--cache-dir / --out-dir |
cache/ / out/ |
Local fetched content / manifest output |
--match-type hostis the default on purpose. A registry entity like OTI or the Mayor's Office lives at a subpath of the sharednyc.govhost; adomainmatch onnyc.govwould try to enumerate all of nyc.gov. Opt into the wider net explicitly.A domain shared by several entities (e.g.
nyc.gov) is enumerated once per entity, each recording under its ownbetanyc_id.
Written to --out-dir (default out/):
manifest.csv/manifest.jsonlβ one row per capture:betanyc_id, role, domain, original_url, wayback_timestamp, digest, mime_type, status_code, wayback_url, local_path.local_pathis populated only when fetched.gaps.mdβ domains that returned zero captures, plus any per-capture fetch errors.
Fetched content caches under --cache-dir (default cache/), git-ignored and
re-fetchable β the manifest, not the blobs, is the durable artifact.
scripts/smoke_test_live.py is the only code that touches the live
Internet Archive. It is not run by CI, agents, or the test suite. Run it as
a human, under BetaNYC's go-slow authorization:
python scripts/smoke_test_live.py \
--domain mta.info --from 2012 --to 2012 \
--i-am-a-human-running-this-supervisedIt enumerates one domain over a tiny window and prints the first few captures. Without the acknowledgement flag it refuses to run.
v1 is fetch + manifest only. It does not OCR PDFs or extract text. Turning
harvested PDFs into searchable text is a downstream (EO-corpus) concern β it
belongs to the application consuming this harvester's output, not to the general
archiver. The manifest records mime_type so a downstream OCR/parse stage can
select what it needs.
uv run --with pytest --with wayback python -m pytestAll tests run offline β no network, no live Internet Archive traffic.
- Input:
ny-gov-web-registryβ the entity/domain registry. - Client library:
wayback(EDGI). - First downstream consumer:
nyc-executive-orders.
This harvester is one layer of a family of free, open civic data assets BetaNYC builds and stewards:
ny-gov-web-registryβ which NY government entities exist, and where they live on the web (this tool's input).nyc-executive-ordersβ the open archive of NYC mayoral executive orders, 1974βpresent (this tool's first downstream application).nyc-boundariesβ NYC administrative boundaries, mapped and queryable.- Seven MCP servers giving AI agents direct access to NYC/NYS civic data: Council legislation, City Record, Checkbook spending, 311, Charter/Code/Rules, NYS legislation, and the Council budget.
BetaNYC uses AI tools openly and with human accountability. This harvester was developed with AI assistance (Anthropic's Claude) under the direction and review of BetaNYC staff β the specification and rate posture were human-set, and live harvests are an explicit, supervised, human-run step (see "Supervised live smoke test").
The tool does not generate content: it downloads historical Wayback captures verbatim as retrieved from the Internet Archive, writing a provenance manifest either way. All harvesting is throttled, identifies itself with a contact address, and honors the Internet Archive's access guidelines rather than circumventing them.
Questions about our approach: hello@beta.nyc.
Two licenses, split by what the file is:
- Code (
src/,scripts/,tests/): MIT Β© 2026 BetaNYC β use and modify freely with attribution preserved. - Documentation (
README.md,CONTRIBUTING.md): Creative Commons Attribution-ShareAlike 4.0 β reuse and adapt with credit to BetaNYC, and share adaptations under the same license.
Downloaded captures remain the property of their original publishers and the Internet Archive; this repository's licenses cover the tool and its documentation, not the harvested material.