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
@@ -0,0 +1,746 @@
1
+ """company faces — flat face-method mixin (0.2.6 per-domain split, migrated from client.py _Domain).
2
+
3
+ Mixed into the root _Domain (assembled via MRO, see client.py); discipline: no __init__, depends only on root
4
+ internals (_flat_env/_c/_request…), method names globally unique across domains (a collision would shadow another domain's face).
5
+ """
6
+ from ..errors import SdkError
7
+
8
+ from .. import company # domain-module path invocation (historical shape, kept for minimal churn in C2)
9
+
10
+ class CompanyFaces:
11
+ def _co_only(self, tool: str):
12
+ if self.name != "company":
13
+ raise SdkError("usage_error", detail=f"{tool} is a company-domain capability (not available on the {self.name} domain)")
14
+
15
+ def gleif_lei_records(self, *, lei, execution=None, no_cache=None):
16
+ """company gleif_lei_records: LEI → entity identity card (GLEIF /lei-records/{lei} single-record face; ISO 17442 20-char
17
+ shape gate). 2026-10-30 param-fidelity round: the card additionally surfaces bic/mic, legal_form{elf id, other},
18
+ expiration{date, reason} and successor_entities (successor LEI list — the M&A successor edge, resolve each via
19
+ this same face)."""
20
+ self._co_only("gleif_lei_records")
21
+ v = str(lei or "").strip().upper()
22
+ if not v:
23
+ raise SdkError("usage_error", provider="gleif", detail="gleif_lei_records requires lei (ISO 17442: 20 alphanumeric chars)")
24
+ r = company.gleif.lookup_lei(v, cache=self._c._cache,
25
+ no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
26
+ result = {k: x for k, x in r.items() if k != "_meta"}
27
+ succ = result.get("successor_entities") or []
28
+ return self._flat_env(
29
+ tool="gleif_lei_records", input={"lei": v},
30
+ reason=(f"GLEIF normalization: LEI {r['lei']} → {r.get('legal_name') or '(no legal name)'}"
31
+ f" ({r.get('entity_status') or 'unknown status'})"
32
+ + (f"; {len(succ)} successor entity LEI(s) on record (M&A successor edge): {', '.join(succ[:3])}"
33
+ if succ else "")),
34
+ result=result,
35
+ evidence=[{"quote": (f"LEI {r['lei']} = {r.get('legal_name') or '—'}"
36
+ f" ({r.get('entity_status') or '—'}, {r.get('jurisdiction') or '—'})"),
37
+ "url": f"https://api.gleif.org/api/v1/lei-records/{r['lei']}"}],
38
+ provider_id="gleif", metas=[r["_meta"]],
39
+ query={"source": "raw", "upstream": {"lei": r["lei"]}},
40
+ )
41
+
42
+ def gleif_lei_records_search(self, *, filter_entity_legal_name, page_size=10, filter_entity_jurisdiction=None,
43
+ filter_entity_status=None, page_number=None, execution=None, no_cache=None):
44
+ """company gleif_lei_records_search: company name → LEI candidates (GLEIF filter[entity.legalName] contains match;
45
+ page[size] 1-200 upstream hard wall — probed 201→400, 200 served in full). Candidates are not an authoritative normalization.
46
+ The parameter names are bracket transcriptions of upstream filter[entity.legalName]/filter[entity.jurisdiction]/
47
+ filter[entity.status] (naming convention §5). 2026-10-30 param-fidelity round: jurisdiction (ISO country or
48
+ region code, CH/US-MO — probed US-MO→Nestlé Purina registeredAs=00007498), status (ACTIVE/INACTIVE/NULL entity
49
+ lifecycle — LAPSED is registration.status, a different field), page_number (walk while page <= page.lastPage)."""
50
+ self._co_only("gleif_lei_records_search")
51
+ v = str(filter_entity_legal_name or "").strip()
52
+ if not v:
53
+ raise SdkError("usage_error", provider="gleif",
54
+ detail="gleif_lei_records_search requires filter_entity_legal_name (company name)")
55
+ size = int(page_size)
56
+ if not 1 <= size <= 200:
57
+ raise SdkError("usage_error", provider="gleif",
58
+ detail="page_size must be 1-200 (probed: page[size] 201→400, 200 served in full)")
59
+ j = str(filter_entity_jurisdiction or "").strip().upper() or None # normalize before the call so the
60
+ s = str(filter_entity_status or "").strip().upper() or None # upstream echo equals what was sent
61
+ extra = {}
62
+ if j:
63
+ extra["jurisdiction"] = j
64
+ if s:
65
+ extra["status"] = s
66
+ if page_number is not None:
67
+ if int(page_number) < 1:
68
+ raise SdkError("usage_error", provider="gleif",
69
+ detail="page_number is 1-based (upstream param: page[number])")
70
+ extra["page_number"] = int(page_number)
71
+ r = company.gleif.search_lei_by_name(v, limit=size, cache=self._c._cache,
72
+ no_cache=self._c.no_cache if no_cache is None else bool(no_cache), **extra)
73
+ result = {k: x for k, x in r.items() if k != "_meta"}
74
+ result["limit_applied"] = size
75
+ top = (result["candidates"] or [None])[0]
76
+ scope = (f", filtered to jurisdiction {j}" if j else "") + (f", entity status {s}" if s else "")
77
+ up_query = {"filter[entity.legalName]": v, "page[size]": size}
78
+ if j:
79
+ up_query["filter[entity.jurisdiction]"] = j
80
+ if s:
81
+ up_query["filter[entity.status]"] = s
82
+ if "page_number" in extra:
83
+ up_query["page[number]"] = extra["page_number"]
84
+ inp = {"filter_entity_legal_name": v, "page_size": size,
85
+ **({"filter_entity_jurisdiction": j} if j else {}),
86
+ **({"filter_entity_status": s} if s else {}),
87
+ **({"page_number": extra["page_number"]} if "page_number" in extra else {})}
88
+ return self._flat_env(
89
+ tool="gleif_lei_records_search", input=inp,
90
+ reason=(f"GLEIF reverse name search: {result['total']} candidates{scope}, top hit {top['legal_name'] or '—'}"
91
+ f" (LEI {top['lei']}, {top['entity_status'] or '—'})"
92
+ + (f", registered as {top['registered_as']}" if top.get("registered_as") else "")
93
+ + " — legal-name contains match, not fuzzy tolerance, "
94
+ f"candidates are not an authoritative normalization; re-verify with gleif_lei_records before persisting"
95
+ if top else f"GLEIF reverse name search zero hits: {v}{scope} (an empty result is a legal answer)"),
96
+ result=result,
97
+ evidence=[{"quote": f"LEI {c['lei']} = {c['legal_name'] or '—'} ({c['entity_status'] or '—'}"
98
+ + (f", registered as {c['registered_as']}" if c.get("registered_as") else "") + ")",
99
+ "url": f"https://api.gleif.org/api/v1/lei-records/{c['lei']}"}
100
+ for c in result["candidates"][:5]],
101
+ provider_id="gleif", metas=[r["_meta"]],
102
+ query={"source": "raw", "upstream": up_query},
103
+ )
104
+
105
+ def _gleif_parent_face(self, tool, which, *, lei, no_cache):
106
+ """Shared envelope for gleif_direct_parent / gleif_ultimate_parent (naming convention §4: one command one endpoint, each endpoint gets its own face)."""
107
+ self._co_only(tool)
108
+ v = str(lei or "").strip().upper()
109
+ if not v:
110
+ raise SdkError("usage_error", provider="gleif",
111
+ detail=f"{tool} requires lei (ISO 17442: 20 alphanumeric chars)")
112
+ fn = (company.gleif.get_direct_parent if which == "direct_parent"
113
+ else company.gleif.get_ultimate_parent)
114
+ r = fn(v, cache=self._c._cache,
115
+ no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
116
+ p = r["parent"]
117
+ label = "direct parent" if which == "direct_parent" else "ultimate parent"
118
+ return self._flat_env(
119
+ tool=tool, input={"lei": r["lei"]},
120
+ reason=(f"GLEIF {label}: {r['lei']} → {p['legal_name'] or p['lei']}"
121
+ if p else f"GLEIF {label}: {r['lei']} none reported (independent entity or simply unreported — an honest 404 null, not an error)"),
122
+ result={"lei": r["lei"], which: p},
123
+ evidence=([{"quote": f"{p['legal_name'] or p['lei']} (LEI {p['lei']}, {p['entity_status'] or '—'})",
124
+ "url": f"https://api.gleif.org/api/v1/lei-records/{p['lei']}"}] if p else []),
125
+ provider_id="gleif", metas=[r["_meta"]],
126
+ query={"source": "raw",
127
+ "upstream": {"lei": r["lei"],
128
+ "path": f"/lei-records/{r['lei']}/{which.replace('_', '-')}"}},
129
+ )
130
+
131
+ def gleif_direct_parent(self, *, lei, execution=None, no_cache=None):
132
+ """company gleif_direct_parent: LEI → direct parent (GLEIF /lei-records/{lei}/direct-parent dedicated endpoint)."""
133
+ return self._gleif_parent_face("gleif_direct_parent", "direct_parent", lei=lei, no_cache=no_cache)
134
+
135
+ def gleif_ultimate_parent(self, *, lei, execution=None, no_cache=None):
136
+ """company gleif_ultimate_parent: LEI → ultimate parent (GLEIF /lei-records/{lei}/ultimate-parent dedicated endpoint)."""
137
+ return self._gleif_parent_face("gleif_ultimate_parent", "ultimate_parent", lei=lei, no_cache=no_cache)
138
+
139
+ def fdic_institutions(self, *, search=None, filters=None, limit=5,
140
+ execution=None, no_cache=None):
141
+ """company fdic_institutions: US insured-bank master lookup (api.fdic.gov /banks/institutions;
142
+ search/filters upstream names (combinable — phrase search 'NAME:\"…\"' / exact-key filters 'CERT:n');
143
+ limit has no upstream wall — 10000 measured, served in full)."""
144
+ self._co_only("fdic_institutions")
145
+ raw_search = str(search or "").strip() or None
146
+ raw_filters = str(filters or "").strip() or None
147
+ if not (raw_search or raw_filters):
148
+ raise SdkError("usage_error", provider="fdic-bankfind",
149
+ detail="fdic_institutions requires at least one of search / filters "
150
+ "(local safety wall: enumerating the whole 27,835-row keyless database is served by the corpus line; the lookup face does not allow bare scans)")
151
+ r = company.fdic_bankfind.institutions_raw(search=raw_search, filters=raw_filters,
152
+ limit=limit, cache=self._c._cache,
153
+ no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
154
+ result = {k: x for k, x in r.items() if k != "_meta"}
155
+ top = (result["institutions"] or [None])[0]
156
+ quartet = {"source": "raw", "upstream": r["params"]}
157
+ active = "active" if top and top["active"] is True else "closed" if top and top["active"] is False else None
158
+ return self._flat_env(
159
+ tool="fdic_institutions",
160
+ input={"search": raw_search, "filters": raw_filters,
161
+ "limit": r["params"]["limit"]},
162
+ reason=(f"FDIC BankFind: {result['total']} rows matched"
163
+ + (f", top hit {top['cert']} {top['name'] or '—'} ({active}"
164
+ + f"{', ' + top['end_date'] if top['active'] is False and top.get('end_date') else ''}"
165
+ + f", {top['city'] or '—'} {top['state'] or '—'})" if top else ", zero hits (an empty result is a legal answer)")),
166
+ result=result,
167
+ evidence=[{"quote": f"CERT {b['cert']} = {b['name'] or '—'} ({b['state'] or '—'}, "
168
+ f"{'active' if b['active'] else 'closed' if b['active'] is False else 'status unknown'})",
169
+ "url": f"https://banks.data.fdic.gov/bankfind-suite/detail/institution/{b['cert']}"}
170
+ for b in result["institutions"][:5]],
171
+ provider_id="fdic-bankfind", metas=[r["_meta"]],
172
+ query=quartet,
173
+ )
174
+
175
+ def sec_submissions(self, *, cik, execution=None, no_cache=None):
176
+ """company sec_submissions: SEC CIK → registrant identity card (data.sec.gov submissions single record;
177
+ bare digits / leading zeros / the CIK prefix — all three spellings accepted — non-digits stripped, padded to 10)."""
178
+ self._co_only("sec_submissions")
179
+ v = str(cik or "").strip()
180
+ if not v:
181
+ raise SdkError("usage_error", provider="edgar-cik", detail="sec_submissions requires cik (1-10 digits, CIK prefix allowed)")
182
+ r = company.edgar_cik.lookup_cik(v, cache=self._c._cache,
183
+ no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
184
+ result = {k: x for k, x in r.items() if k != "_meta"}
185
+ tk = (f" ({'/'.join(result['tickers'])}·{'/'.join(result['exchanges']) or '?'})"
186
+ if result.get("tickers") else " (no ticker — unlisted or no trading venue registered)")
187
+ return self._flat_env(
188
+ tool="sec_submissions", input={"cik": v},
189
+ reason=(f"SEC normalization: CIK {int(result['cik'])} → {result.get('name') or '(no legal name)'}{tk}"
190
+ + (f", {result['filer_category']}" if result.get("filer_category") else "")),
191
+ result=result,
192
+ evidence=[{"quote": (f"CIK {result['cik']} = {result.get('name') or '—'} (EIN {result.get('ein') or '—'}"
193
+ + (f", SIC {result['sic']['code']} {result['sic'].get('description') or ''}"
194
+ if result.get("sic") else "")
195
+ + f"; filings total {result['filings_total']})"),
196
+ "url": f"https://data.sec.gov/submissions/CIK{result['cik']}.json"}],
197
+ provider_id="edgar-cik", metas=[r["_meta"]],
198
+ query={"source": "raw", "upstream": {"cik10": result["cik"]}},
199
+ )
200
+
201
+ def ror_organizations(self, *, query=None, ror_id=None, page=None, all_status=None,
202
+ execution=None, no_cache=None):
203
+ """company ror_organizations: research-org master record (ROR v2; query search face XOR ror_id single-record face —
204
+ two separate upstream endpoints; all_status added in the 0.2.9 audit — upstream default returns only active,
205
+ inactive/withdrawn are hidden wholesale by default)."""
206
+ self._co_only("ror_organizations")
207
+ q = str(query or "").strip() or None
208
+ rid = str(ror_id or "").strip() or None
209
+ if q and rid:
210
+ raise SdkError("usage_error", provider="ror",
211
+ detail="query (search face) and ror_id (single-record face) are mutually exclusive — upstream is two endpoints, give exactly one of the two required")
212
+ if not q and not rid:
213
+ raise SdkError("usage_error", provider="ror", detail="ror_organizations requires query (org name) or ror_id (0+8 chars)")
214
+ if rid and all_status is not None:
215
+ raise SdkError("usage_error", provider="ror",
216
+ detail="all_status belongs to the search face (query) only — the single-record face (ror_id) has no such upstream param")
217
+ cache, nc = self._c._cache, self._c.no_cache if no_cache is None else bool(no_cache)
218
+ if rid:
219
+ r = company.ror.lookup_ror(rid, cache=cache, no_cache=nc)
220
+ upstream = {"id": f"https://ror.org/{(r['candidates'] or [{}])[0].get('ror_id') or rid.lower()}"}
221
+ else:
222
+ r = company.ror.search_orgs(q, page=page, all_status=all_status, cache=cache, no_cache=nc)
223
+ upstream = {"query": q, **({"page": int(page)} if page is not None else {}),
224
+ **({"all_status": "true" if all_status else "false"} if all_status is not None else {})}
225
+ result = {k: x for k, x in r.items() if k != "_meta"}
226
+ top = (result["candidates"] or [None])[0]
227
+ mode = "id normalization" if rid else "name search"
228
+ subject = "ROR id " + rid if rid else f'"{q}"'
229
+ return self._flat_env(
230
+ tool="ror_organizations", input={"query": q, "ror_id": rid, "page": page, "all_status": all_status},
231
+ reason=(f"ROR {mode}: {subject} → {top['name'] or '—'}"
232
+ f" ({top['country'] or '—'}, {top['type'] or '—'}"
233
+ + (f"; parent org {top['parent']['name'] or '—'}" if top.get("parent") else "")
234
+ + f") — {'authoritative single record' if rid else 'candidates, not an authoritative normalization (fixed 20 rows/page)'}"
235
+ if top else (f"ROR zero hits: no such ROR id ({rid})" if rid
236
+ else f"ROR zero hits: \"{q}\" is not in the research-org master record (an empty result is a legal answer; for purely commercial entities use gleif_lei_records_search)")),
237
+ result=result,
238
+ evidence=[{"quote": (f"{c['ror_id'] or '—'} = {c['name'] or '—'} ({c['country'] or '—'}, {c['type'] or '—'})"),
239
+ "url": f"https://ror.org/{c['ror_id'] or ''}"}
240
+ for c in result["candidates"][:5]],
241
+ provider_id="ror", metas=[r["_meta"]],
242
+ query={"source": "raw", "upstream": upstream},
243
+ )
244
+
245
+ def vies_vat(self, *, vat, execution=None, no_cache=None):
246
+ """company vies_vat: EU VAT number online validation (official VIES face; INVALID is a legal answer, not an error;
247
+ member-state masked name/address '---' → null, not missing data)."""
248
+ self._co_only("vies_vat")
249
+ v = str(vat or "").strip()
250
+ if not v:
251
+ raise SdkError("usage_error", provider="vies", detail="vies_vat requires vat (with member-state prefix, e.g. DE811907980)")
252
+ r = company.vies.check_vat(v, cache=self._c._cache,
253
+ no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
254
+ result = {k: x for k, x in r.items() if k != "_meta"}
255
+ cc, number = company.vies.parse_vat(v)
256
+ masked = ", member-state masked name/address (--- → null, not missing data)" if result["valid"] and not result["name"] else ""
257
+ addr = str(result["address"] or "").split(chr(10))[0][:80]
258
+ return self._flat_env(
259
+ tool="vies_vat", input={"vat": v.upper()},
260
+ reason=(f"VIES check: {result['vat_number']} valid ({result['country_code']}"
261
+ + (f" → {result['name']}" if result["name"] else masked) + ")"
262
+ if result["valid"] else
263
+ f"VIES check: {result['vat_number']} invalid/unregistered (userError={result['user_error'] or '—'} — this is a legal answer, not an error)"),
264
+ result=result,
265
+ evidence=[{"quote": f"{result['vat_number']} = {'valid' if result['valid'] else 'invalid'}"
266
+ + (f": {result['name']}" if result["name"] else "")
267
+ + (f", {addr}" if result["address"] else "")
268
+ + (f" (audit trace {result['request_identifier']})" if result.get("request_identifier") else ""),
269
+ "url": f"https://ec.europa.eu/taxation_customs/vies/rest-api/ms/{cc}/vat/{number}"}],
270
+ provider_id="vies", metas=[r["_meta"]],
271
+ query={"source": "raw", "upstream": {"cc": cc, "number": number}},
272
+ )
273
+
274
+ def usaspending_spending_by_award(self, *, recipient_search_text, award_type_codes=None, start_date=None,
275
+ end_date=None, fields=None, limit=10, page=1, subawards=False,
276
+ execution=None, no_cache=None):
277
+ """company usaspending_spending_by_award: federal contracts/grants by recipient (spending_by_award POST params
278
+ verbatim; recipient_search_text has upstream array semantics — a scalar is auto-wrapped; time_period auto-fills
279
+ a single end)."""
280
+ self._co_only("usaspending_spending_by_award")
281
+ q = str(recipient_search_text or "").strip()
282
+ if not q:
283
+ raise SdkError("usage_error", provider="usaspending",
284
+ detail="usaspending_spending_by_award requires recipient_search_text (recipient name)")
285
+ r = company.usaspending.awards_raw(q, award_type_codes=award_type_codes,
286
+ start_date=start_date, end_date=end_date,
287
+ fields=fields, limit=limit, page=page,
288
+ subawards=subawards, cache=self._c._cache,
289
+ no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
290
+ result = {k: x for k, x in r.items() if k != "_meta"}
291
+ dr = result.get("date_range")
292
+ span = f", recipient period {dr['startdt']}~{dr['enddt']}" if dr else ""
293
+ types_note = ",".join(r["params"]["filters"]["award_type_codes"])
294
+ return self._flat_env(
295
+ tool="usaspending_spending_by_award",
296
+ input={"recipient_search_text": q, "award_type_codes": types_note,
297
+ "start_date": start_date, "end_date": end_date,
298
+ "limit": r["params"]["limit"], "page": r["params"]["page"],
299
+ "subawards": r["params"]["subawards"]},
300
+ reason=(f"USAspending: \"{q}\" type {types_note}, {len(result['awards'])} rows{span}"
301
+ f"{' (page_metadata.hasNext=more available, continue with --page)' if result.get('has_more') else ''}"
302
+ f" — name-matched recipients, parent/subsidiary groups not merged; narrow by CIK/UEI before persisting"),
303
+ result=result,
304
+ evidence=[{"quote": f"{w['award_id'] or '—'} = {w['recipient_name'] or '—'}"
305
+ f" (${w['award_amount'] or '—'}, {w['awarding_agency'] or '—'})",
306
+ "url": "https://www.usaspending.gov/"}
307
+ for w in result["awards"][:5]],
308
+ provider_id="usaspending", metas=[r["_meta"]],
309
+ query={"source": "raw", "upstream": r["params"]},
310
+ )
311
+
312
+ def ted_notices_search(self, *, query=None, fields=None,
313
+ limit=10, page=1, execution=None, no_cache=None):
314
+ """company ted_notices_search: EU procurement notices (TED v3 expert search POST;
315
+ query = expert-syntax verbatim channel (buyer face organisation-name-buyer=*/organisation-country-buyer=,
316
+ classification-cpv=); limit 1-250 upstream hard wall, measured 2026-10-23)."""
317
+ self._co_only("ted_notices_search")
318
+ q = str(query or "").strip() or None
319
+ if not q:
320
+ raise SdkError("usage_error", provider="ted",
321
+ detail="ted_notices_search requires query (expert syntax, e.g. organisation-name-buyer=Siemens*)")
322
+ r = company.ted.search_raw(q, fields=fields, limit=limit, page=page,
323
+ cache=self._c._cache,
324
+ no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
325
+ result = {k: x for k, x in r.items() if k != "_meta"}
326
+ quartet = {"source": "raw", "search": q, "upstream": r["params"]}
327
+ return self._flat_env(
328
+ tool="ted_notices_search",
329
+ input={"query": q,
330
+ "limit": r["params"]["limit"], "page": r["params"]["page"]},
331
+ reason=(f"TED v3: \"{q}\" {len(result['notices'])} rows"
332
+ f"{' (full page, more may exist — limit cap 250)' if result.get('has_more') else ''}"
333
+ f" — name-prefix match on the buyer face (trailing wildcard), parent/subsidiary groups not merged; narrow by VAT/LEI before persisting"),
334
+ result=result,
335
+ evidence=[{"quote": f"{n['publication_number'] or '—'} = {n['buyer'] or '—'}"
336
+ f" ({n['buyer_country'] or '—'}, {n['notice_type'] or '—'}, {n['publication_date'] or '—'})",
337
+ "url": f"https://ted.europa.eu/udl?srcid={n['publication_number'] or ''}"}
338
+ for n in result["notices"][:5]],
339
+ provider_id="ted", metas=[r["_meta"]],
340
+ query=quartet,
341
+ )
342
+
343
+ def gb_fts_release_packages(self, *, name=None, stages="award", limit=10, cursor=None,
344
+ updated_from=None, updated_to=None, execution=None, no_cache=None):
345
+ """company gb_fts_release_packages: UK WTO-line procurement (FTS OCDS five params verbatim;
346
+ upstream has no name-search param — name is a client-side contains filter (convenience: always fetch the full
347
+ 100-row page then filter, matching either the buyer or supplier side)."""
348
+ self._co_only("gb_fts_release_packages")
349
+ nm = str(name or "").strip() or None
350
+ fetch_limit = 100 if nm else limit # name-filter line "always fetch the full 100 then filter" (fetching the original page at limit then filtering is almost guaranteed empty)
351
+ r = company.fts.releases_raw(stages=stages, limit=fetch_limit, cursor=cursor,
352
+ updated_from=updated_from, updated_to=updated_to,
353
+ cache=self._c._cache,
354
+ no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
355
+ rows, matched_on = r["rows"], None
356
+ if nm:
357
+ q = nm.lower()
358
+ rows = [x for x in r["rows"]
359
+ if q in str(x.get("buyer") or "").lower()
360
+ or any(q in str(s or "").lower() for s in (x.get("suppliers") or []))][:int(limit)]
361
+ matched_on = "buyer|suppliers contains"
362
+ result = {"rows": rows, "next_cursor": r["next_cursor"], "has_more": r["has_more"],
363
+ "total_in_page": len(r["rows"])}
364
+ if matched_on:
365
+ result["matched_on"] = matched_on
366
+ quartet = {"source": "assembled" if nm else "raw", "upstream": r["params"]}
367
+ if nm:
368
+ quartet["assembled_from"] = {"name": nm, "fetch_limit_forced": 100,
369
+ "filter": "client-side contains"}
370
+ return self._flat_env(
371
+ tool="gb_fts_release_packages",
372
+ input={"name": nm, "stages": r["params"]["stages"], "limit": int(limit),
373
+ "cursor": cursor, "updated_from": updated_from, "updated_to": updated_to},
374
+ reason=(f"UK FTS: \"{nm}\" matched {len(rows)} rows / original page {len(r['rows'])} rows ({matched_on}"
375
+ + (", next page available to walk" if r.get("next_cursor") else ", last page")
376
+ + ") — the OCDS face has no name-search param; contains-matching is the agreed sense, not a defect"
377
+ if nm else
378
+ f"UK FTS: {len(rows)} rows (stages={r['params']['stages']}"
379
+ + (", next page available to walk" if r.get("next_cursor") else ", last page") + ")"),
380
+ result=result,
381
+ evidence=[{"quote": f"{n['ocid'] or '—'} = {n['buyer'] or '—'} × "
382
+ f"{' / '.join(n['suppliers']) or '(not awarded)'} ({n['date'] or '—'})",
383
+ "url": f"https://www.find-tender.service.gov.uk/Search/Results?stages={r['params']['stages']}"}
384
+ for n in rows[:5]],
385
+ provider_id="fts", metas=[r["_meta"]],
386
+ query=quartet,
387
+ )
388
+
389
+ def gb_contracts_finder_published_notices(self, *, name=None, published_from=None, published_to=None,
390
+ stages=None, limit=10, cursor=None, execution=None, no_cache=None):
391
+ """company gb_contracts_finder_published_notices: UK below-threshold small contracts (CF OCDS params verbatim;
392
+ publishedFrom/To bare dates are legal — the opposite of FTS's full-timestamp requirement; both-default = rolling
393
+ 7-day window (our default; upstream is always empty without a window);
394
+ stages=tender|award added in the 0.2.9 audit (upstream 400 self-validates, default = mixed feed);
395
+ name = client-side contains filter (convenience: always fetch the full 100-row page then filter)."""
396
+ self._co_only("gb_contracts_finder_published_notices")
397
+ nm = str(name or "").strip() or None
398
+ fetch_limit = 100 if nm else limit
399
+ r = company.cf.search_raw(published_from=published_from, published_to=published_to,
400
+ limit=fetch_limit, cursor=cursor, stages=stages,
401
+ cache=self._c._cache,
402
+ no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
403
+ rows, matched_on = r["rows"], None
404
+ if nm:
405
+ q = nm.lower()
406
+ rows = [x for x in r["rows"]
407
+ if q in str(x.get("buyer") or "").lower()
408
+ or any(q in str(s or "").lower() for s in (x.get("suppliers") or []))][:int(limit)]
409
+ matched_on = "buyer|suppliers contains"
410
+ win = r["window"]
411
+ result = {"rows": rows, "next_cursor": r["next_cursor"], "has_more": r["has_more"],
412
+ "total_in_page": len(r["rows"]), "window": win}
413
+ if matched_on:
414
+ result["matched_on"] = matched_on
415
+ quartet = {"source": "assembled" if nm else "raw", "upstream": r["params"]}
416
+ if nm:
417
+ quartet["assembled_from"] = {"name": nm, "fetch_limit_forced": 100,
418
+ "filter": "client-side contains"}
419
+ window_note = (f" (window {win['published_from']}~{win['published_to']}"
420
+ + ("=our default rolling 7 days" if not (published_from or published_to) else "") + ")")
421
+ return self._flat_env(
422
+ tool="gb_contracts_finder_published_notices",
423
+ input={"name": nm, "published_from": published_from, "published_to": published_to,
424
+ "stages": stages, "limit": int(limit), "cursor": cursor},
425
+ reason=(f"UK CF: \"{nm}\" matched {len(rows)} rows / original page {len(r['rows'])} rows ({matched_on}"
426
+ + (", next page available to walk" if r.get("next_cursor") else ", last page")
427
+ + f"; window {win['published_from']}~{win['published_to']})"
428
+ + " — the below-threshold face complements FTS; supplier id is the GB-COH/GB-CFS registry key"
429
+ if nm else
430
+ f"UK CF: {len(rows)} rows{window_note}"
431
+ + (", next page available to walk (carry the same window when walking)" if r.get("next_cursor") else ", last page")),
432
+ result=result,
433
+ evidence=[{"quote": f"{n['ocid'] or '—'} = {n['buyer'] or '—'} × "
434
+ f"{' / '.join(n['suppliers']) or '(not awarded)'} ({n['date'] or '—'})",
435
+ "url": "https://www.contractsfinder.service.gov.uk/"}
436
+ for n in rows[:5]],
437
+ provider_id="cf", metas=[r["_meta"]],
438
+ query=quartet,
439
+ )
440
+
441
+ def sec_fulltext_search(self, *, q, forms=None, startdt=None, enddt=None, limit=10,
442
+ execution=None, no_cache=None):
443
+ """company sec_fulltext_search: SEC filing full-text search 2001+ (EDGAR FTS four params verbatim;
444
+ either startdt/enddt given → the other is auto-filled — a single end is silently ignored upstream
445
+ (measured 2026-10-20); after filling, both ends are always present)."""
446
+ self._co_only("sec_fulltext_search")
447
+ v = str(q or "").strip()
448
+ if not v:
449
+ raise SdkError("usage_error", provider="edgar-fts", detail="sec_fulltext_search requires q (EDGAR FTS syntax)")
450
+ r = company.edgar_fts.search_raw(v, forms=forms, startdt=startdt, enddt=enddt,
451
+ limit=limit, cache=self._c._cache,
452
+ no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
453
+ result = {k: x for k, x in r.items() if k != "_meta"}
454
+ dr = result.get("date_range")
455
+ span = f", window {dr['startdt']}~{dr['enddt']}" if dr else ""
456
+ cap = ", capped at ≥ this count" if result.get("total_relation") == "gte" else ""
457
+ return self._flat_env(
458
+ tool="sec_fulltext_search", input={"q": v, "forms": forms, "startdt": startdt, "enddt": enddt,
459
+ "limit": result["limit_applied"]},
460
+ reason=(f"EDGAR FTS: {result['total']} hits (2001+ full-text{cap}{span}), "
461
+ f"returning {len(result['hits'])} rows — adsh shares its key with the SEC corpus line"),
462
+ result=result,
463
+ evidence=[{"quote": f"{h['form_type'] or '—'} {h['adsh'] or '—'} = "
464
+ f"{h['company'] or '—'} ({h['file_date'] or '—'})",
465
+ "url": h["doc_url"] or "https://efts.sec.gov/LATEST/search-index"}
466
+ for h in result["hits"][:5]],
467
+ provider_id="edgar-fts", metas=[r["_meta"]],
468
+ query={"source": "raw", "search": v, "upstream": r["params"]},
469
+ )
470
+
471
+ # ---------- 0.3.3 European national registry live faces (CH Zefix / NO Brreg / FI YTJ) ----------
472
+
473
+ def ch_zefix_firm_search(self, *, name, max_entries=100, offset=0,
474
+ execution=None, no_cache=None):
475
+ """company ch_zefix_firm_search: Swiss commercial register name search (Zefix POST search.json three params
476
+ verbatim; name is a contains-match that also hits historical names — candidates are not an authoritative
477
+ normalization)."""
478
+ self._co_only("ch_zefix_firm_search")
479
+ v = str(name or "").strip()
480
+ if not v:
481
+ raise SdkError("usage_error", provider="ch_zefix", detail="ch_zefix_firm_search requires name (company name)")
482
+ n = min(max(1, int(max_entries or 100)), 100)
483
+ off = max(0, int(offset or 0))
484
+ r = company.ch_zefix.search_firms(v, max_entries=n, offset=off,
485
+ cache=self._c._cache,
486
+ no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
487
+ result = {k: x for k, x in r.items() if k != "_meta"}
488
+ top = (result["firms"] or [None])[0]
489
+ return self._flat_env(
490
+ tool="ch_zefix_firm_search", input={"name": v, "max_entries": n, "offset": off},
491
+ reason=(f"Zefix search: {len(result['firms'])} candidates"
492
+ + (f", top hit {top['name']} (uid {top['uid'] or '—'}, {top['status'] or '—'}, {top['legal_seat'] or '—'})"
493
+ if top else "") + " — contains-match, not an authoritative normalization; the three keys (ehraid/uid/chid) are surfaced directly"
494
+ if top else f"Zefix search zero hits: \"{v}\" (an empty result is a legal answer)"),
495
+ result=result,
496
+ evidence=[{"quote": (f"{f['name']} (ehraid {f['ehraid']}, uid {f['uid'] or '—'}, "
497
+ f"{f['status'] or '—'}, {f['legal_seat'] or '—'})"),
498
+ "url": f"https://www.zefix.ch/ZefixREST/api/v1/firm/{f['ehraid']}.json"}
499
+ for f in result["firms"][:5]],
500
+ provider_id="ch_zefix", metas=[r["_meta"]],
501
+ query={"source": "raw", "upstream": r["params"]},
502
+ )
503
+
504
+ def ch_zefix_firm(self, *, ehraid, execution=None, no_cache=None):
505
+ """company ch_zefix_firm: full record card by ehraid (Zefix /firm/{ehraid}.json; oldNames + M&A edges)."""
506
+ self._co_only("ch_zefix_firm")
507
+ v = str(ehraid or "").strip()
508
+ if not v.isdigit():
509
+ raise SdkError("usage_error", provider="ch_zefix",
510
+ detail=f"ch_zefix_firm requires ehraid (numeric master id, surfaced by search rows), got \"{ehraid}\"")
511
+ r = company.ch_zefix.get_firm(v, cache=self._c._cache,
512
+ no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
513
+ f = r["firm"]
514
+ edges = (f"; predecessors {len(f['has_taken_over'])} / successors {len(f['taken_over_by'])} M&A edges"
515
+ if (f["has_taken_over"] or f["taken_over_by"]) else "")
516
+ evidence = [{"quote": f"{f['name'] or '—'} ({f['status'] or '—'}, {f['legal_seat'] or '—'})",
517
+ "url": f"https://www.zefix.ch/ZefixREST/api/v1/firm/{f['ehraid'] or v}.json"}]
518
+ if f["taken_over_by"]:
519
+ e = f["taken_over_by"][0]
520
+ evidence.append({"quote": f"successor edge: taken over by {e.get('name') or '—'} (ehraid {e.get('ehraid')})",
521
+ "url": f"https://www.zefix.ch/ZefixREST/api/v1/firm/{e.get('ehraid')}.json"})
522
+ elif f["has_taken_over"]:
523
+ e = f["has_taken_over"][0]
524
+ evidence.append({"quote": f"predecessor edge: has taken over {e.get('name') or '—'} (ehraid {e.get('ehraid')})",
525
+ "url": f"https://www.zefix.ch/ZefixREST/api/v1/firm/{e.get('ehraid')}.json"})
526
+ return self._flat_env(
527
+ tool="ch_zefix_firm", input={"ehraid": v},
528
+ reason=(f"Zefix single record: {f['name'] or '(no name)'} (uid {f['uid'] or '—'}, {f['status'] or '—'}, "
529
+ f"{f['legal_seat'] or '—'}){edges} — purpose/oldNames full text included"),
530
+ result=f,
531
+ evidence=evidence,
532
+ provider_id="ch_zefix", metas=[r["_meta"]],
533
+ query={"source": "raw", "upstream": {"ehraid": f["ehraid"]}},
534
+ )
535
+
536
+ def no_brreg_enheter(self, *, navn=None, organisasjonsnummer=None, page=0, size=20,
537
+ execution=None, no_cache=None):
538
+ """company no_brreg_enheter: Norwegian registry master record (Enhetsregisteret; navn search face XOR org-number
539
+ single-record face — two separate upstream endpoints; page/size 0-based HAL pagination belongs to the search face)."""
540
+ self._co_only("no_brreg_enheter")
541
+ q = str(navn or "").strip() or None
542
+ org = str(organisasjonsnummer or "").replace(" ", "").strip() or None
543
+ if q and org:
544
+ raise SdkError("usage_error", provider="no_brreg",
545
+ detail="navn (search face) and organisasjonsnummer (single-record face) are mutually exclusive — upstream is two endpoints, give exactly one of the two required")
546
+ if not q and not org:
547
+ raise SdkError("usage_error", provider="no_brreg",
548
+ detail="no_brreg_enheter requires navn (name search) or organisasjonsnummer (9-digit org number)")
549
+ cache, nc = self._c._cache, self._c.no_cache if no_cache is None else bool(no_cache)
550
+ if org:
551
+ if not org.isdigit() or len(org) != 9:
552
+ raise SdkError("usage_error", provider="no_brreg",
553
+ detail=f"organisasjonsnummer must be 9 digits (Norwegian org number), got \"{organisasjonsnummer}\"")
554
+ r = company.no_brreg.get_enhet(org, cache=cache, no_cache=nc)
555
+ result = {"enhet": r["enhet"]}
556
+ upstream = {"organisasjonsnummer": org}
557
+ top = r["enhet"]
558
+ head = (f"single record {top['organisasjonsnummer']} = {top['navn'] or '—'}"
559
+ f" ({top['organisasjonsform']['beskrivelse'] or '—'}, {top['naeringskode1']['kode'] or '—'}"
560
+ + (f", {top['antall_ansatte']} employees" if top.get("antall_ansatte") is not None else "") + ")")
561
+ else:
562
+ p, s = max(0, int(page or 0)), min(max(1, int(size or 20)), 100)
563
+ r = company.no_brreg.search_enheter(q, page=p, size=s, cache=cache, no_cache=nc)
564
+ result = {"enheter": r["enheter"], "page": r["page"]}
565
+ upstream = r["params"]
566
+ top = (r["enheter"] or [None])[0]
567
+ head = (f"{result['page']['total_elements'] if result['page']['total_elements'] is not None else len(r['enheter'])} companies matched"
568
+ + (f", top hit {top['navn'] or '—'} ({top['organisasjonsnummer']})" if top else " (zero hits is a legal answer)"))
569
+ mode = "id normalization" if org else "name search"
570
+ return self._flat_env(
571
+ tool="no_brreg_enheter", input={"navn": q, "organisasjonsnummer": org,
572
+ "page": page if not org else None, "size": size if not org else None},
573
+ reason=(f"Brreg {mode}: {head} — navn is the verbatim Norwegian param (?name= 400s)"),
574
+ result=result,
575
+ evidence=[{"quote": (f"{(top or {}).get('organisasjonsnummer') or '—'} = "
576
+ f"{(top or {}).get('navn') or '—'}"
577
+ f" ({(top.get('organisasjonsform') or {}).get('beskrivelse') or '—'}, "
578
+ f"{(top.get('naeringskode1') or {}).get('kode') or '—'})"),
579
+ "url": f"https://data.brreg.no/enhetsregisteret/api/enheter/"
580
+ f"{(top or {}).get('organisasjonsnummer') or ''}"}
581
+ for top in (result.get("enheter") or ([result.get("enhet")] if result.get("enhet") else []))[:5]
582
+ if top],
583
+ provider_id="no_brreg", metas=[r["_meta"]],
584
+ query={"source": "raw", "upstream": upstream},
585
+ )
586
+
587
+ def no_brreg_roller(self, *, organisasjonsnummer, execution=None, no_cache=None):
588
+ """company no_brreg_roller: org number → roles face (Enhetsregisteret /roller; DAGL CEO / STYR board /
589
+ REVI auditor groups; person rows = name + birth date (no national id number on the public face), corporate
590
+ rows = org number; avregistrert = retired)."""
591
+ self._co_only("no_brreg_roller")
592
+ v = str(organisasjonsnummer or "").replace(" ", "").strip()
593
+ if not v.isdigit() or len(v) != 9:
594
+ raise SdkError("usage_error", provider="no_brreg",
595
+ detail=f"no_brreg_roller requires organisasjonsnummer (9-digit Norwegian org number), got \"{organisasjonsnummer}\"")
596
+ r = company.no_brreg.get_roller(v, cache=self._c._cache,
597
+ no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
598
+ groups = r["rollegrupper"]
599
+ roles_n = sum(len(g["roller"]) for g in groups)
600
+ persons = [m["person"]["navn"] for g in groups for m in g["roller"] if m.get("person")]
601
+ return self._flat_env(
602
+ tool="no_brreg_roller", input={"organisasjonsnummer": v},
603
+ reason=(f"Brreg roles face: {v} → {len(groups)} groups, {roles_n} seats"
604
+ + (f" ({'; '.join(persons[:3])}{'…' if len(persons) > 3 else ''})" if persons else "")
605
+ + " — name + birth date are collectible (no national id number on the public face); avregistrert=true = retired"),
606
+ result={"organisasjonsnummer": v, "rollegrupper": groups},
607
+ evidence=[{"quote": (f"{g['type_beskrivelse'] or g['type_kode']}: "
608
+ + "; ".join((m.get("person") or {}).get("navn")
609
+ or f"enhet {m.get('enhet_organisasjonsnummer')}"
610
+ or m.get("kode") or "—" for m in g["roller"][:3])),
611
+ "url": f"https://data.brreg.no/enhetsregisteret/api/enheter/{v}/roller"}
612
+ for g in groups[:5]],
613
+ provider_id="no_brreg", metas=[r["_meta"]],
614
+ query={"source": "raw", "upstream": {"organisasjonsnummer": v}},
615
+ )
616
+
617
+ def fi_ytj_companies(self, *, name=None, business_id=None,
618
+ execution=None, no_cache=None):
619
+ """company fi_ytj_companies: Finnish YTJ business master (PRH v3 one endpoint, two lookups; name search face
620
+ XOR businessId direct face; names[] = versioned name history; **pagination params are silently ignored
621
+ upstream — page size is always ~63 rows, total_results is the real total**)."""
622
+ self._co_only("fi_ytj_companies")
623
+ nm = str(name or "").strip() or None
624
+ bid = str(business_id or "").replace(" ", "").strip() or None
625
+ if bool(nm) == bool(bid):
626
+ raise SdkError("usage_error", provider="fi_ytj",
627
+ detail="name (search face) and business_id (direct face) — provide exactly one of the two required")
628
+ r = company.fi_ytj.search_companies(nm, bid, cache=self._c._cache,
629
+ no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
630
+ result = {k: x for k, x in r.items() if k != "_meta"}
631
+ top = (result["companies"] or [None])[0]
632
+ mode = "id normalization" if bid else "name search"
633
+ return self._flat_env(
634
+ tool="fi_ytj_companies", input={"name": nm, "business_id": bid},
635
+ reason=(f"YTJ {mode}: \"{nm or bid}\" → {result['total_results'] if result['total_results'] is not None else len(result['companies'])} companies"
636
+ + (f" (returning {len(result['companies'])} rows — pagination params ignored upstream, page size always ~63)"
637
+ if result["total_results"] is not None and result["total_results"] > len(result["companies"]) else "")
638
+ + (f", top hit {top['latest_name'] or '—'} ({top['business_id']}, status {top['status'] or '—'})"
639
+ if top else " (zero hits is a legal answer)")
640
+ + " — name matches historical/branch names by contains"),
641
+ result=result,
642
+ evidence=[{"quote": (f"{c.get('business_id')} = {c.get('latest_name') or '—'}"
643
+ f" ({(c.get('main_business_line') or {}).get('code') or '—'}, "
644
+ f"{(c.get('main_business_line') or {}).get('description') or '—'})"),
645
+ "url": f"https://avoindata.prh.fi/opendata-ytj-api/v3/companies?businessId={c.get('business_id')}"}
646
+ for c in result["companies"][:5]],
647
+ provider_id="fi_ytj", metas=[r["_meta"]],
648
+ query={"source": "raw", "upstream": r["params"]},
649
+ )
650
+
651
+ # ---------- 0.3.4 FR annuaire live search face (recherche-entreprises) ----------
652
+
653
+ def fr_recherche_search(self, *, q, page=None, per_page=10, etat_administratif=None,
654
+ execution=None, no_cache=None):
655
+ """company fr_recherche_search: French annuaire-entreprises official aggregate search (q takes name/officer
656
+ name/SIREN alike; per_page hard wall 1-25; etat_administratif A active / C ceased is a true filter)."""
657
+ self._co_only("fr_recherche_search")
658
+ v = str(q or "").strip()
659
+ pp = int(per_page if per_page is not None else 10)
660
+ ea = str(etat_administratif).strip().upper() if etat_administratif is not None else None
661
+ r = company.fr_recherche.search(v, page=page, per_page=pp, etat_administratif=ea,
662
+ cache=self._c._cache,
663
+ no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
664
+ result = {k: x for k, x in r.items() if k != "_meta"}
665
+ top = (result["results"] or [None])[0]
666
+ nd = len(top.get("dirigeants") or []) if top else 0
667
+ return self._flat_env(
668
+ tool="fr_recherche_search",
669
+ input={"q": v, "page": page, "per_page": pp, "etat_administratif": ea},
670
+ reason=(f"FR recherche: \"{v}\" → {result['total_results'] if result['total_results'] is not None else len(result['results'])} companies"
671
+ + (f" (page {result['page']}/{result['total_pages']})" if result.get("total_pages") else "")
672
+ + (f", top hit {top['nom_complet'] or '—'} (SIREN {top['siren']}, {top['etat_administratif'] or '—'}"
673
+ f"{', dirigeants ' + str(nd) + ' rows' if nd else ''})" if top else " (zero hits is a legal answer)")
674
+ + (", ceased rows only" if ea == "C" else ", active rows only" if ea == "A" else "")
675
+ + " — q takes name/officer name/SIREN alike"),
676
+ result=result,
677
+ evidence=[{"quote": (f"{c['siren']} = {c['nom_complet'] or '—'}"
678
+ f" ({c['categorie_entreprise'] or '—'}, {c['nature_juridique'] or '—'}"
679
+ f"{(', dirigeants ' + str(len(c.get('dirigeants') or [])) + ' rows') if c.get('dirigeants') else ''})"),
680
+ "url": f"https://recherche-entreprises.api.gouv.fr/search?q={c['siren']}"}
681
+ for c in result["results"][:5]],
682
+ provider_id="fr_recherche", metas=[r["_meta"]],
683
+ query={"source": "raw", "upstream": r["params"]},
684
+ )
685
+
686
+ # ---------- 0.3.11 CO RUES + KR hometax dual-country batch (LatAm/Korea company master & status faces) ----------
687
+
688
+ def co_rues_companies(self, where=None, *, limit=20, offset=0, order=None,
689
+ execution=None, no_cache=None):
690
+ """company co_rues_companies: Colombia RUES commercial-registry master (datos.gov.co Socrata c82u-588k;
691
+ where → $where raw SoQL verbatim channel; limit/offset/order mirrored by name). 9.44M-row corpus —
692
+ matricula registry key x numero_identificacion (NIT tax id) x CIIU industry x legal form x RL officer-name edge;
693
+ **default base filter admits tax-entity rows only + RL cedula-number column stripped (PII double gate)**."""
694
+ self._co_only("co_rues_companies")
695
+ r = company.co_rues.companies_raw(where, limit=limit, offset=offset, order=order,
696
+ cache=self._c._cache,
697
+ no_cache=no_cache if no_cache is not None else self._c.no_cache)
698
+ res = {k: v for k, v in r.items() if k != "_meta"}
699
+ rows = res["rows"]
700
+ if rows:
701
+ reason = (f"CO RUES registry master: {len(rows)} rows returned (from offset={offset}; Socrata echoes no total)"
702
+ f" — matricula x NIT x CIIU (sample {rows[0].get('razon_social') or '—'}, "
703
+ f"NIT {rows[0].get('numero_identificacion') or '—'}, "
704
+ f"{rows[0].get('organizacion_juridica') or '—'})"
705
+ f"; base filter clase_identificacion='NIT' (sole-proprietor personal-id rows stay out of the face), RL number column stripped")
706
+ else:
707
+ reason = ("CO RUES: 0 rows — the where filter matched nothing or offset is past the end; both are legal answers (not errors); "
708
+ "base filter clase_identificacion='NIT' always on (sole proprietors go through the bulk line, not this face)")
709
+ ev = [{"quote": f"{x.get('matricula') or '—'} {x.get('razon_social') or '—'} — "
710
+ f"NIT {x.get('numero_identificacion') or '—'}-{x.get('digito_verificacion') or '—'}"
711
+ f" ({x.get('estado_matricula') or '—'}, CIIU {x.get('cod_ciiu_act_econ_pri') or '—'})",
712
+ "url": "https://www.datos.gov.co/resource/c82u-588k.json"}
713
+ for x in rows[:3] if x.get("razon_social")]
714
+ return self._flat_env(tool="co_rues_companies",
715
+ input={"where": where, "limit": limit, "offset": offset, "order": order},
716
+ reason=reason, result=res, evidence=ev, provider_id="co_rues",
717
+ metas=[r["_meta"]],
718
+ query={"source": "raw", "upstream": r["params"]})
719
+
720
+ def kr_hometax_biz_status(self, *, biz_no, execution=None, no_cache=None):
721
+ """company kr_hometax_biz_status: Korea NTA business-registration-status single-shot (휴포업/tax type;
722
+ biz_no 10 digits (3-2-5 hyphens optional); all four states are legal answers — operating / closed (ISO 폭업일자) /
723
+ unregistered (result=S) / error; txprNm stays null when anonymous — no owner-name leak)."""
724
+ self._co_only("kr_hometax_biz_status")
725
+ r = company.kr_hometax.biz_status_raw(biz_no, cache=self._c._cache,
726
+ no_cache=no_cache if no_cache is not None else self._c.no_cache)
727
+ s = r["status"]
728
+ state_label = {"active": "operating", "closed": "closed", "unregistered": "unregistered",
729
+ "error": "malformed shape"}[s["state"]]
730
+ tail = f", 폭업일자 {s['closed_date']}" if s["closed_date"] else ""
731
+ reason = (f"KR hometax: {s['biz_no']} → {state_label}"
732
+ + (f" ({s['tax_type'] or s['simple_status'] or '—'})" if s["state"] != "error" else " (verbatim passthrough)")
733
+ + tail
734
+ + " — unregistered is also a legal result=S answer (same family as VIES INVALID); txprNm stays null when anonymous")
735
+ return self._flat_env(
736
+ tool="kr_hometax_biz_status", input={"biz_no": s["biz_no"]},
737
+ reason=reason, result={"status": s},
738
+ evidence=[{"quote": (f"{s['biz_no']}: {s['tax_type'] or '—'}"
739
+ + (f" | EN: {s['english_status']}" if s["english_status"] else "")),
740
+ "url": "https://hometax.go.kr/wqAction.do?actionId=ATTABZAA001R08"}],
741
+ provider_id="kr_hometax", metas=[r["_meta"]],
742
+ query={"source": "raw", "upstream": {"actionId": "ATTABZAA001R08", "txprDscmNo": s["biz_no"],
743
+ "dongCode": s["biz_no"][3:5]}},
744
+ )
745
+
746
+ # ---------- 0.2.0 flat panel commands (esg: source-qualified names + upstream param names + our defaults) ----------