company_dns

EDGAR backend for the Rust + DataFusion rewrite

Status: Decided: Option 2, edgarkit, on DataFusion 55.x. This doc started as research (what the current Python EDGAR implementation does, what exists in the Rust ecosystem) and worked through two open questions before landing here — both now closed:


1. What the current Python implementation actually does

It’s worth being precise here, because “EDGAR uses pyedgar” undersells it — the current code is really two separate capabilities, only one of which touches pyedgar at all:

1.1 Index-building: lib/prepare_edgar_data.py, via pyedgar

ExtractEdgarData._initialize() calls pyedgar.utilities.indices. IndexMaker().extract_indexes(start_date=start_year), which downloads SEC’s quarterly full-text filing index (a large, gzipped, tab-delimited file listing every filing by every company for that period) and unpacks it locally. extract_data() then reads that file directly (not through any further pyedgar API — plain csv/gzip stdlib) and filters it down to rows whose form type starts with 10- (10-K, 10-K/A, 10-Q — FORM_TYPE_FILTER), which is the only form family lib/edgar.py ever queries against. This filtered set becomes the companies SQLite table that EdgarQueries.get_all_ciks() and get_all_details() query with plain LIKE/= SQL — this table is the “cached/seeded set” described in go-duckdb-rewrite.md §5, just built by a bespoke Python/SQLite pipeline today instead of arriving as a Mediumroast .feather package.

pyedgar’s actual job here is narrow: it knows how to find, fetch, and decompress SEC’s quarterly index files. Nothing about SIC/company enrichment, nothing about individual filing content — just “here’s the index of what was filed, by whom, when.”

1.2 Live firmographics fetch: lib/edgar.py’s get_firmographics

This is the fallback path go-duckdb-rewrite.md §5 already describes (cache a limited set, fall back to live EDGAR on a miss) — and it does not use pyedgar at all. get_firmographics(cik) builds a CIK##########.json filename, makes a plain requests.get() against https://data.sec.gov/submissions/{cik_file} (a public, stable JSON REST API), and reshapes the response (fills blanks with "Unknown", extracts address fields, cross-references SIC via lib/sic.py). A shared, module-level requests.Session() handles connection reuse (see the comment at lib/edgar.py:20-28, referenced from go-duckdb-rewrite.md §5.3).

This split matters for the rewrite: item 1.1 (index-building) is where pyedgar’s real value is — parsing SEC’s index file format correctly, handling quarterly boundaries, etc. Item 1.2 (live firmographics) is a generic JSON-over-HTTP call that doesn’t need pyedgar, or any specialized library at all, in either language.

1.3 What’s not being replicated

The current codebase does not use pyedgar for filing content retrieval, XBRL parsing, or anything beyond the index. Whatever the rewrite does, it only needs to match §1.1 and §1.2 above — not the larger surface pyedgar (or Rust equivalents like edgarkit, §2 below) expose for actually parsing filing documents themselves.

2. What exists in the Rust ecosystem

Research below, current as of 2026-09-27 (crates.io), following this document’s own project-wide habit of checking real numbers rather than taking a README’s framing at face value (go-duckdb-rewrite.md §7’s whole premise, applied here too).

2.1 edgarkit (crates.io, r007/edgarkit)

The closest match found to what §1 actually needs. Feature-flagged: search, filings, company, feeds, index (all on by default). The index feature specifically — “Download and parse daily and quarterly filing indices” — appears to cover the same ground as pyedgar’s IndexMaker (§1.1), and the company feature covers company facts/submissions retrieval (edgar.submissions(cik) in its own README example) — the same data get_firmographics fetches today (§1.2). Built on reqwest + tokio + governor (rate limiting) + quick-xml, async-first. Ships its own adaptive rate limiter for SEC’s fair-access rules, which the current Python code doesn’t have (today’s headers dict sets a User-Agent but has no rate-limiting logic at all) — a potential improvement, not just parity.

Maturity, stated plainly rather than glossed over: v0.4.0, released ~2 months ago, 638 downloads all-time, 6 versions published, single author (Sergey Monin), explicitly labeled “unofficial.” This is a new, low-adoption, solo-maintained crate — real functionality on paper, no independent track record yet. Worth the same posture this project has taken toward other self-described “production-ready” or promising- looking dependencies elsewhere (go-duckdb-rewrite.md Annex C’s minikv, Annex A’s DuckDB VSS): plausible, not yet verified against this project’s actual data, needs a real spike before being trusted.

Spiked (2026-09-28), and it held up: experiments/edgar-spike/ (see its own README for full numbers) ran both features against real, live SEC data — a company lookup (submissions("0000051143"), IBM) returned every field get_firmographics needs, already structured (addresses split into mailing/business, not a blob to flatten by hand); a real Q2 2025 quarterly index download+parse took ~1.65s and returned 331,786 entries, 2.79% matching the 10-% filter — closely matching the ~3% figure lib/prepare_edgar_data.py’s own comment already documents, a genuine cross-validation between the two pipelines, not just “it ran without erroring.” Two output-shape gaps found, both closed by a follow-up check in the same spike: IndexEntry has no separate accession-number field, but it’s the filename in IndexEntry.url — extracted it for 5 real 10-% entries, rebuilt lib/edgar.py’s filing_idx_url from it, and fetched each one for real: 5/5 HTTP 200, not just a plausible-looking derivation. date_filed is an unsplit "YYYY-MM-DD" string rather than pre-parsed year/month/day — a one-line split handled all 5 samples correctly, confirmed trivial as suspected. Not tested: rate-limiter behavior under real sustained load — this spike made only a handful of requests total. This doesn’t settle Option 1 vs. Option 2 (§3) on its own, but it’s a real, passing result against this project’s actual data, not just documentation.

Pressed further (2026-09-28): does the resulting data actually load into a running query engine, not just get fetched? All 9,241 filtered 10-% entries got written to a real .feather file and loaded back with DataFusion (experiments/edgar-index-query/) — clean round trip (9,241 rows out, matching 9,241 in), and a real query for IBM’s CIK found its actual Q1 2025 10-Q filing. Two results worth carrying forward:

2.2 sec_edgar (crates.io, tieje/rs_sec_edgar)

v1.0.5, released ~3 years ago, 9,918 downloads all-time (more adoption than edgarkit, but stale — no updates in 3 years). Narrower scope than edgarkit: CIK lookup (CIKQuery) plus Atom-feed-based filing search (EdgarQueryBuilder → get_feed_entries) built around EDGAR’s older Atom feed interface, not the modern data.sec.gov JSON APIs get_firmographics actually calls, and no index-file parsing equivalent to pyedgar’s IndexMaker found in its README. Doesn’t cover §1.1 or §1.2 as directly as edgarkit does.

2.3 Nothing else found

No other actively-relevant crate turned up in this search. (A crate literally named sec exists but is unrelated — it’s a secret-redaction helper for Debug/Display, not EDGAR-related; noting only so it isn’t mistaken for a hit later.)

2.4 Bottom line

edgarkit is the one real candidate for a native-Rust path, and it’s promising specifically because its feature boundaries (index, company) line up with this project’s actual two-part need (§1.1, §1.2) — new enough at the time this section was written that “promising on paper” and “actually works against real SEC data” were still two different claims. §2.1’s spikes closed that gap since. Decided (2026-09-28): edgarkit is the choice (§3’s Option 2) — this section’s caution held up as the right posture to take before deciding, not a reason to have decided differently.

3. Two options — decided: Option 2

Decided (2026-09-28): Option 2 (edgarkit), on DataFusion 55.x — see the status line above. Option 1 is kept below unedited, as the real alternative that was weighed, not because it lost on evidence — no spike was ever run for it (§5 next steps item 3, never done). This was a pragmatic choice to move forward with the option already backed by real, passing spikes, not a finding that Option 1 would have been worse.

Option 1 (not chosen): pyedgar as a CLI, feeding a conversion-to-.feather pipeline

Keep pyedgar doing what it already does well (§1.1’s index-building), run it as an external step (script or small CLI wrapper) outside the Rust service itself, and convert its output to .feather as part of a data pipeline that feeds the new service — the same shape as how Mediumroast’s own IC-classification and company data packages already arrive (go-duckdb-rewrite.md §0/§1).

Option 2 (chosen): A small Rust module replicating what pyedgar does

Either build directly against data.sec.gov’s index files (the same raw format pyedgar parses, §1.1) using plain reqwest + a tab- delimited/gzip parser, or adopt edgarkit’s index feature (§2.1) to avoid re-implementing that parsing from scratch.

4. Open questions

5. Next steps

  1. Settle the “does company_dns still own index-building” question (§4)… Done — yes, it’s a direct company_dns concern (this doc’s status line).
  2. A small, disposable spike… testing edgarkit’s index and company features against real SEC data… Done (§2.1) — experiments/edgar-spike/, both features passed against real, live SEC data — the evidence base the Option 2 decision rests on.
  3. Confirm what a pyedgar-as-CLI step (Option 1) would actually look like… Moot (2026-09-28) — Option 2 (edgarkit) is decided (status line, §3), so Option 1’s spike is no longer needed to compare the two head-to-head. §3’s Option 1 section stays as the record of the alternative that was weighed, not something still being spiked.
  4. Item 1.2 (live firmographics fallback) can likely move forward independently and sooner… Done (2026-09-28, experiments/edgar-spike/ + experiments/edgar-cache-spike/) — edgarkit’s submissions() call rebuilds get_firmographics()’s full output shape, and the full live-fallback path (fetch + §5.1’s cache) works end to end against real data, no pyedgar needed.
  5. Beyond the disposable spikes in items 2/4, no production code yet… Done (2026-09-28): v4/ (per v4-server-prototype.md) is a real, running server built on the edgar-spike/edgar-index-query/ edgar-cache-spike code, promoted rather than rewritten - the CIK catalog ingest, cached live firmographics fetch, and both real endpoints tested against real EDGAR data.