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,811 @@
1
+ """healthcare faces — flat face methods mixed in (moved out of client.py's _Domain in the 0.2.6 domain split).
2
+
3
+ Mixin root _Domain (MRO assembly, see client.py); discipline: no __init__, it may only depend on root internals
4
+ (_flat_env/_c/_request…), method names are globally unique across domains (a collision would shadow another domain's face).
5
+ """
6
+ from ..errors import SdkError
7
+ from ..transport import _tty_tag
8
+ import urllib.error
9
+ import urllib.parse
10
+ import urllib.request
11
+
12
+
13
+ from .. import healthcare # the domain module is called by path (the historical shape, kept for the least-perturbation C2)
14
+
15
+ class HealthcareFaces:
16
+ def _hc_only(self, tool: str):
17
+ if self.name != "healthcare":
18
+ raise SdkError("usage_error", detail=f"{tool} is a healthcare domain capability (not available in the {self.name} domain)")
19
+ def rxnav_rxcui_by_name(self, *, name, execution=None, no_cache=None):
20
+ """healthcare rxnav_rxcui_by_name: drug/brand name → an RxCUI normalization card (the upstream rxcuiByName single-record face, no paging)."""
21
+ self._hc_only("rxnav_rxcui_by_name")
22
+ v = str(name or "").strip()
23
+ if not v:
24
+ raise SdkError("usage_error", provider="rxnav", detail="rxnav_rxcui_by_name needs name (a drug or brand name)")
25
+ r = healthcare.rxnav.resolve_drug_name(v, cache=self._c._cache,
26
+ no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
27
+ result = {k: x for k, x in r.items() if k != "_meta"}
28
+ metas = [m for m in (r["_meta"].get("lookup"), r["_meta"].get("props")) if m]
29
+ return self._flat_env(
30
+ tool="rxnav_rxcui_by_name", input={"name": v},
31
+ reason=f"RxNav normalization: drug name '{v}' → RxCUI {r['rxcui']}{_tty_tag(r.get('tty'))}",
32
+ result=result,
33
+ evidence=[{"quote": f"RxCUI {r['rxcui']} = {result.get('normalized_name') or v}",
34
+ "url": f"https://rxnav.nlm.nih.gov/REST/rxcui/{r['rxcui']}/properties.json"}],
35
+ provider_id="rxnav", metas=metas,
36
+ query={"source": "raw", "upstream": {"name": v}},
37
+ )
38
+
39
+ def rxnav_rxcui_atc(self, *, rxcui, execution=None, no_cache=None):
40
+ """healthcare rxnav_rxcui_atc: RxCUI → ATC code (the upstream property.json?propName=ATC; run rxnav_rxcui_by_name first for drug names)."""
41
+ self._hc_only("rxnav_rxcui_atc")
42
+ v = str(rxcui or "").strip()
43
+ if not v.isdigit():
44
+ raise SdkError("usage_error", provider="rxnav",
45
+ detail="rxnav_rxcui_atc needs rxcui (a numeric string; normalize drug names first with rxnav_rxcui_by_name)")
46
+ at = healthcare.rxnav.get_atc(v, cache=self._c._cache,
47
+ no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
48
+ codes = at["atc"]
49
+ return self._flat_env(
50
+ tool="rxnav_rxcui_atc", input={"rxcui": v},
51
+ reason=(f"RxNav ATC: RxCUI {v} → {'/'.join(codes) if codes else 'no ATC (this concept carries no ATC level — ATC attaches to the ingredient level, and brand-name concepts are usually empty)'}"),
52
+ result={"rxcui": v, "atc": codes},
53
+ evidence=[{"quote": f"RxCUI {v} ATC = {', '.join(codes) or '(empty)'}",
54
+ "url": f"https://rxnav.nlm.nih.gov/REST/rxcui/{v}/property.json?propName=ATC"}],
55
+ provider_id="rxnav", metas=[at["_meta"]],
56
+ query={"source": "raw", "upstream": {"rxcui": v, "propName": "ATC"}},
57
+ )
58
+
59
+ def mesh_descriptor(self, *, label, match="contains", limit=10, execution=None, no_cache=None):
60
+ """healthcare mesh_descriptor: drug/disease term → MeSH descriptor candidates + the preferred record's synonym pool (the upstream names label/match/limit;
61
+ the name mirrors the /mesh/lookup/descriptor path segment — the verb face resolve kind=mesh_term stays frozen)."""
62
+ self._hc_only("mesh_descriptor")
63
+ v = str(label or "").strip()
64
+ if not v:
65
+ raise SdkError("usage_error", provider="mesh", detail="mesh_descriptor needs label (a drug/disease name, etc.)")
66
+ m = str(match or "contains").strip()
67
+ if m not in ("contains", "exact", "startswith"):
68
+ raise SdkError("usage_error", provider="mesh",
69
+ detail=f"the match closed set is contains|exact|startswith (the real upstream limit), got '{m}'")
70
+ size = min(50, max(1, int(limit)))
71
+ cache, nc = self._c._cache, self._c.no_cache if no_cache is None else bool(no_cache)
72
+ r = healthcare.mesh.search_descriptors(v, match=m, limit=size, cache=cache, no_cache=nc)
73
+ result = {k: x for k, x in r.items() if k != "_meta"}
74
+ top = (result["candidates"] or [None])[0]
75
+ top_descriptor, dmeta = None, None
76
+ if top: # details of the preferred record: synonym pool + qualifier anchors (a fixed two-hop cost, the vocabulary is released yearly)
77
+ d = healthcare.mesh.descriptor_details(top["mesh_ui"], cache=cache, no_cache=nc)
78
+ top_descriptor = {"mesh_ui": d["descriptor"], "label": top["label"],
79
+ "preferred_term": d["preferred_term"], "synonyms": d["synonyms"],
80
+ "qualifiers": d["qualifiers"], "seealso": d["seealso"]}
81
+ dmeta = d["_meta"]
82
+ return self._flat_env(
83
+ tool="mesh_descriptor", input={"label": v, "match": m, "limit": size},
84
+ reason=(f"MeSH normalization: '{v}' → {top_descriptor['mesh_ui']} {top_descriptor['label']}"
85
+ f" ({len(top_descriptor['synonyms'])} synonyms, authoritative name {top_descriptor['preferred_term'] or '—'})"
86
+ if top_descriptor else
87
+ f"MeSH zero hits: '{v}' is not in the controlled vocabulary (an empty result is a legal answer; for a brand name try the ingredient name)"),
88
+ result={**result, "top_descriptor": top_descriptor},
89
+ evidence=[{"quote": f"{c['mesh_ui'] or '—'} = {c['label'] or '—'}",
90
+ "url": c["descriptor_uri"] or "https://id.nlm.nih.gov/mesh/lookup/descriptor"}
91
+ for c in result["candidates"][:5]],
92
+ provider_id="mesh", metas=[r["_meta"], dmeta],
93
+ query={"source": "raw", "upstream": {"label": v, "match": m, "limit": size}},
94
+ )
95
+
96
+ def gudid_devices(self, *, di=None, udi=None, record_key=None, execution=None, no_cache=None):
97
+ """healthcare gudid_devices: the US device master record (AccessGUDID v3, a single endpoint with exactly one of di/udi/record_key)."""
98
+ self._hc_only("gudid_devices")
99
+ r = healthcare.gudid.lookup_device(di=di, udi=udi, record_key=record_key,
100
+ cache=self._c._cache,
101
+ no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
102
+ result = {k: x for k, x in r.items() if k != "_meta"}
103
+ key = r["by"]
104
+ return self._flat_env(
105
+ tool="gudid_devices", input={k: v for k, v in (("di", di), ("udi", udi), ("record_key", record_key)) if v},
106
+ reason=(f"AccessGUDID v3 master record: {key}={r[key]} → {r.get('brand_name') or '(no brand name)'}"
107
+ f" / {r.get('company_name') or '(no manufacturer name)'}"),
108
+ result=result,
109
+ evidence=[{"quote": (r.get("device_description") or f"{key}={r[key]}")[:200],
110
+ "url": f"https://accessgudid.nlm.nih.gov/api/v3/devices/lookup.json?{key}={r[key]}"}],
111
+ provider_id="gudid", metas=[r["_meta"]],
112
+ query={"source": "raw", "upstream": {key: r[key]}},
113
+ )
114
+
115
+ def eudamed_devices_udi_di_data(self, *, primary_di, page=0, page_size=5, iso2_code="en",
116
+ execution=None, no_cache=None):
117
+ """healthcare eudamed_devices_udi_di_data: the EU device master record (an EUDAMED primaryDi exact lookup + paging under the upstream names page/pageSize/iso2Code)."""
118
+ self._hc_only("eudamed_devices_udi_di_data")
119
+ r = healthcare.eudamed.query_udi_di(primary_di, page=page, page_size=page_size, iso2_code=iso2_code,
120
+ cache=self._c._cache,
121
+ no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
122
+ result = {k: x for k, x in r.items() if k != "_meta"}
123
+ mfr = result.get("manufacturer") or {}
124
+ ar = result.get("authorised_representative")
125
+ return self._flat_env(
126
+ tool="eudamed_devices_udi_di_data",
127
+ input={"primary_di": str(primary_di).strip(), "page": int(page),
128
+ "page_size": int(page_size), "iso2_code": str(iso2_code)},
129
+ reason=(f"EUDAMED master record: DI {result.get('primary_di')} → {result.get('trade_name') or '(no brand name)'}"
130
+ f" ({mfr.get('name') or '—'}, SRN {mfr.get('srn') or '—'}"
131
+ + (f"; authorised rep {ar['srn']}" if ar and ar.get("srn") else "")
132
+ + f", {result.get('risk_class') or '—'}) — total_elements={result.get('total_elements')}"
133
+ f" (page={result.get('page')}, page_size={result.get('page_size')})"),
134
+ result=result,
135
+ evidence=[{"quote": (f"{result.get('primary_di')} = {result.get('trade_name') or '—'} / "
136
+ f"{mfr.get('name') or '—'} (SRN {mfr.get('srn') or '—'})"),
137
+ "url": (f"https://ec.europa.eu/tools/eudamed/api/devices/udiDiData"
138
+ f"?primaryDi={result.get('primary_di')}&page={int(page)}&pageSize={int(page_size)}")}],
139
+ provider_id="eudamed", metas=[r["_meta"]],
140
+ query={"source": "raw",
141
+ "upstream": {"primaryDi": str(primary_di).strip(), "page": int(page),
142
+ "pageSize": int(page_size), "size": int(page_size),
143
+ "iso2Code": str(iso2_code)}},
144
+ )
145
+
146
+ def epmc_search(self, *, search=None,
147
+ result_type="lite", page_size=25, cursor_mark="*", sort=None,
148
+ execution=None, no_cache=None):
149
+ """healthcare epmc_search: literature evidence search (Europe PMC; search=query is the verbatim param channel, AFF: for affiliation search)."""
150
+ self._hc_only("epmc_search")
151
+ q = str(search or "").strip() or None
152
+ if not q:
153
+ raise SdkError("usage_error", provider="epmc",
154
+ detail="epmc_search needs search (the expression uses the upstream name, e.g. AFF:\"Novartis\")")
155
+ r = healthcare.epmc.search_raw(q, result_type=result_type, page_size=page_size,
156
+ cursor_mark=cursor_mark, sort=sort,
157
+ cache=self._c._cache,
158
+ no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
159
+ rows = r["rows"]
160
+ quartet = {"source": "raw", "search": q,
161
+ "upstream": {"query": q, "format": "json", "resultType": str(result_type),
162
+ "pageSize": r["page_size"], "cursorMark": str(cursor_mark or "*"),
163
+ **({"sort": str(sort)} if sort else {})}}
164
+ return self._flat_env(
165
+ tool="epmc_search",
166
+ input={"search": q,
167
+ "result_type": result_type, "page_size": r["page_size"],
168
+ "cursor_mark": str(cursor_mark or "*"), "sort": sort},
169
+ reason=(f"Europe PMC: {r['hitCount']} hits, {len(rows)} returned"
170
+ + f" (expression {q})"
171
+ + (f", next page cursorMark={r['next_cursor_mark']}" if r.get("next_cursor_mark") else "")),
172
+ result={"hitCount": r["hitCount"], "count": len(rows), "rows": rows,
173
+ "next_cursor_mark": r.get("next_cursor_mark"), "page_size": r["page_size"]},
174
+ evidence=[{"quote": x["title"], **({"url": x["url"]} if x["url"] else {}), # PAT rows have no EPMC page — omit the key rather than emitting null (0.2.8)
175
+ "pmid": x["pmid"], "journal": x["journal"], "pub_year": x["pub_year"],
176
+ "cited_by": x["cited_by"]}
177
+ for x in rows[:5]],
178
+ provider_id="epmc", metas=[r["_meta"]],
179
+ query=quartet,
180
+ )
181
+
182
+ def ctgov_studies(self, *, query_term=None, query_titles=None, query_intr=None,
183
+ query_cond=None, filter_advanced=None, agg_filters=None, fields=None,
184
+ page_size=20, page_token=None, sort=None,
185
+ count_total=True, execution=None, no_cache=None):
186
+ """healthcare ctgov_studies: clinical-trial search (CT.gov v2; query_term is the query.term verbatim channel,
187
+ query_titles/query_intr/agg_filters/fields are independent upstream params that combine with any of them (opened by probing on 2026-10-28),
188
+ query_cond/filter_advanced likewise; a bare call = the first page of the whole database, which is legal)."""
189
+ self._hc_only("ctgov_studies")
190
+ qt = str(query_term or "").strip() or None
191
+ r = healthcare.ctgov.search_raw(query_term=qt, query_titles=query_titles, query_intr=query_intr,
192
+ query_cond=query_cond, filter_advanced=filter_advanced,
193
+ agg_filters=agg_filters, fields=fields,
194
+ page_size=page_size, page_token=page_token, sort=sort,
195
+ count_total=count_total, cache=self._c._cache,
196
+ no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
197
+ rows = r["rows"]
198
+ total = r["total"] if r["total"] is not None else len(rows)
199
+ reason = f"CT.gov: {total} hits, {len(rows)} returned"
200
+ if r.get("next_page_token"):
201
+ reason += f" (next page page_token={r['next_page_token']})"
202
+ quartet = {"source": "raw", "upstream": r["params"]}
203
+ if qt:
204
+ quartet["search"] = qt
205
+ return self._flat_env(
206
+ tool="ctgov_studies",
207
+ input={"query_term": qt, "query_titles": query_titles, "query_intr": query_intr,
208
+ "query_cond": query_cond, "filter_advanced": filter_advanced,
209
+ "agg_filters": agg_filters, "fields": fields,
210
+ "page_size": r["page_size"], "page_token": page_token, "sort": sort,
211
+ "count_total": bool(count_total)},
212
+ reason=reason,
213
+ result={"total": r["total"], "count": len(rows), "rows": rows,
214
+ "next_page_token": r.get("next_page_token"), "page_size": r["page_size"]},
215
+ evidence=[{"quote": f"{x['nct_id']} {x['title']}", "url": x["url"],
216
+ "status": x["status"], "phase": x["phase"], "sponsor": x["sponsor"]}
217
+ for x in rows[:5]],
218
+ provider_id="ctgov", metas=[r["_meta"]],
219
+ query=quartet,
220
+ )
221
+
222
+ def dailymed_spls(self, *, search=None, manufacturer=None, setid=None,
223
+ page=None, pagesize=None, sort=None, execution=None, no_cache=None):
224
+ """healthcare dailymed_spls: SPL labeling metadata (the six DailyMed spls.json params under their upstream names;
225
+ search→drug_name; manufacturer/setid/page/pagesize/sort combine freely)."""
226
+ self._hc_only("dailymed_spls")
227
+ drug_name = str(search or "").strip() or None
228
+ r = healthcare.dailymed.spls_raw(drug_name=drug_name, manufacturer=manufacturer, setid=setid,
229
+ page=page, pagesize=pagesize, sort=sort,
230
+ cache=self._c._cache,
231
+ no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
232
+ rows = r["rows"]
233
+ # Silent-clamp visibility: empty pagination values in metadata are the string 'null' — compare only when both ends parse as integers
234
+ clamp_note = ""
235
+ if pagesize is not None:
236
+ try:
237
+ epp = int(str(r["elements_per_page"]))
238
+ clamp_note = f", upstream actually sent elements_per_page={epp}" + (" (silently clamped to 100 — a larger value has no effect)" if epp < int(pagesize) else "")
239
+ except (TypeError, ValueError):
240
+ clamp_note = f", elements_per_page={r['elements_per_page']}"
241
+ quartet = {"source": "raw", "upstream": r["params"]}
242
+ if drug_name:
243
+ quartet["search"] = drug_name
244
+ return self._flat_env(
245
+ tool="dailymed_spls",
246
+ input={"search": drug_name, "manufacturer": manufacturer, "setid": setid,
247
+ "page": page, "pagesize": pagesize, "sort": sort},
248
+ reason=f"DailyMed SPL: {r['total']} labels hit, {len(rows)} returned{clamp_note}",
249
+ result={"total": r["total"], "count": len(rows), "rows": rows,
250
+ "page": r["page"], "elements_per_page": r["elements_per_page"]},
251
+ evidence=[{"quote": x["title"], "url": x["url"], "setid": x["setid"],
252
+ "published_date": x["published_date"]} for x in rows[:5]],
253
+ provider_id="dailymed", metas=[r["_meta"]],
254
+ query=quartet,
255
+ )
256
+
257
+ def fda_drug_shortages(self, *, search=None,
258
+ limit=20, skip=0, sort="update_date:desc", execution=None, no_cache=None):
259
+ """healthcare fda_drug_shortages: a single lookup of FDA drug shortages (openFDA;
260
+ the search channel uses the upstream name (e.g. openfda.manufacturer_name.exact:"…" / generic_name:"…" / status:Current);
261
+ limit 1-1000 is the upstream hard limit)."""
262
+ self._hc_only("fda_drug_shortages")
263
+ q = str(search or "").strip() or None
264
+ if not q:
265
+ raise SdkError("usage_error", provider="shortages",
266
+ detail="fda_drug_shortages needs search (the expression uses the upstream name, e.g. generic_name:\"bupivacaine\")")
267
+ r = healthcare.shortages.search_raw(q, limit=limit, skip=skip, sort=sort,
268
+ cache=self._c._cache,
269
+ no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
270
+ result = {k: x for k, x in r.items() if k != "_meta"}
271
+ quartet = {"source": "raw", "search": q, "upstream": {"search": q, "limit": result["limit_applied"],
272
+ **({"skip": result["skip"]} if result["skip"] else {}),
273
+ **({"sort": r["sort"]} if r.get("sort") else {})}}
274
+ return self._flat_env(
275
+ tool="fda_drug_shortages",
276
+ input={"search": q,
277
+ "limit": result["limit_applied"], "skip": result["skip"], "sort": r.get("sort")},
278
+ reason=(f"openFDA drug shortages: {result['total']} hits (update_date descending), {result['count']} returned"
279
+ + (f", next page skip={result['skip'] + result['count']}" if result.get("has_more") else "")
280
+ + " — manufacturer names match the openFDA normalization layer (exact phrase); parent and subsidiary groups are not merged, so narrow by NDC/application_number before storing"
281
+ if result["total"] else
282
+ f"openFDA drug shortages: zero hits (zero hits is a legal answer — this manufacturer/drug name has no shortage registration)"),
283
+ result=result,
284
+ evidence=[{"quote": (f"{row['generic_name'] or '—'} = "
285
+ f"{row['company_name'] or (row['manufacturers'] or ['—'])[0]}"
286
+ f" ({row['availability'] or '—'} / {row['status'] or '—'}, updated {row['update_date'] or '—'})"),
287
+ "url": f"https://api.fda.gov/drug/shortages.json?search={urllib.parse.quote(q, safe='')}",
288
+ "package_ndc": row["package_ndc"]}
289
+ for row in result["rows"][:5]],
290
+ provider_id="shortages", metas=[r["_meta"]],
291
+ query=quartet,
292
+ )
293
+
294
+ def _fda_recall_face(self, tool, face, *, search, limit, skip, sort, evidence_fmt, lead, no_cache=None):
295
+ """The envelope shared by the three Wave 10 recall faces (born flat: the upstream param names search/limit/skip/sort, zero sugar, zero deprecations)."""
296
+ raw = str(search or "").strip() or None
297
+ r = healthcare.fda_recalls.search(face, raw, limit=limit, skip=skip, sort=sort,
298
+ cache=self._c._cache,
299
+ no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
300
+ result = {k: x for k, x in r.items() if k != "_meta"}
301
+ quartet = {"source": "raw", "search": result["search"],
302
+ "upstream": {"search": result["search"], "limit": result["limit_applied"],
303
+ **({"skip": result["skip"]} if result["skip"] else {}),
304
+ **({"sort": r["sort"]} if r.get("sort") else {})}}
305
+ return self._flat_env(
306
+ tool=tool,
307
+ input={"search": raw, "limit": result["limit_applied"], "skip": result["skip"], "sort": r.get("sort")},
308
+ reason=(f"{lead}: {result['total']} hits ({r.get('sort') or 'default order'}), {result['count']} returned"
309
+ + (f", next page skip={result['skip'] + result['count']}" if result.get("has_more") else "")
310
+ + " — firm names follow the registration convention (each subsidiary is recorded separately), so group due diligence must aggregate before searching"
311
+ if result["total"] else
312
+ f"{lead}: zero hits (an openFDA 404=NOT_FOUND is a legal answer — this expression has no recall registration)"),
313
+ result=result,
314
+ evidence=[{"quote": evidence_fmt(row),
315
+ "url": f"https://api.fda.gov{healthcare.fda_recalls.provider['ops'][face]['path']}"
316
+ f"?search={urllib.parse.quote(result['search'], safe='')}"}
317
+ for row in result["rows"][:5]],
318
+ provider_id=healthcare.fda_recalls.provider["id"], metas=[r["_meta"]],
319
+ query=quartet,
320
+ )
321
+
322
+ def fda_drug_recalls(self, *, search=None, limit=20, skip=0, sort=None, execution=None, no_cache=None):
323
+ """healthcare fda_drug_recalls: FDA drug recalls (openFDA drug/enforcement;
324
+ search passes through under the upstream name, e.g. recalling_firm.exact:"Firm Name"; limit 1-1000 is a hard limit; the default is report_date descending)."""
325
+ self._hc_only("fda_drug_recalls")
326
+ return self._fda_recall_face(
327
+ "fda_drug_recalls", "drug", search=search, limit=limit, skip=skip, sort=sort,
328
+ evidence_fmt=lambda row: (f"{row['recall_number'] or '—'} · {row['recalling_firm'] or '—'}"
329
+ f" ({row['classification'] or '—'} / {row['status'] or '—'}, "
330
+ f"recall initiated {row['recalled_on'] or '—'})"),
331
+ lead="openFDA drug recalls (the drug/enforcement press-release face, 2012+)", no_cache=no_cache)
332
+
333
+ def fda_device_recalls(self, *, search=None, limit=20, skip=0, sort=None, execution=None, no_cache=None):
334
+ """healthcare fda_device_recalls: FDA device recalls (openFDA device/recall = the RES system;
335
+ firm search uses the phrase form recalling_firm:"Firm Name" (.exact is unverified); it is a different system from the bulk device/enforcement face)."""
336
+ self._hc_only("fda_device_recalls")
337
+ return self._fda_recall_face(
338
+ "fda_device_recalls", "device", search=search, limit=limit, skip=skip, sort=sort,
339
+ evidence_fmt=lambda row: (f"{row['res_event_number'] or '—'} · {row['recalling_firm'] or '—'}"
340
+ f" ({row['recall_status'] or '—'}, initiated {row['initiated_on'] or '—'})"),
341
+ lead="openFDA device recalls (the device/recall RES face, including the full Ongoing lifecycle)", no_cache=no_cache)
342
+
343
+ def fda_food_recalls(self, *, search=None, limit=20, skip=0, sort=None, execution=None, no_cache=None):
344
+ """healthcare fda_food_recalls: FDA food recalls (openFDA food/enforcement;
345
+ search passes through under the upstream name, e.g. recalling_firm.exact:"Firm Name"; the default is report_date descending)."""
346
+ self._hc_only("fda_food_recalls")
347
+ return self._fda_recall_face(
348
+ "fda_food_recalls", "food", search=search, limit=limit, skip=skip, sort=sort,
349
+ evidence_fmt=lambda row: (f"{row['recall_number'] or '—'} · {row['recalling_firm'] or '—'}"
350
+ f" ({row['classification'] or '—'} / {row['status'] or '—'}, "
351
+ f"recall initiated {row['recalled_on'] or '—'})"),
352
+ lead="openFDA food recalls (the food/enforcement face, 2012+)", no_cache=no_cache)
353
+
354
+ def fda_crl_letters(self, *, search=None, limit=20, skip=0, sort="letter_date:desc",
355
+ execution=None, no_cache=None):
356
+ """healthcare fda_crl_letters: FDA Complete Response Letters (openFDA transparency/crl;
357
+ company_name:"Firm Name" finds the company's negative-approval events; company_rep/approver_name (person names, PII) and text (OCR noise) are not returned)."""
358
+ self._hc_only("fda_crl_letters")
359
+ raw = str(search or "").strip() or None
360
+ r = healthcare.fda_crl.search(raw, limit=limit, skip=skip, sort=sort,
361
+ cache=self._c._cache,
362
+ no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
363
+ result = {k: x for k, x in r.items() if k != "_meta"}
364
+ quartet = {"source": "raw", "search": result["search"],
365
+ "upstream": {"search": result["search"], "limit": result["limit_applied"],
366
+ **({"skip": result["skip"]} if result["skip"] else {}),
367
+ **({"sort": r["sort"]} if r.get("sort") else {})}}
368
+ return self._flat_env(
369
+ tool="fda_crl_letters",
370
+ input={"search": raw, "limit": result["limit_applied"], "skip": result["skip"], "sort": r.get("sort")},
371
+ reason=(f"openFDA CRLs: {result['total']} letters hit (letter_date descending), {result['count']} returned"
372
+ + (f", next page skip={result['skip'] + result['count']}" if result.get("has_more") else "")
373
+ + " — the negative-approval event stream (approval_status = the application outcome after the letter)"
374
+ if result["total"] else
375
+ "openFDA CRLs: zero hits (an openFDA 404=NOT_FOUND is a legal answer — this company has no CRL registration or is not covered)"),
376
+ result=result,
377
+ evidence=[{"quote": f"{row['company_name'] or '—'} ({row['letter_date'] or '—'}, "
378
+ f"outcome {row['approval_status'] or '—'})",
379
+ "url": f"https://api.fda.gov/transparency/crl.json"
380
+ f"?search={urllib.parse.quote(result['search'], safe='')}"}
381
+ for row in result["rows"][:5]],
382
+ provider_id=healthcare.fda_crl.provider["id"], metas=[r["_meta"]],
383
+ query=quartet,
384
+ )
385
+
386
+ def _fda_wave10b_face(self, tool, mod, face, *, search, limit, skip, sort, evidence_fmt, lead, no_cache=None):
387
+ """The envelope shared by the seven Wave 10b faces (born flat: the upstream param names search/limit/skip/sort, zero sugar, zero deprecations;
388
+ mod = the fda_drugs|fda_devices provider module; the same shape as _fda_recall_face but parameterised by module)."""
389
+ raw = str(search or "").strip() or None
390
+ r = mod.search(face, raw, limit=limit, skip=skip, sort=sort,
391
+ cache=self._c._cache,
392
+ no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
393
+ result = {k: x for k, x in r.items() if k != "_meta"}
394
+ quartet = {"source": "raw", "search": result["search"],
395
+ "upstream": {"search": result["search"], "limit": result["limit_applied"],
396
+ **({"skip": result["skip"]} if result["skip"] else {}),
397
+ **({"sort": r["sort"]} if r.get("sort") else {})}}
398
+ return self._flat_env(
399
+ tool=tool,
400
+ input={"search": raw, "limit": result["limit_applied"], "skip": result["skip"], "sort": r.get("sort")},
401
+ reason=(f"{lead}: {result['total']} hits ({r.get('sort') or 'default order'}), {result['count']} returned"
402
+ + (f", next page skip={result['skip'] + result['count']}" if result.get("has_more") else "")
403
+ + " — company names follow the registration convention (each subsidiary is recorded separately), so group due diligence must aggregate before searching"
404
+ if result["total"] else
405
+ f"{lead}: zero hits (an openFDA 404=NOT_FOUND is a legal answer — this expression has no registration record)"),
406
+ result=result,
407
+ evidence=[{"quote": evidence_fmt(row),
408
+ "url": f"https://api.fda.gov{mod.provider['ops'][face]['path']}"
409
+ f"?search={urllib.parse.quote(result['search'], safe='')}"}
410
+ for row in result["rows"][:5]],
411
+ provider_id=mod.provider["id"], metas=[r["_meta"]],
412
+ query=quartet,
413
+ )
414
+
415
+ def fda_drug_approvals(self, *, search=None, limit=20, skip=0, sort=None, execution=None, no_cache=None):
416
+ """healthcare fda_drug_approvals: FDA drug application/approval dossiers (openFDA drug/drugsfda;
417
+ sponsor_name:\"TEVA\" finds the company (applicant_name is a documentation illusion, measured with _exists_ on 2026-10-29; the keyword field is case-sensitive — only all-caps matches); the default is the latest submission date descending)."""
418
+ self._hc_only("fda_drug_approvals")
419
+ return self._fda_wave10b_face(
420
+ "fda_drug_approvals", healthcare.fda_drugs, "approvals", search=search, limit=limit, skip=skip, sort=sort,
421
+ evidence_fmt=lambda row: (f"{row['application_number'] or '—'} · {row['sponsor_name'] or '—'}"
422
+ f" ({(row['submissions'] or [{}])[0].get('submission_status') or '—'}, "
423
+ f"{(row['submissions'] or [{}])[0].get('submission_status_date') or '—'})"),
424
+ lead="openFDA drug application dossiers (drug/drugsfda, 29,420 applications)", no_cache=no_cache)
425
+
426
+ def fda_drug_catalog(self, *, search=None, limit=20, skip=0, sort=None, execution=None, no_cache=None):
427
+ """healthcare fda_drug_catalog: the NDC catalogue (openFDA drug/ndc; labeler_name finds the labeler;
428
+ it includes the marketing_start/end_date + marketing_category merged in from NSDE — the standalone NSDE endpoint died, measured 2026-10-29)."""
429
+ self._hc_only("fda_drug_catalog")
430
+ return self._fda_wave10b_face(
431
+ "fda_drug_catalog", healthcare.fda_drugs, "catalog", search=search, limit=limit, skip=skip, sort=sort,
432
+ evidence_fmt=lambda row: (f"{row['product_ndc'] or '—'} · {row['labeler_name'] or '—'}"
433
+ f" ({row['brand_name'] or '—'} / {row['marketing_category'] or '—'}, "
434
+ f"marketed {row['marketing_start_date'] or '—'})"),
435
+ lead="openFDA NDC drug catalogue (drug/ndc, daily refresh)", no_cache=no_cache)
436
+
437
+ def fda_orangebook(self, *, search=None, limit=20, skip=0, sort=None, execution=None, no_cache=None):
438
+ """healthcare fda_orangebook: Orange Book approved drugs (the openFDA drug/orangebook API face;
439
+ the company key is products[].application_name/_full_name (the top-level applicant fields do not exist, measured); the default is approval date descending)."""
440
+ self._hc_only("fda_orangebook")
441
+ return self._fda_wave10b_face(
442
+ "fda_orangebook", healthcare.fda_drugs, "orange_book", search=search, limit=limit, skip=skip, sort=sort,
443
+ evidence_fmt=lambda row: (f"{(row['products'] or [{}])[0].get('application_number') or '—'} · "
444
+ f"{(row['products'] or [{}])[0].get('application_full_name') or '—'}"
445
+ f" ({(row['products'] or [{}])[0].get('application_type') or '—'}, "
446
+ f"approved {row['approval_date'] or '—'})"),
447
+ lead="openFDA Orange Book (drug/orangebook, 48,865 rows, the therapeutic-equivalence reference face)", no_cache=no_cache)
448
+
449
+ def fda_device_510k(self, *, search=None, limit=20, skip=0, sort=None, execution=None, no_cache=None):
450
+ """healthcare fda_device_510k: device premarket notification 510(k) (openFDA device/510k; applicant finds the company
451
+ (the same name as pma; applicant_name is a documentation illusion); contact (person names, PII) is not returned; the default is decision_date descending)."""
452
+ self._hc_only("fda_device_510k")
453
+ return self._fda_wave10b_face(
454
+ "fda_device_510k", healthcare.fda_devices, "clearance_510k", search=search, limit=limit, skip=skip, sort=sort,
455
+ evidence_fmt=lambda row: (f"{row['k_number'] or '—'} · {row['applicant'] or '—'}"
456
+ f" ({row['decision_code'] or '—'}, {row['decision_date'] or '—'})"),
457
+ lead="openFDA device 510(k) clearances (device/510k, 176,466 rows)", no_cache=no_cache)
458
+
459
+ def fda_device_pma(self, *, search=None, limit=20, skip=0, sort=None, execution=None, no_cache=None):
460
+ """healthcare fda_device_pma: device premarket approval PMA (openFDA device/pma; applicant finds the company;
461
+ the ao_statement long text and street addresses are not returned; the default is decision_date descending)."""
462
+ self._hc_only("fda_device_pma")
463
+ return self._fda_wave10b_face(
464
+ "fda_device_pma", healthcare.fda_devices, "pma", search=search, limit=limit, skip=skip, sort=sort,
465
+ evidence_fmt=lambda row: (f"{row['pma_number'] or '—'}{row['supplement_number'] or ''} · "
466
+ f"{row['applicant'] or '—'} ({row['decision_code'] or '—'}, "
467
+ f"{row['decision_date'] or '—'})"),
468
+ lead="openFDA device PMA approvals (device/pma, 12,896 rows)", no_cache=no_cache)
469
+
470
+ def fda_device_registrations(self, *, search=None, limit=20, skip=0, sort=None, execution=None, no_cache=None):
471
+ """healthcare fda_device_registrations: the device establishment register (openFDA device/registrationlisting;
472
+ company search is registration.name:\"Firm Name\" (every field lives in the registration.* namespace; the top-level fields all 404, measured);
473
+ us_agent/official_correspondent (person names, PII) are not returned; the default is the registration expiry year descending)."""
474
+ self._hc_only("fda_device_registrations")
475
+ return self._fda_wave10b_face(
476
+ "fda_device_registrations", healthcare.fda_devices, "registrations", search=search, limit=limit, skip=skip, sort=sort,
477
+ evidence_fmt=lambda row: (f"{row['registration_number'] or '—'} · {row['firm_name'] or '—'}"
478
+ f" ({row['iso_country_code'] or '—'}, expires {row['reg_expiry_date_year'] or '—'})"),
479
+ lead="openFDA device establishment register (device/registrationlisting, 336,946 rows, quarterly)", no_cache=no_cache)
480
+
481
+ def fda_device_classification(self, *, search=None, limit=20, skip=0, sort=None, execution=None, no_cache=None):
482
+ """healthcare fda_device_classification: the device product-code taxonomy face (openFDA device/classification;
483
+ device_name:\"syringe\" finds the category; no date field means no default sort; product_code links to the 510k/pma/recall faces)."""
484
+ self._hc_only("fda_device_classification")
485
+ return self._fda_wave10b_face(
486
+ "fda_device_classification", healthcare.fda_devices, "classification", search=search, limit=limit, skip=skip, sort=sort,
487
+ evidence_fmt=lambda row: (f"{row['product_code'] or '—'} · {row['device_name'] or '—'}"
488
+ f" (Class {row['device_class'] or '—'} / "
489
+ f"{row['medical_specialty_description'] or '—'}, 21 CFR {row['regulation_number'] or '—'})"),
490
+ lead="openFDA device product-code classification (device/classification)", no_cache=no_cache)
491
+
492
+ # ---------- 0.3.16 UK MHRA safety-alert twin faces (gov.uk search/content API; the same contract family as the legal gb_cma case chain) ----------
493
+
494
+ def gb_mhra_alerts(self, q=None, *, count=20, start=0, execution=None, no_cache=None):
495
+ """healthcare gb_mhra_alerts: UK MHRA safety-alert search (gov.uk search API, filter_format=
496
+ medical_safety_alert fixed; q/count/start go through with verbatim names — count politeness
497
+ wall 1-100 (family cap as gb_cma), start = offset paging. q= is a company-name penetration
498
+ channel into the alert corpus (Abbott → 160 alerts 2019-2026, measured 2026-10-30)."""
499
+ self._hc_only("gb_mhra_alerts")
500
+ term = str(q or "").strip() or None
501
+ r = healthcare.gb_mhra.alerts_raw(q=term, count=count, start=start,
502
+ cache=self._c._cache,
503
+ no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
504
+ result = {k: x for k, x in r.items() if k != "_meta"}
505
+ return self._flat_env(
506
+ tool="gb_mhra_alerts",
507
+ input={"q": term, "count": result["limit_applied"], "start": start},
508
+ reason=(f"UK MHRA safety-alert register: {result['total']} alerts hit, returning {len(result['rows'])} rows"
509
+ + (f" (query \"{term}\")" if term else " (full-register enumeration)")
510
+ + " — drug alerts, device alerts (MDA) and the weekly field-safety-notice digests in one stream; "
511
+ "for the single-alert dossier use gb_mhra_alert (slug from the row)"
512
+ if result["rows"] else
513
+ f"UK MHRA zero hits (an empty result is a legitimate answer — query too narrow or no alert on record; enumerate the full register of {result['total']} alerts by omitting q)"),
514
+ result=result,
515
+ evidence=[{"quote": f"{c['title'] or '—'} (updated {c['public_timestamp'] or '—'})",
516
+ "url": c["url"] or "https://www.gov.uk/search/all?keywords=&order=updated-newest"}
517
+ for c in result["rows"][:5]],
518
+ provider_id="gb_mhra", metas=[r["_meta"]],
519
+ query={"source": "raw", "upstream": r["params"]},
520
+ )
521
+
522
+ def gb_mhra_alert(self, slug, *, execution=None, no_cache=None):
523
+ """healthcare gb_mhra_alert: the full UK MHRA single-alert dossier (gov.uk content API; content_id =
524
+ the UUID stable key; alert_type/issued_date are the MHRA metadata fields — medicines-recall-notification,
525
+ device-field-safety-notice …; attachments = the notice PDF family — the manufacturer × product × action
526
+ detail face). The content API has no .json variant — a trailing .json serves the gov.uk 404 page (the
527
+ pasted-slug form with it gets it stripped)."""
528
+ self._hc_only("gb_mhra_alert")
529
+ if not str(slug or "").strip():
530
+ raise SdkError("usage_error", provider="gb_mhra",
531
+ detail="gb_mhra_alert requires slug (the alert-page tail segment, echoed verbatim from "
532
+ "gb_mhra_alerts rows; a pasted full base_path also gets its prefix stripped)")
533
+ r = healthcare.gb_mhra.alert_raw(slug, cache=self._c._cache,
534
+ no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
535
+ result = {k: x for k, x in r.items() if k != "_meta"}
536
+ latest = (result["change_history"] or [{}])[0] # upstream order = newest→oldest ([0] = latest, last = First published)
537
+ return self._flat_env(
538
+ tool="gb_mhra_alert",
539
+ input={"slug": result["slug"]},
540
+ reason=(f"UK MHRA single alert: {result['title'] or result['slug']}"
541
+ + (" (withdrawn)" if result["withdrawn"] else "")
542
+ + f" — type {result['alert_type'] or '—'}, issued {result['issued_date'] or '—'}"
543
+ + f", {len(result['change_history'])} change-history entries (latest: {latest.get('note') or '—'})"
544
+ + f", {len(result['attachments'])} notice PDFs; first published {result['first_published_at'] or '—'}")
545
+ 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)",
546
+ result=result,
547
+ evidence=[{"quote": f"{a['title'] or '—'} ({a['content_type'] or '—'})",
548
+ "url": a["url"] or result["url"]}
549
+ for a in result["attachments"][:5]]
550
+ or [{"quote": latest.get("note") or result["title"] or result["slug"], "url": result["url"]}],
551
+ provider_id="gb_mhra", metas=[r["_meta"]],
552
+ query={"source": "raw", "upstream": {"path": f"/api/content/drug-device-alerts/{result['slug']}"}},
553
+ )
554
+
555
+ def au_tga_artg(self, *, query=None, limit=20, skip=0,
556
+ execution=None, no_cache=None):
557
+ """healthcare au_tga_artg: search of the AU ARTG register (the TGA eBS Domino face;
558
+ query=Query is a verbatim all-fields AND channel (a quoted phrase also matches the Sponsor column exactly))."""
559
+ self._hc_only("au_tga_artg")
560
+ raw = str(query or "").strip() or None
561
+ size = min(max(1, int(limit or 20)), 100) # SearchView Count measured 100/page (the real upstream limit)
562
+ r = healthcare.au_tga.search_art_register(None, raw, limit=size, skip=skip,
563
+ cache=self._c._cache,
564
+ no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
565
+ result = {k: x for k, x in r.items() if k != "_meta"}
566
+ who = f"search '{raw}'" if raw else "the whole database"
567
+ if result["count"] == 0:
568
+ reason = f"TGA ARTG register: {who} zero hits (zero hits is a legal answer — a quoted phrase matches exactly; if the full name does not match, try all-field words)"
569
+ else:
570
+ more = " — full page, there may be more (continue with --skip)" if result.get("has_more") else ""
571
+ reason = f"TGA ARTG register: {who} {result['count']} hits (this page, self-evident from the Sponsor column){more}"
572
+ quartet = {"source": "raw",
573
+ "search": result["query"],
574
+ "upstream": {"SearchView": "", "Query": result["query"], "Count": result["limit_applied"],
575
+ "charset": "utf-8", **({"Start": result["skip"]} if result["skip"] else {})}}
576
+ return self._flat_env(
577
+ tool="au_tga_artg",
578
+ input={"query": raw, "limit": result["limit_applied"], "skip": result["skip"]},
579
+ reason=reason, result=result,
580
+ evidence=[{"quote": (f"{row['artg_id'] or '—'} {row['product_name'] or '—'} = "
581
+ f"{row['sponsor'] or '—'} ({row['product_type'] or '—'})"),
582
+ "url": row["doc_url"]}
583
+ for row in result["rows"][:5]],
584
+ provider_id="au_tga", metas=[r["_meta"]],
585
+ query=quartet,
586
+ )
587
+
588
+ def au_tga_manufacturers(self, *, state=None, limit=100, execution=None, no_cache=None):
589
+ """healthcare au_tga_manufacturers: the AU manufacturer licence register (MIS: 444 licences in one full shot + a state filter; limit ≤500 is the upstream single-page hard limit)."""
590
+ self._hc_only("au_tga_manufacturers")
591
+ size = min(max(1, int(limit or 100)), 500)
592
+ r = healthcare.au_tga.list_manufacturers(state, limit=size, cache=self._c._cache,
593
+ no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
594
+ result = {k: x for k, x in r.items() if k != "_meta"}
595
+ by_state = {}
596
+ for row in result["rows"]:
597
+ by_state[row.get("state") or "—"] = by_state.get(row.get("state") or "—", 0) + 1
598
+ state_note = " / ".join(f"{k} {v}" for k, v in by_state.items())
599
+ return self._flat_env(
600
+ tool="au_tga_manufacturers",
601
+ input={"state": state, "limit": result["limit_applied"]},
602
+ reason=(f"TGA MIS licence register: {result['total']} licences in the register, {result['count']} returned"
603
+ + (f" (state filter {state})" if state else "")
604
+ + (f" — {state_note}" if state_note else "")),
605
+ result=result,
606
+ evidence=[{"quote": f"{row['licence_number'] or '—'} = {row['manufacturer'] or '—'} ({row['state'] or '—'})",
607
+ "url": "https://www.ebs.tga.gov.au/ebs/ANZTPAR/PublicWeb.nsf/luPortalManufacturers"
608
+ "?ReadViewEntries&outputformat=json&count=500"}
609
+ for row in result["rows"][:5]],
610
+ provider_id="au_tga", metas=[r["_meta"]],
611
+ query={"source": "raw",
612
+ # state is a local post-filter (not an upstream param): it is echoed in input.state while upstream stays purely what was sent
613
+ "upstream": {"ReadViewEntries": "", "outputformat": "json", "count": 500, "charset": "utf-8"}},
614
+ )
615
+
616
+ # ---------- 0.2.0 flat panel commands (the company main face: source-qualified name + upstream param names + our defaults) ----------
617
+
618
+ # ---------- 0.3.19 CA + EU product-safety faces (ca_dpd / ca_mdall / rasff / safety_gate; born flat, probed 2026-11-02) ----------
619
+
620
+ def ca_dpd_companies(self, *, limit=200, skip=0, execution=None, no_cache=None):
621
+ """healthcare ca_dpd_companies: the Health Canada DPD company register, one full shot
622
+ (5,253 DIN owners measured 2026-11-02; company_code is the DPD join key; limit/skip is a
623
+ local post-slice over the single cached register — the upstream has no paging)."""
624
+ self._hc_only("ca_dpd_companies")
625
+ r = healthcare.ca_dpd.companies_raw(limit=limit, skip=skip,
626
+ cache=self._c._cache,
627
+ no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
628
+ result = {k: x for k, x in r.items() if k != "_meta"}
629
+ return self._flat_env(
630
+ tool="ca_dpd_companies",
631
+ input={"limit": result["count"], "skip": skip},
632
+ reason=(f"DPD company register: {result['total']} companies in the one-shot register, "
633
+ f"{result['count']} returned (this slice) — DIN OWNER / import-type rows with the "
634
+ "full postal address; join into ca_dpd_drug_products via company_name"),
635
+ result=result,
636
+ evidence=[{"quote": f"{row['company_code']} {row['company_name'] or '—'} ({row['company_type'] or '—'}) "
637
+ f"— {(row['address'] or {}).get('city_name') or '—'}",
638
+ "url": "https://health-products.canada.ca/api/drug/company/?lang=en&type=json"}
639
+ for row in result["rows"][:5]],
640
+ provider_id="ca_dpd", metas=[r["_meta"]],
641
+ query={"source": "raw", "upstream": r["params"]},
642
+ )
643
+
644
+ def ca_dpd_drug_products(self, *, status, limit=200, skip=0, execution=None, no_cache=None):
645
+ """healthcare ca_dpd_drug_products: DPD drug products by market status — status is the Read Me
646
+ closed set 1|2|3|4|6 (1=Approved 2=Marketed 3=Cancelled Pre-Market 4=Cancelled Post-Market
647
+ 6=Dormant), REQUIRED: an unknown value is silently ignored upstream and dumps the whole 15MB
648
+ register. Rows carry DIN + brand + company_name inline; limit/skip is a local post-slice."""
649
+ self._hc_only("ca_dpd_drug_products")
650
+ r = healthcare.ca_dpd.drug_products_raw(status, limit=limit, skip=skip,
651
+ cache=self._c._cache,
652
+ no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
653
+ result = {k: x for k, x in r.items() if k != "_meta"}
654
+ return self._flat_env(
655
+ tool="ca_dpd_drug_products",
656
+ input={"status": result["status"], "limit": result["count"], "skip": skip},
657
+ reason=(f"DPD drug products (status {result['status']} = {result['status_label']}): "
658
+ f"{result['total']} products in the register, {result['count']} returned — "
659
+ "DIN + brand + company rows; company_code is not echoed on this face (join via company_name)"),
660
+ result=result,
661
+ evidence=[{"quote": f"{row['din']} {row['brand_name'] or '—'} = {row['company_name'] or '—'} ({row['class_name'] or '—'})",
662
+ "url": f"https://health-products.canada.ca/api/drug/drugproduct/?status={result['status']}&lang=en&type=json"}
663
+ for row in result["rows"][:5]],
664
+ provider_id="ca_dpd", metas=[r["_meta"]],
665
+ query={"source": "raw", "upstream": r["params"]},
666
+ )
667
+
668
+ def ca_mdall_companies(self, *, limit=200, skip=0, execution=None, no_cache=None):
669
+ """healthcare ca_mdall_companies: the Health Canada MDALL company register, one full shot
670
+ (7,682 medical-device companies measured 2026-11-02 — a two-country register, US manufacturers
671
+ carry country_cd US; company_id is the MDALL join key; limit/skip is a local post-slice)."""
672
+ self._hc_only("ca_mdall_companies")
673
+ r = healthcare.ca_mdall.companies_raw(limit=limit, skip=skip,
674
+ cache=self._c._cache,
675
+ no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
676
+ result = {k: x for k, x in r.items() if k != "_meta"}
677
+ return self._flat_env(
678
+ tool="ca_mdall_companies",
679
+ input={"limit": result["count"], "skip": skip},
680
+ reason=(f"MDALL company register: {result['total']} companies in the one-shot register, "
681
+ f"{result['count']} returned (this slice) — active/inactive device makers and "
682
+ "importers with postal addresses; the licence edge lives in ca_mdall_licence"),
683
+ result=result,
684
+ evidence=[{"quote": f"{row['company_id']} {row['company_name'] or '—'} ({row['company_status'] or '—'}) "
685
+ f"— {(row['address'] or {}).get('city') or '—'}, {(row['address'] or {}).get('country_cd') or '—'}",
686
+ "url": "https://health-products.canada.ca/api/medical-devices/company/?lang=en&type=json"}
687
+ for row in result["rows"][:5]],
688
+ provider_id="ca_mdall", metas=[r["_meta"]],
689
+ query={"source": "raw", "upstream": r["params"]},
690
+ )
691
+
692
+ def ca_mdall_licence(self, *, licence_id, licence_type="active", lang="eng",
693
+ execution=None, no_cache=None):
694
+ """healthcare ca_mdall_licence: the MDALL single-licence dossier (the LIMH information page —
695
+ a stateless GET, the PRG dance is not needed). licence_id = the numeric MDALL licence number;
696
+ licence_type active|archived are SEPARATE upstream indexes (an inactive licence must be asked
697
+ for under archived); the dossier carries the manufacturer block + Company ID + device class +
698
+ the device-identifier family)."""
699
+ self._hc_only("ca_mdall_licence")
700
+ r = healthcare.ca_mdall.licence_raw(licence_id, licence_type=licence_type, lang=lang,
701
+ cache=self._c._cache,
702
+ no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
703
+ result = {k: x for k, x in r.items() if k != "_meta"}
704
+ mfg = result.get("manufacturer") or {}
705
+ lic_url = (f"https://health-products.canada.ca/mdall-limh/information"
706
+ f"?licenceId={result['licence_id']}&type={result['type']}&lang=eng")
707
+ return self._flat_env(
708
+ tool="ca_mdall_licence",
709
+ input={"licence_id": result["licence_id"], "licence_type": result["type"], "lang": lang},
710
+ reason=(f"MDALL licence {result['licence_no'] or result['licence_id']} "
711
+ f"({result.get('licence_type') or '—'}): {result.get('licence_name') or '—'} — "
712
+ f"class {result.get('device_class') or '—'}, first issued {result.get('first_issue_date') or '—'}, "
713
+ f"{len(result.get('device_identifiers') or [])} device identifiers; manufacturer "
714
+ f"{mfg.get('name') or '—'} (Company ID {mfg.get('company_id') or '—'})"),
715
+ result=result,
716
+ evidence=[{"quote": f"{d.get('device_name') or '—'} = {d.get('device_identifier') or '—'}",
717
+ "url": lic_url}
718
+ for d in (result.get("device_identifiers") or [])[:5]]
719
+ or [{"quote": f"{mfg.get('name') or '—'} — licence {result['licence_no']}", "url": lic_url}],
720
+ provider_id="ca_mdall", metas=[r["_meta"]],
721
+ query={"source": "raw", "upstream": r["params"]},
722
+ )
723
+
724
+ def rasff_notifications(self, subject=None, *, notification_reference=None,
725
+ ec_valid_date_from=None, ec_valid_date_to=None,
726
+ page_number=1, items_per_page=25, execution=None, no_cache=None):
727
+ """healthcare rasff_notifications: the EU RASFF food-safety alert window (the consolidated
728
+ English backend). Three filters are exposed — the proven ones (subject free text /
729
+ notificationReference exact / ecValidDateFrom-To window, dates dd-MM-yyyy) + paging; the
730
+ enum-shaped country/category filters stay unprobed upstream (every object shape tried was
731
+ rejected with a generic error), so this face does not guess them."""
732
+ self._hc_only("rasff_notifications")
733
+ r = healthcare.rasff.search_raw(subject, notification_reference=notification_reference,
734
+ ec_valid_date_from=ec_valid_date_from,
735
+ ec_valid_date_to=ec_valid_date_to,
736
+ page_number=page_number, items_per_page=items_per_page,
737
+ cache=self._c._cache,
738
+ no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
739
+ result = {k: x for k, x in r.items() if k != "_meta"}
740
+ return self._flat_env(
741
+ tool="rasff_notifications",
742
+ input={"subject": subject, "notification_reference": notification_reference,
743
+ "ec_valid_date_from": ec_valid_date_from, "ec_valid_date_to": ec_valid_date_to,
744
+ "page_number": page_number, "items_per_page": result["count"]},
745
+ reason=(f"RASFF window: {result['total_elements']} notifications in the corpus "
746
+ f"({result['total_pages']} pages), {result['count']} returned on page {page_number}"
747
+ + (f" (subject '{subject}')" if subject else "")
748
+ + (f" (reference '{notification_reference}')" if notification_reference else "")
749
+ + " — notifying country × product × risk rows; the subject line is the "
750
+ "company/product penetration channel"),
751
+ result=result,
752
+ evidence=[{"quote": f"{row['reference']} {row['notifying_country'] or '—'}: {row['subject'] or '—'} "
753
+ f"({row['risk_decision'] or '—'})",
754
+ "url": "https://webgate.ec.europa.eu/rasff-window/screen/search"}
755
+ for row in result["rows"][:5]],
756
+ provider_id="rasff", metas=[r["_meta"]],
757
+ query={"source": "raw", "upstream": r["params"]},
758
+ )
759
+
760
+ def safety_gate_alerts(self, *, year=None, limit=200, skip=0, execution=None, no_cache=None):
761
+ """healthcare safety_gate_alerts: the EU Safety Gate weekly-report index (1,122 reports
762
+ 2005→now, one cached XML). year is a local post-filter (the upstream index always serves the
763
+ whole list); report_id in each row is the join key into safety_gate_report."""
764
+ self._hc_only("safety_gate_alerts")
765
+ r = healthcare.safety_gate.weekly_list_raw(year=year, limit=limit, skip=skip,
766
+ cache=self._c._cache,
767
+ no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
768
+ result = {k: x for k, x in r.items() if k != "_meta"}
769
+ newest = (result["rows"] or [{}])[0]
770
+ return self._flat_env(
771
+ tool="safety_gate_alerts",
772
+ input={"year": year, "limit": result["count"], "skip": skip},
773
+ reason=(f"Safety Gate weekly-report index: {result['total']} reports in the full index"
774
+ + (f" matching year {year}" if year else "")
775
+ + f", {result['count']} returned — newest {newest.get('reference') or '—'} "
776
+ f"({newest.get('publication_date') or '—'}); drill into a week with safety_gate_report (report_id)"),
777
+ result=result,
778
+ evidence=[{"quote": f"{row['reference']} — published {row['publication_date']} (report_id {row['report_id']})",
779
+ "url": row["url"] or "https://ec.europa.eu/safety-gate-alerts/api/download/weeklyReport/list/xml/en"}
780
+ for row in result["rows"][:5]],
781
+ provider_id="safety_gate", metas=[r["_meta"]],
782
+ query={"source": "raw", "upstream": {"path": "/safety-gate-alerts/api/download/weeklyReport/list/xml/en",
783
+ **({"year": year} if year else {})}},
784
+ )
785
+
786
+ def safety_gate_report(self, *, report_id, limit=200, skip=0, execution=None, no_cache=None):
787
+ """healthcare safety_gate_report: one Safety Gate weekly report's alerts (the detail XML —
788
+ 67 alerts for 2026-W43). Rows carry caseNumber/riskType/level/notifyingCountry/
789
+ countryOfOrigin/onlineTrader — the company-relevant edges of the non-food product-safety
790
+ regime; report_id comes verbatim from safety_gate_alerts rows."""
791
+ self._hc_only("safety_gate_report")
792
+ r = healthcare.safety_gate.weekly_detail_raw(report_id, limit=limit, skip=skip,
793
+ cache=self._c._cache,
794
+ no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
795
+ result = {k: x for k, x in r.items() if k != "_meta"}
796
+ return self._flat_env(
797
+ tool="safety_gate_report",
798
+ input={"report_id": result["report_id"], "limit": result["count"], "skip": skip},
799
+ reason=(f"Safety Gate report {result['report_id']} (week {result.get('report_week') or '—'} "
800
+ f"{result.get('report_year') or '—'}, published {result.get('report_date') or '—'}): "
801
+ f"{result['count']} alerts returned — product × risk × measure rows with "
802
+ "notifying country and country of origin; alert_id joins into the alert-detail screen"),
803
+ result=result,
804
+ evidence=[{"quote": f"{row['case_number'] or '—'} {row['category'] or '—'}: {row['risk_type'] or '—'} "
805
+ f"({row['level'] or '—'}, origin {row['country_of_origin'] or '—'})",
806
+ "url": row["alert_url"] or "https://ec.europa.eu/safety-gate-alerts/"}
807
+ for row in result["rows"][:5]],
808
+ provider_id="safety_gate", metas=[r["_meta"]],
809
+ query={"source": "raw", "upstream": {"path": f"/safety-gate-alerts/api/download/weeklyReport/detail/xml/{result['report_id']}"}},
810
+ )
811
+