agentdata 0.3.26__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. agentdata/__init__.py +33 -0
  2. agentdata/__main__.py +4 -0
  3. agentdata/cache.py +115 -0
  4. agentdata/cli.py +173 -0
  5. agentdata/client.py +158 -0
  6. agentdata/company/__init__.py +3 -0
  7. agentdata/company/cf.py +145 -0
  8. agentdata/company/ch_zefix.py +100 -0
  9. agentdata/company/cli.py +243 -0
  10. agentdata/company/co_rues.py +72 -0
  11. agentdata/company/edgar_cik.py +84 -0
  12. agentdata/company/edgar_fts.py +101 -0
  13. agentdata/company/faces.py +746 -0
  14. agentdata/company/fdic_bankfind.py +107 -0
  15. agentdata/company/fi_ytj.py +89 -0
  16. agentdata/company/fr_recherche.py +124 -0
  17. agentdata/company/fts.py +121 -0
  18. agentdata/company/gleif.py +178 -0
  19. agentdata/company/kr_hometax.py +112 -0
  20. agentdata/company/no_brreg.py +124 -0
  21. agentdata/company/ror.py +105 -0
  22. agentdata/company/ted.py +117 -0
  23. agentdata/company/usaspending.py +124 -0
  24. agentdata/company/vies.py +66 -0
  25. agentdata/daterange.py +35 -0
  26. agentdata/economy/__init__.py +10 -0
  27. agentdata/economy/bis.py +132 -0
  28. agentdata/economy/cli.py +199 -0
  29. agentdata/economy/ecb.py +117 -0
  30. agentdata/economy/eurostat.py +163 -0
  31. agentdata/economy/faces.py +573 -0
  32. agentdata/economy/fred.py +92 -0
  33. agentdata/economy/ilo_stat.py +99 -0
  34. agentdata/economy/imf_sdmx.py +133 -0
  35. agentdata/economy/un_comtrade.py +199 -0
  36. agentdata/economy/unsd_sdg.py +154 -0
  37. agentdata/economy/wb.py +208 -0
  38. agentdata/economy/who_xmart.py +177 -0
  39. agentdata/entries.py +30 -0
  40. agentdata/errors.py +38 -0
  41. agentdata/esg/__init__.py +5 -0
  42. agentdata/esg/cli.py +180 -0
  43. agentdata/esg/climatetrace.py +260 -0
  44. agentdata/esg/eiti.py +187 -0
  45. agentdata/esg/epa_frs.py +138 -0
  46. agentdata/esg/euasl.py +75 -0
  47. agentdata/esg/eutl.py +66 -0
  48. agentdata/esg/faces.py +498 -0
  49. agentdata/esg/paris_mou.py +240 -0
  50. agentdata/esg/thetis_mrv.py +67 -0
  51. agentdata/healthcare/__init__.py +7 -0
  52. agentdata/healthcare/au_tga.py +142 -0
  53. agentdata/healthcare/ca_dpd.py +123 -0
  54. agentdata/healthcare/ca_mdall.py +155 -0
  55. agentdata/healthcare/cli.py +375 -0
  56. agentdata/healthcare/ctgov.py +101 -0
  57. agentdata/healthcare/dailymed.py +102 -0
  58. agentdata/healthcare/epmc.py +108 -0
  59. agentdata/healthcare/eudamed.py +124 -0
  60. agentdata/healthcare/faces.py +811 -0
  61. agentdata/healthcare/fda_crl.py +86 -0
  62. agentdata/healthcare/fda_devices.py +199 -0
  63. agentdata/healthcare/fda_drugs.py +156 -0
  64. agentdata/healthcare/fda_recalls.py +137 -0
  65. agentdata/healthcare/gb_mhra.py +137 -0
  66. agentdata/healthcare/gudid.py +84 -0
  67. agentdata/healthcare/mesh.py +89 -0
  68. agentdata/healthcare/rasff.py +164 -0
  69. agentdata/healthcare/rxnav.py +67 -0
  70. agentdata/healthcare/safety_gate.py +175 -0
  71. agentdata/healthcare/shortages.py +107 -0
  72. agentdata/legal/__init__.py +4 -0
  73. agentdata/legal/afdb.py +93 -0
  74. agentdata/legal/ca_sema.py +143 -0
  75. agentdata/legal/cli.py +312 -0
  76. agentdata/legal/co_sic.py +70 -0
  77. agentdata/legal/courtlistener.py +77 -0
  78. agentdata/legal/csl.py +139 -0
  79. agentdata/legal/dgcomp.py +125 -0
  80. agentdata/legal/ecfr.py +129 -0
  81. agentdata/legal/faces.py +700 -0
  82. agentdata/legal/federalregister.py +188 -0
  83. agentdata/legal/gb_cma.py +117 -0
  84. agentdata/legal/gb_tna_caselaw.py +245 -0
  85. agentdata/legal/idb.py +85 -0
  86. agentdata/legal/lda.py +147 -0
  87. agentdata/legal/sipri.py +178 -0
  88. agentdata/legal/ukec.py +148 -0
  89. agentdata/legal/wb.py +92 -0
  90. agentdata/procurement/__init__.py +5 -0
  91. agentdata/procurement/br_pncp.py +126 -0
  92. agentdata/procurement/cli.py +155 -0
  93. agentdata/procurement/co_secop.py +88 -0
  94. agentdata/procurement/faces.py +239 -0
  95. agentdata/procurement/ny_open_data.py +82 -0
  96. agentdata/procurement/ungm.py +137 -0
  97. agentdata/procurement/wb_proc.py +84 -0
  98. agentdata/research/__init__.py +4 -0
  99. agentdata/research/biorxiv.py +66 -0
  100. agentdata/research/cli.py +129 -0
  101. agentdata/research/crossref.py +121 -0
  102. agentdata/research/faces.py +349 -0
  103. agentdata/research/gb_gtr.py +58 -0
  104. agentdata/research/icite.py +52 -0
  105. agentdata/research/nih_reporter.py +143 -0
  106. agentdata/research/nsf.py +68 -0
  107. agentdata/research/openalex.py +145 -0
  108. agentdata/research/opencitations.py +50 -0
  109. agentdata/security/__init__.py +3 -0
  110. agentdata/security/cisa_kev.py +42 -0
  111. agentdata/security/cli.py +37 -0
  112. agentdata/security/faces.py +91 -0
  113. agentdata/security/osv.py +72 -0
  114. agentdata/transport.py +541 -0
  115. agentdata-0.3.26.dist-info/METADATA +266 -0
  116. agentdata-0.3.26.dist-info/RECORD +120 -0
  117. agentdata-0.3.26.dist-info/WHEEL +5 -0
  118. agentdata-0.3.26.dist-info/entry_points.txt +10 -0
  119. agentdata-0.3.26.dist-info/licenses/LICENSE +201 -0
  120. agentdata-0.3.26.dist-info/top_level.txt +1 -0
agentdata/legal/cli.py ADDED
@@ -0,0 +1,312 @@
1
+ """legal cli — domain command-face registration (split per domain in 0.2.6, migrated from the root cli.py; purely flat since the 0.3.0 verb-layer + frozen-alias removal).
2
+
3
+ register(v, g): registers flat commands onto the domain subparser (g = root _leaf_globals factory,
4
+ passed in as a parameter to avoid a cli <-> domain-cli cycle); FLAT = flat dispatch table, merged by the root cli for main to use.
5
+ """
6
+ import argparse
7
+
8
+ def register(v, g):
9
+ # Flat commands (2026-10-23 ruling: verb layer abolished across all domains, <domain> <source>_<dataset> direct; new capabilities are born flat)
10
+ # —— 0.3.21 debarment-roster line (afdb/ca_sema/idb/wb, dictionary slots: afdb first, ca_sema second) ——
11
+ fa = v.add_parser("afdb_debarred_entities", parents=[g()],
12
+ help="African Development Bank debarred entities (afdb.org json-feed, local full-register scan)",
13
+ description="AfDB debarment register (keyless): the whole 1,440-row feed (FIRM 1,196 / "
14
+ "INDIVIDUAL 244) is pulled once, cached 24h and scanned locally. Basis = the "
15
+ "cross-debarment edge (CROSS DEBARMENT : WBG / CROSS-DEBARMENT: ADB — upstream "
16
+ "spacing inconsistent, so --basis is a contains-match on the bank token: WBG/ADB/"
17
+ "IDB/EBRD); To empty = ongoing. Empty result = clean screening (legitimate answer)")
18
+ fa.add_argument("--name", default=None, help="Entity/individual name (substring, case-insensitive, over Name)")
19
+ fa.add_argument("--type", default=None, choices=["FIRM", "INDIVIDUAL"], help="Subject type (upstream value domain, exact: Type column)")
20
+ fa.add_argument("--basis", default=None, help="Debarment basis, substring over Basis — filter on the bank token (WBG/ADB/IDB/EBRD; punctuation upstream is inconsistent)")
21
+ fa.add_argument("--limit", type=int, default=20, help="Rows to return (default 20, ≤100 local payload wall; total_matched echoes the full count)")
22
+
23
+ fb = v.add_parser("ca_sema_sanctions", parents=[g()],
24
+ help="Canada SEMA sanctions listings (Global Affairs consolidated list: persons/entities/ships)",
25
+ description="Canada SEMA sanctions (Consolidated Canadian Autonomous Sanctions List, keyless "
26
+ "sema-lmes.xml): 5,708 live listings — persons 3,526 / entities 2,181 of which 731 "
27
+ "ships with an IMO number (the join key onto shipping rosters). Bilingual cells "
28
+ "verbatim (\"Syria / Syrie\"); name screening contains-matches name + aliases "
29
+ "(matched_on echoes the hit face). Empty result = clean screening (legitimate answer)")
30
+ fb.add_argument("--name", default=None, help="Designated name (substring, case-insensitive, over name + aliases)")
31
+ fb.add_argument("--country", default=None, help="Country, substring over the bilingual cell (Syria and Syrie both hit)")
32
+ fb.add_argument("--limit", type=int, default=20, help="Rows to return (default 20, ≤100 local payload wall; total_matched echoes the full count)")
33
+
34
+ f0 = v.add_parser("co_sic_sanctions", parents=[g()],
35
+ help="Colombia SIC firm sanctions (datos.gov.co Socrata 73dx-n59j)",
36
+ description="Colombia SIC sanctions register (keyless): firm sanctions imposed "
37
+ "in firme by the Superintendencia de Industria y Comercio — multado (the sanctioned "
38
+ "party) × valor (fine, COP) × conducta (the violation) × sector, 3,938 rows 2013→. "
39
+ "Socrata $-parameter channel verbatim: --where raw SoQL, --limit 1-500, --offset, "
40
+ "--order column[ ASC|DESC] (same contract as co_secop_contracts; name-only key — no "
41
+ "NIT column, a name join onto co_rues_companies is the corpus line's job)")
42
+ f0.add_argument("--where", default=None, help="Raw SoQL filter (upstream param: $where; e.g. \"upper(multado) like '%%ECOPETROL%%'\")") # %% escaping: argparse help goes through % interpolation
43
+ f0.add_argument("--limit", type=int, default=20, help="Rows (upstream param: $limit; 1-500 politeness cap)")
44
+ f0.add_argument("--offset", type=int, default=0, help="Row offset (upstream param: $offset; Socrata sends no total echo)")
45
+ f0.add_argument("--order", default=None, help="Sort (upstream param: $order; e.g. 'fecha_emitida DESC'; no NULLS syntax — SoQL 400s)")
46
+
47
+ f1 = v.add_parser("courtlistener_opinions_search", parents=[g()],
48
+ help="US federal + state opinions search (CourtListener Search API v4)",
49
+ description="CourtListener opinions search (federal + state courts; CAP/api.case.law retired into this source): at least one filter required (full corpus ~8.3M bulk is served by the S3 batch line); upstream fixes page size at 20 — page via --cursor (next_cursor is echoed back). 0.2.9 audit snapshot (2026-10-28): the search endpoint now returns 401 for anonymous traffic (worked 2026-10-26) while other v4 endpoints stay open — face on watch per the keyless-only ruling; authenticated tiers are 5/min·50/hr·125/day, anonymous numbers unpublished upstream")
50
+ f1.add_argument("--q", default=None, help="Full-text query (upstream param: q)")
51
+ f1.add_argument("--case-name", default=None, help="Case-name filter (upstream param: case_name)")
52
+ f1.add_argument("--court", default=None, help="Court id, e.g. calctapp/scotus (upstream param: court)")
53
+ f1.add_argument("--judge", default=None, help="Judge name filter (upstream param: judge)")
54
+ f1.add_argument("--docket-number", default=None, help="Docket number filter (upstream param: docket_number)")
55
+ f1.add_argument("--filed-after", default=None, help="Filed on/after YYYY-MM-DD (upstream param: filed_after)")
56
+ f1.add_argument("--filed-before", default=None, help="Filed on/before YYYY-MM-DD (upstream param: filed_before)")
57
+ f1.add_argument("--order-by", default=None, help="Sort, e.g. 'score desc' (upstream param: order_by)")
58
+ f1.add_argument("--cursor", default=None, help="Opaque cursor from next_cursor (upstream param: cursor)")
59
+
60
+ # —— 0.2.11 legal new faces: UK CMA twin faces (search/content API, stood up from live probing 2026-10-29) ——
61
+
62
+ f2 = v.add_parser("csl_screening_list", parents=[g()],
63
+ help="Sanctions screening (US CSL — 12 list families, one CSV, local full-register scan)",
64
+ description="Name screening against the US Consolidated Screening List (realtime, local execution): "
65
+ "SDN / Entity List / DPL / ITAR / SSI … 12 list families, 26k rows in one keyless CSV, "
66
+ "cached 24h and scanned locally (matched_on shows which face hit: name or alt_names). "
67
+ "Empty result = clean screening (a legitimate answer)")
68
+ f2.add_argument("--name", default=None, help="Subject name (substring, case-insensitive; matches name + alt_names; required — the client gate returns a JSON usage_error envelope)")
69
+ f2.add_argument("--source", default=None, help="List-family filter, substring (e.g. \"Entity List\", \"SDN\", \"Sectoral\")")
70
+ f2.add_argument("--type", default=None, help="Subject type: Entity / Individual / Vessel / Aircraft (many rows carry no type)")
71
+ f2.add_argument("--limit", type=int, default=20, help="Rows to return (default 20, ≤100; total_matched echoes the full count)")
72
+
73
+ f3 = v.add_parser("dgcomp_cases_search", parents=[g()],
74
+ help="EU competition cases (DG COMP; aid/antitrust/merger/DMA)",
75
+ description="DG COMP case search: --query raw ES bool DSL (JSON text, XOR with sugar) XOR --name/--instrument sugar (assembles the SPA's three-field text query / caseInstrument term); pageSize hard cap 100 — upstream silently returns 100 rows for larger asks (probed 2026-10-26)")
76
+ f3.add_argument("--query", default=None, help='Raw ES bool DSL, JSON text — e.g. \'{"bool": {"must": [{"term": {"caseInstrument": "SA"}}]}}\' (upstream param: query)')
77
+ f3.add_argument("--name", default=None, help="Sugar — party/case-name text query over caseTitle/caseCompanies/caseOriginalTitle (XOR with --query)")
78
+ f3.add_argument("--instrument", default=None, help="Case-type filter: AT antitrust / M merger / SA state aid / DMA Digital Markets Act (sugar channel)")
79
+ f3.add_argument("--text", default="", help="Extra full-text condition (upstream param: text; empty = none)")
80
+ f3.add_argument("--page", type=int, default=1, help="Page number (upstream param: pageNumber)")
81
+ f3.add_argument("--limit", type=int, default=20, help="Rows (upstream param: pageSize; 1-100 hard wall — 200 silently yields 100, probed 2026-10-26)")
82
+
83
+ f4 = v.add_parser("ecfr_search", parents=[g()],
84
+ help="US Code of Federal Regulations current-state section search (eCFR)",
85
+ description="eCFR current-code section search (keyless, realtime): where a topic lives in the "
86
+ "codified CFR today — counterpart of fulltext (Federal Register = what changed today). "
87
+ "coords = seven-level citation; starts_on~ends_on = that version's effective window. "
88
+ "0.2.9 audit: --date = point-in-time retrieval (section versions in force on that date; "
89
+ "probed 2026-10-28 asbestos 1,436 current vs 339 at 2024-01-01)")
90
+ f4.add_argument("--q", default=None, help="Topic term (required — the client gate returns a JSON usage_error envelope)")
91
+ f4.add_argument("--page", type=int, default=1, help="Page number (default 1)")
92
+ f4.add_argument("--limit", type=int, default=20, help="Rows to return (default 20, ≤100)")
93
+ f4.add_argument("--date", default=None,
94
+ help="Point-in-time date YYYY-MM-DD (upstream param: date; section versions in force on that date — omit for current state)")
95
+
96
+ f5 = v.add_parser("ecfr_titles", parents=[g()],
97
+ help="CFR title catalog (eCFR: 50 titles + daily currency watermark)",
98
+ description="eCFR title catalog (keyless): the 50 CFR titles with latest amendment/issue dates "
99
+ "and the library-wide as_of watermark (eCFR integrates daily)")
100
+
101
+ # —— 0.2.0 flat panel commands (legal main faces: source-qualified names + upstream param names verbatim + our defaults) ——
102
+
103
+ f6 = v.add_parser("federalregister_documents", parents=[g()],
104
+ help="Federal Register docs search (1994+ daily release stream)",
105
+ description="Federal Register document search: q → conditions[term] verbatim; startdt/enddt auto-completed to a full window when either end is given (a single end is silently ignored upstream); document_number is the stable key, single-doc raw_text_url feeds the corpus batch line. 0.2.9 audit additions: --doc-type closed set validated locally (invalid values silently return 0 rows upstream — false-negative trap), --agencies slug filter, --cfr-title/--cfr-part CFR-anchored narrowing (part requires title), --order gains executive_order_number")
106
+ f6.add_argument("--q", required=True, help="Query term (upstream param: conditions[term]; quoted phrases go exact)")
107
+ f6.add_argument("--startdt", default=None, help="Start date YYYY-MM-DD (upstream param: conditions[publication_date][gte]; bare end auto-completed to 1994-01-01)")
108
+ f6.add_argument("--enddt", default=None, help="End date YYYY-MM-DD (upstream param: conditions[publication_date][lte]; bare start auto-completed to today)")
109
+ f6.add_argument("--order", default="relevance", choices=["relevance", "newest", "oldest", "executive_order_number"],
110
+ help="Result order (upstream param: order; fourth value executive_order_number audited in 2026-10-28)")
111
+ f6.add_argument("--doc-type", dest="doc_type", default=None, choices=["RULE", "PRORULE", "NOTICE", "PRESDOCU"],
112
+ help="Document type (upstream param: conditions[type][]; closed set — invalid values silently return 0 rows upstream, validated locally)")
113
+ f6.add_argument("--agencies", default=None,
114
+ help="Agency slug, e.g. environmental-protection-agency (upstream param: conditions[agencies][]; value domain from GET /api/v1/agencies)")
115
+ f6.add_argument("--cfr-title", dest="cfr_title", default=None, help="CFR title number, e.g. 40 (upstream param: conditions[cfr][title])")
116
+ f6.add_argument("--cfr-part", dest="cfr_part", default=None,
117
+ help="CFR part or part range, e.g. 763 or 1-50 (upstream param: conditions[cfr][part]; requires --cfr-title)")
118
+ f6.add_argument("--page", type=int, default=1, help="Page number (upstream param: page)")
119
+ f6.add_argument("--limit", type=int, default=20, help="Rows (maps to upstream per_page; politeness cap 50 — upstream cap 1000)")
120
+
121
+ f7 = v.add_parser("gb_cma_case", parents=[g()],
122
+ help="UK CMA single-case dossier (gov.uk content API)",
123
+ description="UK CMA single-case dossier (keyless): content_id is the stable UUID key; "
124
+ "change_history = case evolution newest→oldest ([0] = latest, last = First published); attachments = decision/notice PDF family; "
125
+ "body_html = case-page summary. A pasted full base_path (/cma-cases/<slug> or absolute URL) "
126
+ "is accepted — the prefix is stripped")
127
+ f7.add_argument("--slug", required=True, help="Case slug (from gb_cma_cases rows; bare tail segment, e.g. nexfibre-slash-substantial-merger-inquiry)")
128
+
129
+ f8 = v.add_parser("gb_cma_cases", parents=[g()],
130
+ help="UK competition cases (CMA; merger/market/investigation case list)",
131
+ description="UK CMA case search (gov.uk search API, keyless): q verbatim over case titles "
132
+ "(case name = party pair 'A / B'); count politeness cap 100 (upstream has no wall — "
133
+ "count=1000 yields the full 1000, probed 2026-10-29); start = offset pagination; "
134
+ "omit --q to enumerate the full live register (~2,591 cases). Single-case detail: gb_cma_case "
135
+ "with the slug from each row")
136
+ f8.add_argument("--q", default=None, help="Query term (upstream param: q; searches case titles/descriptions)")
137
+ f8.add_argument("--count", type=int, default=20, help="Rows (upstream param: count; 1-100 politeness cap — upstream wall-free, probed 2026-10-29)")
138
+ f8.add_argument("--start", type=int, default=0, help="Offset (upstream param: start)")
139
+
140
+ f9 = v.add_parser("gb_ec_donations", parents=[g()],
141
+ help="UK political donations (Electoral Commission /donations endpoint)",
142
+ description="UK EC donations face: query verbatim (donor/regulated-entity name); from/to window auto-completed when missing (missing window → upstream Total=-1 fake success); rows hard cap 50 (probed 2026-10-26)")
143
+ f9.add_argument("--query", required=True, help="Donor/regulated-entity name (upstream param: query; contains-match)")
144
+ f9.add_argument("--from", dest="from_", default=None, help="Window start YYYY-MM-DD (upstream param: from; default=register origin 2001-02-16)")
145
+ f9.add_argument("--to", default=None, help="Window end YYYY-MM-DD (upstream param: to; default=today)")
146
+ f9.add_argument("--start", type=int, default=None, help="Offset (upstream param: start)")
147
+ f9.add_argument("--limit", type=int, default=20, help="Rows (maps to upstream rows; 1-50 hard wall — 51/200 silently yield 50, probed 2026-10-26)")
148
+
149
+ f10 = v.add_parser("gb_ec_loans", parents=[g()],
150
+ help="UK political loans (Electoral Commission /loans endpoint)",
151
+ description="UK EC loans face: query verbatim (lender/borrower name); upstream loans endpoint takes no date-window params; rows hard cap 50 (probed 2026-10-26)")
152
+ f10.add_argument("--query", required=True, help="Lender/borrower name (upstream param: query; contains-match)")
153
+ f10.add_argument("--start", type=int, default=None, help="Offset (upstream param: start)")
154
+ f10.add_argument("--limit", type=int, default=20, help="Rows (maps to upstream rows; 1-50 hard wall — 51/200 silently yield 50, probed 2026-10-26)")
155
+
156
+ # —— 0.3.8 legal caselaw line: TNA Find Case Law two faces (atom search + Akoma Ntoso single record, live-probed 2026-10-29; dictionary-order slots between gb_ec_loans and lda; ISO-prefixed 0.3.13) ——
157
+
158
+ f11 = v.add_parser("gb_tna_caselaw_judgment", parents=[g()],
159
+ help="UK single-judgment dossier (Akoma Ntoso XML from Find Case Law)",
160
+ description="UK single-judgment dossier (keyless): neutral_citation is the official key; parties = "
161
+ "[{name, role}] official party-to-role edges (claimant/defendant, companies and individuals); "
162
+ "judges; text = plain-text body. document_uri takes all three forms — legacy ewhc/admin/2026/2298, "
163
+ "new-style d-<uuid>, tna.<fclid> — the uri field of gb_tna_caselaw_search rows; a pasted full URL "
164
+ "is accepted (prefix and /data.xml suffix stripped)")
165
+ f11.add_argument("--document-uri", dest="document_uri", required=True,
166
+ help="Document URI (from gb_tna_caselaw_search rows; e.g. ewhc/admin/2026/2298, d-<uuid> or tna.<fclid>)")
167
+
168
+ f12 = v.add_parser("gb_tna_caselaw_search", parents=[g()],
169
+ help="UK court judgments search (The National Archives Find Case Law; keyless)",
170
+ description="UK judgment search (TNA Find Case Law /atom.xml, keyless): eight upstream params verbatim — "
171
+ "party/judge are full-word matches over party/judge names, query is full text, court/tribunal are "
172
+ "court codes (repeatable, exploded); order in ±date/±updated/±transformation; per_page politeness "
173
+ "cap 50 (=upstream default). Rows carry neutral_citation (e.g. [2026] EWHC 2298 (Admin)), uri "
174
+ "(machine key for gb_tna_caselaw_judgment) and contenthash. Open Justice Licence: commercial reuse OK, "
175
+ "bulk computational analysis forbidden without a (free) licence — keep this a targeted search face")
176
+ f12.add_argument("--query", default=None, help="Full-text search (upstream param: query; quoted phrase matches words in order)")
177
+ f12.add_argument("--court", action="append", default=[], metavar="CODE",
178
+ help="Court code, repeatable (upstream param: court; e.g. uksc ewhc — explode form)")
179
+ f12.add_argument("--tribunal", action="append", default=[], metavar="CODE",
180
+ help="Tribunal code, repeatable (upstream param: tribunal; alias of court, e.g. eat)")
181
+ f12.add_argument("--party", default=None, help="Party-name word, full match (upstream param: party; e.g. Barclays)")
182
+ f12.add_argument("--judge", default=None, help="Judge-name word, full match (upstream param: judge)")
183
+ f12.add_argument("--order", default=None, choices=["date", "-date", "updated", "-updated", "transformation", "-transformation"],
184
+ help="Sort (upstream param: order; leading - = newest first; default -date)")
185
+ f12.add_argument("--page", type=int, default=1, help="Page number (upstream param: page; >=1)")
186
+ f12.add_argument("--per-page", dest="per_page", type=int, default=50,
187
+ help="Rows per page (upstream param: per_page; 1-50 politeness cap = upstream default)")
188
+ f12.add_argument("--minimum-availability", dest="minimum_availability", default=None,
189
+ choices=["metadata", "document", "full-text"],
190
+ help="Minimum representation level (upstream param: minimum_availability; default full-text)")
191
+
192
+ # —— 0.3.21 debarment-roster line cont. (dictionary slot: idb between gb_tna_caselaw_search and lda) ——
193
+ f_idb = v.add_parser("idb_debarred_entities", parents=[g()],
194
+ help="Inter-American Development Bank sanctioned firms/individuals (data.iadb.org CKAN, server-side)",
195
+ description="IDB sanctioned-firms register (keyless CKAN DataStore, server-side search; live "
196
+ "1,343 rows): q = full-text across all fields, filters = JSON field→value exact "
197
+ "matches (e.g. {\"To\": \"Ongoing\"} or {\"Entity\": \"Firm\"}), offset paging, "
198
+ "include_total always on. From arrives as nanosecond text kept verbatim "
199
+ "(\"2009-07-10 00:00:00.000000000\"); Source carries the cross-debarment markers "
200
+ "(ADB/WBG cross debarment). Empty result = clean screening (legitimate answer)")
201
+ f_idb.add_argument("--q", default=None, help="Full-text query across all fields (upstream param: q)")
202
+ f_idb.add_argument("--filters", default=None, help='Field→value exact matches, JSON text — e.g. \'{"To": "Ongoing"}\' (upstream param: filters)')
203
+ f_idb.add_argument("--limit", type=int, default=20, help="Rows (upstream param: limit; 1-100 politeness cap)")
204
+ f_idb.add_argument("--offset", type=int, default=0, help="Row offset (upstream param: offset; total echoes the register size)")
205
+ f_idb.add_argument("--sort", default=None, help="Sort, e.g. 'From desc' (upstream param: sort)")
206
+
207
+ f13 = v.add_parser("lda_filings", parents=[g()],
208
+ help="US lobbying filings (LDA; client/registrant dual name face)",
209
+ description="LDA lobbying-disclosure filings: client_name (lobbied company) / registrant_name (lobbying firm) / filing_year all upstream-verbatim (may combine); page_size hard-clamped 25 — upstream silently returns 25 rows for larger asks (probed 2026-10-26); iterate with --page (true offset pagination)")
210
+ f13.add_argument("--client-name", dest="client_name", default=None, help="Lobbied company name (upstream param: client_name; contains-match)")
211
+ f13.add_argument("--registrant-name", dest="registrant_name", default=None, help="Lobbying firm name (upstream param: registrant_name)")
212
+ f13.add_argument("--filing-year", dest="filing_year", type=int, default=None, help="Filing year from 1999 (upstream param: filing_year)")
213
+ f13.add_argument("--page", type=int, default=None, help="Page number (upstream param: page; each page max 25 rows — upstream hard clamp)")
214
+ f13.add_argument("--ordering", default="-dt_posted", help="Sort (upstream param: ordering; default -dt_posted)")
215
+ f13.add_argument("--limit", type=int, default=20, help="Rows (maps to upstream page_size; 1-25 hard wall — larger asks silently yield 25, probed 2026-10-26)")
216
+
217
+ f14 = v.add_parser("sipri_trades_search", parents=[g()],
218
+ help="Arms transfers, country level (SIPRI; delivered/orders)",
219
+ description="SIPRI arms transfers: supplier/recipient country names (sugar — resolves numeric EntityId via the 30-day cached country table; names in the raw filter value1 upstream → 500) XOR --filters raw JSON ({\"field\": \"Supplier\"|\"Recipient\", \"listData\": [EntityId]}); pageSize cap 100; country-level face, not company-level")
220
+ f14.add_argument("--filters", default=None, help='Raw filter list, JSON text — e.g. \'[{"field": "Recipient", "listData": [8]}]\' (upstream param: filters; numeric EntityIds only)')
221
+ f14.add_argument("--supplier", default=None, help="Supplier country, full English name (sugar — e.g. United States; XOR with --filters)")
222
+ f14.add_argument("--recipient", default=None, help="Recipient country, full English name (sugar — e.g. Angola; XOR with --filters)")
223
+ f14.add_argument("--type", default="delivered", choices=["delivered", "orders"], help="Measure (upstream param: type; delivered=trend-indicator, orders=orders placed)")
224
+ f14.add_argument("--page", type=int, default=1, help="Page number (upstream param: pageNo)")
225
+ f14.add_argument("--limit", type=int, default=20, help="Rows (upstream param: pageSize; 1-100 cap, upstream probed 2026-10-20)")
226
+
227
+ # —— 0.3.21 debarment-roster line cont. (dictionary slot: wb last) ——
228
+ f_wb = v.add_parser("wb_debarred_firms", parents=[g()],
229
+ help="World Bank debarred firms (SAP gateway full dump, local full-register scan)",
230
+ description="World Bank debarred-firms register (keyless via the page-embedded public apikey): "
231
+ "the whole 1,546-row dump is pulled once, cached 24h and scanned locally (same "
232
+ "contract as csl_screening_list). SUPP_ELIG_STAT DEBARRED 890 / X-DEBARRED 656; "
233
+ "DEBAR_TO_DATE 2999-12-31 = indefinite; DEBAR_REASON cites the procurement-"
234
+ "guidelines clause; China leads at 392 firms. The DNB/REGISTER identifier fields "
235
+ "ship empty — name × country is the join channel. Empty result = clean screening "
236
+ "(legitimate answer)")
237
+ f_wb.add_argument("--name", default=None, help="Firm name (substring, case-insensitive, over SUPP_NAME)")
238
+ f_wb.add_argument("--country", default=None, help="Country, substring over COUNTRY_NAME or exact LAND1 code (China and CN both hit)")
239
+ f_wb.add_argument("--type", default=None, choices=["F", "I", "U", "C"], help="Supplier type (SUPP_TYPE_CODE: F firm / I individual / U unspecified / C conglomerate)")
240
+ f_wb.add_argument("--limit", type=int, default=20, help="Rows to return (default 20, ≤100 local payload wall; total_matched echoes the full count)")
241
+
242
+ FLAT = {
243
+ # legal 0.2.0 flat panel (upstream param names verbatim + our defaults; lambdas only shuttle names across namespaces)
244
+ # csl_screening_list was the first flat command (Wave 7), ecfr_* Wave 8 (both merged on the main side) — folded into the same dispatch table
245
+ # 0.3.21 debarment-roster line: afdb/ca_sema head slots, idb/wb tail slots (dictionary order)
246
+ "afdb_debarred_entities": lambda a: {"name": a.name, "type": a.type, "basis": a.basis, "limit": a.limit},
247
+ "ca_sema_sanctions": lambda a: {"name": a.name, "country": a.country, "limit": a.limit},
248
+ "co_sic_sanctions": lambda a: {"where": a.where, "limit": a.limit, "offset": a.offset, "order": a.order},
249
+ "courtlistener_opinions_search": lambda a: {"q": a.q, "case_name": a.case_name,
250
+ "court": a.court, "judge": a.judge,
251
+ "docket_number": a.docket_number,
252
+ "filed_after": a.filed_after,
253
+ "filed_before": a.filed_before,
254
+ "order_by": a.order_by, "cursor": a.cursor},
255
+ # 0.2.11 UK CMA twin faces (gov.uk search/content API; upstream param names verbatim + our polite tier)
256
+ "csl_screening_list": lambda a: {"name": a.name, "source": a.source, "type": a.type, "limit": a.limit},
257
+ "dgcomp_cases_search": lambda a: {"query": a.query, "name": a.name, "instrument": a.instrument,
258
+ "text": a.text, "limit": a.limit, "page": a.page},
259
+ "ecfr_search": lambda a: {"q": a.q, "page": a.page, "limit": a.limit, "date": a.date},
260
+ "ecfr_titles": lambda a: {},
261
+ "federalregister_documents": lambda a: {"q": a.q, "startdt": a.startdt, "enddt": a.enddt,
262
+ "order": a.order, "page": a.page, "limit": a.limit,
263
+ "doc_type": a.doc_type, "agencies": a.agencies,
264
+ "cfr_title": a.cfr_title, "cfr_part": a.cfr_part},
265
+ "gb_cma_case": lambda a: {"slug": a.slug},
266
+ "gb_cma_cases": lambda a: {"q": a.q, "count": a.count, "start": a.start},
267
+ "gb_ec_donations": lambda a: {"query": a.query, "from_": a.from_, "to": a.to,
268
+ "limit": a.limit, "start": a.start},
269
+ "gb_ec_loans": lambda a: {"query": a.query, "limit": a.limit, "start": a.start},
270
+ # 0.3.8 TNA caselaw two faces (eight upstream-verbatim params; court/tribunal append into lists for explode)
271
+ "gb_tna_caselaw_judgment": lambda a: {"document_uri": a.document_uri},
272
+ "gb_tna_caselaw_search": lambda a: {"query": a.query, "court": a.court, "tribunal": a.tribunal,
273
+ "party": a.party, "judge": a.judge, "order": a.order,
274
+ "page": a.page, "per_page": a.per_page,
275
+ "minimum_availability": a.minimum_availability},
276
+ "idb_debarred_entities": lambda a: {"q": a.q, "filters": a.filters, "limit": a.limit,
277
+ "offset": a.offset, "sort": a.sort},
278
+ "lda_filings": lambda a: {"client_name": a.client_name, "registrant_name": a.registrant_name,
279
+ "filing_year": a.filing_year, "limit": a.limit, "page": a.page,
280
+ "ordering": a.ordering},
281
+ "sipri_trades_search": lambda a: {"filters": a.filters, "supplier": a.supplier,
282
+ "recipient": a.recipient, "type": a.type,
283
+ "page": a.page, "limit": a.limit},
284
+ "wb_debarred_firms": lambda a: {"name": a.name, "country": a.country, "type": a.type, "limit": a.limit},
285
+ }
286
+
287
+
288
+ # 0.3.10 jurisdiction chapter: face → jurisdiction (default = issuing authority's jurisdiction; institutions
289
+ # covering global phenomena = GLOBAL; vocabulary ISO-3166 alpha-2 + EU + GLOBAL) —— merged by the root cli.py
290
+ # into _JURISDICTION, the success envelope carries a jurisdiction chapter
291
+ COUNTRY = {
292
+ # 0.3.21: MDB debarment rosters = GLOBAL (cross-national registers, wb_procnotices precedent); CA SEMA = CA
293
+ "afdb_debarred_entities": "GLOBAL",
294
+ "ca_sema_sanctions": "CA",
295
+ "co_sic_sanctions": "CO",
296
+ "courtlistener_opinions_search": "US",
297
+ "csl_screening_list": "US",
298
+ "dgcomp_cases_search": "EU",
299
+ "ecfr_search": "US",
300
+ "ecfr_titles": "US",
301
+ "federalregister_documents": "US",
302
+ "gb_cma_case": "GB",
303
+ "gb_cma_cases": "GB",
304
+ "gb_ec_donations": "GB",
305
+ "gb_ec_loans": "GB",
306
+ "gb_tna_caselaw_judgment": "GB",
307
+ "gb_tna_caselaw_search": "GB",
308
+ "idb_debarred_entities": "GLOBAL",
309
+ "lda_filings": "US",
310
+ "sipri_trades_search": "GLOBAL",
311
+ "wb_debarred_firms": "GLOBAL",
312
+ }
@@ -0,0 +1,70 @@
1
+ """co_sic — Colombia SIC firm sanctions face provider (datos.gov.co Socrata dataset
2
+ 73dx-n59j "Sanciones impuestas en firme por la SIC"; keyless).
3
+
4
+ legal domain (0.3.16; admitted 2026-10-30, probed). Shape measured 2026-10-30:
5
+ GET /resource/73dx-n59j.json?$limit=2&$order=fecha_emitida DESC
6
+ → [rows…7 columns…] radicado (case number) / res_sancion (resolution number) /
7
+ fecha_emitida / multado (the sanctioned party, company or individual name) /
8
+ valor (fine amount, COP) / conducta (the violation) / sector — the register holds
9
+ **3,938 firm sanctions 2013→** ($select=count measured 2026-10-30). Socrata sends
10
+ the row array directly with no total echo — paging goes through $offset (same
11
+ contract as co_secop_contracts on the same platform).
12
+ Row-level golden fields: multado × valor × conducta × sector = the company × fine ×
13
+ violation × industry quartet for negative-signal screening. No document-number column
14
+ exists in this register (name-only key) — sanctioned names are the SIC's own public
15
+ record and are returned as-is; a name-based join onto co_rues_companies / co_secop
16
+ NITs is the corpus line's job, not this face's.
17
+ Socrata $-parameter channel, verbatim names (same shape as co_secop): $where raw SoQL
18
+ (passed through, no local wall) / $limit/$offset / $order = column[ ASC|DESC]
19
+ (no NULLS syntax — SoQL 400s, measured on the sibling dataset).
20
+ TTL: sanctions in firme change slowly (append-mostly register) — 24h; polite tier
21
+ 1 rps (anonymous Socrata throttling, same as the sibling co_secop face).
22
+ """
23
+ from ..errors import SdkError
24
+
25
+ provider = {
26
+ "id": "co_sic",
27
+ "baseUrl": "https://datos.gov.co",
28
+ "rate": {"rps": 1, "burst": 2},
29
+ "ops": {
30
+ "sanctions": {"path": "/resource/73dx-n59j.json"}, # Socrata $limit/$offset/$where/$order
31
+ },
32
+ "ttl": {"sanctions": 24 * 3600},
33
+ }
34
+
35
+ # row narrowing: the register has exactly seven columns — all seven are golden, kept verbatim
36
+ _FIELDS = ("radicado", "res_sancion", "fecha_emitida", "multado", "valor", "conducta", "sector")
37
+
38
+
39
+ def sanctions_from_upstream(data):
40
+ """Upstream SODA row array → sanction rows (pure function, offline-testable;
41
+ column names verbatim, no transformation — the register is already narrow)."""
42
+ rows = [{k: r.get(k) for k in _FIELDS} for r in data or []]
43
+ return {"rows": rows, "count": len(rows)}
44
+
45
+
46
+ def sanctions_raw(where=None, *, limit=20, offset=0, order=None, cache=None, no_cache=False):
47
+ """→ {rows[], count, limit_applied, params, _meta} (Socrata sends no total echo — paging
48
+ goes through offset; the corpus-wide count 3,938 was measured at admission).
49
+
50
+ where = $where raw SoQL (e.g. "upper(multado) like '%AVIANCA%'"); limit/$limit
51
+ politeness cap 1-500; offset/$offset ≥0; order/$order = column[ ASC|DESC]
52
+ (e.g. "fecha_emitida DESC")."""
53
+ from ..transport import call_provider
54
+
55
+ size = int(limit) if limit is not None else 20 # explicit None check — an `or default` swallows 0, the same trap as 0.2.10
56
+ if not 1 <= size <= 500:
57
+ raise SdkError("usage_error", provider="co_sic",
58
+ detail=f"limit must be 1-500 (politeness cap), got '{limit}'")
59
+ off = int(offset) if offset is not None else 0
60
+ if off < 0:
61
+ raise SdkError("usage_error", provider="co_sic", detail=f"offset must be ≥0, got '{offset}'")
62
+ params = {"$limit": size, "$offset": off}
63
+ w = str(where or "").strip() or None
64
+ if w:
65
+ params["$where"] = w
66
+ o = str(order or "").strip() or None
67
+ if o:
68
+ params["$order"] = o
69
+ r = call_provider(provider, "sanctions", params, cache=cache, no_cache=no_cache)
70
+ return {**sanctions_from_upstream(r["data"]), "limit_applied": size, "params": params, "_meta": r}
@@ -0,0 +1,77 @@
1
+ """courtlistener — CourtListener Search API v4 provider (Free Law Project; US federal + state case law; keyless).
2
+
3
+ Anonymous keyless GET search (probed 2026-10-26, type=o count 8.3M across the whole corpus) — this overturns the
4
+ old finding that "the API face needs a token": that held only for the dedicated endpoints. Covers federal +
5
+ **state courts** (the first probe hit California Court of Appeal) — the US state case-law face is carried by this
6
+ source too. Caselaw Access Project (CAP) api.case.law is retired (301 → case.law/docs SPA shell); its Harvard
7
+ case-law history is carried by this source (bulk goes over the S3 monthly archives).
8
+ Wall: page_size is silently ignored (always 20/page; only cursor paging is honored) — the param is not exposed.
9
+ """
10
+ provider = {
11
+ "id": "courtlistener",
12
+ "baseUrl": "https://www.courtlistener.com",
13
+ "rate": {"rps": 0.5, "burst": 3}, # no rate-limit response headers — conservative tier (anonymous shared pool)
14
+ "ops": {
15
+ "opinionsSearch": {"path": "/api/rest/v4/search/", "method": "GET", "idempotent": True},
16
+ },
17
+ "ttl": {"opinionsSearch": 24 * 3600},
18
+ }
19
+
20
+ _FILTER_PARAMS = ("q", "case_name", "court", "judge", "docket_number",
21
+ "filed_after", "filed_before") # faces walled on at-least-one filter (keep copy in sync)
22
+
23
+
24
+ def _opinion_row(o):
25
+ return {
26
+ "cluster_id": o.get("cluster_id"), "docket_id": o.get("docket_id"),
27
+ "case_name": o.get("caseName"), "case_name_full": o.get("caseNameFull"),
28
+ "court": o.get("court"), "court_id": o.get("court_id"),
29
+ "court_citation_string": o.get("court_citation_string"),
30
+ "date_filed": o.get("dateFiled"), "docket_number": o.get("docketNumber"),
31
+ "status": o.get("status"), "judge": o.get("judge"),
32
+ "suit_nature": o.get("suitNature"), "citations": o.get("citation") or [],
33
+ "cite_count": o.get("citeCount"), "neutral_cite": o.get("neutralCite"),
34
+ "absolute_url": o.get("absolute_url"),
35
+ "opinions": [{"id": x.get("id"), "download_url": x.get("download_url"),
36
+ "local_path": x.get("local_path"), "author_id": x.get("author_id"),
37
+ "cites": x.get("cites") or []}
38
+ for x in o.get("opinions") or []],
39
+ }
40
+
41
+
42
+ def opinions_search_raw(*, q=None, case_name=None, court=None, judge=None,
43
+ docket_number=None, filed_after=None, filed_before=None,
44
+ order_by=None, cursor=None, cache=None, no_cache=False):
45
+ """→ {total, count, rows, next_cursor, params, _meta}; /api/rest/v4/search/ GET with verbatim param names.
46
+ type is always "o" (opinions); at least one filter param required (local safety wall: a bare scan of the full
47
+ ~8.3M-opinion corpus belongs to the batch line; bulk goes over the S3 monthly archives);
48
+ page_size is not exposed (upstream silently ignores it, always 20/page);
49
+ cursor passes through for paging (next_cursor is recovered from the next URL in the response)."""
50
+ from urllib.parse import parse_qs, urlparse
51
+
52
+ from ..transport import call_provider
53
+ from ..errors import SdkError
54
+
55
+ given = {"q": q, "case_name": case_name, "court": court, "judge": judge,
56
+ "docket_number": docket_number, "filed_after": filed_after,
57
+ "filed_before": filed_before}
58
+ params = {k: str(v).strip() for k, v in given.items() if v not in (None, "")}
59
+ if not params:
60
+ raise SdkError("usage_error", provider="courtlistener",
61
+ detail="courtlistener_opinions_search requires at least one of q/case_name/court/judge/"
62
+ "docket_number/filed_after/filed_before"
63
+ " (local safety wall: a bare scan of the full ~8.3M-opinion corpus belongs to the batch line)")
64
+ upstream = {"type": "o", **params}
65
+ if order_by not in (None, ""):
66
+ upstream["order_by"] = str(order_by).strip()
67
+ if cursor not in (None, ""):
68
+ upstream["cursor"] = str(cursor).strip()
69
+ r = call_provider(provider, "opinionsSearch", upstream, cache=cache, no_cache=no_cache)
70
+ data = r["data"] or {}
71
+ rows = [_opinion_row(o) for o in data.get("results") or []]
72
+ next_cursor = None
73
+ nxt = data.get("next")
74
+ if nxt:
75
+ next_cursor = (parse_qs(urlparse(nxt).query).get("cursor") or [None])[0]
76
+ return {"total": data.get("count"), "count": len(rows), "rows": rows,
77
+ "next_cursor": next_cursor, "params": upstream, "_meta": r}
agentdata/legal/csl.py ADDED
@@ -0,0 +1,139 @@
1
+ """csl — US Consolidated Screening List provider (12 list families, 26k rows in one CSV).
2
+
3
+ Sanctions-screening negative-signal face. data.trade.gov/downloadable_consolidated_screening_list/v1/consolidated.csv:
4
+ SDN 19,471 + Entity List 3,419 + DPL 1,596 + ITAR Debarred (DTC) 801 + SSI 286 + MEU 70 etc. — 12 list families
5
+ merged into 26,173 rows × 29 columns (full-sweep value domain probed 2026-10-28; the row count drifts as the official
6
+ file is regenerated). The official page lists 13 families — FSE (Foreign Sanctions Evaders) is registered but currently
7
+ has zero rows in the CSV.
8
+ Name screening = **local full-register scan** (upstream search.trade.gov requires a key, not on the roadmap) — pull the
9
+ whole 16.8MB CSV once, cache 24h, then any number of screenings run with zero network (the keyless equivalent of
10
+ quota-free batch screening).
11
+ Multi-value columns are semicolon-delimited (programs/alt_names/addresses/ids/citizenships/places_of_birth);
12
+ the type column can be empty on EL/ACN etc. rows; the source filter is a substring match — "ITAR Debarred (DTC)"
13
+ contains the "ITAR" substring, so --source ITAR matches (value domain verified 2026-10-28).
14
+ Screening semantics: name contains-match (case-insensitive) against both name + alt_names; matched_on echoes the hit face.
15
+ 0.2.9 audit (2026-10-28): row mapping extended from 15 to 28 columns — remarks (SDN passport/DOB/relationship details
16
+ often live only here), places_of_birth×citizenships (identity-disambiguation pair), federal_register_notice (joins the FR
17
+ face), license_policy (the upstream spec keeps both columns — mapped through), standard_order, the six vessel fields
18
+ (call_sign = alternative vessel identifier key), source_list_url, title — all incorporated.
19
+ TTL: screening 24h (official rolling regeneration); polite rate 0.2 rps (single-file face, one pull per day suffices).
20
+ """
21
+ import csv
22
+ import io
23
+
24
+ from ..errors import SdkError
25
+
26
+ provider = {
27
+ "id": "csl",
28
+ "baseUrl": "https://data.trade.gov",
29
+ "rate": {"rps": 0.2, "burst": 1},
30
+ "ops": {
31
+ "screening": {"path": "/downloadable_consolidated_screening_list/v1/consolidated.csv",
32
+ "accept": "text", "timeoutMs": 180000},
33
+ },
34
+ "ttl": {"screening": 24 * 3600},
35
+ }
36
+
37
+
38
+ def _split_semi(v):
39
+ return [x.strip() for x in str(v or "").split(";") if x.strip()]
40
+
41
+
42
+ def rows_from_csv(text):
43
+ """CSV raw text → object rows (header-driven). Returns [] when there is no data row."""
44
+ if not text:
45
+ return []
46
+ reader = csv.reader(io.StringIO(text))
47
+ try:
48
+ head = next(reader)
49
+ except StopIteration:
50
+ return []
51
+ head = [h.strip() for h in head]
52
+ out = []
53
+ for rec in reader:
54
+ if len(rec) != len(head): # ragged rows with a mismatched column count are dropped
55
+ continue
56
+ out.append(dict(zip(head, rec)))
57
+ return out
58
+
59
+
60
+ def _row_to_screening(o, matched_on, matched_value):
61
+ return {
62
+ "name": o.get("name") or None,
63
+ "alt_names": _split_semi(o.get("alt_names")),
64
+ "matched_on": matched_on, # name | alt_names (hit face echo)
65
+ "matched_value": matched_value, # matched text verbatim (≠ name on the alt_names face)
66
+ "type": o.get("type") or None, # Entity/Individual/Vessel/Aircraft; empty on EL/ACN etc. rows
67
+ "source_list": o.get("source") or None, # list family ("Entity List (EL) - Bureau of Industry and Security")
68
+ "programs": _split_semi(o.get("programs")),
69
+ "entity_number": o.get("entity_number") or None,
70
+ "ids": _split_semi(o.get("ids")), # passport/registration-number identifiers ("Passport X" form)
71
+ "addresses": _split_semi(o.get("addresses")),
72
+ "nationalities": _split_semi(o.get("nationalities")),
73
+ "dates_of_birth": _split_semi(o.get("dates_of_birth")),
74
+ "vessel_owner": o.get("vessel_owner") or None,
75
+ "license_requirement": o.get("license_requirement") or None,
76
+ "license_policy": o.get("license_policy") or None, # 0.2.9 audit add (upstream spec keeps both columns — mapped through)
77
+ "title": o.get("title") or None, # person title ("Minister of Defense" form; empty on entity rows)
78
+ "federal_register_notice": o.get("federal_register_notice") or None, # FR citation (joins the FR face)
79
+ "standard_order": o.get("standard_order") or None,
80
+ "citizenships": _split_semi(o.get("citizenships")), # 0.2.9 audit add: identity-disambiguation pair
81
+ "places_of_birth": _split_semi(o.get("places_of_birth")), # 0.2.9 audit add (paired with citizenships)
82
+ "remarks": o.get("remarks") or None, # 0.2.9 audit add: SDN passport/DOB/relationship details often live only here
83
+ "call_sign": o.get("call_sign") or None, # 0.2.9 audit add: vessel six-pack (call_sign = alternative identifier key)
84
+ "vessel_type": o.get("vessel_type") or None,
85
+ "gross_tonnage": o.get("gross_tonnage") or None,
86
+ "gross_registered_tonnage": o.get("gross_registered_tonnage") or None,
87
+ "vessel_flag": o.get("vessel_flag") or None,
88
+ "source_list_url": o.get("source_list_url") or None,
89
+ "start_date": o.get("start_date") or None,
90
+ "end_date": o.get("end_date") or None,
91
+ "source_information_url": o.get("source_information_url") or None,
92
+ }
93
+
94
+
95
+ def filter_rows(rows, name=None, source=None, type=None):
96
+ """Screening hit rows (pure function for offline regression). name: contains-match (case-insensitive)
97
+ against name + alt_names; source: list-family substring; type: exact (case-insensitive)."""
98
+ needle = str(name or "").strip().lower()
99
+ if not needle:
100
+ raise SdkError("usage_error", provider="csl",
101
+ detail="screening requires name (subject name, contains-matches name+alt_names)")
102
+ src_needle = str(source).lower() if source else None
103
+ type_needle = str(type).strip().lower() if type else None
104
+ hits = []
105
+ for o in rows:
106
+ if src_needle and src_needle not in str(o.get("source") or "").lower():
107
+ continue
108
+ if type_needle and str(o.get("type") or "").strip().lower() != type_needle:
109
+ continue
110
+ nm = str(o.get("name") or "")
111
+ if needle in nm.lower():
112
+ hits.append(_row_to_screening(o, "name", nm))
113
+ continue
114
+ alt = next((a for a in _split_semi(o.get("alt_names")) if needle in a.lower()), None)
115
+ if alt:
116
+ hits.append(_row_to_screening(o, "alt_names", alt))
117
+ return hits
118
+
119
+
120
+ def screen_list(name=None, source=None, type=None, limit=20, cache=None, no_cache=False):
121
+ """Full-register screening (pulls the whole CSV, caches 24h, then scans locally)
122
+ → {total_matched, scanned_rows, limit_applied, rows}.
123
+
124
+ The upstream search.trade.gov search face requires a key (not on the roadmap) — the local filter is the
125
+ keyless equivalent of quota-free batch screening.
126
+ """
127
+ from ..transport import call_provider
128
+
129
+ r = call_provider(provider, "screening", {}, cache=cache, no_cache=no_cache)
130
+ rows = rows_from_csv(r["data"])
131
+ size = min(max(1, int(limit or 20)), 100) # local payload wall (envelope row cap): truncation stays visible
132
+ hits = filter_rows(rows, name=name, source=source, type=type)
133
+ return {
134
+ "total_matched": len(hits), # local full scan: hit count is exact (not upstream-clamped)
135
+ "scanned_rows": len(rows), # rows scanned (audit echo: the full register really was scanned)
136
+ "limit_applied": size,
137
+ "rows": hits[:size],
138
+ "_meta": r,
139
+ }