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
agentdata/daterange.py ADDED
@@ -0,0 +1,35 @@
1
+ """daterange — local validation and end-filling for date ranges (shared by EDGAR FTS / USAspending).
2
+
3
+ Why it is needed: neither upstream errors on a "wrong" range, it just **silently returns
4
+ plausible-looking results** —
5
+
6
+ - EDGAR: a reversed range (startdt > enddt) returns 200 + total 0; an invalid date returns 200 + total:null.
7
+ - USAspending: a reversed range returns 200 + **data from outside the window** (2026-01-01~2025-01-01
8
+ measured to return contracts from 2012–2024).
9
+
10
+ Both answer with `ok:true`, indistinguishable from "there really is no data". Since the local side
11
+ fills in the missing end, it should also enforce validity locally first: whichever end is given
12
+ fills the other (the default comes from that source's first covered date / today), and an invalid
13
+ format or a reversed range is always a usage_error (exit 2).
14
+ """
15
+ from datetime import date
16
+
17
+ from .errors import SdkError
18
+
19
+
20
+ def resolve(startdt, enddt, *, default_start, provider, label="date range"):
21
+ """-> (start, end, {"startdt": start, "enddt": end}); an invalid format or a reversed range raises usage_error."""
22
+ s = str(startdt or default_start)
23
+ e = str(enddt or date.today().isoformat())
24
+ for name, v in (("startdt", s), ("enddt", e)):
25
+ try:
26
+ date.fromisoformat(v)
27
+ except ValueError:
28
+ raise SdkError("usage_error", provider=provider,
29
+ detail=f"{name} must be YYYY-MM-DD, got '{v}'"
30
+ f" (the upstream does not error on an invalid date, it just silently returns empty results)")
31
+ if s > e:
32
+ raise SdkError("usage_error", provider=provider,
33
+ detail=f"{label} start/end reversed: {s} > {e}"
34
+ f" (the upstream does not error — EDGAR returns 0 hits, USAspending returns out-of-window data, so we block it locally)")
35
+ return s, e, {"startdt": s, "enddt": e}
@@ -0,0 +1,10 @@
1
+ """economy domain package — flat provider adapters (one file per source) + faces/cli.
2
+
3
+ The eighth domain (0.3.23): the country-level denominator layer — macro indicators, reference
4
+ rates, official statistics and the trade mirror. The subject is the country (ISO code), never the
5
+ company: these faces emit no company fact and occupy no rec_type; they exist so the other five
6
+ domains can normalise against real numbers (TAM denominators, amount conversion, income-band
7
+ buyer screens).
8
+ """
9
+ from . import (bis, ecb, eurostat, fred, ilo_stat, imf_sdmx, # noqa: F401
10
+ un_comtrade, unsd_sdg, wb, who_xmart)
@@ -0,0 +1,132 @@
1
+ """bis — Bank for International Settlements SDMX (stats.bis.org/api/v1; keyless).
2
+
3
+ One face: sdmx_series — one of the 25 dataflows (WS_*) times one series key, over an optional
4
+ period window. The key dimensions differ per flow, so the key is passed through as the raw
5
+ dot-separated SDMX key (the imf_sdmx face hard-codes country×indicator; here the pass-through
6
+ is the honest shape):
7
+ WS_CBPOL FREQ.REF_AREA M.US policy rate, US, monthly
8
+ WS_DSR FREQ.BORROWERS_CTY.DSR_BORROWERS Q.US.N debt-service ratio, US non-financial
9
+ WS_XRU FREQ.REF_AREA.CURRENCY.COLLECTION M.CN.CNY.E USD per CNY, monthly, end-period
10
+
11
+ Channel decision: same SDMX 2.1 generic-CSV idiom as imf_sdmx/ecb (Accept
12
+ `application/vnd.sdmx.data+csv;version=1.0.0`; verified 2026-11-02: WS_CBPOL/M.US -> 200,
13
+ 241,844 B CSV from 1954-07, WS_XRU/M.CN.CNY.E?startPeriod=2026-06-01 honored).
14
+
15
+ Walls and traps (research chapter 89, 2026-11-02):
16
+ * a ONE-segment key (the frequency alone) means every area at once — the multi-megabyte
17
+ wall; the face requires at least two key segments.
18
+ * a wrong key is NOT a block: it answers HTTP 404 with an SDMX <message:Error> (code 100
19
+ "No results" inside) — carried as not-found by the op, never mistaken for an egress wall.
20
+ * the DSD id is NOT the flow-id prefix (WS_XRU's structure is BIS_XR — read from the
21
+ dataflow Structure Ref) and `references=children` answers empty codelists — neither DSD
22
+ path is needed here, the CSV columns carry the dimensions.
23
+
24
+ Rate: no published limit -> polite 0.5 rps / burst 2. TTL 24h (the WS_* family refreshes on
25
+ mixed monthly/quarterly cadences).
26
+ """
27
+ import csv
28
+ import io
29
+ import re
30
+
31
+ from ..errors import SdkError
32
+
33
+ provider = {
34
+ "id": "bis",
35
+ "baseUrl": "https://stats.bis.org/api/v1",
36
+ "rate": {"rps": 0.5, "burst": 2},
37
+ "ops": {
38
+ "sdmx_series": {"path": "/data/{flow}/{key}/all", "accept": "text", "notFound": [404]},
39
+ },
40
+ "ttl": {"sdmx_series": 24 * 3600},
41
+ }
42
+
43
+ _KEY_SEG_RE = re.compile(r"^[A-Z0-9_]+$")
44
+
45
+ _CSV_ACCEPT = {"Accept": "application/vnd.sdmx.data+csv;version=1.0.0"}
46
+
47
+
48
+ def _norm_flow(flow):
49
+ t = str(flow or "").strip().upper()
50
+ if not t or not _KEY_SEG_RE.match(t):
51
+ raise SdkError("usage_error", provider="bis",
52
+ detail=f"--flow must be an upper-case BIS dataflow id (e.g. WS_CBPOL, "
53
+ f"WS_DSR, WS_XRU), got {flow!r}")
54
+ return t
55
+
56
+
57
+ def _norm_key(key):
58
+ """The raw SDMX series key: >= 2 dot-separated segments (FREQ plus at least one dimension —
59
+ a one-segment key is every area at once, the multi-megabyte wall)."""
60
+ k = str(key or "").strip().upper().strip(".")
61
+ segs = k.split(".") if k else []
62
+ if len(segs) < 2 or any(not _KEY_SEG_RE.match(s) for s in segs):
63
+ raise SdkError("usage_error", provider="bis",
64
+ detail="--key must be the dot-separated SDMX series key with AT LEAST "
65
+ "two segments (e.g. M.US for WS_CBPOL, Q.US.N for WS_DSR, "
66
+ "M.CN.CNY.E for WS_XRU — a one-segment key is every area at "
67
+ "once, a multi-megabyte answer)")
68
+ return ".".join(segs)
69
+
70
+
71
+ def _dim_names(head):
72
+ names = []
73
+ for col in head:
74
+ if col in ("DATAFLOW",):
75
+ continue
76
+ if col == "TIME_PERIOD":
77
+ break
78
+ names.append(col)
79
+ return names
80
+
81
+
82
+ def _parse_csv(text):
83
+ lines = [ln for ln in (text or "").splitlines() if ln.strip()]
84
+ if not lines:
85
+ raise SdkError("upstream_unavailable", provider="bis",
86
+ detail="The BIS SDMX CSV answer was empty")
87
+ head = lines[0].split(",")
88
+ if "TIME_PERIOD" not in head or "OBS_VALUE" not in head:
89
+ raise SdkError("upstream_unavailable", provider="bis",
90
+ detail=f"The BIS SDMX answer is not the generic CSV table "
91
+ f"(head {lines[0][:120]!r})")
92
+ dims = _dim_names(head)
93
+ rows = []
94
+ for rec in csv.DictReader(io.StringIO("\n".join(lines))):
95
+ try:
96
+ value = float(rec.get("OBS_VALUE"))
97
+ except (TypeError, ValueError):
98
+ value = None
99
+ rows.append({"flow": (rec.get("DATAFLOW") or "").split(":")[-1].split("(")[0] or None,
100
+ "dims": {d: rec.get(d) for d in dims if rec.get(d) not in (None, "")},
101
+ "period": rec.get("TIME_PERIOD"),
102
+ "value": value,
103
+ "unit_mult": rec.get("UNIT_MULT") or None,
104
+ "decimals": rec.get("DECIMALS") or None,
105
+ "title": (rec.get("TITLE") or "").strip() or None,
106
+ "obs_status": rec.get("OBS_STATUS") or None})
107
+ return rows
108
+
109
+
110
+ def sdmx_series_raw(flow, key, *, start=None, end=None, cache=None, no_cache=False):
111
+ """-> {total, count, rows, params, _meta}; /data/{flow}/{key}/all
112
+ flow = the WS_* dataflow id; key = the raw dot-separated series key (>= 2 segments —
113
+ the segment order is flow-specific: WS_CBPOL FREQ.REF_AREA, WS_DSR
114
+ FREQ.BORROWERS_CTY.DSR_BORROWERS, WS_XRU FREQ.REF_AREA.CURRENCY.COLLECTION);
115
+ start / end = the upstream startPeriod / endPeriod bounds."""
116
+ from ..transport import call_provider
117
+
118
+ fl = _norm_flow(flow)
119
+ k = _norm_key(key)
120
+ s = str(start).strip() if start else None
121
+ e = str(end).strip() if end else None
122
+ params = {"flow": fl, "key": k}
123
+ if s:
124
+ params["startPeriod"] = s
125
+ if e:
126
+ params["endPeriod"] = e
127
+ r = call_provider(provider, "sdmx_series", params, cache=cache, no_cache=no_cache,
128
+ extra_headers=_CSV_ACCEPT)
129
+ rows = _parse_csv(r["data"])
130
+ rows.sort(key=lambda x: (x.get("period") or ""))
131
+ return {"total": len(rows), "count": len(rows), "rows": rows, "key": k,
132
+ "params": {"flow": fl, "key": k, "start": s, "end": e}, "_meta": r}
@@ -0,0 +1,199 @@
1
+ """economy cli — domain command-face registration (the eighth domain, 0.3.23; 0.3.24 expansion).
2
+
3
+ register(v, g): registers the flat commands onto the domain subparser (g = root _leaf_globals
4
+ factory, passed in as a parameter to avoid a cli <-> domain-cli cycle); FLAT = flat dispatch table,
5
+ merged by the root cli for main to use.
6
+
7
+ Face order = lexicographic over the English identifiers (the 0.3.12 contract guard): within the
8
+ domain the faces group by source automatically (bis / ecb / eurostat / fred / ilo_stat /
9
+ imf_sdmx / un_comtrade / unsd_sdg / wb / who_xmart).
10
+ """
11
+
12
+
13
+ def register(v, g):
14
+ f1 = v.add_parser("bis_sdmx_series", parents=[g()],
15
+ description="BIS SDMX series (realtime, local execution; /api/v1/data/{flow}/{key}/all, the SDMX 2.1 generic-CSV channel; key = the raw dot-separated series key with AT LEAST two segments — the segment order is flow-specific: WS_CBPOL FREQ.REF_AREA (M.US), WS_DSR FREQ.BORROWERS_CTY.DSR_BORROWERS (Q.US.N), WS_XRU FREQ.REF_AREA.CURRENCY.COLLECTION (M.CN.CNY.E); a one-segment key is every area at once — a multi-megabyte answer; a wrong key answers 404 with an SDMX error, which is not a block; --start / --end are the upstream startPeriod / endPeriod bounds)")
16
+ f1.add_argument("--flow", required=True, help="BIS dataflow id (e.g. WS_CBPOL, WS_DSR, WS_XRU)")
17
+ f1.add_argument("--key", required=True, help="SDMX series key, >= 2 dot-separated segments (e.g. M.US, Q.US.N, M.CN.CNY.E)")
18
+ f1.add_argument("--start", default=None, help="Period window start (upstream param: startPeriod)")
19
+ f1.add_argument("--end", default=None, help="Period window end (upstream param: endPeriod)")
20
+
21
+ f2 = v.add_parser("ecb_fx_rates", parents=[g()],
22
+ description="ECB euro reference rate for one currency (realtime, local execution; /EXR/D.{CCY}.EUR.SP00.A?format=csvdata; the value is units of {currency} per 1 EUR and the date travels with every row; --start / --end are the upstream startPeriod / endPeriod bounds and one of them is required; the historical eurofxref CSV is a stale 2010 snapshot and is not used)")
23
+ f2.add_argument("--currency", required=True, help="ISO 4217 currency code (e.g. USD, JPY, GBP)")
24
+ f2.add_argument("--start", default=None, help="Window start date (upstream param: startPeriod)")
25
+ f2.add_argument("--end", default=None, help="Window end date (upstream param: endPeriod)")
26
+
27
+ f3 = v.add_parser("eurostat_table", parents=[g()],
28
+ description="Eurostat table query (realtime, local execution; /data/{table}; geo / time / --dim k=v are the upstream dimension filters — SEVERAL values are REPEATED parameters (the '+' form is a 400 and the comma form silently yields zero cells); --since / --until are the upstream sinceTimePeriod / untilTimePeriod bounds; the JSON-stat grid is decoded into flat rows; a geo outside the table yields zero cells and no upstream error)")
29
+ f3.add_argument("--table", required=True, help="Eurostat table code (e.g. tec00001, demo_pjan, nama_10_a64)")
30
+ f3.add_argument("--geo", action="append", default=None, metavar="CODE",
31
+ help="Geopolitical entity filter (repeatable, e.g. DE; several codes become repeated upstream parameters)")
32
+ f3.add_argument("--time", action="append", default=None, metavar="PERIOD",
33
+ help="Time filter (repeatable, e.g. 2023)")
34
+ f3.add_argument("--dim", action="append", default=None, metavar="K=V",
35
+ help="Any other dimension filter as k=v (repeatable, e.g. age=TOTAL, sex=T, unit=NR)")
36
+ f3.add_argument("--since", default=None, help="Time window start (upstream param: sinceTimePeriod)")
37
+ f3.add_argument("--until", default=None, help="Time window end (upstream param: untilTimePeriod)")
38
+ f3.add_argument("--lang", default="EN", help="Upstream language (default EN)")
39
+
40
+ f4 = v.add_parser("fred_series", parents=[g()],
41
+ description="FRED series observations (realtime, local execution; /fredgraph.csv?id=; ONE series id per call — several ids answer a ZIP archive the JSON transport cannot carry; an unknown series id answers an HTML error page and is reported as not-found; a missing observation is null; --start / --end are the upstream cosd / coed bounds)")
42
+ f4.add_argument("--series", required=True, help="FRED series id (e.g. GDP, CPIAUCSL, DGS10)")
43
+ f4.add_argument("--start", default=None, help="Observation window start (upstream param: cosd)")
44
+ f4.add_argument("--end", default=None, help="Observation window end (upstream param: coed)")
45
+
46
+ f5 = v.add_parser("ilo_stat_indicator", parents=[g()],
47
+ description="ILOSTAT indicator for one country (realtime, local execution; /data/indicator/; the format flag is the query parameter format=json — a path suffix is a 400/404; --ref-area (ISO-3) is required — the world-wide answer is orders of magnitude bigger; --time-from / --time-until are the upstream timefrom / timeto bounds — the upstream spells the upper bound 'timeto' and silently ignores 'timeuntil'; the urllib channel is the working one on this host — the curl stack answers an empty 200)")
48
+ f5.add_argument("--indicator", required=True, help="ILOSTAT indicator code (e.g. EMP_TEMP_SEX_ECO_NB_A)")
49
+ f5.add_argument("--ref-area", dest="ref_area", required=True, help="Country ISO-3 code (e.g. USA)")
50
+ f5.add_argument("--time-from", dest="time_from", default=None, help="Year window start (upstream param: timefrom)")
51
+ f5.add_argument("--time-until", dest="time_until", default=None, help="Year window end (upstream param: timeto)")
52
+ f5.add_argument("--sex", default=None, help="Sex dimension filter (upstream param: sex)")
53
+ f5.add_argument("--classif", default=None, help="Classification dimension filter (upstream param: classif1)")
54
+ f5.add_argument("--source", default=None, help="Source filter (upstream param: source)")
55
+
56
+ f6 = v.add_parser("imf_sdmx_series", parents=[g()],
57
+ description="IMF SDMX series (realtime, local execution; /external/sdmx/2.1/data/{flow}/{COUNTRY.INDICATOR[.FREQUENCY]}, the SDMX 2.1 generic-CSV channel; --country AND --indicator are both required — the country-only key is the multi-megabyte wall; the key shape fits the WEO/GDD/CPI family, NOT UNFCCC whose dimensions insert INDUSTRY/GAS_TYPE before the indicator; --start / --end are the upstream startPeriod / endPeriod bounds; OBS_VALUE carries the true value — SCALE is only a display hint)")
58
+ f6.add_argument("--flow", required=True, help="IMF dataflow id (e.g. WEO, GDD, CPI)")
59
+ f6.add_argument("--country", required=True, help="Country code as the dataflow spells it (ISO-3 in WEO/GDD, e.g. USA)")
60
+ f6.add_argument("--indicator", required=True, help="Indicator code (e.g. NGDP_RPCH = real GDP growth)")
61
+ f6.add_argument("--frequency", default=None, help="Optional trailing key dimension (e.g. A)")
62
+ f6.add_argument("--start", default=None, help="Period window start (upstream param: startPeriod)")
63
+ f6.add_argument("--end", default=None, help="Period window end (upstream param: endPeriod)")
64
+
65
+ f7 = v.add_parser("un_comtrade_trade", parents=[g()],
66
+ description="UN Comtrade trade mirror, public preview (realtime, local execution; /public/v1/preview/{type}/{freq}/{cl}; --reporter / --partner take an ISO-3 code or the numeric area code — the ISO3 is translated through the area reference, whose historical and duplicate rows make a naive map wrong (USA = 842, not 840 or 841); --period 2023 / 202301; --cmd is the commodity code (comma list, TOTAL = all goods); --flow X / M / 'X,M'; the preview answers at most 500 records)")
67
+ f7.add_argument("--reporter", required=True, help="Reporting country: ISO-3 (e.g. USA) or the numeric area code")
68
+ f7.add_argument("--period", required=True, help="Period (e.g. 2023 or 202301)")
69
+ f7.add_argument("--partner", default=None, help="Partner area: ISO-3 or numeric code (0 / W00 = the World aggregate)")
70
+ f7.add_argument("--cmd", default=None, help="Commodity code, comma list allowed (default TOTAL = all goods)")
71
+ f7.add_argument("--flow", default=None, help="Trade flow: X (export), M (import), 'X,M'")
72
+ f7.add_argument("--type-code", dest="type_code", default="C", help="Path segment: commodity type (C)")
73
+ f7.add_argument("--freq-code", dest="freq_code", default="A", help="Path segment: frequency (A = annual)")
74
+ f7.add_argument("--cl-code", dest="cl_code", default="HS", help="Path segment: classification (HS)")
75
+ f7.add_argument("--max-records", dest="max_records", type=int, default=500,
76
+ help="Records per call (upstream param: maxRecords; 1-500)")
77
+ f7.add_argument("--partner2", default=None, help="Second partner area (upstream param: partner2Code)")
78
+ f7.add_argument("--customs", default=None, help="Customs regime (upstream param: customsCode)")
79
+ f7.add_argument("--mot", default=None, help="Mode of transport (upstream param: motCode)")
80
+
81
+ f8 = v.add_parser("unsd_sdg_indicators", parents=[g()],
82
+ description="UNSD SDG indicator catalog (realtime, local execution; /Indicator/List — 251 indicators over the 17 goals, each row carrying its series codes (713 in total) nested inside; the upstream answers the whole catalog in one payload — --page / --per-page slice it locally and --search is a LOCAL keyword scan over the whole catalog, never an upstream filter)")
83
+ f8.add_argument("--page", type=int, default=1, help="Local page slice (the upstream payload is whole)")
84
+ f8.add_argument("--per-page", dest="per_page", type=int, default=50, help="Local page size (1-500)")
85
+ f8.add_argument("--search", default=None, help="Local keyword scan (code / description / series codes)")
86
+
87
+ f9 = v.add_parser("unsd_sdg_series", parents=[g()],
88
+ description="UNSD SDG series observations (realtime, local execution; /Series/Data?seriesCode= — series granularity only: the whole-indicator endpoint is a 3.2M-row wall that is never formed; --geo is a LOCAL filter over the fetched page matching the geoAreaCode or geoAreaName — the upstream 'area' parameter is DECORATIVE, byte-identical with and without it; --time is the upstream timePeriod filter, the one upstream filter that works on this endpoint)")
89
+ f9.add_argument("--series", required=True, help="SDG series code (e.g. SI_POV_DAY1, DC_ODA_POVLG — list them with unsd_sdg_indicators)")
90
+ f9.add_argument("--geo", default=None, help="Local geo filter: geoAreaCode or geoAreaName, case-insensitive (e.g. 842, China)")
91
+ f9.add_argument("--time", default=None, help="Period filter (upstream param: timePeriod, e.g. 2020)")
92
+ f9.add_argument("--page", type=int, default=1, help="Page number (upstream param: page)")
93
+ f9.add_argument("--page-size", dest="page_size", type=int, default=100,
94
+ help="Rows per page (upstream param: pageSize; 1-1000)")
95
+
96
+ f10 = v.add_parser("wb_countries", parents=[g()],
97
+ description="World Bank country reference (realtime, local execution; /v2/country; ISO3 / ISO2 / region / income level / lending type / coordinates — the income band is the buyer-screen dimension; the 296 entries include the aggregate regions)")
98
+ f10.add_argument("--page", type=int, default=1, help="Page number (upstream param: page)")
99
+ f10.add_argument("--per-page", dest="per_page", type=int, default=100,
100
+ help="Rows per page (upstream param: per_page; 1-1000)")
101
+
102
+ f11 = v.add_parser("wb_ids_indicators", parents=[g()],
103
+ description="World Bank International Debt Statistics catalog (realtime, local execution; /v2/indicator with source pinned to 6 = IDS — 577 indicators in seven debt families: DOD stocks / INT interest / AMT disbursements / DIS / NFL net flows / NTR / TDS service plus the balance-of-payments mirrors; the per-page default 600 holds the whole catalog in one page; the VALUES of the DT.* codes are served by wb_wdi_values WITHOUT a source parameter — the value endpoint breaks with source=6 and the API silently falls back to sourceid 2)")
104
+ f11.add_argument("--page", type=int, default=1, help="Page number (upstream param: page)")
105
+ f11.add_argument("--per-page", dest="per_page", type=int, default=600,
106
+ help="Rows per page (upstream param: per_page; 1-1000; 600 = the whole catalog)")
107
+ f11.add_argument("--search", default=None, help="Local keyword scan over the page (code / name / organization)")
108
+
109
+ f12 = v.add_parser("wb_wdi_indicators", parents=[g()],
110
+ description="World Bank indicator catalog (realtime, local execution; /v2/indicator?source=; source 2 = WDI — without it the catalog is every World Bank source at once (29,544 rows); --search is a LOCAL keyword scan over the returned page, not an upstream filter)")
111
+ f12.add_argument("--source", default=2, help="World Bank source id (2 = WDI, the default)")
112
+ f12.add_argument("--page", type=int, default=1, help="Page number (upstream param: page)")
113
+ f12.add_argument("--per-page", dest="per_page", type=int, default=500,
114
+ help="Rows per page (upstream param: per_page; 1-1000)")
115
+ f12.add_argument("--search", default=None, help="Local keyword scan over the page (code / name / organization)")
116
+
117
+ f13 = v.add_parser("wb_wdi_values", parents=[g()],
118
+ description="World Bank WDI indicator values (realtime, local execution; /v2/country/{country}/indicator/{indicator}; country = ISO3, several codes joined by ';'; ONE indicator code per call — the upstream rejects several codes in one call (\"120 Invalid value\"); date = YYYY or YYYY:YYYY; a missing observation is null, never fabricated)")
119
+ f13.add_argument("--country", required=True, help="ISO3 country code (e.g. CHN; several codes may be joined by ';')")
120
+ f13.add_argument("--indicator", required=True, help="WDI indicator code (e.g. NY.GDP.MKTP.CD, or a DT.* IDS code — WITHOUT --source)")
121
+ f13.add_argument("--date", default=None, help="Year or year window (upstream param: date; 'YYYY' or 'YYYY:YYYY')")
122
+ f13.add_argument("--per-page", dest="per_page", type=int, default=100,
123
+ help="Rows per page (upstream param: per_page; 1-1000)")
124
+
125
+ f14 = v.add_parser("who_xmart_indicators", parents=[g()],
126
+ description="WHO xMart indicator directory (realtime, local execution; IND_DIRECTORY_WIDE — 1,561 indicators, each row carrying IND_URI_API (the per-indicator API diameter) and IND_URI_WEB; --filter is the raw upstream OData $filter; --search is a LOCAL keyword scan over the returned page)")
127
+ f14.add_argument("--filter", default=None, help="Raw upstream OData $filter (e.g. \"IND_NAME eq 'TB_ppm_enrol_tx_prct'\")")
128
+ f14.add_argument("--top", type=int, default=500, help="Page size (upstream param: $top; 1-5000)")
129
+ f14.add_argument("--skip", type=int, default=0, help="Offset (upstream param: $skip)")
130
+ f14.add_argument("--search", default=None, help="Local keyword scan over the page (name / unit / web URI)")
131
+
132
+ f15 = v.add_parser("who_xmart_values", parents=[g()],
133
+ description="WHO xMart indicator observations (realtime, local execution; DATA_/{entity set}; the entity set is resolved from the directory row's IND_URI_API — when several entity sets carry the same indicator name the candidate list is NOT auto-picked, pass --entity-set; --geo takes an ISO-3 country (translated through REF_GEO_WIDE into the M49 code) or a raw M49 code; --year is the DIM_TIME string value)")
134
+ f15.add_argument("--indicator", required=True, help="Indicator code (e.g. TB_ppm_enrol_tx_prct)")
135
+ f15.add_argument("--geo", default=None, help="Country: ISO-3 (e.g. PAK) or the raw M49 code")
136
+ f15.add_argument("--year", default=None, help="Observation year (DIM_TIME, a string value)")
137
+ f15.add_argument("--entity-set", dest="entity_set", default=None,
138
+ help="WHO entity set (e.g. RELAY_TBPPM) — resolved from the directory when omitted")
139
+ f15.add_argument("--top", type=int, default=500, help="Page size (upstream param: $top; 1-5000)")
140
+ f15.add_argument("--skip", type=int, default=0, help="Offset (upstream param: $skip)")
141
+
142
+
143
+ FLAT = {
144
+ # economy (0.3.23 + 0.3.24): the country-level denominator layer — macro indicators,
145
+ # reference rates, official statistics and the trade mirror (upstream param names verbatim
146
+ # + our defaults; lambdas only shuttle names across namespaces)
147
+ "bis_sdmx_series": lambda a: {"flow": a.flow, "key": a.key, "start": a.start, "end": a.end},
148
+ "ecb_fx_rates": lambda a: {"currency": a.currency, "start": a.start, "end": a.end},
149
+ "eurostat_table": lambda a: {"table": a.table, "geo": a.geo, "time": a.time,
150
+ "dims": a.dim, "since": a.since, "until": a.until,
151
+ "lang": a.lang},
152
+ "fred_series": lambda a: {"series": a.series, "start": a.start, "end": a.end},
153
+ "ilo_stat_indicator": lambda a: {"indicator": a.indicator, "ref_area": a.ref_area,
154
+ "time_from": a.time_from, "time_until": a.time_until,
155
+ "sex": a.sex, "classif": a.classif, "source": a.source},
156
+ "imf_sdmx_series": lambda a: {"flow": a.flow, "country": a.country,
157
+ "indicator": a.indicator, "frequency": a.frequency,
158
+ "start": a.start, "end": a.end},
159
+ "un_comtrade_trade": lambda a: {"reporter": a.reporter, "period": a.period,
160
+ "partner": a.partner, "cmd": a.cmd, "flow": a.flow,
161
+ "type_code": a.type_code, "freq_code": a.freq_code,
162
+ "cl_code": a.cl_code, "max_records": a.max_records,
163
+ "partner2": a.partner2, "customs": a.customs, "mot": a.mot},
164
+ "unsd_sdg_indicators": lambda a: {"page": a.page, "per_page": a.per_page, "search": a.search},
165
+ "unsd_sdg_series": lambda a: {"series": a.series, "geo": a.geo, "time": a.time,
166
+ "page": a.page, "page_size": a.page_size},
167
+ "wb_countries": lambda a: {"page": a.page, "per_page": a.per_page},
168
+ "wb_ids_indicators": lambda a: {"page": a.page, "per_page": a.per_page, "search": a.search},
169
+ "wb_wdi_indicators": lambda a: {"source": a.source, "page": a.page,
170
+ "per_page": a.per_page, "search": a.search},
171
+ "wb_wdi_values": lambda a: {"country": a.country, "indicator": a.indicator,
172
+ "date": a.date, "per_page": a.per_page},
173
+ "who_xmart_indicators": lambda a: {"filter": a.filter, "top": a.top, "skip": a.skip,
174
+ "search": a.search},
175
+ "who_xmart_values": lambda a: {"indicator": a.indicator, "geo": a.geo, "year": a.year,
176
+ "entity_set": a.entity_set, "top": a.top, "skip": a.skip},
177
+ }
178
+
179
+
180
+ # 0.3.10 jurisdiction chapter: face -> jurisdiction (default = the issuing authority's
181
+ # jurisdiction; institutions covering global phenomena = GLOBAL; vocabulary = ISO-3166 alpha-2
182
+ # + EU + GLOBAL) —— merged by the root cli.py into _JURISDICTION
183
+ COUNTRY = {
184
+ "bis_sdmx_series": "GLOBAL",
185
+ "ecb_fx_rates": "EU",
186
+ "eurostat_table": "EU",
187
+ "fred_series": "US",
188
+ "ilo_stat_indicator": "GLOBAL",
189
+ "imf_sdmx_series": "GLOBAL",
190
+ "un_comtrade_trade": "GLOBAL",
191
+ "unsd_sdg_indicators": "GLOBAL",
192
+ "unsd_sdg_series": "GLOBAL",
193
+ "wb_countries": "GLOBAL",
194
+ "wb_ids_indicators": "GLOBAL",
195
+ "wb_wdi_indicators": "GLOBAL",
196
+ "wb_wdi_values": "GLOBAL",
197
+ "who_xmart_indicators": "GLOBAL",
198
+ "who_xmart_values": "GLOBAL",
199
+ }
@@ -0,0 +1,117 @@
1
+ """ecb — European Central Bank reference exchange rates (data-api.ecb.europa.eu; keyless).
2
+
3
+ One face: fx_rates (one currency against the euro, over a date window).
4
+
5
+ Channel decision (this is the important part):
6
+ * The data-api SDMX endpoint with `format=csvdata` answers text/csv — it survives the
7
+ JSON-only transport untouched (verified 2026-11-02: HTTP 200, text/csv, 4,900 B for
8
+ USD 2026-10-01..2026-11-02, header KEY,FREQ,CURRENCY,…,TIME_PERIOD,OBS_VALUE,…).
9
+ BUT the SDMX endpoint negotiates on the Accept header FIRST and ignores `format=csvdata`
10
+ when the client asks for JSON: with the transport's default `Accept: application/json` it
11
+ answers 200 with an SDMX-JSON envelope (6,119 B, "header"/"dataSets") — a 2xx that looks
12
+ healthy and is not the csvdata contract. So the op sends `Accept: text/csv` explicitly
13
+ (the ca_sema XML-accept idiom); with that header the `format=csvdata` parameter is not
14
+ even needed, but it is kept as the fallback for a client that drops the header.
15
+ * The historical eurofxref-hist.zip is a ZIP (binary -> destroyed by the JSON-only
16
+ transport, the 0.3.22 binary-wall ruling) and its eurofxref-hist.csv sibling is a STALE
17
+ snapshot (2,801 rows ending 2010-02-14; the daily csv stops at 2017-07-14) — neither is
18
+ used here; the live SDMX endpoint is the only current series.
19
+ * The eurofxref-hist.xml is an SDMX Envelope (gesmes namespace + ecb vocabulary Cube with
20
+ time=/currency=/rate= attributes), not the simple <eurofxref><dailyrates> shape — a
21
+ different parser would be needed; the csvdata form avoids that entirely.
22
+
23
+ Series semantics: EXR.D.{CCY}.EUR.SP00.A = the daily euro reference rate, quoted as
24
+ units of {CCY} per 1 EUR (USD 1.1298 means 1 EUR = 1.1298 USD). The rate + its date travel
25
+ with every row (the amount-normalisation rule: the rate source and date are never dropped).
26
+ Rate: no official limit published -> polite tier 0.5 rps / burst 2. TTL: 12h (the ECB
27
+ publishes once a working day, ~16:15 CET).
28
+ """
29
+ import csv
30
+ import io
31
+
32
+ from ..errors import SdkError
33
+
34
+ provider = {
35
+ "id": "ecb",
36
+ "baseUrl": "https://data-api.ecb.europa.eu/service/data",
37
+ "rate": {"rps": 0.5, "burst": 2},
38
+ "ops": {
39
+ "fx_rates": {"path": "/EXR/D.{currency}.EUR.SP00.A", "accept": "text", "notFound": [404]},
40
+ },
41
+ "ttl": {"fx_rates": 12 * 3600},
42
+ }
43
+
44
+ _CURRENCY_RE = set("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
45
+
46
+ # The SDMX content negotiation answers the Accept header first (see the module docstring):
47
+ # application/json -> an SDMX-JSON envelope that is NOT the csvdata contract. Ask for CSV.
48
+ _CSV_ACCEPT = {"Accept": "text/csv"}
49
+
50
+
51
+ def _norm_currency(cur):
52
+ """The upstream key part is a 3-letter currency code (USD, JPY, GBP …)."""
53
+ c = str(cur or "").strip().upper()
54
+ if len(c) != 3 or any(ch not in _CURRENCY_RE for ch in c):
55
+ raise SdkError("usage_error", provider="ecb",
56
+ detail="ecb_fx_rates requires --currency as a 3-letter ISO 4217 code "
57
+ "(e.g. USD, JPY, GBP) — the upstream series key is EXR.D.{CCY}.EUR.SP00.A")
58
+ return c
59
+
60
+
61
+ def _parse_csv(text):
62
+ lines = [ln for ln in (text or "").splitlines() if ln.strip()]
63
+ if not lines:
64
+ raise SdkError("upstream_unavailable", provider="ecb", detail="The ECB CSV answer was empty")
65
+ head = lines[0].split(",")
66
+ if "TIME_PERIOD" not in head or "OBS_VALUE" not in head:
67
+ # not a csvdata answer (an error page, an SDMX XML answer, a redirect notice)
68
+ raise SdkError("upstream_unavailable", provider="ecb",
69
+ detail=f"The ECB answer is not a csvdata table (head {lines[0][:120]!r})")
70
+ rows = []
71
+ for rec in csv.DictReader(io.StringIO("\n".join(lines))):
72
+ rows.append(rec)
73
+ return rows
74
+
75
+
76
+ def fx_rates_raw(currency, *, start=None, end=None, cache=None, no_cache=False):
77
+ """-> {total, count, rows, params, _meta}; /EXR/D.{currency}.EUR.SP00.A?format=csvdata
78
+ currency = the 3-letter ISO 4217 code; start / end = the upstream startPeriod / endPeriod
79
+ (one of them is required — a bounded window; the full history is ~8,000 rows);
80
+ the value is units of {currency} per 1 EUR and the date travels with every row."""
81
+ from ..transport import call_provider
82
+
83
+ cur = _norm_currency(currency)
84
+ s = str(start).strip() if start else None
85
+ e = str(end).strip() if end else None
86
+ if not (s or e):
87
+ raise SdkError("usage_error", provider="ecb",
88
+ detail="ecb_fx_rates requires --start and/or --end (the upstream "
89
+ "startPeriod/endPeriod window; the full history is ~8,000 rows)")
90
+ params = {"currency": cur, "format": "csvdata"}
91
+ if s:
92
+ params["startPeriod"] = s
93
+ if e:
94
+ params["endPeriod"] = e
95
+ r = call_provider(provider, "fx_rates", params, cache=cache, no_cache=no_cache,
96
+ extra_headers=_CSV_ACCEPT)
97
+ recs = _parse_csv(r["data"])
98
+ rows = []
99
+ for rec in recs:
100
+ try:
101
+ rate = float(rec.get("OBS_VALUE"))
102
+ except (TypeError, ValueError):
103
+ rate = None
104
+ rows.append({
105
+ "currency": rec.get("CURRENCY") or cur,
106
+ "base_currency": rec.get("CURRENCY_DENOM") or "EUR",
107
+ "rate_type": rec.get("EXR_TYPE"),
108
+ "variation": rec.get("EXR_SUFFIX"),
109
+ "date": rec.get("TIME_PERIOD"),
110
+ "rate": rate,
111
+ "status": rec.get("OBS_STATUS") or None,
112
+ "title": rec.get("TITLE") or None,
113
+ })
114
+ rows.sort(key=lambda x: (x.get("date") or ""))
115
+ return {"total": len(rows), "count": len(rows), "rows": rows,
116
+ "unit": f"{cur} per 1 EUR (ECB reference rate)",
117
+ "params": {"currency": cur, "start": s, "end": e}, "_meta": r}