Skip to content

CLI refactor: cdxt index + cdxt repackage - #79

Open
malteos wants to merge 91 commits into
mainfrom
feat/cli-refactor
Open

malteos wants to merge 91 commits into
mainfrom
feat/cli-refactor

Conversation

@malteos

@malteos malteos commented Oct 8, 2026 •

Copy link
Copy Markdown
Collaborator

This PR refactors the CLI according to the specs defined in the internal Google doc.

The old iter / size / warc commands are replaced by two commands that can be piped:

cdxt index cc --url 'commoncrawl.org/*' --limit 10 | cdxt repackage --prefix COMMONCRAWL

The old command lines (cdxt --cc --limit 10 iter 'commoncrawl.org/*', size, warc) keep working, deprecated
(#80): they are parsed with the 0.9.39 grammar, run with the new code and print a suggested new command line on
stderr (see Breaking changes).

cdxt index — select captures from an index

INDEX backend
cc, ia, https://… (no file extension) CDX API (existing CDXFetcher, unchanged)
local/remote CDX files or globs, e.g. s3://commoncrawl/cc-index/collections/CC-MAIN-2024-30/indexes/cdx-*.gz CDX files (SURT matching with url-is-in)
cc-url-index-s3, cc-url-index-hf, *.parquet, a directory with --index-backend duckdb columnar URL index via DuckDB
athena://athena.<region>.amazonaws.com/<db>?S3OutputLocation=s3://… columnar URL index via Athena

The backend is detected from INDEX; --index-backend cdx-api|cdx-files|duckdb|athena overrides it.

  • Selection: --url (CDX API pattern syntax for every backend), --hostname, --domain,
    --crawl, --from/--to, --limit, --url-fgrep(v);
    backend-specific: --filter/--closest (CDX API), --language/--sql (SQL),
    --url-file/--url-column/--url-file-type (CDX files).
    Options a backend doesn't support are rejected with a usage error, also --cc-mirror/--cc-sort
    for other indexes than cc. For --url-file with SQL, the error explains how to write a join
    query instead. --from/--to/--closest are validated as calendar dates by every backend.
  • Selector semantics, the same for every backend: URL prefixes keep a trailing / or ? as a
    boundary like pywb (example.com/blog/* does not select /blogger); --hostname is the exact host,
    also with the CDX API (whose matchType=host merges www.; the other host's captures are dropped
    before --limit); hostnames, domains and URL patterns include captures with a port.
  • Output: -o (stdout by default, local or remote), JSONL / CDXJ (cdx) / CSV / TSV /
    Parquet ([parquet] extra), --fields. For CDX files, a glob with an -o path without file
    extension (or ending with /) writes one output per CDX file (lines unchanged, --processes N)
    into a new or empty directory; an unknown extension (out.txt) needs --output-format.
    An output that would overwrite an input of the query (the CDX/Parquet files, --url-file; also
    via ./, symlinks or hard links) is refused before anything is opened.
  • --count (CDX API: cheap page-based estimate; SQL/CDX files: exact), --dry-run (prints the SQL /
    API requests / file list), cost guard for SQL scans over >10 or unknown crawls (--yes).
  • SQL host selectors use url_host_name_reversed, which follows the sort order of the columnar
    index, so DuckDB and Athena can skip row groups: --domain and --hostname match on it, and
    --url adds it as a hint to the url_surtkey condition (IP addresses keep the old predicates).
    On CC-MAIN-2026-17 over S3, --domain on one Parquet file went from 87 s / 45 MB to 3 s / 0.8 MB,
    and --domain commoncrawl.org --count over the whole crawl takes 149 s (the old predicate timed out).

cdxt repackage — write the captures to WARC files

  • Reads cdxt index output (file, glob or stdin; format detected from the first line, so CC's
    .cdx files work directly, or set with --input-format). It also accepts the columnar index's warc_filename/warc_record_*
    columns, with full URLs or names relative to --warc-download-prefix.
  • Uses feat: pluggable RangeJob sources for repackage (cdx / sql{athena,duckdb} / csv) #73's async WARC engine: records are copied byte-for-byte, reads from https://, s3://
    and hf://, output to local, s3:// or hf://, --processes, --parallel-readers,
    --add-metadata-record. --wb (replay of captures without a WARC location, e.g. IA captures) runs
    through the same pipeline and writer, also with --parallel-readers and --add-metadata-record
    (not --processes). It needs url, timestamp and status, and refuses Common Crawl captures.
  • Changes compared to feat: pluggable RangeJob sources for repackage (cdx / sql{athena,duckdb} / csv) #73:
    • Multi-process workers share one file counter, so files get their final names and correct
      warcinfo records right away. The shard renumbering/merge step (merge.py, --keep-shards)
      is gone.
    • HF reads resolve each WARC file once and read the records from the signed CDN URL: one
      rate-limited /resolve/ request per WARC file instead of per record. The slower
      HfFileSystem reader (--hf-reader) was removed, based on the benchmarks.
    • No WARC file is written without records. The default --prefix is REPACKAGE.
    • A run refuses a --prefix/--subprefix whose WARC files exist already (exit 2), so runs are
      never mixed.
    • A write failure (or Ctrl-C) stops the run at once, with --processes all processes (a shared
      cancel event; a process that has not stopped after 60 s is terminated); the files being written
      are removed or their S3 uploads aborted, the completed files are kept. A source that blocks
      (e.g. stdin from a running process) no longer delays this.

Network and AWS settings

See docs/settings.md. Requests to a host keep a minimum interval (e.g. 0.55 s for
data.commoncrawl.org), also across --parallel-readers and for every retry. S3, Athena and DuckDB
use Common Crawl's recommended AWS defaults (region us-east-1, adaptive retries, virtual addressing,
...) for whatever the user has not configured (environment, AWS config file, fsspec configuration),
in every worker process too; CDXT_AWS_DEFAULTS=off disables them.

Usage guides

docs/usage/: HTTP (no account), AWS: Athena and S3,
Hugging Face (Storage Buckets, HF Jobs); linked from the README.

Python API

cdxt index and cdxt repackage call the same code that Python users call:

from cdx_toolkit.indexes import IndexQuery, open_index
from cdx_toolkit.repackage import repackage

index = open_index('cc')  # any INDEX of cdxt index
query = IndexQuery(urls='commoncrawl.org/*', limit=10)
index.count(query); index.explain(query)
result = repackage(index.records(query), prefix='out/CC')  # RepackageResult(requested, written, failed)

CDXFetcher and CaptureObject stay as the low-level CDX API client. cdx_toolkit.warc.get_writer() /
CDXToolkitWARCWriter are deprecated (FutureWarning, to be removed in a future version).

Breaking changes

  • The 0.9.39 command lines are deprecated but still work (Keep the 0.9.39 cdxt command lines working (deprecated) #80, cdx_toolkit/cli/legacy.py): options before
    the command, iter (same text / --jsonl / --csv output, --get), size (also --details) and warc
    (streamed into repackage()). Each run prints one stderr line: deprecated, will be removed in a future version,
    and a suggested new command line. iter --all-fields --csv is a usage error (it was a traceback).
  • In the new commands, all options go after the command, the URL is --url instead of a positional argument,
    and output defaults to JSONL with all fields (no k v, k v text format).
  • WARC files of repackage and of the old warc (also with --wb) are <prefix>-001.warc.gz (was
    …-000000.extracted.warc.gz), without the added WARC-Source-URI/WARC-Source-Range headers; no file is
    written without captures, and failed captures give exit status 1.
  • The scripts/cdx_iter and scripts/cdx_size scripts were removed.
  • Dependencies: fsspec, aiofiles and url-is-in are now required. New extras: athena,
    duckdb, hf (huggingface_hub>=1.7.2, the first version whose HfFileSystem handles Storage
    Buckets; an older installed version gets a clear error), parquet; s3 now also includes aioboto3.
  • Fix: pad_timestamp_up() replaced a given day with the last day of the month and ignored leap
    years. This also affected which crawls a CDX API --to <day> query covered.

See docs/cli-migration-guide.md for old → new examples, including the #73 repackage /
filter_cdx options.

Reviewer notes

  1. Start with the behaviour, not the code. Read docs/cli-migration-guide.md and the README section "Command-line tools". cdxt index --help and cdxt repackage --help show the full option set.
  2. cdx_toolkit/cli/: argument parsing only. init.py holds main(), legacy.py parses and runs the deprecated 0.9.39 command lines, index.py/repackage.py define the options and call the backends.
  3. cdx_toolkit/indexes/, the core of the new design:
    • query.py, base.py: the backend-independent IndexQuery; each backend declares the options it supports.
      IndexBackend.records()/count()/explain() validate the query first, then call the backend's select()/select_count().
    • resolve.py: open_index(), which backend an INDEX argument maps to.
    • backends/cdx_api.py: a thin wrapper around the unchanged CDXFetcher.
    • patterns.py: URL pattern → SURT keys, shared by CDX files and SQL.
    • backends/cdx_files.py: CDX files; backends/sql.py, sql_builder.py, duckdb.py, athena.py: the SQL backends.
  4. cdx_toolkit/record_io.py: the JSONL/CDXJ/CSV/TSV/Parquet readers and writers shared by both commands.
  5. cdx_toolkit/repackage/ started as feat: pluggable RangeJob sources for repackage (cdx / sql{athena,duckdb} / csv) #73's filter_warc package, restructured (index reading moved to cdx_toolkit/indexes/):
    • init.py: repackage(), which cdxt repackage calls;
    • jobs.py: index records → range jobs (sorted by WARC file and offset) or wayback replay jobs (--wb);
    • repackager.py: the Repackager (readers → queue → writer), counting failed captures, aborting on write errors;
    • readers/: http, s3, hf (feat: pluggable RangeJob sources for repackage (cdx / sql{athena,duckdb} / csv) #73's hf_utils.py, now resolve-once) and wayback;
    • writers/: local and s3 outputs; series.py: WarcFileSeries (rotation, -001 numbering, warcinfo, the first file created lazily);
    • multiprocess.py: the shared file counter, and no merge step.
  6. Things worth a careful look:
    • SQL escaping (indexes/backends/sql_builder.py: escape_sql_literal, quote_sql_string, sql_like_prefix with ESCAPE ''), and that only validated values reach the generated SQL.
    • The cost guard (indexes/backends/sql.py: confirm_cost): its prompt goes to stderr so piped output stays clean.
    • Semantics: --limit applies before --url-fgrep; SURT matching ignores www.; with --url-file-type, pattern is the default and domain reproduces feat: pluggable RangeJob sources for repackage (cdx / sql{athena,duckdb} / csv) #73's filter_cdx behaviour.
    • Output and dependency changes: the file names, no empty files, and the new required dependencies.
    • The timeutils.pad_timestamp_up() fix: it changes CDX API crawl selection for --to values with a day.
  7. Tests:
    • tests/test_cli.py keeps its test names, so the recorded fixtures prove index sends the same requests as iter did; tests/test_cli_legacy.py runs the 0.9.39 command lines against the same fixtures.
    • The new unit tests are in tests/unit/ (test_indexes, test_sql_indexes, test_sql_builder, test_cdx_files, test_record_io, test_cli, test_usage_errors, ...).
    • The repackage tests (feat: pluggable RangeJob sources for repackage (cdx / sql{athena,duckdb} / csv) #73's, plus the Repackager, readers, WarcFileSeries, --wb and repackage()) are in tests/repackage/.
    • Two workflows. ci.yaml (every push and PR) runs no live test: recorded HTTP responses, local DuckDB/Parquet fixtures and S3 mocked with moto. ci-slow.yaml (manually dispatched) runs all live tests with CDXT_LIVE_TESTS=http,s3,athena,hf, AWS credentials and the HF_TOKEN secret; each live test still skips when its dependency or permission check fails (e.g. Athena needs athena:ListDatabases).
    • CDXT_LIVE_TESTS=http,s3,athena,hf (or all) replaces CDXT_DISABLE_MOCK_RESPONSES, CDXT_DISABLE_S3_TESTS and CDXT_DISABLE_ATHENA_TESTS; the tests that can run live are marked live and live_<service>, e.g. CDXT_LIVE_TESTS=s3 pytest -m live_s3 tests. CONTRIBUTING.md lists the services. The HF tests read from hf://buckets/commoncrawl/commoncrawl and write to hf://buckets/commoncrawl/commoncrawl-ci-temp (CDXT_TEST_HF_BUCKET).
    • Process failures and cancellation are tested with real spawned processes and subprocesses (stdin kept open, Ctrl-C), with timeouts so a regression cannot hang the suite.
    • The tests use a temporary cache directory instead of deleting ~/.cache/cdx_toolkit/.

Live validation (separate from the mocked suite)

Backend Environment Result
HF Storage Buckets (pytest -m live_hf, incl. cdxt index -o hf://) local macOS, Europe; huggingface_hub 2.0.0, and 1.7.2 on Python 3.9 passed (2026-10-15); 1.7.1 fails, hence the minimum
S3 (pytest -m live_s3) local macOS, Europe 48 passed (the DuckDB e2e tests take minutes: 300 Parquet footers from us-east-1)
Athena (test_index_athena_e2e) local macOS, account with ccindex passed after the access-check fix (it was always skipped before, also in CI)
examples/homepages.sql via DuckDB on s3://commoncrawl local macOS, Europe 50 captures in 41 s (2026-10-15)
CDX API exact --hostname — not checked live: index.commoncrawl.org returned empty replies at the time (2026-10-15); mocked tests only

Open tasks

  • Run live tests with all backends (see above); the CDX API --hostname check is pending.
  • Add the HF_TOKEN secret for ci-slow.yaml.
  • Run ci-slow on the final branch.
  • Benchmark throughput for various backend and environments

Replace the old `iter`/`size`/`warc` commands with two commands that can be piped:
`cdxt index` selects captures from an index, `cdxt repackage` writes them to WARC files.

cdxt index
- indexes: CDX API (cc, ia, URLs), CDX files (local/remote, globs), Common Crawl's
  columnar URL index with DuckDB (cc-url-index-s3, cc-url-index-hf, *.parquet) or
  Athena (athena://...); detected from the INDEX argument or --index-format
- selection: --url (CDX API patterns, matched as SURT keys for CDX files and SQL),
  --hostname, --domain, --url-file/--url-column/--url-file-type (CDX files),
  --crawl, --from/--to, --closest, --filter, --language (SQL), --url-fgrep(v),
  --limit, --sql (custom query), --count, --dry-run, cost confirmation (--yes)
- output: -o (local or remote), JSONL, CDX/CDXJ, CSV, TSV, Parquet (optional pyarrow),
  --fields; one output per CDX file for a glob input and an -o path without extension
  (--processes)

cdxt repackage
- reads captures written by `cdxt index` (file, glob or stdin), copies the WARC
  records byte-for-byte with the async WARC fetcher from feat/warc-range-sources
  (http, s3 and hf:// reads; local, s3 and hf:// output; --processes,
  --parallel-readers, --add-metadata-record), --wb for wayback replays
- multi-process workers share one file counter, so files get their final names
  (and warcinfo records) directly; the HF reader resolves each WARC file once
- default --prefix REPACKAGE, human readable --size, no file without captures

Other changes
- `cdxt iter` is a deprecated alias of `cdxt index`; `size` and `warc` exit with the
  equivalent new command line
- fix pad_timestamp_up() for timestamps with a day and for leap years
- remove the deprecated cdx_iter and cdx_size scripts
- new extras: athena, duckdb, hf, parquet; fsspec, aiofiles and url-is-in are required
- README: `cdxt index` (index types, selection, output, SQL and CDX file options)
  and `cdxt repackage`, new extras, migration table
- docs/cli-migration-guide.md: old `iter`/`size`/`warc` commands and the
  feat/warc-range-sources `repackage` options mapped to the new CLI, with examples
- docs/notes/warc-fetcher-performance.md: WARC fetcher benchmarks and tuning notes
  from feat/warc-range-sources
- CHANGELOG entry
@codecov

codecov Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.00815% with 28 lines in your changes missing coverage. Please review.
✅ Project coverage is 98.40%. Comparing base (e87b120) to head (92c76e6).

Files with missing lines Patch % Lines
cdx_toolkit/repackage/repackager.py 96.39% 8 Missing ⚠️
cdx_toolkit/aws.py 94.49% 6 Missing ⚠️
cdx_toolkit/record_io.py 98.60% 3 Missing ⚠️
cdx_toolkit/cli/legacy.py 99.07% 2 Missing ⚠️
cdx_toolkit/myrequests.py 96.07% 2 Missing ⚠️
cdx_toolkit/__init__.py 0.00% 1 Missing ⚠️
cdx_toolkit/cli/__init__.py 97.50% 1 Missing ⚠️
cdx_toolkit/indexes/backends/cdx_files.py 99.50% 1 Missing ⚠️
cdx_toolkit/indexes/patterns.py 97.87% 1 Missing ⚠️
cdx_toolkit/repackage/multiprocess.py 99.19% 1 Missing ⚠️
... and 2 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #79      +/-   ##
==========================================
+ Coverage   97.24%   98.40%   +1.16%     
==========================================
  Files           9       44      +35     
  Lines         943     3575    +2632     
==========================================
+ Hits          917     3518    +2601     
- Misses         26       57      +31     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

malteos added 27 commits October 9, 2026 12:43
CI
- .gitattributes: no line-ending conversion for tests/data, so fixtures (e.g. the
  metadata record with a fixed payload digest) are byte-identical on Windows
- compare relative paths with as_posix() in the per-file output test (Windows)
- `make test` no longer runs examples/iter-and-warc.py against the live Common Crawl
  index (connections from CI runners time out; it also took ~45 minutes in the
  minimal job), it is now `make example`

Fixes found by the new tests
- WARC files: only rotate a file that already has records; a record larger than the
  remaining --size no longer leaves a WARC file with only the warcinfo record
- DuckDB: retry S3 requests longer (8 retries, 1s, 2s, 4s, ...) on 503 SlowDown
- is_s3_url() accepts None (WARCFilter without warc_download_prefix)

Tests (coverage of the new code)
- Athena source and `cdxt index athena://...` with a fake boto3 client
- HF reader: resolve without redirect, resolve/read errors, 429s, cache eviction
- DuckDB source with a local parquet file and a fake connection (HF filesystem)
- WARC pipeline and multi-process mode in-process (fake HTTP/HF reads, memory://
  output), reader/writer errors, uvloop
- usage errors and edge cases of index/repackage

Cleanup: remove dead code (fsspec fallbacks in record_io, the old --jsonl/--csv
translation, the cost estimate of the repackage sources, Athena get_databases, the
Python < 3.9 check, DuckDbSource's query builder)
…ests

cdxt's S3 clients (the WARC fetcher's aioboto3 clients and s3fs for all other
s3:// paths) use region us-east-1, retry_mode adaptive, max_attempts 100000,
addressing_style virtual, payload_signing_enabled false and 2 concurrent requests
(s3fs max_concurrency).

The user stays in control: a default is only used for a setting that is not
configured in the environment (AWS_REGION, AWS_DEFAULT_REGION, AWS_RETRY_MODE,
AWS_MAX_ATTEMPTS), the AWS config file of the active profile or the fsspec
configuration; CDXT_AWS_DEFAULTS=0 turns them off. The s3fs defaults are only set
by the cdxt command (and its worker processes), not for library users.

The multipart_threshold/multipart_chunksize settings are not applied: they are for
whole-file transfers, cdxt reads WARC records with range requests and index files
sequentially (and S3 parts are limited to 5 GiB).
- Athena client: the same botocore defaults (adaptive retries, 100000 attempts); the
  region of the athena:// location still wins
- DuckDB (httpfs) on S3, as far as it has equivalents: region -> s3_region and the
  region of the credential secret, max_attempts -> http_retries (attempts - 1, with a
  constant 1s wait, because DuckDB's exponential backoff has no maximum), addressing
  style -> s3_url_style; settings the user configured (environment, AWS config
  file) are used instead, CDXT_AWS_DEFAULTS=0 keeps DuckDB's own defaults
- fix DuckDB's http_timeout: it is in seconds, 120000 was ~33 hours, now 120s
- the S3 writer keeps a part's bytes until the part is uploaded; a part that cannot be
  uploaded aborts the multipart upload, and close() then raises instead of completing
  an object with missing bytes
- any writer failure is fatal: the current file is aborted (local file removed, S3
  upload aborted), the files written before stay complete
- cancellation and failures cancel all tasks without waiting for queue space; the
  producer thread stops via an event instead of blocking on a full queue
- the separate writer shutdown coordinator (a polling loop) is gone
…tched

Captures that cannot be fetched are counted instead of only logged; the others are
still written. The pipeline returns the requested, written and failed counts (summed
over the worker processes), which the CLI logs; any failed capture makes it exit with
status 1. The same applies to --wb. Ctrl-C exits with status 130.
An HTTP range response is only written if it holds exactly the requested bytes: status 206
with the requested Content-Range, or 200 for a range that is the whole file, and as many
bytes as requested. A server that ignores the range, a redirect (not followed), a wrong
range or a truncated body makes the capture fail. The HF reader uses the same check, the
S3 reader checks the number of bytes.

The tests serve WARC files with realistic range responses (tests/fake_warc_server.py).
The S3 client and the HF reader were only created if --warc-download-prefix was s3://
or hf://, so captures whose filename is a full s3:// or hf:// URL failed with the default
https prefix. The readers are now created on first use, by the URL scheme of each range
job, so mixed input works; the download prefix only resolves relative filenames and is
no longer passed to the fetcher or the worker processes.
The URL filters read record['url'] even when neither --url-fgrep nor --url-fgrepv was
given, so a --sql query that selects only the WARC location crashed with a KeyError.
Records now pass through untouched without URL filters; with a URL filter, a record
without url is a usage error that names the missing column.
… have

The columns were the fields of the first capture, and other fields of later captures
were silently dropped. Now:
- SQL results have a fixed schema: NULL columns stay in the record as None (empty in
  CSV/TSV, left out in JSONL/CDXJ as before), so the first record has all columns
- for the CDX API and CDX files, the captures are first written to a temporary file to
  find all fields
- an explicit --fields still selects the columns
- the CSV writer raises instead of dropping an unexpected field
--domain blog.example.com matched url_host_registered_domain = 'blog.example.com', which
no capture has (their registered domain is example.com), while CDX files selected the
captures below blog.example.com. The SQL query now matches the same SURT prefixes as CDX
files (with a host boundary), and keeps a registered-domain condition (one of the
domain's suffixes) so the engines can still prune.

A shared test runs the same selectors against CDX files and DuckDB.
The results of --url, --hostname and --domain were concatenated, so a capture selected
by two of them was written twice and used up the limit twice. With several selectors,
duplicates are now skipped (by URL, timestamp and WARC location, so distinct captures of
the same URL stay), and --limit is applied after that instead of by the server. The help
of --count says that the API estimate counts overlaps more than once.
… capped by --limit

- CDX files applied --url-fgrep/--url-fgrepv while reading, before --limit, contrary to the
  documented order; they are now applied after the limit like for the other backends
  (one output per CDX file, which rejects --limit, still filters while reading)
- --count is at most --limit on every backend (the CDX API estimate and the CDX file
  count ignored it)
- --count with --url-fgrep/--url-fgrepv is a usage error (they are client-side filters)
- --limit below 1 is a usage error
The HTTP reader called the synchronous request helper from the event loop, so network
waits and retry sleeps blocked the other readers and the writer, and --parallel-readers
had no effect for http(s). The request now runs in a worker thread. The minimum interval
between requests to a host (e.g. 0.55s for data.commoncrawl.org) is kept across threads:
each request reserves its start time under a lock.
…y bounds

- botocore ignores AWS_REGION, so with only AWS_REGION set the client had no region
  (or the default); it is now passed to botocore explicitly (an explicit region still wins,
  then AWS_REGION, AWS_DEFAULT_REGION, the profile, the default)
- an explicit retries.max_attempts (e.g. in the fsspec configuration) was merged with the
  default total_max_attempts=100000, which botocore prefers; an explicit bound of either
  form now replaces the default one

The tests check the configuration of real botocore clients (no requests).
--to 20240801 became fetch_time <= '2024-08-01 23:59:59', which excluded captures at e.g.
23:59:59.500. The end is now exclusive at the next second (fetch_time < '2024-08-02
00:00:00'); an end beyond year 9999 is a usage error.
…e themselves

The files were staged as warc-001.warc.gz and renamed when copied, so their warcinfo
record named the staging file. They are now staged under the basename of the prefix,
so the WARC-Filename is the final name from the start.
… reads index records only

- the SQL query building (escaping, guided WHERE clause, crawl names) is now
  cdx_toolkit.indexes.sql_query; running queries is cdx_toolkit.indexes.athena and
  cdx_toolkit.indexes.duckdb, which yield rows; the range-job paths of the Athena and
  DuckDB sources, build_athena_query and validate_result_columns are gone (cdxt index
  writes the rows, cdxt repackage reads them like any index file)
- confirm_cost takes the number of crawls and the engine (no CostEstimate type, no
  dependency of indexes on repackage); the unused SqlBackend._prepared is gone
- repackage/sources/ is now repackage/records_source.py (RecordsSource, join_warc_url);
  the multi-process shards are JSONL files written and read with record_io instead of a
  separate range-job CSV reader/writer; RangeJob.extra is gone
… S3 and metadata paths offline

- the local and S3 writer tests check behaviour (buffering, exact bytes, part sizes, close,
  abort) with a fake S3 client instead of repeating constructor assignments
- offline tests for S3 output with metadata records, a failing metadata record, S3 read
  lengths, crawl name resolution and URL filters with one output per CDX file
- LocalFileWriter.close closes the file in a finally
…oto) and DuckDB (local index)

- the WARC records the repackage tests download are recorded in
  tests/data/repackage/warc_records.jsonl and served for data.commoncrawl.org range requests
  (responses; DISABLE_MOCK_RESPONSES=1 still uses the real server)
- the S3 tests run against a local moto server with s3://commoncrawl holding the same
  records (configured with environment variables, so worker processes use it too); with
  CDXT_LIVE_TESTS=1 they run against AWS
- the DuckDB end-to-end tests query a local columnar index built from the same captures
  (local_url_index fixture); the tests of Common Crawl's index on S3 and of Athena only run
  with CDXT_LIVE_TESTS=1, and the AWS access checks no longer run at collection time
- the two remaining live requests of main (example.com, a non-existent host) are mocked
- CI sets CDXT_DISABLE_ATHENA_TESTS (it set DISABLE_ATHENA_TESTS, which no test read)
- moto is in the all extra (not in test: it would install boto3 for the minimal tests)
- tests: CDXT_DISABLE_MOCK_RESPONSES, CDXT_SAVE_MOCK_RESPONSES, CDXT_FAKE_GITHUB_ACTION
- the log level: CDXT_LOGLEVEL (LOGLEVEL still works)
…e output per CDX file)

The warcinfo record named the whole local path on Windows (split at '/' only), and one
output per CDX file used the whole path as the output name; both use os.path.basename for
local paths now. The S3 metadata test writes its file in binary mode (no CRLF on Windows).
…may have real AWS settings)

Since Python 3.14, Linux starts worker processes from a forkserver, which an earlier test may
have started with the environment of that time: the workers then read the real
s3://commoncrawl (with CI's AWS credentials) instead of the moto server. Spawned workers get
the current environment, which points to the moto server.
The pull request CI needs no AWS credentials anymore (S3 is mocked); CI-slow logs in to AWS
and runs the tests with CDXT_LIVE_TESTS=1 (and installs .[all] for the S3 and DuckDB tests).
…d from the README; removed the dead link to the moved benchmark notes
… CDX file needs a new or empty directory and an -o path without extension

- the output was opened before the lazy input, so -o equal to the input (also ./, symlinks, hard links,
  a glob member, the --url-file, a Parquet index) truncated it; now exit 2 before anything is opened
- a rerun into the same directory left the outputs of the earlier run (e.g. a run that selected nothing)
- -o out.txt silently wrote one output per file; an unknown extension now needs --output-format
…names, domains and URL patterns include ports

- example.com/blog/* selected /blogger and /blog?x (the SURT drops the trailing slash); now like pywb's
  prefix search range
- CDX files: --hostname and --domain missed captures with a port (com,example:8443)/), which SQL selected
- a URL pattern with a port and without scheme (example.com:8080/*) became an invalid SURT key
- the selector parity tests use SURT keys written out, not computed by the code under test
matchType=host selects the SURT host, the same for example.com and www.example.com; the captures of the
other host are now dropped before --limit, which is then not sent to the server. --count stays an estimate
of the SURT host (documented).
…or Ctrl-C

The source ran in asyncio.to_thread, whose default executor asyncio.run() waits for at shutdown: with stdin
from a process that keeps running (or a blocking generator), a failed writer or Ctrl-C hung. The source now
runs in a daemon thread that nothing waits for (run_in_daemon_thread, as for the HTTP reads), and stops
before its next record once the pipeline stops.
ex.map() returned the results in order and the executor waited for all workers, so the other processes ran
their shards to the end. The workers now share a cancel event that each pipeline watches (Repackager(cancel=...)
aborts the current file and raises RepackageCancelled); the parent raises the first failure (or Ctrl-C) as soon
as it happens, after setting the event, cancelling the pending workers and terminating those that have not
stopped after 60 s.
… the S3 defaults too

A spawned process (macOS, Windows) does not inherit the fsspec configuration set by the main process; only
the repackage workers configured it. Both process pools now come from aws.process_pool(), which applies
configure_fsspec_s3() in every new process.
…er-host rate limiter

The slot was reserved once per request, so with parallel readers a retry could start together with another
reader's request. The limiter also uses time.monotonic() now.
…lear error for older versions

0.30 (the old minimum) and 1.5.0 (the Buckets API) treat hf://buckets/ paths as model repos; 1.6.0 and 1.7.0/1.7.1
raise EntryNotFoundError for a missing path, which broke the existing-output checks. Verified with the live HF
tests on Python 3.9: 1.7.2 passes, 1.7.1 fails. An installed older version (which satisfied >=0.30) is now
reported before any work (exit 2); installing the extra upgrades it. New live test: cdxt index -o hf://.
…s than cc

They were passed to open_index() and silently ignored by the CDX files, DuckDB and Athena backends, and by ia
and custom CDX API URLs (CDXFetcher only uses them for cc).
…ery backend

--to 202413 raised an uncaught IllegalMonthError for SQL indexes (the padding was outside the error handling),
and CDX files accepted impossible dates such as 20240230. timeutils.timestamp_range() checks the padded first
and last second of a timestamp; IndexBackend.validate() uses it for all backends (exit 2 before any request),
and the SQL bounds are built from it.
…] as a glob)

Also in the error messages that suggest an install, and in the CI workflows.
The README, the help texts and the migration guide used a homepages.sql that was nowhere: the homepages of
.va in one crawl, with the columns cdxt repackage needs; tested on a local copy of the index's columns, and
run once on s3://commoncrawl (50 captures in 41 s).
…cstrings

_output_filename split an S3 URL only to rebuild it (generate_warc_filename takes the whole prefix); removed the
stale TODO about .extracted.warc and the docstring that said subprefixes distinguish the processes.
…ction error

index.commoncrawl.org answered with the text of an exception ("Error: ('Connection aborted.', RemoteDisconnected(...")
when it could not read its index, with a status that was not retried, so the response could not be decoded
(seen in the live tests with --closest). Such answers are now retried like a 503; the decode error names the status.
… reader (flaky CI)

- Before 3.11, asyncio.run() raises KeyboardInterrupt wherever the main thread is, e.g. after the writer
  created a file and before it registered it, so abort() missed it (Linux/3.9: 15 of 40 runs left a file,
  10 hung). Repackager.run() now cancels the pipeline on SIGINT, like asyncio.run() does since 3.11.
- If cdxt stops while the source thread still waits for input (stdin from a process that keeps running),
  Python can hang at exit (on Windows, closing stdin waits for the pending read): the CLI then writes its
  message and exits without finalization (os._exit).
…s run

The check looked at every thread named cdxt-source, so a thread left waiting by another run in the same
process (in CI: an earlier test) made an unrelated error exit with status 1, or end the whole process with
os._exit. The CLI now only considers source threads that were not running before the command.
…tart times

On a slow CI runner (macOS) a thread that wakes up late for its turn starts close to the next one; the test now
checks that every attempt, also the retry, reserves its own turn, the minimum interval from the others.
The child's stderr (log lines and a traceback, about 3 KB, more with Windows paths) went to a pipe that was
only read after the child exited; a full pipe (a few KB on Windows) blocks the child, so the test timed out on
Windows. stderr goes to a file now, and the child dumps the stacks of its threads if it hangs.
…hread

It did not fix the Windows timeout (the test's full stderr pipe did, see 9e96e67), and finding the thread of
the current run by name is racy: a late thread of an earlier run made an in-process CLI call (in CI: pytest)
end with os._exit. The Ctrl-C handling for Python < 3.11 stays.
@malteos
malteos marked this pull request as ready for review October 15, 2026 19:42
@malteos
malteos requested a review from lfoppiano October 15, 2026 19:42
@wumpus
wumpus self-requested a review October 15, 2026 20:04

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

enhancement: Rename WARC output

2 participants