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,573 @@
1
+ """economy faces — flat face-method mixin (the eighth domain, stood up in 0.3.23).
2
+
3
+ Mixed into the root _Domain (MRO assembly, see client.py); discipline: no __init__, depends only
4
+ on root internals (_flat_env/_c/_request…), method names globally unique across domains (a
5
+ collision would shadow another domain's face).
6
+
7
+ The economy domain is the country-level denominator layer: macro indicators, reference rates,
8
+ official statistics and the trade mirror — the numbers the other five domains normalise against
9
+ (TAM denominators, amount conversion, income-band buyer screens). The subject is the country
10
+ (ISO code), never the company: these faces emit no company fact and occupy no rec_type.
11
+ """
12
+ from ..errors import SdkError
13
+
14
+
15
+ from .. import economy # call via the domain-module path (historical shape, kept to minimize churn)
16
+
17
+ class EconomyFaces:
18
+ def _economy_only(self, tool: str):
19
+ if self.name != "economy":
20
+ raise SdkError("usage_error", detail=f"{tool} is an economy-domain capability (unavailable on the {self.name} domain)")
21
+
22
+ # ---------- World Bank WDI (global, keyless) ----------
23
+
24
+ def wb_wdi_values(self, *, country=None, indicator=None, date=None, per_page=100,
25
+ execution=None, no_cache=None):
26
+ """economy wb_wdi_values: World Bank WDI indicator values for one or more countries
27
+ (/v2/country/{country}/indicator/{indicator}; country = ISO3, several codes joined by
28
+ ';'; indicator = ONE WDI code — the upstream rejects several codes in one call
29
+ ("120 Invalid value"); date = "YYYY" or "YYYY:YYYY"; a missing observation is null)."""
30
+ self._economy_only("wb_wdi_values")
31
+ r = economy.wb.wdi_values_raw(country, indicator, date=date, per_page=per_page,
32
+ cache=self._c._cache,
33
+ no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
34
+ result = {k: x for k, x in r.items() if k != "_meta"}
35
+ top = (result["rows"] or [None])[0]
36
+ return self._flat_env(
37
+ tool="wb_wdi_values",
38
+ input={"country": country, "indicator": indicator, "date": date,
39
+ "per_page": r["params"]["per_page"]},
40
+ reason=(f"World Bank WDI: {r['params']['country']} × {r['params']['indicator']} — "
41
+ f"{result['count']} rows"
42
+ + (f" (total {result['total']})" if result.get("total") is not None else "")
43
+ + (f", last updated {result['last_updated']}" if result.get("last_updated") else "")
44
+ + (f", top {top['country_iso3']} {top['year']} = {top['value']}" if top else ", zero rows")),
45
+ result=result,
46
+ evidence=[{"quote": f"{x['country_iso3']} {x['year']} = {x['value']} ({x['indicator_name'] or x['indicator_code']})",
47
+ "url": f"https://data.worldbank.org/indicator/{x['indicator_code']}?locations={x['country_iso3']}"}
48
+ for x in result["rows"][:5]],
49
+ provider_id="wb", metas=[r["_meta"]],
50
+ query={"source": "raw", "upstream": r["params"]},
51
+ )
52
+
53
+ def wb_wdi_indicators(self, *, source=2, page=1, per_page=500, search=None,
54
+ execution=None, no_cache=None):
55
+ """economy wb_wdi_indicators: the World Bank indicator catalog (/v2/indicator;
56
+ source = the World Bank source id, 2 = WDI — without it the catalog is every source at
57
+ once (29,544 rows); search = a LOCAL keyword scan over the returned page, not an
58
+ upstream filter)."""
59
+ self._economy_only("wb_wdi_indicators")
60
+ r = economy.wb.wdi_indicators_raw(source=source, page=page, per_page=per_page,
61
+ search=search, cache=self._c._cache,
62
+ no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
63
+ result = {k: x for k, x in r.items() if k != "_meta"}
64
+ top = (result["rows"] or [None])[0]
65
+ return self._flat_env(
66
+ tool="wb_wdi_indicators",
67
+ input={"source": r["params"]["source"], "page": r["params"]["page"],
68
+ "per_page": r["params"]["per_page"], "search": search},
69
+ reason=(f"World Bank indicator catalog (source {r['params']['source']}) — "
70
+ f"{result['total']:,} indicators in total, {result['count']} on this page"
71
+ + (f", top {top['indicator_code']} = {top['name']}" if top else "")
72
+ + (" — the keyword filter is a local scan over this page" if search else "")),
73
+ result=result,
74
+ evidence=[{"quote": f"{x['indicator_code']} = {x['name']} ({x['source_name']})",
75
+ "url": f"https://data.worldbank.org/indicator/{x['indicator_code']}"}
76
+ for x in result["rows"][:5]],
77
+ provider_id="wb", metas=[r["_meta"]],
78
+ query={"source": "raw", "upstream": r["params"]},
79
+ )
80
+
81
+ def wb_countries(self, *, page=1, per_page=100, execution=None, no_cache=None):
82
+ """economy wb_countries: the World Bank country reference (/v2/country — ISO3 / ISO2 /
83
+ region / income level / lending type; the income band is the buyer-screen dimension
84
+ and the 296 entries include the aggregate regions)."""
85
+ self._economy_only("wb_countries")
86
+ r = economy.wb.countries_raw(page=page, per_page=per_page, cache=self._c._cache,
87
+ no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
88
+ result = {k: x for k, x in r.items() if k != "_meta"}
89
+ top = (result["rows"] or [None])[0]
90
+ return self._flat_env(
91
+ tool="wb_countries",
92
+ input={"page": r["params"]["page"], "per_page": r["params"]["per_page"]},
93
+ reason=(f"World Bank country reference — {result['total']} entries in total, "
94
+ f"{result['count']} on this page"
95
+ + (f", top {top['iso3']} {top['name']} ({top['income_level']})" if top else "")),
96
+ result=result,
97
+ evidence=[{"quote": f"{x['iso3']} {x['name']} ({x['income_level']}, {x['region_name']})",
98
+ "url": f"https://data.worldbank.org/country/{x['iso3']}"}
99
+ for x in result["rows"][:5]],
100
+ provider_id="wb", metas=[r["_meta"]],
101
+ query={"source": "raw", "upstream": r["params"]},
102
+ )
103
+
104
+ def wb_ids_indicators(self, *, page=1, per_page=600, search=None,
105
+ execution=None, no_cache=None):
106
+ """economy wb_ids_indicators: the World Bank International Debt Statistics catalog
107
+ (/v2/indicator with source pinned to 6 = IDS — 577 indicators in seven debt families:
108
+ DOD stocks / INT interest / AMT disbursements / DIS / NFL net flows / NTR / TDS service
109
+ plus the balance-of-payments mirrors; the per-page default 600 holds the whole catalog
110
+ in one page). The VALUES of the DT.* codes are served by wb_wdi_values WITHOUT a source
111
+ parameter — the value endpoint breaks with source=6 ("175 indicator was not found";
112
+ the API silently falls back to sourceid 2)."""
113
+ self._economy_only("wb_ids_indicators")
114
+ r = economy.wb.wdi_indicators_raw(source=6, page=page, per_page=per_page,
115
+ search=search, cache=self._c._cache,
116
+ no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
117
+ result = {k: x for k, x in r.items() if k != "_meta"}
118
+ top = (result["rows"] or [None])[0]
119
+ return self._flat_env(
120
+ tool="wb_ids_indicators",
121
+ input={"page": r["params"]["page"], "per_page": r["params"]["per_page"],
122
+ "search": search},
123
+ reason=(f"World Bank International Debt Statistics catalog (source 6) — "
124
+ f"{result['total']} indicators in total, {result['count']} on this page"
125
+ + (f", top {top['indicator_code']} = {top['name']}" if top else "")
126
+ + (" — the keyword filter is a local scan over this page" if search else "")
127
+ + " — the DT.* values are served by wb_wdi_values WITHOUT --source"),
128
+ result=result,
129
+ evidence=[{"quote": f"{x['indicator_code']} = {x['name']}",
130
+ "url": f"https://data.worldbank.org/indicator/{x['indicator_code']}"}
131
+ for x in result["rows"][:5]],
132
+ provider_id="wb", metas=[r["_meta"]],
133
+ query={"source": "raw", "upstream": r["params"]},
134
+ )
135
+
136
+ # ---------- Eurostat (EU) ----------
137
+
138
+ def eurostat_table(self, *, table=None, geo=None, time=None, dims=None, since=None,
139
+ until=None, lang="EN", execution=None, no_cache=None):
140
+ """economy eurostat_table: one Eurostat table through the filters you give
141
+ (/data/{table}; geo / time / dims = the upstream dimension filters — several values
142
+ are REPEATED parameters; dims = the table-specific dimension ids as k=v pairs;
143
+ since / until = the upstream sinceTimePeriod / untilTimePeriod window; the JSON-stat
144
+ grid is decoded row-major into flat rows)."""
145
+ self._economy_only("eurostat_table")
146
+ r = economy.eurostat.table_raw(table, geo=geo, time=time, dims=dims, since=since,
147
+ until=until, lang=lang, cache=self._c._cache,
148
+ no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
149
+ result = {k: x for k, x in r.items() if k != "_meta"}
150
+ top = (result["rows"] or [None])[0]
151
+ # The dimension dictionary carries every code -> label pair (tens of KB for a wide
152
+ # table), so the reason line names the dimension ids and their code counts, and the
153
+ # top cell spells out the dimension codes it sits at.
154
+ dims_shown = ", ".join(
155
+ f"{k} ({len(d.get('codes') or [])} code{'s' if len(d.get('codes') or []) != 1 else ''})"
156
+ for k, d in (result.get("dimensions") or {}).items() if k not in ("geo", "time"))
157
+ top_dims = ""
158
+ if top:
159
+ extra = " ".join(f"{k}={v}" for k, v in top.items()
160
+ if k not in ("geo", "time", "value", "status"))
161
+ top_dims = f" {extra}" if extra else ""
162
+ return self._flat_env(
163
+ tool="eurostat_table",
164
+ input={"table": table, "geo": geo, "time": time, "dims": dims,
165
+ "since": since, "until": until, "lang": lang},
166
+ reason=(f"Eurostat {result.get('label') or table} — {result['count']} cells"
167
+ + (f" (table holds {result['total']:,})" if result.get("total") else "")
168
+ + (f", updated {result['updated']}" if result.get("updated") else "")
169
+ + (f", dims {dims_shown}" if dims_shown else "")
170
+ + (f", top {top.get('geo', '—')} {top.get('time', '—')}{top_dims} = {top.get('value')}"
171
+ if top else ", zero cells")
172
+ + (" — a geo outside the table yields zero cells and NO upstream error" if not result["count"] else "")),
173
+ result=result,
174
+ evidence=[{"quote": " ".join(f"{k}={v}" for k, v in x.items() if k != "value") + f" = {x.get('value')}",
175
+ "url": f"https://ec.europa.eu/eurostat/databrowser/view/{table}/default/table?lang=en"}
176
+ for x in result["rows"][:5]],
177
+ provider_id="eurostat", metas=[r["_meta"]],
178
+ query={"source": "raw", "upstream": r["params"]},
179
+ )
180
+
181
+ # ---------- ECB reference rates (EU) ----------
182
+
183
+ def ecb_fx_rates(self, *, currency=None, start=None, end=None, execution=None, no_cache=None):
184
+ """economy ecb_fx_rates: the ECB euro reference rate for one currency
185
+ (/EXR/D.{CCY}.EUR.SP00.A, csvdata; the value is units of {currency} per 1 EUR; start /
186
+ end = the upstream startPeriod / endPeriod window; the historical eurofxref CSV is a
187
+ stale 2010 snapshot and is NOT used — this live SDMX endpoint is)."""
188
+ self._economy_only("ecb_fx_rates")
189
+ r = economy.ecb.fx_rates_raw(currency, start=start, end=end, cache=self._c._cache,
190
+ no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
191
+ result = {k: x for k, x in r.items() if k != "_meta"}
192
+ # the ECB rows are sorted ASCENDING by date — the latest observation is the LAST row
193
+ top = (result["rows"] or [None])[-1]
194
+ return self._flat_env(
195
+ tool="ecb_fx_rates",
196
+ input={"currency": r["params"]["currency"], "start": start, "end": end},
197
+ reason=(f"ECB reference rate {r['params']['currency']}/EUR — {result['count']} daily observations"
198
+ + (f" {r['params']['start']}→{r['params']['end']}" if r["params"].get("start") or r["params"].get("end") else "")
199
+ + (f", latest {top['date']} = {top['rate']}" if top else ", zero observations")),
200
+ result=result,
201
+ evidence=[{"quote": f"{x['date']}: 1 EUR = {x['rate']} {x['currency']}",
202
+ "url": "https://www.ecb.europa.eu/stats/policy_and_exchange_rates/euro_reference_exchange_rates/html/index.en.html"}
203
+ for x in result["rows"][:5]],
204
+ provider_id="ecb", metas=[r["_meta"]],
205
+ query={"source": "raw", "upstream": r["params"]},
206
+ )
207
+
208
+ # ---------- FRED (US) ----------
209
+
210
+ def fred_series(self, *, series=None, start=None, end=None, execution=None, no_cache=None):
211
+ """economy fred_series: one FRED series over a date window (/fredgraph.csv?id=;
212
+ ONE series id per call — several ids answer a ZIP archive, which the JSON transport
213
+ cannot carry; start / end = the upstream cosd / coed window; an unknown series id
214
+ answers an HTML error page and is reported as not-found)."""
215
+ self._economy_only("fred_series")
216
+ r = economy.fred.series_raw(series, start=start, end=end, cache=self._c._cache,
217
+ no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
218
+ result = {k: x for k, x in r.items() if k != "_meta"}
219
+ # the FRED rows are sorted ASCENDING by date — the latest observation is the LAST row
220
+ top = (result["rows"] or [None])[-1]
221
+ return self._flat_env(
222
+ tool="fred_series",
223
+ input={"series": r["params"]["series"], "start": start, "end": end},
224
+ reason=(f"FRED {r['params']['series']} — {result['count']} observations"
225
+ + (f", first {result['rows'][0]['date']}, latest {top['date']} = {top['value']}"
226
+ if top else ", zero observations")),
227
+ result=result,
228
+ evidence=[{"quote": f"{x['date']}: {x['value']} ({x['series']})",
229
+ "url": f"https://fred.stlouisfed.org/series/{x['series']}"}
230
+ for x in result["rows"][:5]],
231
+ provider_id="fred", metas=[r["_meta"]],
232
+ query={"source": "raw", "upstream": r["params"]},
233
+ )
234
+
235
+ # ---------- WHO xMart (global) ----------
236
+
237
+ def who_xmart_indicators(self, *, filter=None, top=500, skip=0, search=None,
238
+ execution=None, no_cache=None):
239
+ """economy who_xmart_indicators: the WHO xMart indicator directory
240
+ (IND_DIRECTORY_WIDE — 1,561 indicators, each row carrying IND_URI_API, the per-indicator
241
+ API diameter, and IND_URI_WEB; filter = the raw upstream OData $filter; search = a LOCAL
242
+ keyword scan over the returned page)."""
243
+ self._economy_only("who_xmart_indicators")
244
+ r = economy.who_xmart.indicator_directory_raw(filter=filter, top=top, skip=skip,
245
+ search=search, cache=self._c._cache,
246
+ no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
247
+ result = {k: x for k, x in r.items() if k != "_meta"}
248
+ top = (result["rows"] or [None])[0]
249
+ filtered = bool(r["params"].get("filter"))
250
+ head = (f"WHO xMart indicator directory — {result['total']} indicators "
251
+ + ("match the upstream $filter" if filtered else "in the catalog"))
252
+ return self._flat_env(
253
+ tool="who_xmart_indicators",
254
+ input={"filter": r["params"]["filter"], "top": r["params"]["top"],
255
+ "skip": r["params"]["skip"], "search": search},
256
+ reason=(f"{head}, {result['count']} on this page"
257
+ + (f", top {top['IND_NAME']} — {top.get('IND_NAME_FULL') or top.get('IND_UNIT')}" if top else "")
258
+ + (" — the keyword filter is a local scan over this page" if search else "")),
259
+ result=result,
260
+ evidence=[{"quote": f"{x['IND_NAME']} — {x.get('IND_NAME_FULL') or x.get('IND_UNIT') or ''}".strip(),
261
+ "url": x.get("IND_URI_WEB") or f"https://xmart-api-public.who.int/DATA_/IND_DIRECTORY_WIDE"}
262
+ for x in result["rows"][:5]],
263
+ provider_id="who_xmart", metas=[r["_meta"]],
264
+ query={"source": "raw", "upstream": r["params"]},
265
+ )
266
+
267
+ def who_xmart_values(self, *, indicator=None, geo=None, year=None, entity_set=None,
268
+ top=500, skip=0, execution=None, no_cache=None):
269
+ """economy who_xmart_values: the observations of one WHO indicator
270
+ (DATA_/{entity set}; the entity set comes from the directory row's IND_URI_API — when
271
+ several entity sets carry the same indicator name the candidate list is NOT auto-picked;
272
+ geo = an ISO-3 country (translated through REF_GEO_WIDE into the M49 code the value
273
+ tables use) or a raw M49 code; year = the DIM_TIME value (a string, so it is quoted)."""
274
+ self._economy_only("who_xmart_values")
275
+ code = str(indicator or "").strip()
276
+ if not code:
277
+ raise SdkError("usage_error", provider="who_xmart",
278
+ detail="who_xmart_values requires --indicator (the indicator code, "
279
+ "e.g. TB_ppm_enrol_tx_prct — list them with who_xmart_indicators)")
280
+ metas = []
281
+ es = str(entity_set or "").strip().strip("/")
282
+ if not es:
283
+ rows, dir_meta = economy.who_xmart.entity_sets_for_indicator(
284
+ code, cache=self._c._cache,
285
+ no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
286
+ metas.append(dir_meta)
287
+ if not rows:
288
+ raise SdkError("upstream_not_found", provider="who_xmart",
289
+ detail=f"No WHO xMart indicator named '{code}' — list the catalog "
290
+ "with who_xmart_indicators")
291
+ sets = []
292
+ for row in rows:
293
+ uri = row.get("IND_URI_API") or ""
294
+ name = uri.rstrip("/").split("/")[-1]
295
+ if name and name not in sets:
296
+ sets.append(name)
297
+ if len(sets) > 1:
298
+ raise SdkError("usage_error", provider="who_xmart",
299
+ detail=f"The indicator '{code}' is published in {len(sets)} entity "
300
+ f"sets ({', '.join(sets)}) — pick one explicitly with "
301
+ "--entity-set (the candidate list is never auto-picked)")
302
+ es = sets[0]
303
+ geo_value = None
304
+ geo_meta = None
305
+ if geo:
306
+ g = str(geo).strip()
307
+ if g.isdigit():
308
+ geo_value = g
309
+ else:
310
+ m49, geo_rows, geo_meta = economy.who_xmart.m49_for_iso3(
311
+ g, cache=self._c._cache,
312
+ no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
313
+ if not m49:
314
+ raise SdkError("upstream_not_found", provider="who_xmart",
315
+ detail=f"No WHO geography row for '{g}' (REF_GEO_WIDE has no "
316
+ "such ISO-3 code)")
317
+ geo_value = m49
318
+ odata = [f"IND_CODE eq '{code}'"]
319
+ if year:
320
+ odata.append(f"DIM_TIME eq '{str(year).strip()}'")
321
+ if geo_value:
322
+ odata.append(f"DIM_GEO_CODE_M49 eq '{geo_value}'")
323
+ r = economy.who_xmart.indicator_values_raw(es, filter=" and ".join(odata), top=top,
324
+ skip=skip, cache=self._c._cache,
325
+ no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
326
+ metas.append(r["_meta"])
327
+ if geo_meta:
328
+ metas.append(geo_meta)
329
+ result = {k: x for k, x in r.items() if k != "_meta"}
330
+ top_row = (result["rows"] or [None])[0]
331
+ return self._flat_env(
332
+ tool="who_xmart_values",
333
+ input={"indicator": code, "geo": geo, "year": year, "entity_set": es,
334
+ "top": r["params"]["top"], "skip": r["params"]["skip"]},
335
+ reason=(f"WHO xMart {code} (entity set {es}) — {result['count']} observations"
336
+ + (f", geo {geo_value}" if geo_value else "")
337
+ + (f", {year}" if year else "")
338
+ + (f", top {top_row['DIM_TIME']} {top_row['DIM_GEO_CODE_M49']} = {top_row['VALUE_NUMERIC']}"
339
+ if top_row else ", zero observations")),
340
+ result=result,
341
+ evidence=[{"quote": f"{x.get('DIM_TIME')} {x.get('DIM_GEO_CODE_M49')} {x.get('DIM_MEMBER_1_CODE') or ''} = {x.get('VALUE_NUMERIC')}".strip(),
342
+ "url": f"https://xmart-api-public.who.int/DATA_/{es}"}
343
+ for x in result["rows"][:5]],
344
+ provider_id="who_xmart", metas=metas,
345
+ query={"source": "raw", "upstream": r["params"]},
346
+ )
347
+
348
+ # ---------- UN Comtrade (global) ----------
349
+
350
+ def un_comtrade_trade(self, *, reporter=None, period=None, partner=None, cmd=None,
351
+ flow=None, type_code="C", freq_code="A", cl_code="HS",
352
+ max_records=500, partner2=None, customs=None, mot=None,
353
+ execution=None, no_cache=None):
354
+ """economy un_comtrade_trade: the UN Comtrade trade mirror (public preview API;
355
+ reporter / partner = an ISO-3 code or the numeric area code — the ISO3 is translated
356
+ through the area reference, whose historical rows and duplicate ISO3 entries make a
357
+ naive map wrong (USA is 842, not 840 or 841); period = 2023 / 202301; cmd = the
358
+ commodity code (a comma list, TOTAL = all goods); flow = X / M / "X,M")."""
359
+ self._economy_only("un_comtrade_trade")
360
+ r = economy.un_comtrade.trade_raw(reporter, period=period, partner=partner, cmd=cmd,
361
+ flow=flow, type_code=type_code, freq_code=freq_code,
362
+ cl_code=cl_code, max_records=max_records,
363
+ partner2=partner2, customs=customs, mot=mot,
364
+ cache=self._c._cache,
365
+ no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
366
+ result = {k: x for k, x in r.items() if k != "_meta"}
367
+ top = (result["rows"] or [None])[0]
368
+ return self._flat_env(
369
+ tool="un_comtrade_trade",
370
+ input={"reporter": reporter, "period": period, "partner": partner, "cmd": cmd,
371
+ "flow": flow, "type_code": type_code, "freq_code": freq_code,
372
+ "cl_code": cl_code, "max_records": max_records},
373
+ reason=(f"UN Comtrade {reporter} × {partner or 'World'} — {period}, "
374
+ f"cmd {cmd or 'TOTAL'}, flow {flow or 'all'}: {result['count']} rows"
375
+ + (f", top {top['flow_name']} {top['cmd_code']} = {top['primary_value']} USD"
376
+ if top else ", zero rows")),
377
+ result=result,
378
+ evidence=[{"quote": f"{x['period']} {x['reporter_iso3']} → {x['partner_iso3']} "
379
+ f"{x['flow_name']} {x['cmd_code']} = {x['primary_value']} USD",
380
+ "url": "https://comtradeplus.un.org/"}
381
+ for x in result["rows"][:5]],
382
+ provider_id="un_comtrade", metas=[r["_meta"]],
383
+ query={"source": "raw", "upstream": r["params"]},
384
+ )
385
+
386
+ # ---------- ILOSTAT (global) ----------
387
+
388
+ def ilo_stat_indicator(self, *, indicator=None, ref_area=None, time_from=None,
389
+ time_until=None, sex=None, classif=None, source=None,
390
+ execution=None, no_cache=None):
391
+ """economy ilo_stat_indicator: one ILOSTAT indicator for one country
392
+ (/data/indicator/; indicator = the ILOSTAT code; ref_area = the ISO-3 country — the
393
+ world-wide answer is orders of magnitude bigger, so the country is required;
394
+ time_from / time_until = the year bounds; sex / classif / source = the upstream
395
+ dimension filters)."""
396
+ self._economy_only("ilo_stat_indicator")
397
+ r = economy.ilo_stat.indicator_raw(indicator, ref_area=ref_area, time_from=time_from,
398
+ time_until=time_until, sex=sex, classif=classif,
399
+ source=source, cache=self._c._cache,
400
+ no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
401
+ result = {k: x for k, x in r.items() if k != "_meta"}
402
+ top = (result["rows"] or [None])[0] # the rows are sorted newest year first
403
+ top_dims = ""
404
+ if top:
405
+ bits = [str(top.get("sex") or ""), str(top.get("classif1") or "")]
406
+ top_dims = " " + " ".join(b for b in bits if b)
407
+ win = ""
408
+ if r["params"].get("time_from") and r["params"].get("time_until"):
409
+ win = f" {r['params']['time_from']}→{r['params']['time_until']}"
410
+ elif r["params"].get("time_from"):
411
+ win = f" from {r['params']['time_from']}"
412
+ elif r["params"].get("time_until"):
413
+ win = f" until {r['params']['time_until']}"
414
+ return self._flat_env(
415
+ tool="ilo_stat_indicator",
416
+ input={"indicator": r["params"]["indicator"], "ref_area": r["params"]["ref_area"],
417
+ "time_from": time_from, "time_until": time_until, "sex": sex,
418
+ "classif": classif, "source": source},
419
+ reason=(f"ILOSTAT {r['params']['indicator']} × {r['params']['ref_area']} — "
420
+ f"{result['count']} observations{win}"
421
+ + (f", latest {top['time']}{top_dims} = {top['obs_value']}" if top else ", zero observations")),
422
+ result=result,
423
+ evidence=[{"quote": f"{x['time']} {x.get('sex') or ''} {x.get('classif1') or ''} = {x['obs_value']}".strip(),
424
+ "url": f"https://rshiny.ilo.org/dataexplorer50/?lang=en&id={r['params']['indicator']}"}
425
+ for x in result["rows"][:5]],
426
+ provider_id="ilo_stat", metas=[r["_meta"]],
427
+ query={"source": "raw", "upstream": r["params"]},
428
+ )
429
+
430
+ # ---------- IMF SDMX 2.1 (global) ----------
431
+
432
+ def imf_sdmx_series(self, *, flow=None, country=None, indicator=None, frequency=None,
433
+ start=None, end=None, execution=None, no_cache=None):
434
+ """economy imf_sdmx_series: one IMF dataflow series (/external/sdmx/2.1/data/{flow}/
435
+ {COUNTRY.INDICATOR[.FREQUENCY]}, the SDMX 2.1 generic-CSV channel; flow = WEO / GDD /
436
+ CPI …; country AND indicator are both required — the country-only key is the
437
+ multi-megabyte wall; the key shape fits the WEO/GDD/CPI family, NOT UNFCCC whose
438
+ dimensions insert INDUSTRY/GAS_TYPE before the indicator; start / end = the upstream
439
+ startPeriod / endPeriod bounds; OBS_VALUE carries the true value — SCALE is only a
440
+ display hint)."""
441
+ self._economy_only("imf_sdmx_series")
442
+ r = economy.imf_sdmx.sdmx_series_raw(flow, country, indicator, frequency=frequency,
443
+ start=start, end=end, cache=self._c._cache,
444
+ no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
445
+ result = {k: x for k, x in r.items() if k != "_meta"}
446
+ # the rows are sorted ascending by period — the latest observation is the LAST row
447
+ top = (result["rows"] or [None])[-1]
448
+ win = (f" {r['params']['start']}→{r['params']['end']}"
449
+ if r["params"].get("start") and r["params"].get("end")
450
+ else (f" from {r['params']['start']}" if r["params"].get("start")
451
+ else (f" until {r['params']['end']}" if r["params"].get("end") else "")))
452
+ return self._flat_env(
453
+ tool="imf_sdmx_series",
454
+ input={"flow": r["params"]["flow"], "country": r["params"]["country"],
455
+ "indicator": r["params"]["indicator"], "frequency": r["params"]["frequency"],
456
+ "start": start, "end": end},
457
+ reason=(f"IMF SDMX {r['params']['flow']} {result['key']} — {result['count']} observations{win}"
458
+ + (f", first {result['rows'][0]['period']}, latest {top['period']} = {top['value']}"
459
+ if top else ", zero observations")
460
+ + (f" ({top['series_name']})" if top and top.get("series_name") else "")),
461
+ result=result,
462
+ evidence=[{"quote": f"{x['period']} {x['value']}"
463
+ + (f" ({', '.join(f'{k}={v}' for k, v in x['dims'].items())})" if x["dims"] else ""),
464
+ "url": f"https://api.imf.org/external/sdmx/2.1/data/{r['params']['flow']}/{result['key']}"}
465
+ for x in result["rows"][:5]],
466
+ provider_id="imf_sdmx", metas=[r["_meta"]],
467
+ query={"source": "raw", "upstream": r["params"]},
468
+ )
469
+
470
+ # ---------- BIS SDMX (global) ----------
471
+
472
+ def bis_sdmx_series(self, *, flow=None, key=None, start=None, end=None,
473
+ execution=None, no_cache=None):
474
+ """economy bis_sdmx_series: one BIS dataflow series (/api/v1/data/{flow}/{key}/all, the
475
+ SDMX 2.1 generic-CSV channel; key = the raw dot-separated series key with at least two
476
+ segments — the segment order is flow-specific: WS_CBPOL FREQ.REF_AREA (M.US), WS_DSR
477
+ FREQ.BORROWERS_CTY.DSR_BORROWERS (Q.US.N), WS_XRU FREQ.REF_AREA.CURRENCY.COLLECTION
478
+ (M.CN.CNY.E); a one-segment key is every area at once — the multi-megabyte wall; a
479
+ wrong key answers 404 with an SDMX error, it is not a block; start / end = the
480
+ upstream startPeriod / endPeriod bounds)."""
481
+ self._economy_only("bis_sdmx_series")
482
+ r = economy.bis.sdmx_series_raw(flow, key, start=start, end=end, cache=self._c._cache,
483
+ no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
484
+ result = {k: x for k, x in r.items() if k != "_meta"}
485
+ # the rows are sorted ascending by period — the latest observation is the LAST row
486
+ top = (result["rows"] or [None])[-1]
487
+ win = (f" {r['params']['start']}→{r['params']['end']}"
488
+ if r["params"].get("start") and r["params"].get("end")
489
+ else (f" from {r['params']['start']}" if r["params"].get("start")
490
+ else (f" until {r['params']['end']}" if r["params"].get("end") else "")))
491
+ return self._flat_env(
492
+ tool="bis_sdmx_series",
493
+ input={"flow": r["params"]["flow"], "key": r["params"]["key"],
494
+ "start": start, "end": end},
495
+ reason=(f"BIS {r['params']['flow']} {result['key']} — {result['count']} observations{win}"
496
+ + (f", first {result['rows'][0]['period']}, latest {top['period']} = {top['value']}"
497
+ if top else ", zero observations")
498
+ + (f" ({top['title']})" if top and top.get("title") else "")),
499
+ result=result,
500
+ evidence=[{"quote": f"{x['period']} {x['value']}"
501
+ + (f" ({', '.join(f'{k}={v}' for k, v in x['dims'].items())})" if x["dims"] else ""),
502
+ "url": f"https://stats.bis.org/api/v1/data/{r['params']['flow']}/{result['key']}/all"}
503
+ for x in result["rows"][:5]],
504
+ provider_id="bis", metas=[r["_meta"]],
505
+ query={"source": "raw", "upstream": r["params"]},
506
+ )
507
+
508
+ # ---------- UNSD SDG (global) ----------
509
+
510
+ def unsd_sdg_indicators(self, *, page=1, per_page=50, search=None,
511
+ execution=None, no_cache=None):
512
+ """economy unsd_sdg_indicators: the UN SDG indicator catalog (/Indicator/List — 251
513
+ indicators over the 17 goals, each row carrying its series codes (713 in total) nested
514
+ inside; the upstream answers the whole catalog in one payload — page / per_page slice
515
+ it locally and search is a LOCAL keyword scan over the whole catalog, never an
516
+ upstream filter)."""
517
+ self._economy_only("unsd_sdg_indicators")
518
+ r = economy.unsd_sdg.indicators_raw(page=page, per_page=per_page, search=search,
519
+ cache=self._c._cache,
520
+ no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
521
+ result = {k: x for k, x in r.items() if k != "_meta"}
522
+ top = (result["rows"] or [None])[0]
523
+ return self._flat_env(
524
+ tool="unsd_sdg_indicators",
525
+ input={"page": r["params"]["page"], "per_page": r["params"]["per_page"],
526
+ "search": search},
527
+ reason=(f"UNSD SDG indicator catalog — {result['total']} indicators "
528
+ + ("match the keyword" if search else "over the 17 goals")
529
+ + f", {result['count']} on this page"
530
+ + (f", top {top['code']} (goal {top['goal']}, {top['series_count']} series) = "
531
+ f"{(top['description'] or '')[:80]}" if top else "")),
532
+ result=result,
533
+ evidence=[{"quote": f"{x['code']} (goal {x['goal']}) — {(x['description'] or '')[:80]}"
534
+ + f" [{', '.join(x['series_codes'][:3])}{'…' if len(x['series_codes']) > 3 else ''}]",
535
+ "url": f"https://unstats.un.org/SDGAPI{x['uri']}" if x.get("uri")
536
+ else "https://unstats.un.org/SDGAPI/v1/sdg/Indicator/List"}
537
+ for x in result["rows"][:5]],
538
+ provider_id="unsd_sdg", metas=[r["_meta"]],
539
+ query={"source": "raw", "upstream": r["params"]},
540
+ )
541
+
542
+ def unsd_sdg_series(self, *, series=None, geo=None, time=None, page=1, page_size=100,
543
+ execution=None, no_cache=None):
544
+ """economy unsd_sdg_series: the observations of ONE SDG series (/Series/Data?
545
+ seriesCode=… — series granularity only: the whole-indicator endpoint is a 3.2M-row
546
+ wall that is never formed; geo = a LOCAL filter over the fetched page matching the
547
+ geoAreaCode or geoAreaName — the upstream `area` parameter is DECORATIVE,
548
+ byte-identical with and without it; time = the upstream timePeriod filter, the one
549
+ upstream filter that works on this endpoint)."""
550
+ self._economy_only("unsd_sdg_series")
551
+ r = economy.unsd_sdg.series_raw(series, geo=geo, time=time, page=page,
552
+ page_size=page_size, cache=self._c._cache,
553
+ no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
554
+ result = {k: x for k, x in r.items() if k != "_meta"}
555
+ top = (result["rows"] or [None])[0]
556
+ total_bits = (f" of {result['total']:,} total" if result.get("total") is not None else "")
557
+ return self._flat_env(
558
+ tool="unsd_sdg_series",
559
+ input={"series": r["params"]["series"], "geo": geo, "time": r["params"]["time"],
560
+ "page": r["params"]["page"], "page_size": r["params"]["page_size"]},
561
+ reason=(f"UNSD SDG series {r['params']['series']} — {result['count']} rows on this "
562
+ f"page{total_bits}"
563
+ + (f", geo {r['params']['geo']} (local filter)" if r["params"].get("geo") else "")
564
+ + (f", period {r['params']['time']}" if r["params"].get("time") else "")
565
+ + (f", top {top['geo_area_name']} {top['period']} = {top['value']}" if top
566
+ else ", zero rows on this page")),
567
+ result=result,
568
+ evidence=[{"quote": f"{x['geo_area_name']} ({x['geo_area_code']}) {x['period']} = {x['value']}",
569
+ "url": f"https://unstats.un.org/SDGAPI/v1/sdg/Series/Data?seriesCode={r['params']['series']}"}
570
+ for x in result["rows"][:5]],
571
+ provider_id="unsd_sdg", metas=[r["_meta"]],
572
+ query={"source": "raw", "upstream": r["params"]},
573
+ )