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,154 @@
|
|
|
1
|
+
"""unsd_sdg — UN Statistics Division SDG Global Database (unstats.un.org/SDGAPI/v1; keyless).
|
|
2
|
+
|
|
3
|
+
Two faces: indicators (the 251-indicator catalog with the per-indicator series codes nested in
|
|
4
|
+
each row) and series (the observations of ONE series — 17 goals, 713 series codes).
|
|
5
|
+
|
|
6
|
+
Traps (research chapter 90, 2026-11-02 — every one of these decided this face's shape):
|
|
7
|
+
* the `area` parameter on GET Series/Data is DECORATIVE — with and without it the answer is
|
|
8
|
+
byte-identical (areaCodes / geoAreaCode variants are ignored the same way), so the geo
|
|
9
|
+
filter is a LOCAL pass over the fetched page, never an upstream param.
|
|
10
|
+
* `timePeriod` DOES filter upstream (seriesCode=DC_ODA_POVLG&timePeriod=2020 -> 138 rows
|
|
11
|
+
totalElements vs 3,499 unfiltered) — the only upstream filter that works on this endpoint.
|
|
12
|
+
* POST Series/Data is a 404; the GET form is the only live channel.
|
|
13
|
+
* Indicator/Data (whole-indicator granularity) answers 3,198,008 rows for one indicator —
|
|
14
|
+
the beast. This module never forms that call: the series face is seriesCode-granularity
|
|
15
|
+
only, and the catalog face is what tells you the valid seriesCodes.
|
|
16
|
+
|
|
17
|
+
Rate: no published limit -> polite 0.5 rps / burst 2. TTL: 7 days (the 251-indicator catalog)
|
|
18
|
+
and 24h (series observations; the SDG database refreshes per quarterly release).
|
|
19
|
+
"""
|
|
20
|
+
from ..errors import SdkError
|
|
21
|
+
|
|
22
|
+
provider = {
|
|
23
|
+
"id": "unsd_sdg",
|
|
24
|
+
"baseUrl": "https://unstats.un.org/SDGAPI/v1/sdg",
|
|
25
|
+
"rate": {"rps": 0.5, "burst": 2},
|
|
26
|
+
"ops": {
|
|
27
|
+
"indicators": {"path": "/Indicator/List"},
|
|
28
|
+
"series": {"path": "/Series/Data"},
|
|
29
|
+
},
|
|
30
|
+
"ttl": {"indicators": 7 * 24 * 3600, "series": 24 * 3600},
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _norm_series(series):
|
|
35
|
+
t = str(series or "").strip().upper()
|
|
36
|
+
if not t:
|
|
37
|
+
raise SdkError("usage_error", provider="unsd_sdg",
|
|
38
|
+
detail="--series is required (an SDG series code, e.g. SI_POV_DAY1, "
|
|
39
|
+
"DC_ODA_POVLG — list them with unsd_sdg_indicators; the "
|
|
40
|
+
"whole-indicator endpoint is a 3.2M-row wall and is never used)")
|
|
41
|
+
return t
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _norm_period(p):
|
|
45
|
+
"""timePeriodStart arrives as a JSON number when the period is a plain year (2020.0) and as
|
|
46
|
+
a string for split periods — normalise to the plain form the caller reads."""
|
|
47
|
+
if p is None:
|
|
48
|
+
return None
|
|
49
|
+
try:
|
|
50
|
+
f = float(p)
|
|
51
|
+
return str(int(f)) if f.is_integer() else str(f)
|
|
52
|
+
except (TypeError, ValueError):
|
|
53
|
+
return str(p)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _indicator_row(x):
|
|
57
|
+
series = x.get("series") or []
|
|
58
|
+
return {"goal": str(x.get("goal") or "") or None,
|
|
59
|
+
"target": str(x.get("target") or "") or None,
|
|
60
|
+
"code": x.get("code"),
|
|
61
|
+
"description": x.get("description"),
|
|
62
|
+
"tier": x.get("tier"),
|
|
63
|
+
"uri": x.get("uri"),
|
|
64
|
+
"series_count": len(series),
|
|
65
|
+
"series_codes": [s.get("code") for s in series if s.get("code")]}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def indicators_raw(*, page=1, per_page=50, search=None, cache=None, no_cache=False):
|
|
69
|
+
"""-> {total, count, rows, params, _meta}; /Indicator/List
|
|
70
|
+
The upstream answers the whole 251-row catalog in one payload (no upstream pagination) —
|
|
71
|
+
page / per_page slice it locally; search is a LOCAL keyword scan (code / description /
|
|
72
|
+
series codes), never an upstream filter."""
|
|
73
|
+
from ..transport import call_provider
|
|
74
|
+
|
|
75
|
+
pg = int(page or 1)
|
|
76
|
+
pp = int(per_page or 50)
|
|
77
|
+
if pg < 1:
|
|
78
|
+
raise SdkError("usage_error", provider="unsd_sdg", detail="page must be >= 1")
|
|
79
|
+
if not 1 <= pp <= 500:
|
|
80
|
+
raise SdkError("usage_error", provider="unsd_sdg", detail="per_page must be 1-500")
|
|
81
|
+
r = call_provider(provider, "indicators", {}, cache=cache, no_cache=no_cache)
|
|
82
|
+
data = r["data"]
|
|
83
|
+
if not isinstance(data, list):
|
|
84
|
+
raise SdkError("upstream_unavailable", provider="unsd_sdg",
|
|
85
|
+
detail="The SDGAPI Indicator/List answer is not the catalog list")
|
|
86
|
+
rows = [_indicator_row(x) for x in data]
|
|
87
|
+
q = str(search or "").strip().lower()
|
|
88
|
+
if q:
|
|
89
|
+
rows = [x for x in rows
|
|
90
|
+
if q in (x["code"] or "").lower() or q in (x["description"] or "").lower()
|
|
91
|
+
or any(q in (c or "").lower() for c in x["series_codes"])]
|
|
92
|
+
total = len(rows)
|
|
93
|
+
rows = rows[(pg - 1) * pp: pg * pp]
|
|
94
|
+
return {"total": total, "count": len(rows), "rows": rows,
|
|
95
|
+
"params": {"page": pg, "per_page": pp, "search": q or None}, "_meta": r}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def series_raw(series, *, geo=None, time=None, page=1, page_size=100,
|
|
99
|
+
cache=None, no_cache=False):
|
|
100
|
+
"""-> {total, count, rows, params, _meta}; /Series/Data?seriesCode=..
|
|
101
|
+
One series code per call. geo = a LOCAL filter over the fetched page (the upstream `area`
|
|
102
|
+
param is decorative — byte-identical with and without it); it matches the numeric M49-style
|
|
103
|
+
geoAreaCode or the geoAreaName, case-insensitively. time = the upstream timePeriod filter
|
|
104
|
+
(the one upstream filter that works). page / page_size ride the upstream pager."""
|
|
105
|
+
from ..transport import call_provider
|
|
106
|
+
|
|
107
|
+
code = _norm_series(series)
|
|
108
|
+
pg = int(page or 1)
|
|
109
|
+
ps = int(page_size or 100)
|
|
110
|
+
if pg < 1:
|
|
111
|
+
raise SdkError("usage_error", provider="unsd_sdg", detail="page must be >= 1")
|
|
112
|
+
if not 1 <= ps <= 1000:
|
|
113
|
+
raise SdkError("usage_error", provider="unsd_sdg", detail="page_size must be 1-1000")
|
|
114
|
+
params = {"seriesCode": code, "page": pg, "pageSize": ps}
|
|
115
|
+
t = str(time).strip() if time else None
|
|
116
|
+
if t:
|
|
117
|
+
params["timePeriod"] = t
|
|
118
|
+
r = call_provider(provider, "series", params, cache=cache, no_cache=no_cache)
|
|
119
|
+
data = r["data"]
|
|
120
|
+
if not isinstance(data, dict) or not isinstance(data.get("data"), list):
|
|
121
|
+
raise SdkError("upstream_unavailable", provider="unsd_sdg",
|
|
122
|
+
detail="The SDGAPI Series/Data answer is not the paged envelope")
|
|
123
|
+
g = str(geo).strip() if geo else None
|
|
124
|
+
gl = g.lower() if g else None
|
|
125
|
+
|
|
126
|
+
def _row(x):
|
|
127
|
+
return {"series": x.get("series") or code,
|
|
128
|
+
"series_description": x.get("seriesDescription"),
|
|
129
|
+
"goal": (x.get("goal") or [None])[0],
|
|
130
|
+
"target": (x.get("target") or [None])[0],
|
|
131
|
+
"indicator": (x.get("indicator") or [None])[0],
|
|
132
|
+
"geo_area_code": x.get("geoAreaCode"),
|
|
133
|
+
"geo_area_name": x.get("geoAreaName"),
|
|
134
|
+
"period": _norm_period(x.get("timePeriodStart")),
|
|
135
|
+
"value": x.get("value"),
|
|
136
|
+
"value_type": x.get("valueType"),
|
|
137
|
+
"attributes": x.get("attributes") or {},
|
|
138
|
+
"dimensions": x.get("dimensions") or {},
|
|
139
|
+
"source": x.get("source"),
|
|
140
|
+
"footnotes": x.get("footnotes") or []}
|
|
141
|
+
|
|
142
|
+
rows = [_row(x) for x in data["data"]]
|
|
143
|
+
total = data.get("totalElements")
|
|
144
|
+
pages = data.get("totalPages")
|
|
145
|
+
if gl:
|
|
146
|
+
rows = [x for x in rows
|
|
147
|
+
if gl == str(x["geo_area_code"] or "").lower()
|
|
148
|
+
or gl == str(x["geo_area_name"] or "").lower()]
|
|
149
|
+
rows.sort(key=lambda x: (str(x["geo_area_code"] or ""), str(x["period"] or "")))
|
|
150
|
+
return {"total": total, "count": len(rows), "rows": rows,
|
|
151
|
+
"total_pages": pages, "page": data.get("pageNumber"),
|
|
152
|
+
"geo_filtered": bool(gl), "params": {"series": code, "geo": g, "time": t,
|
|
153
|
+
"page": pg, "page_size": ps},
|
|
154
|
+
"_meta": r}
|
agentdata/economy/wb.py
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""wb — World Bank Open Data provider (api.worldbank.org; keyless).
|
|
2
|
+
|
|
3
|
+
Three faces born flat: wdi_values (country x indicator x year window), wdi_indicators (the
|
|
4
|
+
indicator catalog), countries (the ISO3/ISO2 + income-level + region reference).
|
|
5
|
+
|
|
6
|
+
Shapes verified by first-hand live probing 2026-11-02 (tmp/probe-economy/):
|
|
7
|
+
GET /v2/country/{country}/indicator/{indicator}?format=json&date=YYYY:YYYY&per_page=N
|
|
8
|
+
-> [meta, rows]; meta = {page, pages, per_page, total, sourceid, lastupdated}
|
|
9
|
+
-> rows = {indicator:{id,value}, country:{id,value}, countryiso3code, date, value, unit,
|
|
10
|
+
obs_status, decimal}
|
|
11
|
+
* several countries in one call via the `;` separator (CHN;JPN over 2020:2023 -> 8 rows);
|
|
12
|
+
* several indicators via `;` is REJECTED upstream ("120 Invalid value") -> one indicator per
|
|
13
|
+
call, enforced locally (the mirror of an upstream wall, not a local invention);
|
|
14
|
+
* `date` takes "YYYY" or "YYYY:YYYY" (the upstream range form); `value` is null for a missing
|
|
15
|
+
observation — null is kept as null (a gap is a fact, never fabricated);
|
|
16
|
+
* sourceid=2 is the WDI source.
|
|
17
|
+
GET /v2/indicator?format=json&source=2&per_page=500
|
|
18
|
+
-> the 1,445 WDI indicators in one page (572 KB). WITHOUT source= the catalog is every World
|
|
19
|
+
Bank source at once (29,544 rows / 5,909 pages) -> source=2 is the default here.
|
|
20
|
+
-> row = {id, name, unit, source:{id,value}, sourceNote, sourceOrganization, topics[]}
|
|
21
|
+
GET /v2/country?format=json&per_page=N
|
|
22
|
+
-> rows = {id (ISO3), iso2Code, name, region{}, adminregion{}, incomeLevel{}, lendingType{},
|
|
23
|
+
capitalCity, longitude, latitude} — the country reference table (income group =
|
|
24
|
+
the CLASS grouping used as a buyer screen).
|
|
25
|
+
Rate: no official limit published -> polite tier 1 rps / burst 2. TTL: values 24h (quarterly
|
|
26
|
+
cadence, the meta carries lastupdated), catalogs 7 days.
|
|
27
|
+
"""
|
|
28
|
+
from ..errors import SdkError
|
|
29
|
+
|
|
30
|
+
provider = {
|
|
31
|
+
"id": "wb",
|
|
32
|
+
"baseUrl": "https://api.worldbank.org",
|
|
33
|
+
"rate": {"rps": 1, "burst": 2},
|
|
34
|
+
"ops": {
|
|
35
|
+
"wdi_values": {"path": "/v2/country/{country}/indicator/{indicator}",
|
|
36
|
+
"notFound": [404]}, # params: date/per_page
|
|
37
|
+
"wdi_indicators": {"path": "/v2/indicator", "notFound": [404]}, # params: source/page/per_page
|
|
38
|
+
"countries": {"path": "/v2/country", "notFound": [404]}, # params: page/per_page
|
|
39
|
+
},
|
|
40
|
+
"ttl": {"wdi_values": 24 * 3600, "wdi_indicators": 7 * 86400,
|
|
41
|
+
"countries": 7 * 86400},
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
_PER_PAGE_CAP = 1000 # upstream accepts large pages; 1,000 keeps one catalog page a sane payload
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _iso3_list(country):
|
|
48
|
+
"""Country argument -> the upstream path form. Accepts a single ISO3 code or a list;
|
|
49
|
+
the upstream separator is `;` (verified: CHN;JPN in one path segment)."""
|
|
50
|
+
if isinstance(country, (list, tuple)):
|
|
51
|
+
items = [str(x).strip().upper() for x in country if str(x).strip()]
|
|
52
|
+
else:
|
|
53
|
+
items = [x.strip().upper() for x in str(country or "").split(";") if x.strip()]
|
|
54
|
+
if not items:
|
|
55
|
+
raise SdkError("usage_error", provider="wb",
|
|
56
|
+
detail="wb_wdi_values requires --country (ISO3 code, e.g. CHN; several codes may be joined with ';')")
|
|
57
|
+
return ";".join(items)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _split_payload(payload):
|
|
61
|
+
"""The API answers [meta, rows] — an error answer is a single [{message:[...]}] object."""
|
|
62
|
+
if isinstance(payload, list) and payload and isinstance(payload[0], dict) and "message" in payload[0]:
|
|
63
|
+
msg = payload[0]["message"]
|
|
64
|
+
raise SdkError("upstream_unavailable", provider="wb",
|
|
65
|
+
detail=f"World Bank API error {msg[0].get('id') if msg else '?'}: "
|
|
66
|
+
f"{msg[0].get('value') if msg else payload}")
|
|
67
|
+
if not isinstance(payload, list) or len(payload) < 2:
|
|
68
|
+
raise SdkError("upstream_unavailable", provider="wb",
|
|
69
|
+
detail=f"Unexpected World Bank payload shape: {str(payload)[:160]!r}")
|
|
70
|
+
return payload[0] or {}, payload[1] or []
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _value_row(r):
|
|
74
|
+
ind = r.get("indicator") or {}
|
|
75
|
+
ctry = r.get("country") or {}
|
|
76
|
+
return {
|
|
77
|
+
"indicator_code": ind.get("id"),
|
|
78
|
+
"indicator_name": ind.get("value"),
|
|
79
|
+
"country_iso3": r.get("countryiso3code") or ctry.get("id"),
|
|
80
|
+
"country_name": ctry.get("value"),
|
|
81
|
+
"year": r.get("date"),
|
|
82
|
+
"value": r.get("value"),
|
|
83
|
+
"unit": r.get("unit") or None,
|
|
84
|
+
"obs_status": r.get("obs_status") or None,
|
|
85
|
+
"decimal": r.get("decimal"),
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def wdi_values_raw(country, indicator, *, date=None, per_page=100, cache=None, no_cache=False):
|
|
90
|
+
"""-> {total, count, rows, params, last_updated, _meta}; /v2/country/{c}/indicator/{i}.
|
|
91
|
+
country = ISO3 code or a list (upstream `;` separator); indicator = one WDI code (the
|
|
92
|
+
upstream rejects several codes in one path segment -> one per call); date = "YYYY" or
|
|
93
|
+
"YYYY:YYYY" (upstream range form); per_page 1-1000. A null value is a missing
|
|
94
|
+
observation and stays null."""
|
|
95
|
+
from ..transport import call_provider
|
|
96
|
+
|
|
97
|
+
ctry = _iso3_list(country)
|
|
98
|
+
ind = str(indicator or "").strip()
|
|
99
|
+
if not ind:
|
|
100
|
+
raise SdkError("usage_error", provider="wb",
|
|
101
|
+
detail="wb_wdi_values requires --indicator (a WDI code, e.g. NY.GDP.MKTP.CD)")
|
|
102
|
+
if ";" in ind:
|
|
103
|
+
raise SdkError("usage_error", provider="wb",
|
|
104
|
+
detail="The World Bank API rejects several indicator codes in one call "
|
|
105
|
+
"(upstream \"120 Invalid value\") — pass one --indicator per call")
|
|
106
|
+
pp = int(per_page or 100)
|
|
107
|
+
if not 1 <= pp <= _PER_PAGE_CAP:
|
|
108
|
+
raise SdkError("usage_error", provider="wb", detail=f"per_page must be 1-{_PER_PAGE_CAP}")
|
|
109
|
+
params = {"format": "json", "per_page": pp}
|
|
110
|
+
if date:
|
|
111
|
+
params["date"] = str(date).strip()
|
|
112
|
+
r = call_provider(provider, "wdi_values", dict(params, country=ctry, indicator=ind),
|
|
113
|
+
cache=cache, no_cache=no_cache)
|
|
114
|
+
meta, rows = _split_payload(r["data"])
|
|
115
|
+
return {"total": meta.get("total"), "count": len(rows),
|
|
116
|
+
"rows": [_value_row(x) for x in rows],
|
|
117
|
+
"pages": meta.get("pages"), "page": meta.get("page"),
|
|
118
|
+
"last_updated": meta.get("lastupdated"),
|
|
119
|
+
"params": {"country": ctry, "indicator": ind, "date": params.get("date"),
|
|
120
|
+
"per_page": pp},
|
|
121
|
+
"_meta": r}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _indicator_row(x):
|
|
125
|
+
src = x.get("source") or {}
|
|
126
|
+
return {
|
|
127
|
+
"indicator_code": x.get("id"),
|
|
128
|
+
"name": x.get("name"),
|
|
129
|
+
"unit": x.get("unit") or None,
|
|
130
|
+
"source_id": src.get("id"),
|
|
131
|
+
"source_name": src.get("value"),
|
|
132
|
+
"source_note": x.get("sourceNote"),
|
|
133
|
+
"source_organization": x.get("sourceOrganization"),
|
|
134
|
+
"topics": [t.get("value") for t in (x.get("topics") or []) if isinstance(t, dict)],
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def wdi_indicators_raw(*, source=2, page=1, per_page=500, search=None,
|
|
139
|
+
cache=None, no_cache=False):
|
|
140
|
+
"""-> {total, count, rows, params, _meta}; /v2/indicator?format=json&source=..
|
|
141
|
+
source = the World Bank source id (2 = WDI, the default here; without it the catalog is
|
|
142
|
+
every source at once — 29,544 rows). search = a local keyword scan over the returned page
|
|
143
|
+
(name / sourceOrganization / sourceNote), never an upstream filter."""
|
|
144
|
+
from ..transport import call_provider
|
|
145
|
+
|
|
146
|
+
src = str(source if source is not None else 2).strip() or "2"
|
|
147
|
+
pp = int(per_page or 500)
|
|
148
|
+
if not 1 <= pp <= _PER_PAGE_CAP:
|
|
149
|
+
raise SdkError("usage_error", provider="wb", detail=f"per_page must be 1-{_PER_PAGE_CAP}")
|
|
150
|
+
pg = int(page or 1)
|
|
151
|
+
if pg < 1:
|
|
152
|
+
raise SdkError("usage_error", provider="wb", detail="page must be >= 1")
|
|
153
|
+
params = {"format": "json", "source": src, "per_page": pp, "page": pg}
|
|
154
|
+
r = call_provider(provider, "wdi_indicators", params, cache=cache, no_cache=no_cache)
|
|
155
|
+
meta, rows = _split_payload(r["data"])
|
|
156
|
+
out = [_indicator_row(x) for x in rows]
|
|
157
|
+
q = str(search or "").strip().lower()
|
|
158
|
+
if q:
|
|
159
|
+
out = [x for x in out
|
|
160
|
+
if q in (x["indicator_code"] or "").lower() or q in (x["name"] or "").lower()
|
|
161
|
+
or q in (x["source_organization"] or "").lower() or q in (x["source_note"] or "").lower()]
|
|
162
|
+
return {"total": meta.get("total"), "count": len(out), "rows": out,
|
|
163
|
+
"page": meta.get("page"), "pages": meta.get("pages"),
|
|
164
|
+
"params": {"source": src, "page": pg, "per_page": pp, "search": q or None},
|
|
165
|
+
"_meta": r}
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _country_row(x):
|
|
169
|
+
region = x.get("region") or {}
|
|
170
|
+
income = x.get("incomeLevel") or {}
|
|
171
|
+
lending = x.get("lendingType") or {}
|
|
172
|
+
admin = x.get("adminregion") or {}
|
|
173
|
+
return {
|
|
174
|
+
"iso3": x.get("id"),
|
|
175
|
+
"iso2": x.get("iso2Code"),
|
|
176
|
+
"name": x.get("name"),
|
|
177
|
+
"region_id": region.get("id") or None,
|
|
178
|
+
"region_name": region.get("value") or None,
|
|
179
|
+
"admin_region": admin.get("value") or None,
|
|
180
|
+
"income_level_id": income.get("id") or None,
|
|
181
|
+
"income_level": income.get("value") or None,
|
|
182
|
+
"lending_type_id": lending.get("id") or None,
|
|
183
|
+
"lending_type": lending.get("value") or None,
|
|
184
|
+
"capital_city": x.get("capitalCity"),
|
|
185
|
+
"longitude": x.get("longitude"),
|
|
186
|
+
"latitude": x.get("latitude"),
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def countries_raw(*, page=1, per_page=100, cache=None, no_cache=False):
|
|
191
|
+
"""-> {total, count, rows, params, _meta}; /v2/country?format=json
|
|
192
|
+
The country reference: ISO3 / ISO2 / region / income level / lending type / coordinates
|
|
193
|
+
(the buyer-screen dimension; income level = the World Bank CLASS grouping)."""
|
|
194
|
+
from ..transport import call_provider
|
|
195
|
+
|
|
196
|
+
pp = int(per_page or 100)
|
|
197
|
+
if not 1 <= pp <= _PER_PAGE_CAP:
|
|
198
|
+
raise SdkError("usage_error", provider="wb", detail=f"per_page must be 1-{_PER_PAGE_CAP}")
|
|
199
|
+
pg = int(page or 1)
|
|
200
|
+
if pg < 1:
|
|
201
|
+
raise SdkError("usage_error", provider="wb", detail="page must be >= 1")
|
|
202
|
+
params = {"format": "json", "per_page": pp, "page": pg}
|
|
203
|
+
r = call_provider(provider, "countries", params, cache=cache, no_cache=no_cache)
|
|
204
|
+
meta, rows = _split_payload(r["data"])
|
|
205
|
+
return {"total": meta.get("total"), "count": len(rows),
|
|
206
|
+
"rows": [_country_row(x) for x in rows],
|
|
207
|
+
"page": meta.get("page"), "pages": meta.get("pages"),
|
|
208
|
+
"params": {"page": pg, "per_page": pp}, "_meta": r}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""who_xmart — WHO xMart OData service (xmart-api-public.who.int; keyless, no registration).
|
|
2
|
+
|
|
3
|
+
Three ops behind two faces:
|
|
4
|
+
* indicator_directory — IND_DIRECTORY_WIDE, the 1,561-indicator catalog; each row carries
|
|
5
|
+
IND_URI_API (the per-indicator API diameter) and IND_URI_WEB (the human page).
|
|
6
|
+
* indicator_values — DATA_/{entity set}, the observations of one indicator.
|
|
7
|
+
* geo_reference — REF_GEO_WIDE, the M49 <-> ISO3/ISO2 geography join.
|
|
8
|
+
|
|
9
|
+
Shapes verified by first-hand live probing 2026-11-02 (tmp/probe-economy/):
|
|
10
|
+
GET /DATA_/IND_DIRECTORY_WIDE?$filter=IND_NAME eq 'TB_ppm_enrol_tx_prct'
|
|
11
|
+
-> {"@odata.context":..., "@odata.count":1561, "value":[{IND_ID, IND_UUID, IND_PER_CODE,
|
|
12
|
+
IND_NAME, IND_NAME_FULL, IND_CODE_GHO, VALUE_TYPE, IND_UNIT, IND_URI_API, IND_URI_WEB,
|
|
13
|
+
IND_PUBLISH_TIME_PREV, DATA_PROVENANCE, DIM_GEO_COVERAGE, DIM_GEO_GRANULARITY, ...}]}
|
|
14
|
+
GET /DATA_/RELAY_TBPPM?$filter=IND_CODE eq 'TB_ppm_enrol_tx_prct'
|
|
15
|
+
-> {"value":[{IND_CODE, DIM_TIME ("2020"), DIM_TIME_TYPE, DIM_GEO_CODE_M49 ("586"),
|
|
16
|
+
DIM_GEO_CODE_TYPE, DIM_1_CODE/DIM_MEMBER_1_CODE … DIM_9_CODE/DIM_MEMBER_9_CODE,
|
|
17
|
+
VALUE_NUMERIC, VALUE_LABEL, OBSERVATION_STATUS, DIM_PUBLISH_STATE_CODE, IND_ID}]}
|
|
18
|
+
GET /DATA_/REF_GEO_WIDE?$filter=GEO_CODE_M49 eq '586' and TERM_LANG eq 'en'
|
|
19
|
+
-> {"value":[{GEO_CODE_M49 ("586"), GEO_CODE_ISO_3 ("PAK"), GEO_CODE_ISO_2 ("PK"),
|
|
20
|
+
GEO_NAME_SHORT ("Pakistan"), GEO_TYPE, GEO_CLASS, GEO_SOVEREIGN, TERM_LANG}]}
|
|
21
|
+
Traps found by probing (each one cost a call):
|
|
22
|
+
* DIM_TIME is Edm.String -> the year needs QUOTES ("DIM_TIME eq '2021'"; an unquoted year
|
|
23
|
+
is a 400 type error). DIM_GEO_CODE_M49 is a string too ('586');
|
|
24
|
+
* the geography table is multilingual — one row per TERM_LANG; add "and TERM_LANG eq 'en'";
|
|
25
|
+
* the property is GEO_CODE_M49 in REF_GEO_WIDE but DIM_GEO_CODE_M49 in the value tables;
|
|
26
|
+
* the indicator -> entity-set diameter travels in IND_URI_API (the last path segment is the
|
|
27
|
+
entity set name); several entity sets may carry the same indicator name, and the
|
|
28
|
+
candidate list is NOT auto-picked (the caller chooses, OpenAlex-style discipline);
|
|
29
|
+
* a malformed $filter is a 400 — a usage error, not an upstream outage (re-mapped here).
|
|
30
|
+
Rate: no published limit -> polite tier 0.5 rps / burst 2. TTL: 24h (WHO publishes daily).
|
|
31
|
+
"""
|
|
32
|
+
from ..errors import SdkError
|
|
33
|
+
|
|
34
|
+
provider = {
|
|
35
|
+
"id": "who_xmart",
|
|
36
|
+
"baseUrl": "https://xmart-api-public.who.int/DATA_",
|
|
37
|
+
"rate": {"rps": 0.5, "burst": 2},
|
|
38
|
+
"ops": {
|
|
39
|
+
"indicator_directory": {"path": "/IND_DIRECTORY_WIDE", "notFound": [404]},
|
|
40
|
+
"indicator_values": {"path": "/{entity_set}", "notFound": [404]},
|
|
41
|
+
"geo_reference": {"path": "/REF_GEO_WIDE", "notFound": [404]},
|
|
42
|
+
},
|
|
43
|
+
"ttl": {"indicator_directory": 24 * 3600, "indicator_values": 24 * 3600,
|
|
44
|
+
"geo_reference": 7 * 86400},
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
_DIR_COLS = ("IND_ID", "IND_NAME", "IND_NAME_FULL", "IND_NAME_SHORT", "IND_UUID", "IND_PER_CODE",
|
|
48
|
+
"IND_CODE_GHO", "VALUE_TYPE", "IND_UNIT", "IND_URI_API", "IND_URI_WEB",
|
|
49
|
+
"IND_PUBLISH_TIME_PREV", "DATA_PROVENANCE", "DIM_GEO_COVERAGE",
|
|
50
|
+
"DIM_GEO_GRANULARITY", "IND_PROGRESS_DIRECTION_CODE", "SOURCE_TABLE", "TERM_LANG")
|
|
51
|
+
_VAL_COLS = ("IND_ID", "IND_CODE", "DIM_TIME", "DIM_TIME_TYPE", "DIM_GEO_CODE_M49",
|
|
52
|
+
"DIM_GEO_CODE_TYPE", "DIM_1_CODE", "DIM_MEMBER_1_CODE", "DIM_2_CODE",
|
|
53
|
+
"DIM_MEMBER_2_CODE", "DIM_3_CODE", "DIM_MEMBER_3_CODE", "DIM_4_CODE",
|
|
54
|
+
"DIM_MEMBER_4_CODE", "DIM_5_CODE", "DIM_MEMBER_5_CODE", "DIM_6_CODE",
|
|
55
|
+
"DIM_MEMBER_6_CODE", "DIM_7_CODE", "DIM_MEMBER_7_CODE", "DIM_8_CODE",
|
|
56
|
+
"DIM_MEMBER_8_CODE", "DIM_9_CODE", "DIM_MEMBER_9_CODE", "VALUE_NUMERIC",
|
|
57
|
+
"VALUE_LABEL", "VALUE_NUMERIC_LOWER", "VALUE_NUMERIC_UPPER", "OBSERVATION_STATUS",
|
|
58
|
+
"DIM_PUBLISH_STATE_CODE", "VALUE_PROVENANCE")
|
|
59
|
+
_GEO_COLS = ("GEO_CODE_M49", "GEO_CODE_ISO_3", "GEO_CODE_ISO_2", "GEO_NAME_SHORT",
|
|
60
|
+
"GEO_NAME_FORMAL", "GEO_TYPE", "GEO_CLASS", "GEO_SOVEREIGN", "GEO_GROUP_TYPE",
|
|
61
|
+
"GEO_CAPITAL", "TERM_LANG")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _odata(op, params, *, cache=None, no_cache=False):
|
|
65
|
+
"""One OData call; a 400 (malformed $filter / wrong property) is a usage error, not an outage."""
|
|
66
|
+
from ..transport import call_provider
|
|
67
|
+
try:
|
|
68
|
+
r = call_provider(provider, op, params, cache=cache, no_cache=no_cache)
|
|
69
|
+
except SdkError as e:
|
|
70
|
+
if e.status == 400:
|
|
71
|
+
raise SdkError("usage_error", provider="who_xmart",
|
|
72
|
+
detail=f"The WHO xMart service rejected the query (HTTP 400): {e.detail}")
|
|
73
|
+
raise
|
|
74
|
+
data = r["data"]
|
|
75
|
+
if not isinstance(data, dict) or "value" not in data:
|
|
76
|
+
raise SdkError("upstream_unavailable", provider="who_xmart",
|
|
77
|
+
detail=f"Unexpected xMart payload shape: {str(data)[:160]!r}")
|
|
78
|
+
return data, r
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _pick(row, cols):
|
|
82
|
+
return {c: row.get(c) for c in cols if c in row}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def indicator_directory_raw(*, filter=None, top=500, skip=0, search=None,
|
|
86
|
+
cache=None, no_cache=False):
|
|
87
|
+
"""-> {total, count, rows, params, _meta}; /IND_DIRECTORY_WIDE
|
|
88
|
+
filter = the raw upstream OData $filter (advanced; the property names are the upstream's);
|
|
89
|
+
top / skip = the upstream $top / $skip paging; search = a LOCAL keyword scan over the
|
|
90
|
+
returned page (name / full name / unit / web URI) — never an upstream filter."""
|
|
91
|
+
from ..transport import call_provider # noqa: F401 (kept for symmetry with the other ops)
|
|
92
|
+
|
|
93
|
+
f = str(filter).strip() if filter else None
|
|
94
|
+
tp = int(top or 500)
|
|
95
|
+
sk = int(skip or 0)
|
|
96
|
+
if tp < 1 or tp > 5000:
|
|
97
|
+
raise SdkError("usage_error", provider="who_xmart", detail="$top must be 1-5000")
|
|
98
|
+
if sk < 0:
|
|
99
|
+
raise SdkError("usage_error", provider="who_xmart", detail="$skip must be >= 0")
|
|
100
|
+
params = {"$top": tp, "$skip": sk, "$count": "true"}
|
|
101
|
+
if f:
|
|
102
|
+
params["$filter"] = f
|
|
103
|
+
data, r = _odata("indicator_directory", params, cache=cache, no_cache=no_cache)
|
|
104
|
+
rows = [_pick(x, _DIR_COLS) for x in data.get("value") or []]
|
|
105
|
+
q = str(search or "").strip().lower()
|
|
106
|
+
if q:
|
|
107
|
+
rows = [x for x in rows
|
|
108
|
+
if q in (x.get("IND_NAME") or "").lower()
|
|
109
|
+
or q in (x.get("IND_NAME_FULL") or "").lower()
|
|
110
|
+
or q in (x.get("IND_NAME_SHORT") or "").lower()
|
|
111
|
+
or q in (x.get("IND_UNIT") or "").lower()
|
|
112
|
+
or q in (x.get("IND_URI_WEB") or "").lower()]
|
|
113
|
+
return {"total": data.get("@odata.count"), "count": len(rows), "rows": rows,
|
|
114
|
+
"params": {"filter": f, "top": tp, "skip": sk, "search": q or None}, "_meta": r}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def indicator_values_raw(entity_set, *, filter=None, top=500, skip=0,
|
|
118
|
+
cache=None, no_cache=False):
|
|
119
|
+
"""-> {total, count, rows, params, _meta}; /DATA_/{entity_set}
|
|
120
|
+
entity_set = the WHO entity set (e.g. RELAY_TBPPM) — the last path segment of the
|
|
121
|
+
directory's IND_URI_API; filter = the raw upstream OData $filter."""
|
|
122
|
+
es = str(entity_set or "").strip().strip("/")
|
|
123
|
+
if not es:
|
|
124
|
+
raise SdkError("usage_error", provider="who_xmart",
|
|
125
|
+
detail="who_xmart_values needs the entity set (IND_URI_API's last path "
|
|
126
|
+
"segment, e.g. RELAY_TBPPM) — resolve it with who_xmart_indicators")
|
|
127
|
+
tp = int(top or 500)
|
|
128
|
+
sk = int(skip or 0)
|
|
129
|
+
if tp < 1 or tp > 5000:
|
|
130
|
+
raise SdkError("usage_error", provider="who_xmart", detail="$top must be 1-5000")
|
|
131
|
+
params = {"$top": tp, "$skip": sk, "$count": "true"}
|
|
132
|
+
f = str(filter).strip() if filter else None
|
|
133
|
+
if f:
|
|
134
|
+
params["$filter"] = f
|
|
135
|
+
data, r = _odata("indicator_values", dict(params, entity_set=es), cache=cache, no_cache=no_cache)
|
|
136
|
+
rows = [_pick(x, _VAL_COLS) for x in data.get("value") or []]
|
|
137
|
+
return {"total": data.get("@odata.count"), "count": len(rows), "rows": rows,
|
|
138
|
+
"params": {"entity_set": es, "filter": f, "top": tp, "skip": sk}, "_meta": r}
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def geo_reference_raw(*, filter=None, top=100, skip=0, cache=None, no_cache=False):
|
|
142
|
+
"""-> {total, count, rows, params, _meta}; /REF_GEO_WIDE
|
|
143
|
+
filter = the raw upstream OData $filter (e.g. "GEO_CODE_ISO_3 eq 'USA' and TERM_LANG eq 'en'")."""
|
|
144
|
+
f = str(filter).strip() if filter else None
|
|
145
|
+
tp = int(top or 100)
|
|
146
|
+
sk = int(skip or 0)
|
|
147
|
+
params = {"$top": tp, "$skip": sk, "$count": "true"}
|
|
148
|
+
if f:
|
|
149
|
+
params["$filter"] = f
|
|
150
|
+
data, r = _odata("geo_reference", params, cache=cache, no_cache=no_cache)
|
|
151
|
+
rows = [_pick(x, _GEO_COLS) for x in data.get("value") or []]
|
|
152
|
+
return {"total": data.get("@odata.count"), "count": len(rows), "rows": rows,
|
|
153
|
+
"params": {"filter": f, "top": tp, "skip": sk}, "_meta": r}
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def m49_for_iso3(iso3, *, cache=None, no_cache=False):
|
|
157
|
+
"""ISO-3166 alpha-3 -> the M49 code used by DIM_GEO_CODE_M49.
|
|
158
|
+
-> (m49, rows, meta); the lookup is a real upstream call (the geography table is the
|
|
159
|
+
authoritative mapping) and its answer is cached for a week."""
|
|
160
|
+
code = str(iso3 or "").strip().upper()
|
|
161
|
+
if not code:
|
|
162
|
+
return None, [], None
|
|
163
|
+
f = f"GEO_CODE_ISO_3 eq '{code}' and TERM_LANG eq 'en'"
|
|
164
|
+
res = geo_reference_raw(filter=f, top=5, cache=cache, no_cache=no_cache)
|
|
165
|
+
for row in res["rows"]:
|
|
166
|
+
m49 = row.get("GEO_CODE_M49")
|
|
167
|
+
if m49:
|
|
168
|
+
return str(m49), res["rows"], res["_meta"]
|
|
169
|
+
return None, res["rows"], res["_meta"]
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def entity_sets_for_indicator(code, *, cache=None, no_cache=False):
|
|
173
|
+
"""The directory rows of one indicator code -> the candidate entity sets.
|
|
174
|
+
-> (rows, meta). The candidate list is never auto-picked."""
|
|
175
|
+
f = f"IND_NAME eq '{code}'"
|
|
176
|
+
res = indicator_directory_raw(filter=f, top=50, cache=cache, no_cache=no_cache)
|
|
177
|
+
return res["rows"], res["_meta"]
|
agentdata/entries.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""agentdata.entries — 0.2.0 per-domain executable entry points (console_scripts healthcare/company/esg/legal;
|
|
2
|
+
research/security were born domains but missed registration in 0.2.2, fixed in 0.2.4).
|
|
3
|
+
|
|
4
|
+
Naming ruling (2026-10-23): the agentdata prefix on the call face is redundant — each domain gets its own
|
|
5
|
+
executable name (`healthcare fda_drug_shortages …`); the pip package name stays agentdata (the
|
|
6
|
+
`agentdata <domain> …` compatibility alias remains). argv is spliced straight into cli.main, so parsing,
|
|
7
|
+
exit codes and global flags are identical to the agentdata main entry.
|
|
8
|
+
"""
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
from .cli import main
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _domain_entry(domain: str):
|
|
15
|
+
def run() -> int:
|
|
16
|
+
return main([domain, *sys.argv[1:]])
|
|
17
|
+
run.__name__ = domain
|
|
18
|
+
run.__doc__ = f"agentdata {domain} — per-domain executable (args identical to `agentdata {domain} …`)"
|
|
19
|
+
return run
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
healthcare = _domain_entry("healthcare")
|
|
23
|
+
company = _domain_entry("company")
|
|
24
|
+
economy = _domain_entry("economy")
|
|
25
|
+
esg = _domain_entry("esg")
|
|
26
|
+
legal = _domain_entry("legal")
|
|
27
|
+
procurement = _domain_entry("procurement") # 0.3.0: the seventh domain
|
|
28
|
+
research = _domain_entry("research") # added in 0.2.4 (formerly named grants — the alias was removed in 0.3.0; research is the only entry)
|
|
29
|
+
security = _domain_entry("security") # added in 0.2.4 (born domain missed registration in 0.2.2)
|
|
30
|
+
|
agentdata/errors.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""agentdata.errors — the closed error set (docs/domains/api.md sections 8.3/9.4; CLI exit codes 0/1/2).
|
|
2
|
+
|
|
3
|
+
- Upstream/availability family (exit 1): upstream_unavailable / upstream_rate_limited / upstream_not_found /
|
|
4
|
+
thirdparty_upstream_unavailable / thirdparty_rate_limited / capability_unavailable / rate_limited
|
|
5
|
+
- Usage family (exit 2): usage_error / op_unknown / resolve_kind_unknown /
|
|
6
|
+
corpus-contract 4xx (undeclared_field / bad_value / unauthenticated / forbidden …) /
|
|
7
|
+
insufficient_credits (RFC-001 §3.6 — 网关 402 余额不足,携带升级提示)
|
|
8
|
+
New entries require adversarial review (the same closed-set discipline as the Node side).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SdkError(Exception):
|
|
13
|
+
def __init__(self, code, provider=None, status=None, detail=None, retry_after=None):
|
|
14
|
+
super().__init__(detail or code)
|
|
15
|
+
self.code = code
|
|
16
|
+
self.provider = provider
|
|
17
|
+
self.status = status
|
|
18
|
+
self.detail = detail or code
|
|
19
|
+
self.retry_after = retry_after
|
|
20
|
+
|
|
21
|
+
# —— exit-code families (contract: upstream/availability family 1, usage family 2) ——
|
|
22
|
+
EXIT1 = {
|
|
23
|
+
"upstream_unavailable", "upstream_rate_limited", "upstream_not_found",
|
|
24
|
+
"thirdparty_upstream_unavailable", "thirdparty_rate_limited",
|
|
25
|
+
"capability_unavailable", "rate_limited",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def exit_code(self) -> int:
|
|
30
|
+
return 1 if self.code in self.EXIT1 else 2
|
|
31
|
+
|
|
32
|
+
def to_dict(self):
|
|
33
|
+
err = {"code": self.code, "message": self.detail}
|
|
34
|
+
if self.provider:
|
|
35
|
+
err["provider"] = self.provider
|
|
36
|
+
if self.retry_after:
|
|
37
|
+
err["retry_after"] = self.retry_after
|
|
38
|
+
return {"ok": False, "error": err}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
"""esg domain package — flat provider adapters (one file per source) + faces/cli (0.2.6 domain reorg).
|
|
2
|
+
|
|
3
|
+
One domain per package: provider adapters laid flat (one source per file) + faces/cli.
|
|
4
|
+
"""
|
|
5
|
+
from . import climatetrace, eiti, epa_frs, euasl, eutl, paris_mou, thetis_mrv # noqa: F401
|