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.
- agentdata/__init__.py +33 -0
- agentdata/__main__.py +4 -0
- agentdata/cache.py +115 -0
- agentdata/cli.py +173 -0
- agentdata/client.py +158 -0
- agentdata/company/__init__.py +3 -0
- agentdata/company/cf.py +145 -0
- agentdata/company/ch_zefix.py +100 -0
- agentdata/company/cli.py +243 -0
- agentdata/company/co_rues.py +72 -0
- agentdata/company/edgar_cik.py +84 -0
- agentdata/company/edgar_fts.py +101 -0
- agentdata/company/faces.py +746 -0
- agentdata/company/fdic_bankfind.py +107 -0
- agentdata/company/fi_ytj.py +89 -0
- agentdata/company/fr_recherche.py +124 -0
- agentdata/company/fts.py +121 -0
- agentdata/company/gleif.py +178 -0
- agentdata/company/kr_hometax.py +112 -0
- agentdata/company/no_brreg.py +124 -0
- agentdata/company/ror.py +105 -0
- agentdata/company/ted.py +117 -0
- agentdata/company/usaspending.py +124 -0
- agentdata/company/vies.py +66 -0
- agentdata/daterange.py +35 -0
- agentdata/economy/__init__.py +10 -0
- agentdata/economy/bis.py +132 -0
- agentdata/economy/cli.py +199 -0
- agentdata/economy/ecb.py +117 -0
- agentdata/economy/eurostat.py +163 -0
- agentdata/economy/faces.py +573 -0
- agentdata/economy/fred.py +92 -0
- agentdata/economy/ilo_stat.py +99 -0
- agentdata/economy/imf_sdmx.py +133 -0
- agentdata/economy/un_comtrade.py +199 -0
- agentdata/economy/unsd_sdg.py +154 -0
- agentdata/economy/wb.py +208 -0
- agentdata/economy/who_xmart.py +177 -0
- agentdata/entries.py +30 -0
- agentdata/errors.py +38 -0
- agentdata/esg/__init__.py +5 -0
- agentdata/esg/cli.py +180 -0
- agentdata/esg/climatetrace.py +260 -0
- agentdata/esg/eiti.py +187 -0
- agentdata/esg/epa_frs.py +138 -0
- agentdata/esg/euasl.py +75 -0
- agentdata/esg/eutl.py +66 -0
- agentdata/esg/faces.py +498 -0
- agentdata/esg/paris_mou.py +240 -0
- agentdata/esg/thetis_mrv.py +67 -0
- agentdata/healthcare/__init__.py +7 -0
- agentdata/healthcare/au_tga.py +142 -0
- agentdata/healthcare/ca_dpd.py +123 -0
- agentdata/healthcare/ca_mdall.py +155 -0
- agentdata/healthcare/cli.py +375 -0
- agentdata/healthcare/ctgov.py +101 -0
- agentdata/healthcare/dailymed.py +102 -0
- agentdata/healthcare/epmc.py +108 -0
- agentdata/healthcare/eudamed.py +124 -0
- agentdata/healthcare/faces.py +811 -0
- agentdata/healthcare/fda_crl.py +86 -0
- agentdata/healthcare/fda_devices.py +199 -0
- agentdata/healthcare/fda_drugs.py +156 -0
- agentdata/healthcare/fda_recalls.py +137 -0
- agentdata/healthcare/gb_mhra.py +137 -0
- agentdata/healthcare/gudid.py +84 -0
- agentdata/healthcare/mesh.py +89 -0
- agentdata/healthcare/rasff.py +164 -0
- agentdata/healthcare/rxnav.py +67 -0
- agentdata/healthcare/safety_gate.py +175 -0
- agentdata/healthcare/shortages.py +107 -0
- agentdata/legal/__init__.py +4 -0
- agentdata/legal/afdb.py +93 -0
- agentdata/legal/ca_sema.py +143 -0
- agentdata/legal/cli.py +312 -0
- agentdata/legal/co_sic.py +70 -0
- agentdata/legal/courtlistener.py +77 -0
- agentdata/legal/csl.py +139 -0
- agentdata/legal/dgcomp.py +125 -0
- agentdata/legal/ecfr.py +129 -0
- agentdata/legal/faces.py +700 -0
- agentdata/legal/federalregister.py +188 -0
- agentdata/legal/gb_cma.py +117 -0
- agentdata/legal/gb_tna_caselaw.py +245 -0
- agentdata/legal/idb.py +85 -0
- agentdata/legal/lda.py +147 -0
- agentdata/legal/sipri.py +178 -0
- agentdata/legal/ukec.py +148 -0
- agentdata/legal/wb.py +92 -0
- agentdata/procurement/__init__.py +5 -0
- agentdata/procurement/br_pncp.py +126 -0
- agentdata/procurement/cli.py +155 -0
- agentdata/procurement/co_secop.py +88 -0
- agentdata/procurement/faces.py +239 -0
- agentdata/procurement/ny_open_data.py +82 -0
- agentdata/procurement/ungm.py +137 -0
- agentdata/procurement/wb_proc.py +84 -0
- agentdata/research/__init__.py +4 -0
- agentdata/research/biorxiv.py +66 -0
- agentdata/research/cli.py +129 -0
- agentdata/research/crossref.py +121 -0
- agentdata/research/faces.py +349 -0
- agentdata/research/gb_gtr.py +58 -0
- agentdata/research/icite.py +52 -0
- agentdata/research/nih_reporter.py +143 -0
- agentdata/research/nsf.py +68 -0
- agentdata/research/openalex.py +145 -0
- agentdata/research/opencitations.py +50 -0
- agentdata/security/__init__.py +3 -0
- agentdata/security/cisa_kev.py +42 -0
- agentdata/security/cli.py +37 -0
- agentdata/security/faces.py +91 -0
- agentdata/security/osv.py +72 -0
- agentdata/transport.py +541 -0
- agentdata-0.3.26.dist-info/METADATA +266 -0
- agentdata-0.3.26.dist-info/RECORD +120 -0
- agentdata-0.3.26.dist-info/WHEEL +5 -0
- agentdata-0.3.26.dist-info/entry_points.txt +10 -0
- agentdata-0.3.26.dist-info/licenses/LICENSE +201 -0
- agentdata-0.3.26.dist-info/top_level.txt +1 -0
agentdata/legal/faces.py
ADDED
|
@@ -0,0 +1,700 @@
|
|
|
1
|
+
"""legal faces — flat face-method mixin (split per domain in 0.2.6, moved in from client.py _Domain).
|
|
2
|
+
|
|
3
|
+
Mixed into the root _Domain (assembled via MRO, see client.py); discipline: no __init__, depends only on the
|
|
4
|
+
root internals (_flat_env/_c/_request…), method names globally unique across domains (a collision
|
|
5
|
+
would shadow another domain's face).
|
|
6
|
+
"""
|
|
7
|
+
from ..errors import SdkError
|
|
8
|
+
from ..transport import resolve_execution
|
|
9
|
+
from ..transport import enrich_source
|
|
10
|
+
import re
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
from .. import legal # domain-module path call (historical shape, kept for minimal disturbance)
|
|
14
|
+
|
|
15
|
+
class LegalFaces:
|
|
16
|
+
def _lg_only(self, tool: str):
|
|
17
|
+
if self.name != "legal":
|
|
18
|
+
raise SdkError("usage_error", detail=f"{tool} is a legal-domain capability (not available on the {self.name} domain)")
|
|
19
|
+
|
|
20
|
+
def dgcomp_cases_search(self, query=None, *, name=None, instrument=None, text="",
|
|
21
|
+
limit=20, page=1, execution=None, no_cache=None):
|
|
22
|
+
"""legal dgcomp_cases_search: EU competition cases (DG COMP webgate ES; query = the bare DSL
|
|
23
|
+
verbatim-name channel × name/instrument sugar assembling the DSL; pageSize wall 1-100 —
|
|
24
|
+
verified 2026-10-26, 200 → silently 100)."""
|
|
25
|
+
self._lg_only("dgcomp_cases_search")
|
|
26
|
+
deps, assembled_from = [], None
|
|
27
|
+
q = str(name or "").strip() or None
|
|
28
|
+
if isinstance(query, str): # the CLI face hands us JSON text — normalize to a bare DSL dict
|
|
29
|
+
import json as _json
|
|
30
|
+
try:
|
|
31
|
+
query = _json.loads(query)
|
|
32
|
+
except ValueError:
|
|
33
|
+
raise SdkError("usage_error", provider="dgcomp",
|
|
34
|
+
detail="query must be a bare ES bool DSL dict in valid JSON (the CLI face passes JSON text)")
|
|
35
|
+
if query is not None and not isinstance(query, dict):
|
|
36
|
+
raise SdkError("usage_error", provider="dgcomp",
|
|
37
|
+
detail="query must be a dict-shaped bare ES bool DSL (e.g. {\"bool\": {\"must\": […]}})")
|
|
38
|
+
if query is not None and (q or instrument):
|
|
39
|
+
raise SdkError("usage_error", provider="dgcomp",
|
|
40
|
+
detail="query (verbatim-name channel: bare ES bool DSL) and name/instrument (sugar assembles the DSL) are mutually exclusive — give only one")
|
|
41
|
+
if query is None and not q and not instrument:
|
|
42
|
+
raise SdkError("usage_error", provider="dgcomp",
|
|
43
|
+
detail="dgcomp_cases_search requires query (bare ES DSL) or name/instrument — at least one of them")
|
|
44
|
+
if query is None and (q or instrument):
|
|
45
|
+
assembled_from = {k: v for k, v in (("name", q), ("instrument", instrument)) if v}
|
|
46
|
+
deps.append("name/instrument sugar assembles the ES bool DSL (text three-field / term caseInstrument) — "
|
|
47
|
+
"the sugar stays (not deprecated); use query (bare DSL dict) for the verbatim-name channel")
|
|
48
|
+
r = legal.dgcomp.search_raw(query, name=q, instrument=instrument, text=text,
|
|
49
|
+
limit=limit, page=page, cache=self._c._cache,
|
|
50
|
+
no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
|
|
51
|
+
result = {k: x for k, x in r.items() if k != "_meta"}
|
|
52
|
+
quartet = {"source": "assembled" if assembled_from else "raw",
|
|
53
|
+
"upstream": r["params"]}
|
|
54
|
+
if assembled_from:
|
|
55
|
+
quartet["assembled_from"] = assembled_from
|
|
56
|
+
return self._flat_env(
|
|
57
|
+
tool="dgcomp_cases_search",
|
|
58
|
+
input={"query": query, "name": q, "instrument": instrument, "text": text,
|
|
59
|
+
"limit": result["limit_applied"], "page": page},
|
|
60
|
+
reason=(f"DG COMP: {result['total'] or len(result['rows'])} cases hit"
|
|
61
|
+
+ (f" (party/case name contains \"{q or instrument}\")" if q or instrument else " (bare DSL query)")
|
|
62
|
+
+ f", returning {len(result['rows'])} rows — on SA cases title = the beneficiary company name (the subsidy event stream)"
|
|
63
|
+
if result["rows"] else f"DG COMP zero hits (an empty result is a legitimate answer — the query was too narrow or the party is not in the case register)"),
|
|
64
|
+
result=result,
|
|
65
|
+
evidence=[{"quote": f"{c['case_number'] or '—'} {c['title'] or '—'}"
|
|
66
|
+
f" ({c['instrument'] or '—'}, {'; '.join(c['companies']) if c['companies'] else '—'})",
|
|
67
|
+
"url": c["url"] or "https://competition-cases.ec.europa.eu/search"}
|
|
68
|
+
for c in result["rows"][:5]],
|
|
69
|
+
provider_id="dgcomp", metas=[r["_meta"]],
|
|
70
|
+
query=quartet, deprecations=deps or None,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
def lda_filings(self, client_name=None, registrant_name=None, filing_year=None, *,
|
|
74
|
+
limit=20, page=None, ordering="-dt_posted", execution=None, no_cache=None):
|
|
75
|
+
"""legal lda_filings: US lobbying filings (LDA DRF; client_name/registrant_name/filing_year/
|
|
76
|
+
page/ordering go through with verbatim names, limit→page_size with a 1-25 wall — verified
|
|
77
|
+
2026-10-26, 26/50/100/500 silently return only 25)."""
|
|
78
|
+
self._lg_only("lda_filings")
|
|
79
|
+
cn = str(client_name or "").strip() or None
|
|
80
|
+
rn = str(registrant_name or "").strip() or None
|
|
81
|
+
if not cn and not rn and filing_year is None:
|
|
82
|
+
raise SdkError("usage_error", provider="lda",
|
|
83
|
+
detail="lda_filings requires client_name / registrant_name / filing_year — at least one of the three"
|
|
84
|
+
" (local safety wall: enumerating the full 70,545+ row corpus belongs to the batch line)")
|
|
85
|
+
r = legal.lda.search_raw(client_name=cn, registrant_name=rn, filing_year=filing_year,
|
|
86
|
+
limit=limit, page=page, ordering=ordering,
|
|
87
|
+
cache=self._c._cache,
|
|
88
|
+
no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
|
|
89
|
+
result = {k: x for k, x in r.items() if k != "_meta"}
|
|
90
|
+
top = (result["rows"] or [None])[0]
|
|
91
|
+
return self._flat_env(
|
|
92
|
+
tool="lda_filings",
|
|
93
|
+
input={"client_name": cn, "registrant_name": rn, "filing_year": filing_year,
|
|
94
|
+
"limit": result["limit_applied"], "page": page, "ordering": ordering},
|
|
95
|
+
reason=(f"LDA lobbying face: {result['total'] or len(result['rows'])} filings hit, returning {len(result['rows'])} rows"
|
|
96
|
+
+ (f" (lobbied company \"{cn}\")" if cn else "")
|
|
97
|
+
+ (f" (lobbying firm \"{rn}\")" if rn else "")
|
|
98
|
+
+ " — income/expenses appear only on quarterly reports (always null on RR/RA registration filings); page through page (25-row per-page cap, upstream hard clamp)"
|
|
99
|
+
if result["rows"] else "LDA zero hits (an empty result is a legitimate answer; names match by contains — try swapping short/full name forms)"),
|
|
100
|
+
result=result,
|
|
101
|
+
evidence=[{"quote": f"{f['period'] or f['filing_type'] or '—'} {f['client']['name'] if f['client'] else '—'}"
|
|
102
|
+
f" ← {f['registrant']['name'] if f['registrant'] else '—'}"
|
|
103
|
+
+ (f" income=${f['income']}" if f["income"] else ""),
|
|
104
|
+
"url": f["document_url"] or "https://lda.gov/api/v1/filings/"}
|
|
105
|
+
for f in result["rows"][:5]],
|
|
106
|
+
provider_id="lda", metas=[r["_meta"]],
|
|
107
|
+
query={"source": "raw", "upstream": r["params"]},
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
def gb_ec_donations(self, query, *, from_=None, to=None, limit=20, start=None,
|
|
111
|
+
execution=None, no_cache=None):
|
|
112
|
+
"""legal gb_ec_donations: UK political donations (the UK EC /donations single-endpoint face; query goes
|
|
113
|
+
through verbatim, from_/to→from/to — we complete a missing window because a missing window → upstream
|
|
114
|
+
Total=-1 fake success; rows wall 1-50, verified 2026-10-26)."""
|
|
115
|
+
self._lg_only("gb_ec_donations")
|
|
116
|
+
q = str(query or "").strip()
|
|
117
|
+
if not q:
|
|
118
|
+
raise SdkError("usage_error", provider="ukec",
|
|
119
|
+
detail="gb_ec_donations requires query (donor / regulated-entity name, upstream verbatim name)")
|
|
120
|
+
cache, nc = self._c._cache, self._c.no_cache if no_cache is None else bool(no_cache)
|
|
121
|
+
d = legal.ukec.donations_raw(q, _from=from_, to=to, limit=limit,
|
|
122
|
+
start=start or 0, cache=cache, no_cache=nc)
|
|
123
|
+
rows = d["rows"]
|
|
124
|
+
dr = d.get("date_range") or {}
|
|
125
|
+
return self._flat_env(
|
|
126
|
+
tool="gb_ec_donations",
|
|
127
|
+
input={"query": q, "from_": from_, "to": to,
|
|
128
|
+
"limit": d["limit_applied"], "start": start or 0},
|
|
129
|
+
reason=(f"UK EC political donations: {d['total'] or len(rows)} records (\"{q}\", window {dr.get('from')}~{dr.get('to')}; "
|
|
130
|
+
f"50-row cap — upstream rows hard clamp, verified 2026-10-26)"
|
|
131
|
+
if rows else f"UK EC zero hits: no political donation records for \"{q}\" (an empty result is a legitimate answer)"),
|
|
132
|
+
result={"total": d["total"], "rows": rows, "date_range": dr},
|
|
133
|
+
evidence=[{"quote": f"donation {x['donor_or_lender'] or '—'} → {x['recipient'] or '—'} "
|
|
134
|
+
f"£{x['value_gbp'] or '—'} ({x['date'] or '—'})",
|
|
135
|
+
"url": "https://search.electoralcommission.org.uk/"}
|
|
136
|
+
for x in rows[:5]],
|
|
137
|
+
provider_id="ukec", metas=[d["_meta"]],
|
|
138
|
+
query={"source": "raw", "query": q, "upstream": d["params"]},
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
def gb_ec_loans(self, query, *, limit=20, start=None, execution=None, no_cache=None):
|
|
142
|
+
"""legal gb_ec_loans: UK political loans (the UK EC /loans single-endpoint face; the upstream loans
|
|
143
|
+
endpoint takes no date-window params)."""
|
|
144
|
+
self._lg_only("gb_ec_loans")
|
|
145
|
+
q = str(query or "").strip()
|
|
146
|
+
if not q:
|
|
147
|
+
raise SdkError("usage_error", provider="ukec",
|
|
148
|
+
detail="gb_ec_loans requires query (lender / borrower name, upstream verbatim name)")
|
|
149
|
+
cache, nc = self._c._cache, self._c.no_cache if no_cache is None else bool(no_cache)
|
|
150
|
+
l = legal.ukec.loans_raw(q, limit=limit, start=start or 0, cache=cache, no_cache=nc)
|
|
151
|
+
rows = l["rows"]
|
|
152
|
+
return self._flat_env(
|
|
153
|
+
tool="gb_ec_loans",
|
|
154
|
+
input={"query": q, "limit": l["limit_applied"], "start": start or 0},
|
|
155
|
+
reason=(f"UK EC political loans: {l['total'] or len(rows)} records (\"{q}\"; 50-row cap — upstream rows hard clamp, verified 2026-10-26)"
|
|
156
|
+
if rows else f"UK EC zero hits: no political loan records for \"{q}\" (an empty result is a legitimate answer)"),
|
|
157
|
+
result={"total": l["total"], "rows": rows},
|
|
158
|
+
evidence=[{"quote": f"loan {x['donor_or_lender'] or '—'} → {x['recipient'] or '—'} "
|
|
159
|
+
f"£{x['value_gbp'] or '—'} ({x['date'] or '—'})",
|
|
160
|
+
"url": "https://search.electoralcommission.org.uk/"}
|
|
161
|
+
for x in rows[:5]],
|
|
162
|
+
provider_id="ukec", metas=[l["_meta"]],
|
|
163
|
+
query={"source": "raw", "query": q, "upstream": l["params"]},
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
def sipri_trades_search(self, filters=None, *, supplier=None, recipient=None, type="delivered",
|
|
167
|
+
page=1, limit=20, execution=None, no_cache=None):
|
|
168
|
+
"""legal sipri_trades_search: country-level arms-transfer face (SIPRI atbackend; filters = the raw POST
|
|
169
|
+
body verbatim-name channel (numeric EntityId in listData) × supplier/recipient country-name sugar
|
|
170
|
+
(resolved against the 30-day cached EntityId table first); pageSize wall 1-100, upstream cap)."""
|
|
171
|
+
self._lg_only("sipri_trades_search")
|
|
172
|
+
deps, assembled_from = [], None
|
|
173
|
+
sp = str(supplier or "").strip() or None
|
|
174
|
+
rc = str(recipient or "").strip() or None
|
|
175
|
+
if isinstance(filters, str): # the CLI face hands us JSON text — normalize to a bare filter list
|
|
176
|
+
import json as _json
|
|
177
|
+
try:
|
|
178
|
+
filters = _json.loads(filters)
|
|
179
|
+
except ValueError:
|
|
180
|
+
raise SdkError("usage_error", provider="sipri",
|
|
181
|
+
detail="filters must be a valid-JSON filter list (the CLI face passes JSON text)")
|
|
182
|
+
if filters is not None and (sp or rc):
|
|
183
|
+
raise SdkError("usage_error", provider="sipri",
|
|
184
|
+
detail="filters (verbatim-name channel: numeric EntityId in listData) and supplier/recipient (sugar: country names resolved first) are mutually exclusive — give only one")
|
|
185
|
+
if filters is None and not sp and not rc:
|
|
186
|
+
raise SdkError("usage_error", provider="sipri",
|
|
187
|
+
detail="sipri_trades_search requires supplier / recipient (country names) or filters (the raw EntityId table) — at least one of them")
|
|
188
|
+
if filters is None:
|
|
189
|
+
assembled_from = {k: v for k, v in (("supplier", sp), ("recipient", rc)) if v}
|
|
190
|
+
deps.append("supplier/recipient country-name sugar resolves EntityIds first (30-day cached table; a country name in value1 causes a 500) — "
|
|
191
|
+
"the sugar stays (not deprecated); use filters (numeric EntityId in listData) for the verbatim-name channel")
|
|
192
|
+
r = legal.sipri.search_raw(filters, supplier=sp, recipient=rc, type=type,
|
|
193
|
+
page=page, limit=limit, cache=self._c._cache,
|
|
194
|
+
no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
|
|
195
|
+
result = {k: x for k, x in r.items() if k not in ("_meta", "_metas")}
|
|
196
|
+
metas = r.get("_metas") or [r["_meta"]]
|
|
197
|
+
resolved = result.get("resolved_countries") or {}
|
|
198
|
+
quartet = {"source": "assembled" if assembled_from else "raw",
|
|
199
|
+
"upstream": r["params"]}
|
|
200
|
+
if assembled_from: # resolution results live in result.resolved_countries — assembled_from only records the sugar inputs verbatim
|
|
201
|
+
quartet["assembled_from"] = assembled_from
|
|
202
|
+
return self._flat_env(
|
|
203
|
+
tool="sipri_trades_search",
|
|
204
|
+
input={"filters": filters, "supplier": sp, "recipient": rc, "type": type,
|
|
205
|
+
"limit": result["limit_applied"], "page": page},
|
|
206
|
+
reason=(f"SIPRI arms transfers ({type}): {f'supplier {sp}' if sp else ''}"
|
|
207
|
+
f"{' × ' if sp and rc else ''}{f'recipient {rc}' if rc else ''}"
|
|
208
|
+
+ ("" if sp or rc else " (bare filters query)")
|
|
209
|
+
+ f" returning {len(result['rows'])} rows (model-level, the Est flag = estimated value; trades is a country-level face, not company-level — "
|
|
210
|
+
f"the company-level universe lives in the Top-100 XLSX batch file)"
|
|
211
|
+
if result["rows"] else "SIPRI zero hits (an empty result is a legitimate answer; use full English country names, e.g. United States / Angola)"),
|
|
212
|
+
result=result,
|
|
213
|
+
evidence=[{"quote": f"{t['delivery_year'] or t['order_year'] or '—'} {t['supplier'] or '—'}"
|
|
214
|
+
f" → {t['recipient'] or '—'}: {t['designation'] or t['description'] or '—'}"
|
|
215
|
+
f" ×{t['units'] or '?'}{'(est)' if t['units_est'] else ''}",
|
|
216
|
+
"url": "https://www.sipri.org/databases/armstransfers"}
|
|
217
|
+
for t in result["rows"][:5]],
|
|
218
|
+
provider_id="sipri", metas=metas,
|
|
219
|
+
query=quartet, deprecations=deps or None,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
def federalregister_documents(self, q, *, startdt=None, enddt=None, order="relevance",
|
|
223
|
+
page=1, limit=20, doc_type=None, agencies=None,
|
|
224
|
+
cfr_title=None, cfr_part=None, execution=None, no_cache=None):
|
|
225
|
+
"""legal federalregister_documents: search over the federal regulation/notice daily release stream
|
|
226
|
+
(conditions[term]/[publication_date]/[type][]/[agencies][]/[cfr][title|part] go through with verbatim
|
|
227
|
+
names; a missing end is auto-completed so both ends are always sent — a single end is silently ignored
|
|
228
|
+
upstream, verified 2026-10-23; per_page politeness cap 50, upstream ceiling 1000.
|
|
229
|
+
0.2.9 audit param additions (live-probed 2026-10-28): doc_type closed set of four values validated
|
|
230
|
+
locally (an invalid value silently returns 0 rows upstream = a false negative), agencies agency slug,
|
|
231
|
+
cfr code anchoring (part must accompany title); order gains the fourth official value executive_order_number."""
|
|
232
|
+
self._lg_only("federalregister_documents")
|
|
233
|
+
v = str(q or "").strip()
|
|
234
|
+
if not v:
|
|
235
|
+
raise SdkError("usage_error", provider="federalregister",
|
|
236
|
+
detail="federalregister_documents requires q (quote phrases for exact-phrase matching)")
|
|
237
|
+
r = legal.federalregister.search_raw(v, startdt=startdt, enddt=enddt,
|
|
238
|
+
order=order, page=page, limit=limit,
|
|
239
|
+
doc_type=doc_type, agencies=agencies,
|
|
240
|
+
cfr_title=cfr_title, cfr_part=cfr_part,
|
|
241
|
+
cache=self._c._cache,
|
|
242
|
+
no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
|
|
243
|
+
result = {k: x for k, x in r.items() if k != "_meta"}
|
|
244
|
+
dr = result.get("date_range")
|
|
245
|
+
span = f", window {dr['startdt']}~{dr['enddt']}" if dr else ""
|
|
246
|
+
return self._flat_env(
|
|
247
|
+
tool="federalregister_documents",
|
|
248
|
+
input={"q": v, "startdt": startdt, "enddt": enddt, "order": order,
|
|
249
|
+
"page": page, "limit": result["limit_applied"],
|
|
250
|
+
"doc_type": doc_type, "agencies": agencies,
|
|
251
|
+
"cfr_title": cfr_title, "cfr_part": cfr_part},
|
|
252
|
+
reason=(f"Federal Register: {result.get('total', len(result['rows']))} hits"
|
|
253
|
+
f" (1994+ full-text level{span}), returning {len(result['rows'])} rows"
|
|
254
|
+
" — document_number is the stable key; a single record's raw_text_url feeds the corpus batch line for full text"),
|
|
255
|
+
result=result,
|
|
256
|
+
evidence=[{"quote": f"{d['type'] or '—'} {d['document_number'] or '—'} {d['title'] or '—'}"
|
|
257
|
+
f" ({d['publication_date'] or '—'}, {d['agencies'][0] if d['agencies'] else '—'})",
|
|
258
|
+
"url": d["url"] or "https://www.federalregister.gov/api/v1/documents.json"}
|
|
259
|
+
for d in result["rows"][:5]],
|
|
260
|
+
provider_id="federalregister", metas=[r["_meta"]],
|
|
261
|
+
query={"source": "raw", "search": v, "upstream": r["params"]},
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
def courtlistener_opinions_search(self, *, q=None, case_name=None, court=None,
|
|
265
|
+
judge=None, docket_number=None, filed_after=None,
|
|
266
|
+
filed_before=None, order_by=None, cursor=None,
|
|
267
|
+
execution=None, no_cache=None):
|
|
268
|
+
"""legal courtlistener_opinions_search: US federal + state case-law search (Search API v4, anonymous and
|
|
269
|
+
keyless; covers federal + state courts — the first probe hit California Court of Appeal; CAP
|
|
270
|
+
(api.case.law) is retired and this source carries it; type is always "o"; at least one filter param
|
|
271
|
+
(local safety wall); page_size is silently ignored upstream, always 20/page — page through cursor
|
|
272
|
+
(next_cursor is recovered from the response).
|
|
273
|
+
**0.2.9 audit snapshot (2026-10-28)**: the search endpoint returns 401 "Authentication credentials were
|
|
274
|
+
not provided" for the anonymous tier — anonymous access still worked on 2026-10-26 (type=o count 8.3M);
|
|
275
|
+
upstream added an auth gate within two days; the other v4 endpoints remain anonymously 200 (courts/
|
|
276
|
+
verified as a control). Under the keyless-only ruling this face is on watch: restoring anonymous access
|
|
277
|
+
or requiring a token is an upstream decision, not a param gap. The rate-limit wording was rewritten in
|
|
278
|
+
step: upstream publishes only the authenticated tiers 5/min·50/hr·125/day; the anonymous numbers are not
|
|
279
|
+
published upstream (api-usage page wording); 30/day was our own conservative tier; 429+Retry-After is the
|
|
280
|
+
only signal verified live."""
|
|
281
|
+
self._lg_only("courtlistener_opinions_search")
|
|
282
|
+
r = legal.courtlistener.opinions_search_raw(
|
|
283
|
+
q=q, case_name=case_name, court=court, judge=judge,
|
|
284
|
+
docket_number=docket_number, filed_after=filed_after,
|
|
285
|
+
filed_before=filed_before, order_by=order_by, cursor=cursor,
|
|
286
|
+
cache=self._c._cache,
|
|
287
|
+
no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
|
|
288
|
+
result = {k: x for k, x in r.items() if k != "_meta"}
|
|
289
|
+
top = (result["rows"] or [None])[0]
|
|
290
|
+
filt = {k: v for k, v in r["params"].items() if k != "type"}
|
|
291
|
+
who = ", ".join(f"{k}={v}" for k, v in filt.items()) or "whole corpus"
|
|
292
|
+
reason = (f"CourtListener: {result['total']:,} opinions for {who}, {result['count']} on this page"
|
|
293
|
+
+ (f", top hit {top['case_name'] or '—'}"
|
|
294
|
+
f" ({top['court'] or '—'}, {top['date_filed'] or '—'})" if top else "")
|
|
295
|
+
+ (" — continue with next_cursor" if result.get("next_cursor") else ""))
|
|
296
|
+
return self._flat_env(
|
|
297
|
+
tool="courtlistener_opinions_search",
|
|
298
|
+
input={**filt, "order_by": order_by, "cursor": cursor},
|
|
299
|
+
reason=reason,
|
|
300
|
+
result=result,
|
|
301
|
+
evidence=[{"quote": f"{o['case_name'] or '—'}"
|
|
302
|
+
f" ({o['court'] or '—'}, {o['date_filed'] or '—'}, "
|
|
303
|
+
f"{'/'.join(o['citations'][:1]) or 'no citation'})",
|
|
304
|
+
"url": ("https://www.courtlistener.com" + o["absolute_url"])
|
|
305
|
+
if o["absolute_url"] and o["absolute_url"].startswith("/")
|
|
306
|
+
else (o["absolute_url"] or "https://www.courtlistener.com/?q=o&type=o")}
|
|
307
|
+
for o in result["rows"][:5]],
|
|
308
|
+
provider_id="courtlistener", metas=[r["_meta"]],
|
|
309
|
+
query={"source": "raw", "upstream": r["params"]},
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
# ---------- enrichment (local) ----------
|
|
313
|
+
def csl_screening_list(self, name=None, *, value=None, source=None, type=None, limit=20,
|
|
314
|
+
execution=None, no_cache=None):
|
|
315
|
+
"""US Consolidated Screening List name screening (realtime, local execution): 12 list families
|
|
316
|
+
(SDN / Entity List / DPL / ITAR / SSI …) 26k rows in one CSV, local full-register scan —
|
|
317
|
+
empty result = clean screening (a legitimate answer). source = list-family substring
|
|
318
|
+
(e.g. "Entity List"), type = Entity|Individual|Vessel|Aircraft."""
|
|
319
|
+
if self.name != "legal":
|
|
320
|
+
raise SdkError("usage_error", detail=f"csl_screening_list is a legal-domain command (got \"{self.name}\")")
|
|
321
|
+
ex = resolve_execution(execution or self._c.default_execution)
|
|
322
|
+
nc = self._c.no_cache if no_cache is None else bool(no_cache)
|
|
323
|
+
q = name if name is not None else value
|
|
324
|
+
if not q:
|
|
325
|
+
raise SdkError("usage_error", detail="csl_screening_list requires name (screening subject, contains-matches name+alt_names)")
|
|
326
|
+
r = legal.csl.screen_list(q, source=source, type=type, limit=limit or 20,
|
|
327
|
+
cache=self._c._cache, no_cache=nc)
|
|
328
|
+
result = {k: v for k, v in r.items() if k != "_meta"}
|
|
329
|
+
return {
|
|
330
|
+
"ok": True, "domain": "legal", "tool": "csl_screening_list",
|
|
331
|
+
"input": {"name": q, "source": source, "type": type, "limit": int(limit or 20)},
|
|
332
|
+
"from_cache": bool(r["_meta"].get("from_cache")),
|
|
333
|
+
"reason": (f"CSL consolidated screening positive: {result['total_matched']} of {result['scanned_rows']} full-register rows "
|
|
334
|
+
f"(12 list families) matched \"{q}\", returning {len(result['rows'])} rows — matched_on=name|alt_names is the hit face; zero hits = clean screening"
|
|
335
|
+
if result["rows"] else
|
|
336
|
+
f"CSL screening negative: \"{q}\" is not in the {result['scanned_rows']} consolidated-screening rows (SDN/EL/DPL/ITAR/SSI etc., 12 families)"
|
|
337
|
+
" — an empty result is a legitimate answer = clean screening"),
|
|
338
|
+
"result": result,
|
|
339
|
+
"evidence": [{"quote": f"{row['source_list'] or '—'} | {row['type'] or '—'} | {row['name'] or '—'}"
|
|
340
|
+
+ (f" (alt: {'; '.join(row['alt_names'][:3])}…)" if row["alt_names"] else "")
|
|
341
|
+
+ (f" | {';'.join(row['programs'][:3])}" if row["programs"] else ""),
|
|
342
|
+
"url": row["source_information_url"] or "https://www.trade.gov/consolidated-screening-list"}
|
|
343
|
+
for row in result["rows"][:5]],
|
|
344
|
+
"source": enrich_source("csl", ex, r["_meta"]),
|
|
345
|
+
"request_id": self._c._request_id(),
|
|
346
|
+
}
|
|
347
|
+
def ecfr_search(self, q=None, *, query=None, value=None, page=1, limit=20,
|
|
348
|
+
date=None, execution=None, no_cache=None):
|
|
349
|
+
"""eCFR current-code section search (keyless): where a topic lives in the codified
|
|
350
|
+
US Code of Federal Regulations today — counterpart of legal fulltext (FR daily flow).
|
|
351
|
+
coords = seven-level citation; starts_on~ends_on = that version's effective window
|
|
352
|
+
(section-level change semantics, not plain full-text)."""
|
|
353
|
+
if self.name != "legal":
|
|
354
|
+
raise SdkError("usage_error", detail=f"ecfr_search is a legal-domain command (got \"{self.name}\")")
|
|
355
|
+
ex = resolve_execution(execution or self._c.default_execution)
|
|
356
|
+
nc = self._c.no_cache if no_cache is None else bool(no_cache)
|
|
357
|
+
term = q if q is not None else (query if query is not None else value)
|
|
358
|
+
if not term:
|
|
359
|
+
raise SdkError("usage_error", detail="ecfr_search requires q (topic term; returns section-level current-state hits, not the FR daily feed)")
|
|
360
|
+
r = legal.ecfr.search_results(term, page=page, limit=limit or 20, date=date,
|
|
361
|
+
cache=self._c._cache, no_cache=nc)
|
|
362
|
+
result = {k: v for k, v in r.items() if k != "_meta"}
|
|
363
|
+
return {
|
|
364
|
+
"ok": True, "domain": "legal", "tool": "ecfr_search",
|
|
365
|
+
"input": {"q": str(term), "page": int(page or 1), "limit": int(limit or 20), "date": date or None},
|
|
366
|
+
"from_cache": bool(r["_meta"].get("from_cache")),
|
|
367
|
+
"reason": (f"eCFR {'point-in-time ' + str(date) + ' ' if date else 'current state'}: {result['total']} sections hit library-wide (page {result['current_page']}/{result['total_pages']}), "
|
|
368
|
+
f"returning {len(result['rows'])} rows — coords = the seven-level code citation; starts_on~ends_on = that version's effective window"
|
|
369
|
+
" (section-level change semantics, not full-text search); for the current full-text stream use legal fulltext (FR daily)"
|
|
370
|
+
if result["rows"] else
|
|
371
|
+
f"eCFR zero hits: \"{term}\" is not in the current code's section-level index (an empty result is a legitimate answer; try a synonym or check the spelling)"),
|
|
372
|
+
"result": result,
|
|
373
|
+
"evidence": [{"quote": f"{row['coords'] or '—'} | {row['type'] or '—'} | "
|
|
374
|
+
+ (re.sub(r"<[^>]+>", "", (row['headings'] or {}).get('part')
|
|
375
|
+
or (row['headings'] or {}).get('chapter') or "") or '—')
|
|
376
|
+
+ (f" | effective {row['starts_on']}" + (f"~{row['ends_on']}" if row['ends_on'] else "→")
|
|
377
|
+
if row["starts_on"] else ""),
|
|
378
|
+
"url": (f"https://www.ecfr.gov/current/title-{(row['hierarchy'] or {}).get('title')}"
|
|
379
|
+
if (row["hierarchy"] or {}).get("title") else "https://www.ecfr.gov/")}
|
|
380
|
+
for row in result["rows"][:5]],
|
|
381
|
+
"source": enrich_source("ecfr", ex, r["_meta"]),
|
|
382
|
+
"request_id": self._c._request_id(),
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
def ecfr_titles(self, *, execution=None, no_cache=None):
|
|
386
|
+
"""eCFR title catalog (keyless): the 50 CFR titles with latest_amended_on /
|
|
387
|
+
latest_issue_date and the library-wide currency watermark (as_of — eCFR integrates
|
|
388
|
+
daily; observed T-2)."""
|
|
389
|
+
if self.name != "legal":
|
|
390
|
+
raise SdkError("usage_error", detail=f"ecfr_titles is a legal-domain command (got \"{self.name}\")")
|
|
391
|
+
ex = resolve_execution(execution or self._c.default_execution)
|
|
392
|
+
nc = self._c.no_cache if no_cache is None else bool(no_cache)
|
|
393
|
+
r = legal.ecfr.titles_catalog(cache=self._c._cache, no_cache=nc)
|
|
394
|
+
result = {k: v for k, v in r.items() if k != "_meta"}
|
|
395
|
+
return {
|
|
396
|
+
"ok": True, "domain": "legal", "tool": "ecfr_titles",
|
|
397
|
+
"input": {},
|
|
398
|
+
"from_cache": bool(r["_meta"].get("from_cache")),
|
|
399
|
+
"reason": f"eCFR catalog: {result['count']} titles ({result['active']} active), daily-update watermark as_of={result['as_of']}"
|
|
400
|
+
" — title number + name + latest amendment/issue dates; for section-level current-state search use ecfr_search",
|
|
401
|
+
"result": result,
|
|
402
|
+
"evidence": [{"quote": f"Title {t['number']} {t['name']} (amended {t['latest_amended_on'] or '—'}, "
|
|
403
|
+
f"integrated through {t['up_to_date_as_of'] or '—'}" + (", reserved" if t["reserved"] else "") + ")",
|
|
404
|
+
"url": f"https://www.ecfr.gov/current/title-{t['number']}"}
|
|
405
|
+
for t in result["titles"][:5]],
|
|
406
|
+
"source": enrich_source("ecfr", ex, r["_meta"]),
|
|
407
|
+
"request_id": self._c._request_id(),
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
def gb_cma_cases(self, q=None, *, count=20, start=0, execution=None, no_cache=None):
|
|
411
|
+
"""legal gb_cma_cases: UK CMA case search (gov.uk search API, filter_format=cma_case fixed;
|
|
412
|
+
q/count/start go through with verbatim names — count politeness wall 1-100 (upstream has no wall,
|
|
413
|
+
count=1000 delivers in full, verified 2026-10-29), start = offset paging; case name = the party-pair
|
|
414
|
+
"A / B" form, title is the party search face)."""
|
|
415
|
+
self._lg_only("gb_cma_cases")
|
|
416
|
+
term = str(q or "").strip() or None
|
|
417
|
+
r = legal.gb_cma.search_raw(q=term, count=count, start=start,
|
|
418
|
+
cache=self._c._cache,
|
|
419
|
+
no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
|
|
420
|
+
result = {k: x for k, x in r.items() if k != "_meta"}
|
|
421
|
+
return self._flat_env(
|
|
422
|
+
tool="gb_cma_cases",
|
|
423
|
+
input={"q": term, "count": result["limit_applied"], "start": start},
|
|
424
|
+
reason=(f"UK CMA case register: {result['total']} cases hit, returning {len(result['rows'])} rows"
|
|
425
|
+
+ (f" (query \"{term}\")" if term else " (full-register enumeration)")
|
|
426
|
+
+ " — case name = the party pair (merger/inquiry cases); for single-case detail use gb_cma_case (slug from the row)"
|
|
427
|
+
if result["rows"] else
|
|
428
|
+
f"UK CMA zero hits (an empty result is a legitimate answer — query too narrow or no registered cases; enumerate the full register of {result['total']} cases by omitting q)"),
|
|
429
|
+
result=result,
|
|
430
|
+
evidence=[{"quote": f"{c['title'] or '—'} (updated {c['public_timestamp'] or '—'}, "
|
|
431
|
+
f"{'; '.join(c['organisations']) if c['organisations'] else '—'})",
|
|
432
|
+
"url": c["url"] or "https://www.gov.uk/search/all?keywords=&level_one_taxon=&"
|
|
433
|
+
"manual_close_coronavirus=none&order=updated-newest"}
|
|
434
|
+
for c in result["rows"][:5]],
|
|
435
|
+
provider_id="gb_cma", metas=[r["_meta"]],
|
|
436
|
+
query={"source": "raw", "upstream": r["params"]},
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
def gb_cma_case(self, slug, *, execution=None, no_cache=None):
|
|
440
|
+
"""legal gb_cma_case: the full UK CMA single-case dossier (gov.uk content API; content_id = the UUID
|
|
441
|
+
stable key; change_history = the evolution stream newest→oldest (last = First published),
|
|
442
|
+
attachments = the decision/notice PDF family — the company × case negative-signal detail face)."""
|
|
443
|
+
self._lg_only("gb_cma_case")
|
|
444
|
+
if not str(slug or "").strip():
|
|
445
|
+
raise SdkError("usage_error", provider="gb_cma",
|
|
446
|
+
detail="gb_cma_case requires slug (the case-page tail segment, echoed verbatim from "
|
|
447
|
+
"gb_cma_cases rows; a pasted full base_path also gets its prefix stripped)")
|
|
448
|
+
r = legal.gb_cma.case_raw(slug, cache=self._c._cache,
|
|
449
|
+
no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
|
|
450
|
+
result = {k: x for k, x in r.items() if k != "_meta"}
|
|
451
|
+
latest = (result["change_history"] or [{}])[0] # upstream order = newest→oldest ([0] = latest, last = First published)
|
|
452
|
+
return self._flat_env(
|
|
453
|
+
tool="gb_cma_case",
|
|
454
|
+
input={"slug": result["slug"]},
|
|
455
|
+
reason=(f"UK CMA single case: {result['title'] or result['slug']}"
|
|
456
|
+
+ (" (withdrawn)" if result["withdrawn"] else "")
|
|
457
|
+
+ f" — {len(result['change_history'])} change-history entries (latest: {latest.get('note') or '—'})"
|
|
458
|
+
+ f", {len(result['attachments'])} decision/notice PDFs; first published {result['first_published_at'] or '—'}")
|
|
459
|
+
if result["content_id"] else "gov.uk content API returned empty (slug not in the register — go back to the search face and check the spelling)",
|
|
460
|
+
result=result,
|
|
461
|
+
evidence=[{"quote": f"{a['title'] or '—'} ({a['content_type'] or '—'})",
|
|
462
|
+
"url": a["url"] or result["url"]}
|
|
463
|
+
for a in result["attachments"][:5]]
|
|
464
|
+
or [{"quote": latest.get("note") or result["title"] or result["slug"], "url": result["url"]}],
|
|
465
|
+
provider_id="gb_cma", metas=[r["_meta"]],
|
|
466
|
+
query={"source": "raw", "upstream": {"path": f"/api/content/cma-cases/{result['slug']}"}},
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
# ---------- 0.3.16 legal CO SIC 制裁面(datos.gov.co Socrata 73dx-n59j;与 co_secop_contracts 同 $-参数契约) ----------
|
|
470
|
+
|
|
471
|
+
def co_sic_sanctions(self, where=None, *, limit=20, offset=0, order=None, execution=None, no_cache=None):
|
|
472
|
+
"""legal co_sic_sanctions: Colombia SIC firm-sanctions register (datos.gov.co Socrata 73dx-n59j, keyless):
|
|
473
|
+
multado (the sanctioned party) × valor (fine, COP) × conducta (the violation) × sector — 3,938 firm
|
|
474
|
+
sanctions 2013→. $where raw SoQL / $limit 1-500 / $offset / $order column[ ASC|DESC] verbatim (same
|
|
475
|
+
contract as co_secop_contracts); Socrata sends no total echo — paging goes through offset. Name-only
|
|
476
|
+
key (no NIT column): a name join onto co_rues_companies is the corpus line's job."""
|
|
477
|
+
self._lg_only("co_sic_sanctions")
|
|
478
|
+
w = str(where or "").strip() or None
|
|
479
|
+
r = legal.co_sic.sanctions_raw(w, limit=limit, offset=offset, order=order,
|
|
480
|
+
cache=self._c._cache,
|
|
481
|
+
no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
|
|
482
|
+
result = {k: x for k, x in r.items() if k != "_meta"}
|
|
483
|
+
return self._flat_env(
|
|
484
|
+
tool="co_sic_sanctions",
|
|
485
|
+
input={"where": w, "limit": result["limit_applied"], "offset": offset, "order": order},
|
|
486
|
+
reason=(f"Colombia SIC sanctions register: {len(result['rows'])} rows returned"
|
|
487
|
+
+ (f" (SoQL filter \"{w}\")" if w else " (enumerate from the head of the register; the corpus holds 3,938 firm sanctions 2013→, counted at admission)")
|
|
488
|
+
+ " — row = multado (sanctioned party) × valor (COP) × conducta × sector; name-only key, join onto NIT via co_rues_companies on the corpus line"
|
|
489
|
+
if result["rows"] else
|
|
490
|
+
"Colombia SIC zero hits (an empty result is a legitimate answer — SoQL too narrow or no firm sanction on record; drop --where to enumerate from the register head)"),
|
|
491
|
+
result=result,
|
|
492
|
+
evidence=[{"quote": f"{c['multado'] or '—'} — {c['conducta'] or '—'} ({c['sector'] or '—'}), "
|
|
493
|
+
f"COP {c['valor'] or '—'}, res. {c['res_sancion'] or '—'} of {(c['fecha_emitida'] or '')[:10]}",
|
|
494
|
+
"url": "https://www.datos.gov.co/Comercio-Industria-y-Turismo/Sanciones-impuestas-en-firme-por-la-SIC/73dx-n59j"}
|
|
495
|
+
for c in result["rows"][:5]],
|
|
496
|
+
provider_id="co_sic", metas=[r["_meta"]],
|
|
497
|
+
query={"source": "raw", "upstream": r["params"]},
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
# ---------- legal caselaw line (TNA Find Case Law: search + Akoma Ntoso single record; born 0.3.8, ISO-prefixed 0.3.13, Englished in the 0.3.14 collection round) ----------
|
|
501
|
+
|
|
502
|
+
def gb_tna_caselaw_search(self, query=None, *, court=None, tribunal=None, party=None, judge=None,
|
|
503
|
+
order=None, page=1, per_page=50, minimum_availability=None,
|
|
504
|
+
execution=None, no_cache=None):
|
|
505
|
+
"""legal gb_tna_caselaw_search: UK judgment search (TNA Find Case Law /atom.xml; party/judge = whole-word
|
|
506
|
+
party/judge-name matching, query = full text, court/tribunal = court codes, repeatable (explode),
|
|
507
|
+
order = ±date/±updated/±transformation — eight upstream-named params passed straight through;
|
|
508
|
+
per_page polite tier 1-50 (the upstream default is 50). Rows carry the neutral_citation (the
|
|
509
|
+
human-readable official key, [2026] EWHC 2298 (Admin)) + uri (the machine-stable key, fed straight
|
|
510
|
+
into the judgment face) + contenthash.
|
|
511
|
+
**The Open Justice Licence forbids bulk computational analysis (free-application regime) — this
|
|
512
|
+
face is directed search / rolling monitoring; do not enumerate the corpus**."""
|
|
513
|
+
self._lg_only("gb_tna_caselaw_search")
|
|
514
|
+
r = legal.gb_tna_caselaw.search_raw(query=query, court=court, tribunal=tribunal,
|
|
515
|
+
party=party, judge=judge, order=order, page=page,
|
|
516
|
+
per_page=per_page, minimum_availability=minimum_availability,
|
|
517
|
+
cache=self._c._cache,
|
|
518
|
+
no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
|
|
519
|
+
result = {k: x for k, x in r.items() if k != "_meta"}
|
|
520
|
+
return self._flat_env(
|
|
521
|
+
tool="gb_tna_caselaw_search",
|
|
522
|
+
input={"query": query, "court": court, "tribunal": tribunal, "party": party,
|
|
523
|
+
"judge": judge, "order": order, "page": result["page"],
|
|
524
|
+
"per_page": result["per_page"], "minimum_availability": minimum_availability},
|
|
525
|
+
reason=(f"UK caselaw library (TNA Find Case Law): page {result['page']}/{result['total_pages'] or '?'}, "
|
|
526
|
+
f"{result['count']} rows — row = case name (party string)/court/judgment date/neutral citation; "
|
|
527
|
+
"the full judgment text goes through gb_tna_caselaw_judgment (uri = the row's uri)"
|
|
528
|
+
if result["rows"] else
|
|
529
|
+
"TNA zero hits (an empty result is a valid answer — party/judge are whole-word matches; "
|
|
530
|
+
"try a single word of the name or switch to a full-text query)"),
|
|
531
|
+
result=result,
|
|
532
|
+
evidence=[{"quote": f"{c['neutral_citation'] or c['uri'] or '—'} {c['title'] or ''}"
|
|
533
|
+
f" ({c['court'] or '—'}, {(c['published'] or '')[:10]})",
|
|
534
|
+
"url": c["html_url"] or c["data_url"] or "https://caselaw.nationalarchives.gov.uk/atom.xml"}
|
|
535
|
+
for c in result["rows"][:5]],
|
|
536
|
+
provider_id="gb_tna_caselaw", metas=[r["_meta"]],
|
|
537
|
+
query={"source": "raw", "upstream": r["params"]},
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
def gb_tna_caselaw_judgment(self, document_uri, *, execution=None, no_cache=None):
|
|
541
|
+
"""legal gb_tna_caselaw_judgment: the full UK single-judgment dossier (Akoma Ntoso data.xml;
|
|
542
|
+
neutral_citation = the neutral-citation official key, parties=[{name, role}] the party→role official
|
|
543
|
+
edge (claimant/defendant, company and personal names), judges, text = the plain-text extraction of
|
|
544
|
+
the judgment body). document_uri takes all three forms: legacy ewhc/admin/2026/2298, new-style
|
|
545
|
+
d-<uuid>, tna.<fclid> — fed straight from the search row's uri; a pasted full URL is unwrapped
|
|
546
|
+
automatically."""
|
|
547
|
+
self._lg_only("gb_tna_caselaw_judgment")
|
|
548
|
+
if not str(document_uri or "").strip():
|
|
549
|
+
raise SdkError("usage_error", provider="gb_tna_caselaw",
|
|
550
|
+
detail="gb_tna_caselaw_judgment requires document_uri (fed straight from the "
|
|
551
|
+
"gb_tna_caselaw_search row's uri; three forms accepted: legacy "
|
|
552
|
+
"ewhc/admin/2026/2298, new-style d-<uuid>, tna.<fclid>)")
|
|
553
|
+
r = legal.gb_tna_caselaw.judgment_raw(document_uri, cache=self._c._cache,
|
|
554
|
+
no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
|
|
555
|
+
result = {k: x for k, x in r.items() if k != "_meta"}
|
|
556
|
+
n_cl = sum(1 for p in result["parties"] or [] if p.get("role") == "claimant")
|
|
557
|
+
n_df = sum(1 for p in result["parties"] or [] if p.get("role") == "defendant")
|
|
558
|
+
return self._flat_env(
|
|
559
|
+
tool="gb_tna_caselaw_judgment",
|
|
560
|
+
input={"document_uri": result["uri"]},
|
|
561
|
+
reason=(f"UK judgment dossier: {result['neutral_citation'] or result['uri']} ({result['court_code'] or '—'}, "
|
|
562
|
+
f"judged {result['date'] or '—'}) — {len(result['parties'] or [])} parties"
|
|
563
|
+
f" (claimant {n_cl}/defendant {n_df}) · {len(result['judges'] or [])} judges · "
|
|
564
|
+
f"body {result['text_length']} chars; licence = Open Justice (reuse compliant, bulk analysis requires an application)"
|
|
565
|
+
if result["neutral_citation"] else
|
|
566
|
+
"data.xml came back empty or is not a judgment structure (the uri is not in the register — "
|
|
567
|
+
"go back to gb_tna_caselaw_search and check)"),
|
|
568
|
+
result=result,
|
|
569
|
+
evidence=[{"quote": f"{p['role'] or 'party'}: {p['name'] or '—'}", "url": result["url"]}
|
|
570
|
+
for p in (result["parties"] or [])[:5]]
|
|
571
|
+
or [{"quote": result["title"] or result["uri"], "url": result["url"]}],
|
|
572
|
+
provider_id="gb_tna_caselaw", metas=[r["_meta"]],
|
|
573
|
+
query={"source": "raw", "upstream": {"path": f"/{result['uri']}/data.xml"}},
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
# ---------- 0.3.21 legal debarment-roster line (WB + AfDB + IDB MDB debarment + CA SEMA sanctions; born flat; local-scan trio carries no quartet — there are no upstream params to replay, the csl_screening_list precedent) ----------
|
|
577
|
+
|
|
578
|
+
def afdb_debarred_entities(self, name=None, *, type=None, basis=None, limit=20,
|
|
579
|
+
execution=None, no_cache=None):
|
|
580
|
+
"""legal afdb_debarred_entities: African Development Bank debarment register (afdb.org json-feed,
|
|
581
|
+
keyless; full 1,440-row dump cached 24h and scanned locally). Basis = the cross-debarment edge
|
|
582
|
+
self-evidenced (CROSS DEBARMENT : WBG/ADB/IDB/EBRD vs DEBARMENT:AFDB) — contains-match because
|
|
583
|
+
upstream punctuation is inconsistent; To empty = ongoing."""
|
|
584
|
+
self._lg_only("afdb_debarred_entities")
|
|
585
|
+
nm = str(name or "").strip() or None
|
|
586
|
+
r = legal.afdb.debarred_entities_raw(nm, type=type, basis=basis, limit=limit,
|
|
587
|
+
cache=self._c._cache,
|
|
588
|
+
no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
|
|
589
|
+
result = {k: x for k, x in r.items() if k != "_meta"}
|
|
590
|
+
return self._flat_env(
|
|
591
|
+
tool="afdb_debarred_entities",
|
|
592
|
+
input={"name": nm, "type": type, "basis": basis, "limit": result["limit_applied"]},
|
|
593
|
+
reason=(f"AfDB debarment register: {result['total_matched']} of {result['scanned_rows']} rows matched"
|
|
594
|
+
+ (f" (name contains \"{nm}\")" if nm else "")
|
|
595
|
+
+ (f", type {type}" if type else "") + (f", basis contains \"{basis}\"" if basis else "")
|
|
596
|
+
+ ", returning top rows — Basis carries the cross-debarment edge (WBG/ADB/IDB/EBRD tokens; To empty = ongoing); zero hits = not on the AfDB register"
|
|
597
|
+
if result["rows"] else
|
|
598
|
+
f"AfDB zero hits (an empty result is a legitimate answer = clean screening on the {result['scanned_rows']}-row register; drop filters to enumerate from the head)"),
|
|
599
|
+
result=result,
|
|
600
|
+
evidence=[{"quote": f"{x['Name'] or '—'} ({x['Type'] or '—'}, {x['Nationality'] or '—'}) "
|
|
601
|
+
f"{x['From'] or '?'}→{x['To'] or 'ongoing'} · {x['Basis'] or '—'}",
|
|
602
|
+
"url": "https://www.afdb.org/en/projects-operations/debarment-and-sanctions-procedures"}
|
|
603
|
+
for x in result["rows"][:5]],
|
|
604
|
+
provider_id="afdb", metas=[r["_meta"]],
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
def ca_sema_sanctions(self, name=None, *, country=None, limit=20,
|
|
608
|
+
execution=None, no_cache=None):
|
|
609
|
+
"""legal ca_sema_sanctions: Canada SEMA sanctions listings (Global Affairs Canada sema-lmes.xml,
|
|
610
|
+
keyless; the Consolidated Canadian Autonomous Sanctions List — 5,708 live records: persons, entities
|
|
611
|
+
and ships; bilingual cells verbatim; 731 ships carry an IMO number = the join key onto shipping
|
|
612
|
+
rosters). Name screening contains-matches name + aliases (matched_on echoes the hit face)."""
|
|
613
|
+
self._lg_only("ca_sema_sanctions")
|
|
614
|
+
nm = str(name or "").strip() or None
|
|
615
|
+
r = legal.ca_sema.sanctions_raw(nm, country=country, limit=limit,
|
|
616
|
+
cache=self._c._cache,
|
|
617
|
+
no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
|
|
618
|
+
result = {k: x for k, x in r.items() if k != "_meta"}
|
|
619
|
+
return self._flat_env(
|
|
620
|
+
tool="ca_sema_sanctions",
|
|
621
|
+
input={"name": nm, "country": country, "limit": result["limit_applied"]},
|
|
622
|
+
reason=(f"Canada SEMA sanctions list: {result['total_matched']} of {result['scanned_rows']} listings matched"
|
|
623
|
+
+ (f" (name/alias contains \"{nm}\")" if nm else "")
|
|
624
|
+
+ (f", country contains \"{country}\"" if country else "")
|
|
625
|
+
+ " — bilingual cells verbatim (Syria / Syrie), schedule = the SEMA schedule reference; zero hits = not designated under SEMA"
|
|
626
|
+
if result["rows"] else
|
|
627
|
+
f"Canada SEMA zero hits (an empty result is a legitimate answer = clean screening on the {result['scanned_rows']}-listing register; the bilingual country cell matches either language)"),
|
|
628
|
+
result=result,
|
|
629
|
+
evidence=[{"quote": f"{x['name'] or '—'} ({x['kind'] or '—'}, {x['country'] or '—'})"
|
|
630
|
+
+ (f" IMO {x['ship_imo']}" if x["ship_imo"] else "")
|
|
631
|
+
+ f" · listed {x['date_of_listing'] or '—'}",
|
|
632
|
+
"url": "https://www.international.gc.ca/world-monde/international_relations-relations_internationales/sanctions/consolidated-consolide.aspx?lang=eng"}
|
|
633
|
+
for x in result["rows"][:5]],
|
|
634
|
+
provider_id="ca_sema", metas=[r["_meta"]],
|
|
635
|
+
)
|
|
636
|
+
|
|
637
|
+
def idb_debarred_entities(self, q=None, *, filters=None, limit=20, offset=0, sort=None,
|
|
638
|
+
execution=None, no_cache=None):
|
|
639
|
+
"""legal idb_debarred_entities: Inter-American Development Bank sanctioned-firms register
|
|
640
|
+
(data.iadb.org CKAN DataStore, keyless, server-side search: q full-text verbatim, filters =
|
|
641
|
+
JSON field→value exact matches verbatim, offset paging, include_total always; live register
|
|
642
|
+
1,343 rows; Source carries the cross-debarment markers ADB/WBG cross debarment)."""
|
|
643
|
+
self._lg_only("idb_debarred_entities")
|
|
644
|
+
qq = str(q or "").strip() or None
|
|
645
|
+
r = legal.idb.debarred_entities_raw(qq, filters=filters, limit=limit, offset=offset, sort=sort,
|
|
646
|
+
cache=self._c._cache,
|
|
647
|
+
no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
|
|
648
|
+
result = {k: x for k, x in r.items() if k != "_meta"}
|
|
649
|
+
return self._flat_env(
|
|
650
|
+
tool="idb_debarred_entities",
|
|
651
|
+
input={"q": qq, "filters": filters, "limit": result["limit_applied"], "offset": offset, "sort": sort},
|
|
652
|
+
reason=(f"IDB sanctioned-firms register: {result['total']} rows total, {len(result['rows'])} returned"
|
|
653
|
+
+ (f" (q \"{qq}\")" if qq else "")
|
|
654
|
+
+ (f" (filters {filters})" if filters else "")
|
|
655
|
+
+ " — Entity=Firm rows are the company roster; Source carries cross-debarment markers (ADB/WBG cross debarment); page through offset"
|
|
656
|
+
if result["rows"] else
|
|
657
|
+
"IDB zero hits (an empty result is a legitimate answer; q is a full-text match and filters are exact-match — drop them to enumerate from the register head)"),
|
|
658
|
+
result=result,
|
|
659
|
+
evidence=[{"quote": f"{x.get('Title') or '—'} ({x.get('Entity') or '—'}, {x.get('Nationality') or '—'}) "
|
|
660
|
+
f"{(x.get('From') or '?')[:10]}→{x.get('To') or '?'} · {(x.get('Prohibited Practice') or '—').strip()}"
|
|
661
|
+
+ (f" · {x.get('Source')}" if x.get("Source") and str(x.get("Source")) != "IDB" else ""),
|
|
662
|
+
"url": "https://www.iadb.org/en/transparency/sanctioned-firms-and-individuals"}
|
|
663
|
+
for x in result["rows"][:5]],
|
|
664
|
+
provider_id="idb", metas=[r["_meta"]],
|
|
665
|
+
query={"source": "raw", "upstream": r["params"]},
|
|
666
|
+
)
|
|
667
|
+
|
|
668
|
+
def wb_debarred_firms(self, name=None, *, country=None, type=None, limit=20,
|
|
669
|
+
execution=None, no_cache=None):
|
|
670
|
+
"""legal wb_debarred_firms: World Bank debarred-firms register (apigwext SAP gateway via the
|
|
671
|
+
page-embedded public apikey; full 1,546-row dump cached 24h and scanned locally). SUPP_ID = the WB
|
|
672
|
+
supplier key; DEBAR_TO_DATE 2999-12-31 = indefinite; eligibility DEBARRED/X-DEBARRED; China leads
|
|
673
|
+
the roster (392 firms); the DNB/REGISTER identifier fields ship empty — name × country is the
|
|
674
|
+
join channel."""
|
|
675
|
+
self._lg_only("wb_debarred_firms")
|
|
676
|
+
nm = str(name or "").strip() or None
|
|
677
|
+
r = legal.wb.debarred_firms_raw(nm, country=country, type=type, limit=limit,
|
|
678
|
+
cache=self._c._cache,
|
|
679
|
+
no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
|
|
680
|
+
result = {k: x for k, x in r.items() if k != "_meta"}
|
|
681
|
+
return self._flat_env(
|
|
682
|
+
tool="wb_debarred_firms",
|
|
683
|
+
input={"name": nm, "country": country, "type": type, "limit": result["limit_applied"]},
|
|
684
|
+
reason=(f"World Bank debarred-firms register: {result['total_matched']} of {result['scanned_rows']} rows matched"
|
|
685
|
+
+ (f" (name contains \"{nm}\")" if nm else "")
|
|
686
|
+
+ (f", country contains \"{country}\"" if country else "")
|
|
687
|
+
+ (f", type {type}" if type else "")
|
|
688
|
+
+ " — DEBAR_REASON cites the procurement-guidelines clause; DEBAR_TO_DATE 2999-12-31 = indefinite; zero hits = not on the WB register"
|
|
689
|
+
if result["rows"] else
|
|
690
|
+
f"World Bank zero hits (an empty result is a legitimate answer = clean screening on the {result['scanned_rows']}-row register; country matches the name or the LAND1 code, e.g. China/CN)"),
|
|
691
|
+
result=result,
|
|
692
|
+
evidence=[{"quote": f"{x['SUPP_NAME'] or '—'} ({x['COUNTRY_NAME'] or '—'}) "
|
|
693
|
+
f"{x['DEBAR_FROM_DATE'] or '?'}→{x['DEBAR_TO_DATE'] or '?'} · {x['DEBAR_REASON'] or '—'}",
|
|
694
|
+
"url": "https://www.worldbank.org/en/projects-operations/procurement/debarred-firms"}
|
|
695
|
+
for x in result["rows"][:5]],
|
|
696
|
+
provider_id="wb", metas=[r["_meta"]],
|
|
697
|
+
)
|
|
698
|
+
|
|
699
|
+
# ---------- 0.2.4 flat-panel commands (research: grants × papers × institutions × impact — upstream verbatim params) ----------
|
|
700
|
+
# The grants axis was established in 0.2.2 and renamed research in 0.2.4 (the grants alias was removed in 0.3.0).
|