agentdata 0.3.26__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agentdata/__init__.py +33 -0
- agentdata/__main__.py +4 -0
- agentdata/cache.py +115 -0
- agentdata/cli.py +173 -0
- agentdata/client.py +158 -0
- agentdata/company/__init__.py +3 -0
- agentdata/company/cf.py +145 -0
- agentdata/company/ch_zefix.py +100 -0
- agentdata/company/cli.py +243 -0
- agentdata/company/co_rues.py +72 -0
- agentdata/company/edgar_cik.py +84 -0
- agentdata/company/edgar_fts.py +101 -0
- agentdata/company/faces.py +746 -0
- agentdata/company/fdic_bankfind.py +107 -0
- agentdata/company/fi_ytj.py +89 -0
- agentdata/company/fr_recherche.py +124 -0
- agentdata/company/fts.py +121 -0
- agentdata/company/gleif.py +178 -0
- agentdata/company/kr_hometax.py +112 -0
- agentdata/company/no_brreg.py +124 -0
- agentdata/company/ror.py +105 -0
- agentdata/company/ted.py +117 -0
- agentdata/company/usaspending.py +124 -0
- agentdata/company/vies.py +66 -0
- agentdata/daterange.py +35 -0
- agentdata/economy/__init__.py +10 -0
- agentdata/economy/bis.py +132 -0
- agentdata/economy/cli.py +199 -0
- agentdata/economy/ecb.py +117 -0
- agentdata/economy/eurostat.py +163 -0
- agentdata/economy/faces.py +573 -0
- agentdata/economy/fred.py +92 -0
- agentdata/economy/ilo_stat.py +99 -0
- agentdata/economy/imf_sdmx.py +133 -0
- agentdata/economy/un_comtrade.py +199 -0
- agentdata/economy/unsd_sdg.py +154 -0
- agentdata/economy/wb.py +208 -0
- agentdata/economy/who_xmart.py +177 -0
- agentdata/entries.py +30 -0
- agentdata/errors.py +38 -0
- agentdata/esg/__init__.py +5 -0
- agentdata/esg/cli.py +180 -0
- agentdata/esg/climatetrace.py +260 -0
- agentdata/esg/eiti.py +187 -0
- agentdata/esg/epa_frs.py +138 -0
- agentdata/esg/euasl.py +75 -0
- agentdata/esg/eutl.py +66 -0
- agentdata/esg/faces.py +498 -0
- agentdata/esg/paris_mou.py +240 -0
- agentdata/esg/thetis_mrv.py +67 -0
- agentdata/healthcare/__init__.py +7 -0
- agentdata/healthcare/au_tga.py +142 -0
- agentdata/healthcare/ca_dpd.py +123 -0
- agentdata/healthcare/ca_mdall.py +155 -0
- agentdata/healthcare/cli.py +375 -0
- agentdata/healthcare/ctgov.py +101 -0
- agentdata/healthcare/dailymed.py +102 -0
- agentdata/healthcare/epmc.py +108 -0
- agentdata/healthcare/eudamed.py +124 -0
- agentdata/healthcare/faces.py +811 -0
- agentdata/healthcare/fda_crl.py +86 -0
- agentdata/healthcare/fda_devices.py +199 -0
- agentdata/healthcare/fda_drugs.py +156 -0
- agentdata/healthcare/fda_recalls.py +137 -0
- agentdata/healthcare/gb_mhra.py +137 -0
- agentdata/healthcare/gudid.py +84 -0
- agentdata/healthcare/mesh.py +89 -0
- agentdata/healthcare/rasff.py +164 -0
- agentdata/healthcare/rxnav.py +67 -0
- agentdata/healthcare/safety_gate.py +175 -0
- agentdata/healthcare/shortages.py +107 -0
- agentdata/legal/__init__.py +4 -0
- agentdata/legal/afdb.py +93 -0
- agentdata/legal/ca_sema.py +143 -0
- agentdata/legal/cli.py +312 -0
- agentdata/legal/co_sic.py +70 -0
- agentdata/legal/courtlistener.py +77 -0
- agentdata/legal/csl.py +139 -0
- agentdata/legal/dgcomp.py +125 -0
- agentdata/legal/ecfr.py +129 -0
- agentdata/legal/faces.py +700 -0
- agentdata/legal/federalregister.py +188 -0
- agentdata/legal/gb_cma.py +117 -0
- agentdata/legal/gb_tna_caselaw.py +245 -0
- agentdata/legal/idb.py +85 -0
- agentdata/legal/lda.py +147 -0
- agentdata/legal/sipri.py +178 -0
- agentdata/legal/ukec.py +148 -0
- agentdata/legal/wb.py +92 -0
- agentdata/procurement/__init__.py +5 -0
- agentdata/procurement/br_pncp.py +126 -0
- agentdata/procurement/cli.py +155 -0
- agentdata/procurement/co_secop.py +88 -0
- agentdata/procurement/faces.py +239 -0
- agentdata/procurement/ny_open_data.py +82 -0
- agentdata/procurement/ungm.py +137 -0
- agentdata/procurement/wb_proc.py +84 -0
- agentdata/research/__init__.py +4 -0
- agentdata/research/biorxiv.py +66 -0
- agentdata/research/cli.py +129 -0
- agentdata/research/crossref.py +121 -0
- agentdata/research/faces.py +349 -0
- agentdata/research/gb_gtr.py +58 -0
- agentdata/research/icite.py +52 -0
- agentdata/research/nih_reporter.py +143 -0
- agentdata/research/nsf.py +68 -0
- agentdata/research/openalex.py +145 -0
- agentdata/research/opencitations.py +50 -0
- agentdata/security/__init__.py +3 -0
- agentdata/security/cisa_kev.py +42 -0
- agentdata/security/cli.py +37 -0
- agentdata/security/faces.py +91 -0
- agentdata/security/osv.py +72 -0
- agentdata/transport.py +541 -0
- agentdata-0.3.26.dist-info/METADATA +266 -0
- agentdata-0.3.26.dist-info/RECORD +120 -0
- agentdata-0.3.26.dist-info/WHEEL +5 -0
- agentdata-0.3.26.dist-info/entry_points.txt +10 -0
- agentdata-0.3.26.dist-info/licenses/LICENSE +201 -0
- agentdata-0.3.26.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"""paris_mou — Paris MoU port state control (THETIS public REST, EMSA portal) provider (keyless).
|
|
2
|
+
|
|
3
|
+
esg port-state supervision axis (2026-10-29, 0.3.5 batch): parismou.org itself is all iframe shells; the real
|
|
4
|
+
data door is the EMSA Liferay portlet same-origin REST `portal.emsa.europa.eu/o/portlet-public/rest/*`
|
|
5
|
+
(contextRoot is emitted by the inline JS of the portlet page; the full endpoint table lives in the 160KB ExtJS
|
|
6
|
+
bundle thetis-public.js). Measured shape (2026-10-29):
|
|
7
|
+
- The three GET snapshot faces share the envelope `{results, total, success}`.
|
|
8
|
+
- Detentions: detention/getCurrentDetentions 53 rows (daily rolling — the 10-20 probe saw 48 rows).
|
|
9
|
+
- Bans: ban/getBanShips 486 rows ~1MB, with **ismCompany{id/imoNumber/name/address/city/countryCode}
|
|
10
|
+
embedded = the official ship x management-company edge** (310 rows carry a company IMO number).
|
|
11
|
+
- Low-performance ISM companies: companyperformance/getLowPerformance6MonthsISMCompanies 65 rows
|
|
12
|
+
(**100% carry an IMO company number**, a zero-padded 7-digit string; companyPerformance is mostly LOW with a few MEDIUM).
|
|
13
|
+
- Historical inspections: inspection/getPublicInspections **POST form-encoded** — a JSON body is silently
|
|
14
|
+
dropped upstream (200 + total=0, a poisoned empty-window response, measured 2026-10-29) -> the op declares
|
|
15
|
+
`urlencoded: True` (transport extension 7).
|
|
16
|
+
Params: pscInspectionRegime=1 is mandatory (PSC default); periodMin/periodMax dd/mm/yyyy, window <= 3 years;
|
|
17
|
+
**page is the paging switch** — without page the limit is ignored and the whole window is dumped (measured
|
|
18
|
+
limit=25 returned 1,202 rows); with page+start+limit it is start-offset paging (page1/page2 measured zero
|
|
19
|
+
overlap); ismCompanyName/ismCompanyImoNumber query a company directly (+resume window stats
|
|
20
|
+
ships/inspections/detentions/ismDeficiencies/bans).
|
|
21
|
+
TTL: detentions 1h (daily rolling); bans/low-performance 6h; inspections 30min (window query replay).
|
|
22
|
+
Politeness tier 0.5 rps.
|
|
23
|
+
"""
|
|
24
|
+
from ..errors import SdkError
|
|
25
|
+
|
|
26
|
+
provider = {
|
|
27
|
+
"id": "paris_mou",
|
|
28
|
+
"baseUrl": "https://portal.emsa.europa.eu",
|
|
29
|
+
"rate": {"rps": 0.5, "burst": 2},
|
|
30
|
+
"ops": {
|
|
31
|
+
"detentions": {"path": "/o/portlet-public/rest/detention/getCurrentDetentions.json"},
|
|
32
|
+
"bans": {"path": "/o/portlet-public/rest/ban/getBanShips.json", "timeout": 45.0}, # ~1MB in one response, the default 20s timed out as measured
|
|
33
|
+
"low_performance": {"path": "/o/portlet-public/rest/companyperformance/getLowPerformance6MonthsISMCompanies.json"},
|
|
34
|
+
"inspections": {
|
|
35
|
+
"path": "/o/portlet-public/rest/inspection/getPublicInspections.json",
|
|
36
|
+
"method": "POST",
|
|
37
|
+
"urlencoded": True, # extension 7: the body is standard form encoding (a JSON body = silently 0 rows, a poisoned response)
|
|
38
|
+
"idempotent": True, # read-semantics POST (a search), retryable
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
"ttl": {"detentions": 3600, "bans": 21600, "low_performance": 21600, "inspections": 1800},
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _flag(d):
|
|
46
|
+
return {"code": d.get("code"), "name": d.get("description")} if isinstance(d, dict) else {"code": None, "name": None}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _ism_company(c):
|
|
50
|
+
"""Embedded ISM management company -> flat row keys (the zero-padded 7-digit official imoNumber string is kept as-is)."""
|
|
51
|
+
if not isinstance(c, dict):
|
|
52
|
+
return {"company_imo_number": None, "company_name": None, "company_address": None,
|
|
53
|
+
"company_city": None, "company_country_code": None, "company_country_name": None}
|
|
54
|
+
return {
|
|
55
|
+
"company_imo_number": c.get("imoNumber"),
|
|
56
|
+
"company_name": c.get("name"),
|
|
57
|
+
"company_address": c.get("address"),
|
|
58
|
+
"company_city": c.get("city"),
|
|
59
|
+
"company_country_code": c.get("countryCode"),
|
|
60
|
+
"company_country_name": c.get("countryDescription"),
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def detentions_from_upstream(data):
|
|
65
|
+
"""Detentions snapshot -> rows (pure function, offline-testable)."""
|
|
66
|
+
rows = [{
|
|
67
|
+
"id": d.get("id"),
|
|
68
|
+
"imo_number": d.get("imoNumber"), # ship IMO number (asset key)
|
|
69
|
+
"ship_name": d.get("shipName"),
|
|
70
|
+
"ship_type": (d.get("shipType") or {}).get("description") if isinstance(d.get("shipType"), dict) else None,
|
|
71
|
+
"flag_code": _flag(d.get("flag"))["code"],
|
|
72
|
+
"flag_name": _flag(d.get("flag"))["name"],
|
|
73
|
+
"detention_date": d.get("detentionDate"), # dd/mm/yyyy
|
|
74
|
+
"detention_port": (d.get("detentionPort") or {}).get("name") if isinstance(d.get("detentionPort"), dict) else None,
|
|
75
|
+
"detention_country_code": ((d.get("detentionPort") or {}).get("country") or {}).get("code") if isinstance(d.get("detentionPort"), dict) else None,
|
|
76
|
+
"reporting_authority": _flag(d.get("detentionReportingAuthority"))["name"],
|
|
77
|
+
} for d in ((data or {}).get("results") or []) if isinstance(d, dict)]
|
|
78
|
+
return {"rows": rows, "count": len(rows)}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def bans_from_upstream(data):
|
|
82
|
+
"""Bans face -> rows (the embedded ismCompany is flattened into a ship x management-company edge)."""
|
|
83
|
+
rows = []
|
|
84
|
+
for b in ((data or {}).get("results") or []):
|
|
85
|
+
if not isinstance(b, dict):
|
|
86
|
+
continue
|
|
87
|
+
rows.append({
|
|
88
|
+
"id": b.get("id"),
|
|
89
|
+
"imo_number": b.get("imoNumber"),
|
|
90
|
+
"ship_name": b.get("shipName"),
|
|
91
|
+
"flag_code": _flag(b.get("flag"))["code"],
|
|
92
|
+
"flag_name": _flag(b.get("flag"))["name"],
|
|
93
|
+
"ban_date": b.get("banDate"), # dd/mm/yyyy
|
|
94
|
+
"ban_occurrence": b.get("banOccurrence"),
|
|
95
|
+
"ban_order_status": b.get("banOrderStatus"),
|
|
96
|
+
"ban_reason": b.get("banReason"),
|
|
97
|
+
"minimum_duration": b.get("minimumDuration"),
|
|
98
|
+
"banning_authority": _flag(b.get("banningAuthority"))["name"],
|
|
99
|
+
**_ism_company(b.get("ismCompany")),
|
|
100
|
+
})
|
|
101
|
+
return {"rows": rows, "count": len(rows)}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def low_performance_from_upstream(data):
|
|
105
|
+
"""Low-performance ISM company list -> rows (rolling 6-month window; 100% carry an IMO company number)."""
|
|
106
|
+
rows = [{
|
|
107
|
+
"id": c.get("id"),
|
|
108
|
+
"imo_number": c.get("imoNumber"), # IMO company number (zero-padded 7-digit string)
|
|
109
|
+
"name": c.get("name"),
|
|
110
|
+
"address": c.get("address"),
|
|
111
|
+
"city": c.get("city"),
|
|
112
|
+
"country_code": c.get("countryCode"),
|
|
113
|
+
"country_name": c.get("countryDescription"),
|
|
114
|
+
"performance": c.get("companyPerformance"), # mostly LOW, with a few MEDIUM
|
|
115
|
+
"effect_date": c.get("effectDate"), # dd/mm/yyyy (listing date)
|
|
116
|
+
"expiry_date": c.get("expiryDate"),
|
|
117
|
+
"status": c.get("ismCompanyStatus"), # ACTIVE/null
|
|
118
|
+
} for c in ((data or {}).get("results") or []) if isinstance(c, dict)]
|
|
119
|
+
return {"rows": rows, "count": len(rows)}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def inspections_from_upstream(data, params):
|
|
123
|
+
"""Historical inspections -> rows + resume window stats (envelope {results, total, resume, success})."""
|
|
124
|
+
rows = [{
|
|
125
|
+
"id": i.get("id"), # -> getInspectionDetail?inspectionId= key for the full per-inspection record
|
|
126
|
+
"imo_number": i.get("imoNumber"),
|
|
127
|
+
"ship_name": i.get("shipName"),
|
|
128
|
+
"ship_type": i.get("shipTypeDescription"),
|
|
129
|
+
"flag_code": _flag(i.get("flag"))["code"],
|
|
130
|
+
"flag_name": _flag(i.get("flag"))["name"],
|
|
131
|
+
"age": i.get("age"),
|
|
132
|
+
"inspection_date": i.get("inspectionDate"), # dd/mm/yyyy
|
|
133
|
+
"inspection_type": (i.get("inspectionType") or {}).get("description") if isinstance(i.get("inspectionType"), dict) else None,
|
|
134
|
+
"member_state": i.get("memberStateDescription"),
|
|
135
|
+
"port_name": i.get("portName"),
|
|
136
|
+
"port_and_ms": i.get("portAndMS"), # "Slovenia - Koper"
|
|
137
|
+
"ndeficiencies": i.get("ndeficiencies"),
|
|
138
|
+
"results_desc": i.get("resultsDesc"),
|
|
139
|
+
} for i in ((data or {}).get("results") or []) if isinstance(i, dict)]
|
|
140
|
+
resume = (data or {}).get("resume") or {}
|
|
141
|
+
return {
|
|
142
|
+
"rows": rows,
|
|
143
|
+
"count": len(rows),
|
|
144
|
+
"total": (data or {}).get("total"),
|
|
145
|
+
"resume": {
|
|
146
|
+
"ships": resume.get("ships"),
|
|
147
|
+
"inspections": resume.get("inspections"),
|
|
148
|
+
"detentions": resume.get("detentions"),
|
|
149
|
+
"ism_deficiencies": resume.get("ismDeficiencies"),
|
|
150
|
+
"non_ism_deficiencies": resume.get("nonISMDeficiencies"),
|
|
151
|
+
"bans": resume.get("bans"),
|
|
152
|
+
} if isinstance(resume, dict) else None,
|
|
153
|
+
"paging": {"page": params.get("page"), "start": params.get("start"), "limit": params.get("limit")},
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _check_date(s, label):
|
|
158
|
+
"""Strict dd/mm/yyyy validation (the upstream format really is day/month/year — a wrong order silently returns empty)."""
|
|
159
|
+
from datetime import datetime
|
|
160
|
+
try:
|
|
161
|
+
return datetime.strptime(s, "%d/%m/%Y")
|
|
162
|
+
except (TypeError, ValueError):
|
|
163
|
+
raise SdkError("usage_error", provider=provider["id"],
|
|
164
|
+
detail=f"{label} must be dd/mm/yyyy (e.g. 01/01/2026) — {s!r} does not match as measured")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def detentions_raw(*, cache=None, no_cache=False):
|
|
168
|
+
"""0.3.5 flat face internals: the current detentions snapshot in one request (upstream has no filter params).
|
|
169
|
+
|
|
170
|
+
-> {rows[], count, params, _meta} — params={} (zero upstream params)."""
|
|
171
|
+
from ..transport import call_provider
|
|
172
|
+
|
|
173
|
+
params = {}
|
|
174
|
+
r = call_provider(provider, "detentions", params, cache=cache, no_cache=no_cache)
|
|
175
|
+
return {**detentions_from_upstream(r["data"]), "params": params, "_meta": r}
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def bans_raw(*, cache=None, no_cache=False):
|
|
179
|
+
"""0.3.5 flat face internals: the bans register in one request (upstream has no filter params).
|
|
180
|
+
|
|
181
|
+
-> {rows[], count, params, _meta}."""
|
|
182
|
+
from ..transport import call_provider
|
|
183
|
+
|
|
184
|
+
params = {}
|
|
185
|
+
r = call_provider(provider, "bans", params, cache=cache, no_cache=no_cache)
|
|
186
|
+
return {**bans_from_upstream(r["data"]), "params": params, "_meta": r}
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def low_performance_raw(*, cache=None, no_cache=False):
|
|
190
|
+
"""0.3.5 flat face internals: the low-performance ISM company list in one request (rolling 6-month window, upstream has no filter params).
|
|
191
|
+
|
|
192
|
+
-> {rows[], count, params, _meta}."""
|
|
193
|
+
from ..transport import call_provider
|
|
194
|
+
|
|
195
|
+
params = {}
|
|
196
|
+
r = call_provider(provider, "low_performance", params, cache=cache, no_cache=no_cache)
|
|
197
|
+
return {**low_performance_from_upstream(r["data"]), "params": params, "_meta": r}
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def inspections_raw(*, period_min, period_max, psc_inspection_regime=1, page=1, start=0, limit=50,
|
|
201
|
+
ism_company_name=None, ism_company_imo_number=None,
|
|
202
|
+
cache=None, no_cache=False):
|
|
203
|
+
"""0.3.5 flat face internals: historical PSC inspection window search (form-encoded POST).
|
|
204
|
+
|
|
205
|
+
-> {rows[], count, total, resume, paging, params, _meta}. The <= 3 year window is validated locally first
|
|
206
|
+
(upstream silently returns empty beyond it); **page always travels with the request** (without page the whole
|
|
207
|
+
window is dumped and the limit is void — the face pins the paging semantics). 0.3.20 audit truths (probed
|
|
208
|
+
2026-11-02): pscInspectionRegime=1 is the only live value (0 -> poisoned empty, 2 -> HTTP 500 — walled locally
|
|
209
|
+
with evidence); the 500 limit cap is a **local politeness tier** (upstream honors 1000 — measured 1,000 rows
|
|
210
|
+
of a 1,395-total Jan-2026 window); ismCompanyImoNumber accepts both the zero-padded 7-digit and the unpadded
|
|
211
|
+
form (measured 52193 and 0052193 both -> total=1) — passed verbatim, no normalization."""
|
|
212
|
+
from ..transport import call_provider
|
|
213
|
+
|
|
214
|
+
d1, d2 = _check_date(period_min, "period_min"), _check_date(period_max, "period_max")
|
|
215
|
+
if d2 < d1:
|
|
216
|
+
raise SdkError("usage_error", provider=provider["id"],
|
|
217
|
+
detail=f"period_max({period_max}) is earlier than period_min({period_min}) — inverted window")
|
|
218
|
+
if (d2 - d1).days > 366 * 3:
|
|
219
|
+
raise SdkError("usage_error", provider=provider["id"],
|
|
220
|
+
detail=f"the search window of {(d2 - d1).days} days exceeds the 3-year cap (an upstream limit) — split the search")
|
|
221
|
+
if int(psc_inspection_regime) != 1:
|
|
222
|
+
# 0.3.20 audit (probed 2026-11-02, Jan-2026 window): regime=1 is the only live value — 0 returns
|
|
223
|
+
# 200 success with total=0 (a poisoned empty), 2 returns HTTP 500. Wall it with the evidence instead
|
|
224
|
+
# of letting either silent shape through.
|
|
225
|
+
raise SdkError("usage_error", provider=provider["id"],
|
|
226
|
+
detail=f"psc_inspection_regime={psc_inspection_regime!r} is not a live upstream value — 1 (PSC) is the "
|
|
227
|
+
"only regime the public endpoint serves (measured: 0 -> 200 poisoned-empty total=0, "
|
|
228
|
+
"2 -> HTTP 500)")
|
|
229
|
+
params = {
|
|
230
|
+
"pscInspectionRegime": int(psc_inspection_regime), # mandatory (1 = PSC default regime; without it = 200 empty)
|
|
231
|
+
"periodMin": period_min,
|
|
232
|
+
"periodMax": period_max,
|
|
233
|
+
"page": max(1, int(page)),
|
|
234
|
+
"start": max(0, int(start)),
|
|
235
|
+
"limit": min(500, max(1, int(limit))),
|
|
236
|
+
"ismCompanyName": ism_company_name,
|
|
237
|
+
"ismCompanyImoNumber": ism_company_imo_number,
|
|
238
|
+
}
|
|
239
|
+
r = call_provider(provider, "inspections", params, cache=cache, no_cache=no_cache)
|
|
240
|
+
return {**inspections_from_upstream(r["data"], params), "params": params, "_meta": r}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""thetis_mrv — EU MRV ship emissions reporting (THETIS-MRV, EMSA) versioned bulk-file catalog provider (keyless).
|
|
2
|
+
|
|
3
|
+
esg port-state emissions axis (2026-10-29, 0.3.5 batch): mrv.emsa.europa.eu is an ExtJS shell (app.json ->
|
|
4
|
+
a 9.5MB app.js was reverse-engineered) that yielded the `/api/public-emission-report/` family of 19 endpoints;
|
|
5
|
+
this face adopts the **reporting-period x versioned-publication listing** query (the XLSX bodies, ~9.5MB per
|
|
6
|
+
file, still go through the batch pipeline; this SDK does not proxy large-file downloads — same posture as eutl).
|
|
7
|
+
Measured shape (2026-10-29):
|
|
8
|
+
- `reporting-periods` -> `[{value: 2018..2025}]`, eight years (2018 is the first reporting year).
|
|
9
|
+
- `downloadable-files` -> `{results: [{reportingPeriod, version, generationDate "DD-MM-YYYY HH:MM:SS",
|
|
10
|
+
fileName}], total}` — **versioned rolling regeneration** (the 2025 file was v76 at the 10-20 probe and had
|
|
11
|
+
already jumped to v76/v254 by 10-29; the 2024 file is v254) — reconcile against the listing, never guess
|
|
12
|
+
file names; generationDate is mostly an early-morning batch (05:2x).
|
|
13
|
+
- The download path is the template `reporting-period-document/binary/{year}/{version}` (an XLSX attachment;
|
|
14
|
+
Range is ignored and the full 200 body comes back — mind this in the batch pipeline). The file schema gains
|
|
15
|
+
columns year by year (no CH₄/N₂O columns before 2024).
|
|
16
|
+
TTL 1 day (the version ledger rolls with the daily batch); politeness tier 0.5 rps.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
provider = {
|
|
20
|
+
"id": "thetis_mrv",
|
|
21
|
+
"baseUrl": "https://mrv.emsa.europa.eu",
|
|
22
|
+
"rate": {"rps": 0.5, "burst": 2},
|
|
23
|
+
"ops": {
|
|
24
|
+
"periods": {"path": "/api/public-emission-report/reporting-periods"}, # [{value: year}] in one response
|
|
25
|
+
"files": {"path": "/api/public-emission-report/downloadable-files"}, # {results, total} version ledger
|
|
26
|
+
},
|
|
27
|
+
"ttl": {"periods": 86400, "files": 86400},
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
_BINARY_PATH = "/api/public-emission-report/reporting-period-document/binary/{period}/{version}"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def datasets_from_upstream(periods_data, files_data):
|
|
34
|
+
"""Two op responses -> datasets face result (pure function, offline-testable).
|
|
35
|
+
|
|
36
|
+
rows are sorted by reportingPeriod descending (newest reporting period first); download_path is a template
|
|
37
|
+
path (the batch pipeline assembles the download URL from it)."""
|
|
38
|
+
periods = [p.get("value") for p in (periods_data or []) if isinstance(p, dict) and p.get("value") is not None]
|
|
39
|
+
rows = []
|
|
40
|
+
for f in ((files_data or {}).get("results") or []):
|
|
41
|
+
if not isinstance(f, dict):
|
|
42
|
+
continue
|
|
43
|
+
period, version = f.get("reportingPeriod"), f.get("version")
|
|
44
|
+
rows.append({
|
|
45
|
+
"reporting_period": period,
|
|
46
|
+
"version": version,
|
|
47
|
+
"generation_date": f.get("generationDate"), # "DD-MM-YYYY HH:MM:SS" (early-morning batch timestamp)
|
|
48
|
+
"file_name": f.get("fileName"), # "2025-v76-29102026-EU MRV Publication of information"
|
|
49
|
+
"download_path": _BINARY_PATH.format(period=period, version=version) if period is not None and version is not None else None,
|
|
50
|
+
})
|
|
51
|
+
rows.sort(key=lambda r: (r["reporting_period"] or 0, r["version"] or 0), reverse=True)
|
|
52
|
+
return {"rows": rows, "count": len(rows), "reporting_periods": periods}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def datasets_raw(*, cache=None, no_cache=False):
|
|
56
|
+
"""0.3.5 flat face internals: two requests merged into the reporting-period + version ledger (upstream has no filter params — the client filters rows locally).
|
|
57
|
+
|
|
58
|
+
-> {rows[], count, reporting_periods[], params, _meta} — params={} (zero upstream params);
|
|
59
|
+
_meta is the round trip of the files op (the main ledger surface); a failing periods op is a hard
|
|
60
|
+
usage-level error (the whole result is untrustworthy)."""
|
|
61
|
+
from ..transport import call_provider
|
|
62
|
+
|
|
63
|
+
params = {}
|
|
64
|
+
periods = call_provider(provider, "periods", params, cache=cache, no_cache=no_cache)
|
|
65
|
+
files = call_provider(provider, "files", params, cache=cache, no_cache=no_cache)
|
|
66
|
+
out = datasets_from_upstream(periods["data"], files["data"])
|
|
67
|
+
return {**out, "params": params, "_meta": files}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""healthcare domain package — flat provider adapters (one file per source) + faces/cli (domain reorg in 0.2.6).
|
|
2
|
+
|
|
3
|
+
One domain per package: provider adapters laid flat (one source per file) + faces/cli.
|
|
4
|
+
"""
|
|
5
|
+
from . import (ctgov, dailymed, epmc, eudamed, gudid, mesh, # noqa: F401
|
|
6
|
+
fda_crl, fda_devices, fda_drugs, fda_recalls, rxnav, shortages, au_tga, gb_mhra,
|
|
7
|
+
ca_dpd, ca_mdall, rasff, safety_gate)
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""au_tga — Australian TGA eBS public face provider (a Domino .nsf site family; keyless).
|
|
2
|
+
|
|
3
|
+
Wave 6 of the healthcare domain: related
|
|
4
|
+
kind=artg_register / mis_manufacturers — the Australian therapeutic goods register ARTG, 97,988 product × sponsor
|
|
5
|
+
company edges, plus the Australian manufacturer licence register MIS with 444 licence × manufacturer rows.
|
|
6
|
+
Shapes measured 2026-10-23:
|
|
7
|
+
Host gating is split by host: ebs.tga.gov.au is fully open (both the curl and undici stacks), www.tga.gov.au hangs after connecting from this egress (do not probe the main site).
|
|
8
|
+
ARTG search: GET /ebs/ANZTPAR/PublicWeb.nsf/luPortalRegisterPublic?SearchView=&Query="Sun Pharma ANZ Pty Ltd"&Count=N&Start=M&charset=utf-8
|
|
9
|
+
— an **HTML surface** (SearchView does not honour outputformat=json, it was measured returning text/html): the op passes the raw text through with accept:'text'.
|
|
10
|
+
Query semantics: a quoted phrase = an exact phrase (measured Sun Pharma ANZ 100/100 hits); bare words = AND across all fields.
|
|
11
|
+
Row columns Name|ARTG|PI/CMI|Type|Sponsor (rows carry an OpenDocument detail link, and the row also starts with a sort-icon td that is not a data column);
|
|
12
|
+
Count=100 gives a 100-row page; Start = the number of rows to skip (skip semantics). Field-level search ([Sponsor] CONTAINS …) is bounced back to the search form — not done.
|
|
13
|
+
ARTG enumeration (the raw bulk line): the same view ?ReadViewEntries&outputformat=json&Count≤15000&Start=N (count=100000 → 500 OOM).
|
|
14
|
+
MIS licences: GET /ebs/ANZTPAR/PublicWeb.nsf/luPortalManufacturers?ReadViewEntries&outputformat=json&count=500 → JSON,
|
|
15
|
+
@toplevelentries=444; row columns Issue Date|Licence Number|Manufacturer ID|Address|**SiteState**|Manufacturer
|
|
16
|
+
(the state column's real name is SiteState, not the portal config's ColTitles «State» — the column name is the viewentry @name).
|
|
17
|
+
TTL: the register is on a daily cadence → 24h; no published quota → polite tier 1 rps.
|
|
18
|
+
"""
|
|
19
|
+
import re
|
|
20
|
+
|
|
21
|
+
from ..errors import SdkError
|
|
22
|
+
|
|
23
|
+
provider = {
|
|
24
|
+
"id": "au_tga",
|
|
25
|
+
"baseUrl": "https://www.ebs.tga.gov.au/ebs/ANZTPAR/PublicWeb.nsf",
|
|
26
|
+
"rate": {"rps": 1, "burst": 2},
|
|
27
|
+
"ops": {
|
|
28
|
+
"search": {"path": "/luPortalRegisterPublic", "accept": "text"}, # params: SearchView/Query/Count/Start/charset → HTML
|
|
29
|
+
"manufacturers": {"path": "/luPortalManufacturers"}, # params: ReadViewEntries/outputformat/count/charset → JSON
|
|
30
|
+
},
|
|
31
|
+
"ttl": {"search": 24 * 3600, "manufacturers": 24 * 3600},
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
_TR_RE = re.compile(r"<tr[\s\S]*?</tr>", re.I)
|
|
35
|
+
_TD_RE = re.compile(r"<td[^>]*>([\s\S]*?)</td>", re.I)
|
|
36
|
+
_DOC_RE = re.compile(r'href="([^"]*OpenDocument[^"]*)"', re.I)
|
|
37
|
+
_TAG_RE = re.compile(r"<[^>]+>")
|
|
38
|
+
_IMG_RE = re.compile(r"<img", re.I)
|
|
39
|
+
_A_RE = re.compile(r"<a[\s>]", re.I)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _cell_text(h):
|
|
43
|
+
s = _TAG_RE.sub(" ", str(h or ""))
|
|
44
|
+
s = (s.replace("&", "&").replace(""", '"')
|
|
45
|
+
.replace("'", "'").replace("’", "'").replace(" ", " "))
|
|
46
|
+
return re.sub(r"\s+", " ", s).strip()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def rows_from_search_html(html):
|
|
50
|
+
"""SearchView HTML → ARTG rows (pure function, testable offline): only rows with an OpenDocument detail link count; pure icon tds are stripped (measured 6 td → 5 data tds)."""
|
|
51
|
+
rows = []
|
|
52
|
+
for m in _TR_RE.finditer(str(html or "")):
|
|
53
|
+
tr = m.group(0)
|
|
54
|
+
if "OpenDocument" not in tr:
|
|
55
|
+
continue
|
|
56
|
+
cells = [c for c in _TD_RE.findall(tr)
|
|
57
|
+
if not (_IMG_RE.search(c) and not _A_RE.search(c) and not _TAG_RE.sub("", c).strip())]
|
|
58
|
+
if len(cells) < 5:
|
|
59
|
+
continue
|
|
60
|
+
href = _DOC_RE.search(tr)
|
|
61
|
+
rows.append({
|
|
62
|
+
"product_name": _cell_text(cells[0]) or None,
|
|
63
|
+
"artg_id": _cell_text(cells[1]) or None,
|
|
64
|
+
"pi_cmi": _cell_text(cells[2]) or None,
|
|
65
|
+
"product_type": _cell_text(cells[3]) or None,
|
|
66
|
+
"sponsor": _cell_text(cells[4]) or None, # company edge, primary column (the registrant = the entity accountable for the Australian market)
|
|
67
|
+
"doc_url": ("https://www.ebs.tga.gov.au" + href.group(1).replace("&", "&")) if href else None,
|
|
68
|
+
})
|
|
69
|
+
return rows
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _entry_val(d):
|
|
73
|
+
if not d:
|
|
74
|
+
return None
|
|
75
|
+
# Domino value-object shape {"0": "..."} (mirrors the d.text[0] property access on the Node side)
|
|
76
|
+
if d.get("text"):
|
|
77
|
+
return d["text"].get("0") or None
|
|
78
|
+
if d.get("number") is not None:
|
|
79
|
+
return str(d["number"].get("0"))
|
|
80
|
+
if d.get("datetime") is not None:
|
|
81
|
+
return re.sub(r"^(\d{4})(\d{2})(\d{2}).*$", r"\1-\2-\3", str(d["datetime"].get("0")))
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def rows_from_view_json(data):
|
|
86
|
+
"""ReadViewEntries JSON (the Domino viewentry shape) → MIS licence rows (pure function, testable offline); datetime→YYYY-MM-DD, the state column's real name is SiteState."""
|
|
87
|
+
out = []
|
|
88
|
+
for e in (data or {}).get("viewentry") or []:
|
|
89
|
+
col = {d.get("@name"): d for d in e.get("entrydata") or []}
|
|
90
|
+
out.append({
|
|
91
|
+
"issue_date": _entry_val(col.get("Issue Date")),
|
|
92
|
+
"licence_number": _entry_val(col.get("Licence Number")),
|
|
93
|
+
"manufacturer_id": _entry_val(col.get("Manufacturer ID")),
|
|
94
|
+
"address": _entry_val(col.get("Address")),
|
|
95
|
+
"state": _entry_val(col.get("SiteState")),
|
|
96
|
+
"manufacturer": _entry_val(col.get("Manufacturer")), # company edge, primary column (the licence holder)
|
|
97
|
+
})
|
|
98
|
+
return out
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def build_search_query(sponsor=None, query=None):
|
|
102
|
+
"""Domino Query assembly (pure function, testable offline): sponsor → a quoted phrase (measured exact);
|
|
103
|
+
query → passed through as given (a quoted phrase is upstream-native syntax; since 0.3.0 the quotes are not stripped — stripping them would make the exact Sponsor match unreachable)."""
|
|
104
|
+
sp, q = str(sponsor or "").strip(), str(query or "").strip()
|
|
105
|
+
if not sp and not q:
|
|
106
|
+
raise SdkError("usage_error", provider="au_tga", detail="sponsor or query is required (a bare query would enumerate the whole 98,000-row register, so it is refused)")
|
|
107
|
+
if sp:
|
|
108
|
+
return '"' + sp.replace('"', "") + '"'
|
|
109
|
+
return q
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def search_art_register(sponsor=None, query=None, *, limit=20, skip=0, cache=None, no_cache=False):
|
|
113
|
+
"""→ {rows[], count, skip, limit_applied, has_more, query, _meta}; has_more is a full-page heuristic (Domino does not return the hit total)."""
|
|
114
|
+
from ..transport import call_provider
|
|
115
|
+
|
|
116
|
+
q = build_search_query(sponsor, query)
|
|
117
|
+
size = min(max(1, int(limit or 20)), 100) # SearchView Count measured 100/page
|
|
118
|
+
sk = max(0, int(skip or 0))
|
|
119
|
+
params = {"SearchView": "", "Query": q, "Count": size, "charset": "utf-8"}
|
|
120
|
+
if sk:
|
|
121
|
+
params["Start"] = sk # Start = the number of rows to skip (skip semantics; measured Start=100 continues onto the next page)
|
|
122
|
+
r = call_provider(provider, "search", params, cache=cache, no_cache=no_cache)
|
|
123
|
+
rows = rows_from_search_html(r.get("data"))
|
|
124
|
+
return {"rows": rows, "count": len(rows), "skip": sk, "limit_applied": size,
|
|
125
|
+
"has_more": len(rows) == size, "query": q, "_meta": r}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def list_manufacturers(state=None, *, limit=100, cache=None, no_cache=False):
|
|
129
|
+
"""→ {rows[], total, count, limit_applied, _meta} — the MIS licence register: 444 licences in one full shot (count=500 on a single page) + the state filter."""
|
|
130
|
+
from ..transport import call_provider
|
|
131
|
+
|
|
132
|
+
size = min(max(1, int(limit or 100)), 500)
|
|
133
|
+
r = call_provider(provider, "manufacturers",
|
|
134
|
+
{"ReadViewEntries": "", "outputformat": "json", "count": 500, "charset": "utf-8"},
|
|
135
|
+
cache=cache, no_cache=no_cache)
|
|
136
|
+
data = r.get("data") or {}
|
|
137
|
+
total = int(data.get("@toplevelentries") or 0)
|
|
138
|
+
rows = rows_from_view_json(data)
|
|
139
|
+
if state:
|
|
140
|
+
rows = [x for x in rows if (x.get("state") or "").upper() == str(state).strip().upper()]
|
|
141
|
+
return {"rows": rows[:size], "total": total, "count": min(len(rows), size),
|
|
142
|
+
"limit_applied": size, "_meta": r}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""ca_dpd — Health Canada Drug Product Database open API face provider
|
|
2
|
+
(health-products.canada.ca/api/drug/; keyless).
|
|
3
|
+
|
|
4
|
+
healthcare domain (0.3.19; admitted 2026-11-02, probed). Shapes verified by first-hand live probing
|
|
5
|
+
2026-11-02 (the probe corpus lives in tmp/probe-ca-eu-faces/):
|
|
6
|
+
GET /api/drug/company/?lang=en&type=json
|
|
7
|
+
→ the full company register in ONE shot (no pagination surface on this host — 5,253 rows
|
|
8
|
+
measured 2026-11-02; rows carry company_code (the DPD join key) + company_name +
|
|
9
|
+
company_type (DIN OWNER / …) + the full postal address).
|
|
10
|
+
**Pit nailed: the Read Me page (GET /api/drug/ — the HTML index of the API family) is the
|
|
11
|
+
only enum documentation; the naive guess "status codes endpoint" (/api/drug/status) returns
|
|
12
|
+
the per-drug status *history* table (10.3MB), not the enum.**
|
|
13
|
+
GET /api/drug/drugproduct/?status=<n>&lang=en&type=json
|
|
14
|
+
→ drug products filtered by market status. **status is a closed set 1|2|3|4|6 per the Read Me
|
|
15
|
+
page — 1=Approved, 2=Marketed, 3=Cancelled Pre-Market, 4=Cancelled Post-Market, 6=Dormant
|
|
16
|
+
(no 5 in the official set); any other value is silently ignored and the endpoint dumps the
|
|
17
|
+
whole unfiltered register (15MB, ~55k rows — the silent-filter-ignored trap, same family as
|
|
18
|
+
the Socrata ones), so status is REQUIRED here and validated against the closed set.**
|
|
19
|
+
Rows carry drug_code + drug_identification_number (DIN) + brand_name + company_name inline
|
|
20
|
+
(company_code is NOT echoed on this face — join via company_name or run the company face);
|
|
21
|
+
the row does not echo the requested status either.
|
|
22
|
+
Measured: status=3 → 4,116 rows (2026-11-02).
|
|
23
|
+
Param-minimalism pitfall (this host publishes no per-endpoint docs beyond the Read Me links):
|
|
24
|
+
unprobed param combinations → either silent no-op or a full dump — this face accepts only the
|
|
25
|
+
documented lang/type/status family, do not widen it.
|
|
26
|
+
Local row slicing (limit/skip) is a post-slice over the single cached full shot — the upstream
|
|
27
|
+
has no paging, and the register itself is the payload (a monthly static extract).
|
|
28
|
+
TTL 7 days (the DPD monthly extract changes slowly); polite rate 1 rps (the host publishes no
|
|
29
|
+
quota — conservative; the same host also serves ca_mdall, the shared host bucket coordinates them).
|
|
30
|
+
"""
|
|
31
|
+
from ..errors import SdkError
|
|
32
|
+
|
|
33
|
+
provider = {
|
|
34
|
+
"id": "ca_dpd",
|
|
35
|
+
"baseUrl": "https://health-products.canada.ca",
|
|
36
|
+
"rate": {"rps": 1, "burst": 2},
|
|
37
|
+
"ops": {
|
|
38
|
+
"companies": {"path": "/api/drug/company/"}, # params: lang/type fixed for this face
|
|
39
|
+
"drug_products": {"path": "/api/drug/drugproduct/"}, # params: lang/type fixed + status (required, closed set)
|
|
40
|
+
},
|
|
41
|
+
"ttl": {"companies": 7 * 86400, "drug_products": 7 * 86400},
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
_STATUS = {1: "Approved", 2: "Marketed", 3: "Cancelled Pre-Market",
|
|
45
|
+
4: "Cancelled Post-Market", 6: "Dormant"} # the Read Me closed set (measured: no 5 in the family links)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def companies_from_upstream(data):
|
|
49
|
+
"""Upstream company array → company rows (pure function, offline-testable)."""
|
|
50
|
+
rows = [{
|
|
51
|
+
"company_code": r.get("company_code"), # the DPD join key
|
|
52
|
+
"company_name": r.get("company_name"),
|
|
53
|
+
"company_type": r.get("company_type"), # e.g. DIN OWNER
|
|
54
|
+
"address": {k: r.get(k) for k in ("street_name", "city_name", "province_name",
|
|
55
|
+
"country_name", "postal_code")},
|
|
56
|
+
} for r in (data or []) if isinstance(r, dict)]
|
|
57
|
+
return {"rows": rows, "count": len(rows)}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def drug_products_from_upstream(data, status):
|
|
61
|
+
"""Upstream drug-product array → product rows (pure function, offline-testable)."""
|
|
62
|
+
rows = [{
|
|
63
|
+
"drug_code": r.get("drug_code"),
|
|
64
|
+
"din": r.get("drug_identification_number"), # the 8-digit DIN (the dispensed-product key)
|
|
65
|
+
"brand_name": r.get("brand_name"),
|
|
66
|
+
"class_name": r.get("class_name"), # Human / Veterinary
|
|
67
|
+
"company_name": r.get("company_name"), # inline on this face; company_code is NOT echoed
|
|
68
|
+
"number_of_ais": r.get("number_of_ais"),
|
|
69
|
+
"ai_group_no": r.get("ai_group_no"),
|
|
70
|
+
"last_update_date": r.get("last_update_date"),
|
|
71
|
+
} for r in (data or []) if isinstance(r, dict)]
|
|
72
|
+
return {"rows": rows, "count": len(rows), "total": len(rows),
|
|
73
|
+
"status": int(status), "status_label": _STATUS[int(status)]}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _slice(rows, limit, skip):
|
|
77
|
+
"""Local post-slice over the single cached full shot (the upstream has no paging surface)."""
|
|
78
|
+
off = int(skip) if skip is not None else 0
|
|
79
|
+
if off < 0:
|
|
80
|
+
raise SdkError("usage_error", provider="ca_dpd", detail=f"skip must be >=0, got \"{skip}\"")
|
|
81
|
+
size = int(limit) if limit is not None else 200
|
|
82
|
+
if size < 1:
|
|
83
|
+
raise SdkError("usage_error", provider="ca_dpd", detail=f"limit must be >=1 (local post-slice, no upstream cap), got \"{limit}\"")
|
|
84
|
+
return rows[off:off + size]
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def companies_raw(*, limit=200, skip=0, cache=None, no_cache=False):
|
|
88
|
+
"""Flat-panel low-level face: the DPD company register, one full shot (5,253 rows measured
|
|
89
|
+
2026-11-02) cached whole; limit/skip is a local post-slice.
|
|
90
|
+
|
|
91
|
+
→ {rows[], count, total, params, _meta} — total = the whole register size (not the page)."""
|
|
92
|
+
from ..transport import call_provider
|
|
93
|
+
|
|
94
|
+
params = {"lang": "en", "type": "json"}
|
|
95
|
+
r = call_provider(provider, "companies", params, cache=cache, no_cache=no_cache)
|
|
96
|
+
out = companies_from_upstream(r["data"])
|
|
97
|
+
rows = _slice(out["rows"], limit, skip)
|
|
98
|
+
return {"rows": rows, "count": len(rows), "total": out["count"],
|
|
99
|
+
"params": params, "_meta": r}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def drug_products_raw(status, *, limit=200, skip=0, cache=None, no_cache=False):
|
|
103
|
+
"""Flat-panel low-level face: DPD drug products by market status.
|
|
104
|
+
|
|
105
|
+
status = the Read Me closed set 1|2|3|4|6 (1=Approved 2=Marketed 3=Cancelled Pre-Market
|
|
106
|
+
4=Cancelled Post-Market 6=Dormant) — REQUIRED and validated: any other value is silently
|
|
107
|
+
ignored upstream and dumps the whole 15MB register (the silent-filter trap).
|
|
108
|
+
→ {rows[], count, total, status, status_label, params, _meta}."""
|
|
109
|
+
from ..transport import call_provider
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
st = int(status)
|
|
113
|
+
except (TypeError, ValueError):
|
|
114
|
+
raise SdkError("usage_error", provider="ca_dpd",
|
|
115
|
+
detail=f"status must be one of the Read Me closed set {sorted(_STATUS)} (1=Approved 2=Marketed 3=Cancelled Pre-Market 4=Cancelled Post-Market 6=Dormant), got \"{status}\"")
|
|
116
|
+
if st not in _STATUS:
|
|
117
|
+
raise SdkError("usage_error", provider="ca_dpd",
|
|
118
|
+
detail=f"status must be one of {sorted(_STATUS)} ({'; '.join(f'{k}={v}' for k, v in sorted(_STATUS.items()))}) — an unknown value is silently ignored upstream and dumps the whole 15MB register, got \"{status}\"")
|
|
119
|
+
params = {"lang": "en", "type": "json", "status": st}
|
|
120
|
+
r = call_provider(provider, "drug_products", params, cache=cache, no_cache=no_cache)
|
|
121
|
+
out = drug_products_from_upstream(r["data"], st)
|
|
122
|
+
rows = _slice(out["rows"], limit, skip)
|
|
123
|
+
return {**out, "rows": rows, "count": len(rows), "params": params, "_meta": r}
|