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,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}
@@ -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