Repository navigation
feat(crossref): offline OpenSanctions screening and API key support (#117) - #123
Merged
Merged
Conversation
…117) The hosted OpenSanctions API now answers keyless requests with 401 {"detail":"No API key provided."} (checked 2026-10-21), so the existing opensanctions source errored for everyone. - OPENFOIA_OPENSANCTIONS_API_KEY (environment only) is sent as the documented "Authorization: ApiKey <key>" header. 401/403 become a source error that says a key is required and names the variable. - New offline source opensanctions-local (in OFFLINE_SOURCES): --opensanctions-data <dir> streams targets.simple.csv (449.34 MB per the default dataset page) row by row and matches the normalized name column (score 1.0) and the ;-nested aliases column (0.8). Same extra keys as the API checker. Missing file -> NoOpenSanctionsData; read errors surface with partial hits, like ICIJ. - The CLI treats every OFFLINE_SOURCES entry as offline when deciding whether to warn, and refuses --sources icij/opensanctions-local with no data flag instead of running zero sources. - README and --help carry the CC BY-NC 4.0 data license. names.txt (105.1 MB) was not used: it has no ids or dataset columns, so it cannot fill is_sanctioned/datasets. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wdqj51cPquyTRKmoJLQV6y
- SANCTIONED comes only from list titles (contains "sanction", or an OFAC list, since "US OFAC Specially Designated Nationals (SDN) List" lacks the word). The `sanctions` column is filled for SAM debarments, HHS/Medicaid exclusions, FINRA bars and INTERPOL notices, so it no longer flags. - PEP matches "pep"/"peps" as a word or "politically exposed", not substrings like "pepper". Both API and local checkers share the rule. - Details now name the first two list titles. - A 401/403 from the OpenSanctions API takes the source out of the rest of the run (status skipped(api-key-required)), so no further entity names leave the machine. The report still says "API key required: set OPENFOIA_OPENSANCTIONS_API_KEY". - --opensanctions-data pointing at the CSV file asks for its directory. - Fixture `dataset` cells are realistic titles; adds an HHS exclusion row with a non-empty sanctions cell. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wdqj51cPquyTRKmoJLQV6y
The flag errors (offline source without data, --opensanctions-data given the CSV file) were only reachable with an initialized database, so CI printed "Database not initialized" instead. The CLI tests now run with an empty data dir so they cannot pass on a local database. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wdqj51cPquyTRKmoJLQV6y
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #117.
What changed
API key.
OPENFOIA_OPENSANCTIONS_API_KEY, read from the environment only (never config, never logged), is sent asAuthorization: ApiKey <key>, the header documented at https://www.opensanctions.org/docs/api/authentication/. Without it the request goes out keyless as before. One live keyless request (/search/default?q=test, 2026-10-21) returned HTTP 401{"detail":"No API key provided."}, so the API source now needs a key for everyone. 401/403 are reported asAPI key required: set OPENFOIA_OPENSANCTIONS_API_KEYinstead of a bare HTTP status.Offline screening. New source
opensanctions-local, added toOFFLINE_SOURCES, so it runs with no network opt-in, no warning and no prompt:The user downloads
targets.simple.csv(449.34 MB) from https://www.opensanctions.org/datasets/default/ into./os/. It is streamed row by row, never loaded whole. Thenamecolumn (score 1.0,exact) and the;-nestedaliasescolumn (score 0.8,alias) are matched after lowercasing, turning punctuation into spaces and collapsing whitespace. Hits carry the sameextrakeys as the API checker (score,is_pep,is_sanctioned,datasets). A directory with notargets.simple.csvis reported asNoOpenSanctionsData, not as a clean result. Read errors surface with partial hits, the same way the ICIJ checker does.I did not use
names.txt(105.1 MB): it has no ids or dataset columns, so it cannot fillis_sanctionedordatasets. The issue's optional rigour normalization is not in the repo yet, so matching uses the plain normalization described above.CLI. The network warning now skips every
OFFLINE_SOURCESentry, where before it skipped onlyicij.--sources icijor--sources opensanctions-localwithout its data flag now exits with an error. Before, it ran zero sources and looked like a clean screening.Docs. The README row and
--helpgive the data license, CC BY-NC 4.0 (https://www.opensanctions.org/licensing/): free for journalists and non-commercial use. The CHANGELOG entry is under Unreleased.Known limit
The offline check makes one full pass over the file for each entity. If screening hundreds of entities against the 449 MB file gets slow, the next step is a name index (marked
ponytail:in the code).Tests
tests/test_crossref_opensanctions.pyuses a 20-row fixture with the documented columns and fictional names. It covers exact, alias and non-Latin matches, a missing file, a read error that never logs the name, the offline run under the no-network fixture through bothcrossref_entitiesand the CLI, the key header being sent only when the variable is set (the key never appears in logs or output), and 401/403 producing the key-required message. Full suite: 541 passed, 20 skipped, exit 0 (HF_HUB_OFFLINE=1).ruff checkandruff format --checkare clean.Review fixes
bool(row["sanctions"]). The real file fills that column for SAM procurement exclusions, HHS/Medicaid excluded providers, FINRA bars, INTERPOL notices and ship detentions.is_sanctionednow comes only from list titles that contain "sanction", plus OFAC lists: the reviewer's own example, "US OFAC Specially Designated Nationals (SDN) List", does not contain the word, so a "sanction"-only rule would have stopped flagging the SDN list. Details name the first two list titles (Lists: ...), so an HHS exclusion shows its list and never prints SANCTIONED._classify_datasets.opensanctionsout of the rest of the run (statusskipped(api-key-required)), the same way a rate limit does, so no further entity names leave the machine. The report line is stillAPI key required: set OPENFOIA_OPENSANCTIONS_API_KEY.--opensanctions-datapointing at the CSV file exits with "pass the directory containing targets.simple.csv". The data-flag checks now run before the database check. Before, they could only be reached with an initialized database. The CLI tests use an empty data dir, so they cannot pass just because a local database exists.datasetcells are now realistic titles. It adds an HHS OIG exclusion row with a non-emptysanctionscell, asserted not sanctioned, and a "Peppercorn" title, asserted not PEP. A new test sends two entities to a mocked 401 and asserts exactly one request went out.HF_HUB_OFFLINE=1). Ruff check and format are clean.🤖 Generated with Claude Code
https://claude.ai/code/session_01Wdqj51cPquyTRKmoJLQV6y