econdash 0.1.0__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.
econdash/__init__.py ADDED
@@ -0,0 +1,40 @@
1
+ """Python client for the EconDash macroeconomic data API (https://econdash.org)."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ from ._client import DEFAULT_BASE_URL, AsyncEconDash, EconDash # noqa: E402
6
+ from .errors import ( # noqa: E402
7
+ EconDashAuthError,
8
+ EconDashError,
9
+ EconDashNotFound,
10
+ EconDashPaymentRequired,
11
+ EconDashRateLimitError,
12
+ )
13
+ from .models import ( # noqa: E402
14
+ Citation,
15
+ Country,
16
+ Indicator,
17
+ Point,
18
+ Series,
19
+ TopCountries,
20
+ TopCountry,
21
+ )
22
+
23
+ __all__ = [
24
+ "DEFAULT_BASE_URL",
25
+ "AsyncEconDash",
26
+ "Citation",
27
+ "Country",
28
+ "EconDash",
29
+ "EconDashAuthError",
30
+ "EconDashError",
31
+ "EconDashNotFound",
32
+ "EconDashPaymentRequired",
33
+ "EconDashRateLimitError",
34
+ "Indicator",
35
+ "Point",
36
+ "Series",
37
+ "TopCountries",
38
+ "TopCountry",
39
+ "__version__",
40
+ ]
econdash/_client.py ADDED
@@ -0,0 +1,349 @@
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ import json
5
+ import os
6
+ from datetime import date
7
+ from typing import Any, TypeVar
8
+
9
+ import httpx
10
+ from pydantic import BaseModel, TypeAdapter
11
+
12
+ from . import __version__
13
+ from .errors import (
14
+ EconDashAuthError,
15
+ EconDashError,
16
+ EconDashNotFound,
17
+ EconDashPaymentRequired,
18
+ EconDashRateLimitError,
19
+ )
20
+ from .models import Citation, Country, Indicator, Series, TopCountries
21
+
22
+ DEFAULT_BASE_URL = "https://econdash.org/api/v1"
23
+ M = TypeVar("M", bound=BaseModel)
24
+ DateLike = str | int | date
25
+
26
+
27
+ def _date(value: DateLike | None, *, end: bool) -> str | None:
28
+ """'2000' -> 2000-01-01 (start) / 2000-12-31 (end); '2000-06' -> month bounds."""
29
+ if value is None:
30
+ return None
31
+ if isinstance(value, date):
32
+ return value.isoformat()
33
+ s = str(value)
34
+ if len(s) == 4 and s.isdigit():
35
+ return f"{s}-12-31" if end else f"{s}-01-01"
36
+ if len(s) == 7:
37
+ if not end:
38
+ return f"{s}-01"
39
+ y, m = int(s[:4]), int(s[5:])
40
+ nxt = date(y + m // 12, m % 12 + 1, 1)
41
+ return date.fromordinal(nxt.toordinal() - 1).isoformat()
42
+ return date.fromisoformat(s).isoformat()
43
+
44
+
45
+ def _is_code(indicator: str) -> bool:
46
+ # Codes look like "world_bank__gdp_nominal", slugs like "gdp-nominal".
47
+ return "__" in indicator
48
+
49
+
50
+ def _search(indicators: list[Indicator], query: str, limit: int) -> list[Indicator]:
51
+ words = query.lower().split()
52
+ hits = []
53
+ for ind in indicators:
54
+ hay = " ".join(
55
+ x or ""
56
+ for x in (
57
+ ind.name_en,
58
+ ind.name_ru,
59
+ ind.slug,
60
+ ind.code,
61
+ ind.category,
62
+ ind.source,
63
+ ind.description,
64
+ )
65
+ ).lower()
66
+ if all(w in hay for w in words):
67
+ hits.append(ind)
68
+ q = query.lower()
69
+ hits.sort(key=lambda i: (q not in (i.name_en or "").lower(), -i.data_points))
70
+ return hits[:limit]
71
+
72
+
73
+ def _payment_required(resp: httpx.Response, message: str) -> EconDashPaymentRequired:
74
+ challenge = None
75
+ raw = resp.headers.get("payment-required")
76
+ if raw:
77
+ try:
78
+ challenge = json.loads(base64.b64decode(raw))
79
+ except ValueError:
80
+ challenge = None
81
+ return EconDashPaymentRequired(
82
+ message,
83
+ challenge=challenge,
84
+ www_authenticate=resp.headers.get_list("www-authenticate"),
85
+ status_code=402,
86
+ body=_body(resp),
87
+ )
88
+
89
+
90
+ def _body(resp: httpx.Response) -> Any:
91
+ try:
92
+ return resp.json()
93
+ except ValueError:
94
+ return resp.text
95
+
96
+
97
+ def _check(resp: httpx.Response) -> Any:
98
+ if resp.is_success:
99
+ return resp.json()
100
+ body = _body(resp)
101
+ detail = body.get("detail") if isinstance(body, dict) else None
102
+ msg = f"{resp.status_code}: {detail or body}"
103
+ code = resp.status_code
104
+ if code == 402:
105
+ raise _payment_required(resp, msg)
106
+ if code in (401, 403):
107
+ raise EconDashAuthError(msg, status_code=code, body=body)
108
+ if code == 404:
109
+ raise EconDashNotFound(msg, status_code=code, body=body)
110
+ if code == 429:
111
+ ra = resp.headers.get("retry-after")
112
+ raise EconDashRateLimitError(
113
+ msg, retry_after=int(ra) if ra and ra.isdigit() else None, status_code=code, body=body
114
+ )
115
+ raise EconDashError(msg, status_code=code, body=body)
116
+
117
+
118
+ def _x402_client(signer: Any, max_usd_per_call: float, sync: bool) -> Any:
119
+ try:
120
+ from x402 import x402Client, x402ClientSync
121
+ from x402.http import x402HTTPClient, x402HTTPClientSync
122
+ from x402.mechanisms.evm.exact import register_exact_evm_client
123
+ except ImportError as e: # pragma: no cover
124
+ raise ImportError("x402 payments need the extra: pip install 'econdash[x402]'") from e
125
+ client: Any = x402ClientSync() if sync else x402Client()
126
+ register_exact_evm_client(client, signer)
127
+ # x402 refuses to sign anything above this cap; nothing is paid then.
128
+ client.set_spend_controls({"max_amount_per_payment": f"${max_usd_per_call}"})
129
+ return x402HTTPClientSync(client) if sync else x402HTTPClient(client)
130
+
131
+
132
+ class _Base:
133
+ def __init__(
134
+ self,
135
+ api_key: str | None = None,
136
+ *,
137
+ base_url: str = DEFAULT_BASE_URL,
138
+ timeout: float = 30.0,
139
+ pay_with_x402: bool = False,
140
+ x402_signer: Any = None,
141
+ max_usd_per_call: float | None = None,
142
+ ) -> None:
143
+ self.api_key = api_key or os.environ.get("ECONDASH_API_KEY") or None
144
+ self.base_url = base_url.rstrip("/")
145
+ self.timeout = timeout
146
+ self.headers = {"User-Agent": f"econdash-python/{__version__}"}
147
+ if self.api_key:
148
+ self.headers["Authorization"] = f"Bearer {self.api_key}"
149
+ if pay_with_x402 and (x402_signer is None or not max_usd_per_call):
150
+ raise ValueError("pay_with_x402=True needs x402_signer and max_usd_per_call")
151
+ self.pay_with_x402 = pay_with_x402
152
+ self._x402_signer = x402_signer
153
+ self._max_usd = max_usd_per_call or 0.0
154
+ # Paid (x402) access lives under /m2m; plain /api/v1 answers trial limits, not 402.
155
+ self._data = self.base_url + ("/m2m" if pay_with_x402 else "")
156
+ self._indicators: list[Indicator] | None = None
157
+ self._codes: dict[str, str] = {}
158
+
159
+ def _x402_for(self, sync: bool) -> Any:
160
+ if not self.pay_with_x402:
161
+ return None
162
+ return _x402_client(self._x402_signer, self._max_usd, sync=sync)
163
+
164
+ def _remember(self, ind: Indicator) -> Indicator:
165
+ if ind.slug:
166
+ self._codes[ind.slug] = ind.code
167
+ return ind
168
+
169
+ @staticmethod
170
+ def _one(model: type[M], data: Any) -> M:
171
+ return model.model_validate(data)
172
+
173
+ @staticmethod
174
+ def _many(model: type[M], data: Any) -> list[M]:
175
+ return TypeAdapter(list[model]).validate_python(data) # type: ignore[valid-type]
176
+
177
+ def _series_params(
178
+ self, country: str, start: DateLike | None, end: DateLike | None
179
+ ) -> dict[str, str]:
180
+ params = {"country": country.upper()}
181
+ if (s := _date(start, end=False)) is not None:
182
+ params["start_date"] = s
183
+ if (e := _date(end, end=True)) is not None:
184
+ params["end_date"] = e
185
+ return params
186
+
187
+
188
+ class EconDash(_Base):
189
+ """Synchronous client.
190
+
191
+ >>> ed = EconDash() # trial, or ECONDASH_API_KEY from env
192
+ >>> ed.get_series("gdp-nominal", "USA", start=2000).data[-1].value
193
+ """
194
+
195
+ def __init__(self, api_key: str | None = None, **kw: Any) -> None:
196
+ super().__init__(api_key, **kw)
197
+ self._http = httpx.Client(headers=self.headers, timeout=self.timeout)
198
+ self._x402 = self._x402_for(sync=True)
199
+
200
+ def __enter__(self) -> EconDash:
201
+ return self
202
+
203
+ def __exit__(self, *exc: object) -> None:
204
+ self.close()
205
+
206
+ def close(self) -> None:
207
+ self._http.close()
208
+
209
+ def _get(self, url: str, params: dict[str, Any] | None = None) -> Any:
210
+ resp = self._http.get(url, params=params)
211
+ if resp.status_code == 402 and self._x402 is not None:
212
+ try:
213
+ pay, _ = self._x402.handle_402_response(
214
+ dict(resp.headers), resp.content, str(resp.request.url)
215
+ )
216
+ except Exception as e:
217
+ raise _payment_required(resp, f"x402 payment not made: {e}") from e
218
+ resp = self._http.get(url, params=params, headers=pay)
219
+ return _check(resp)
220
+
221
+ def _code(self, indicator: str) -> str:
222
+ if _is_code(indicator):
223
+ return indicator
224
+ if indicator not in self._codes:
225
+ self.get_indicator(indicator)
226
+ return self._codes[indicator]
227
+
228
+ def list_indicators(self) -> list[Indicator]:
229
+ """All active indicators (cached per client)."""
230
+ if self._indicators is None:
231
+ data = self._get(f"{self._data}/indicators", {"limit": 1200})
232
+ self._indicators = [self._remember(i) for i in self._many(Indicator, data)]
233
+ return self._indicators
234
+
235
+ def search_indicators(self, query: str, *, limit: int = 20) -> list[Indicator]:
236
+ """Case-insensitive search over names, slugs, codes, categories and sources."""
237
+ return _search(self.list_indicators(), query, limit)
238
+
239
+ def get_indicator(self, indicator: str) -> Indicator:
240
+ """Metadata by slug ("gdp-nominal") or code ("world_bank__gdp_nominal")."""
241
+ path = indicator if _is_code(indicator) else f"by-slug/{indicator}"
242
+ return self._remember(self._one(Indicator, self._get(f"{self._data}/indicators/{path}")))
243
+
244
+ def get_series(
245
+ self,
246
+ indicator: str,
247
+ country: str,
248
+ *,
249
+ start: DateLike | None = None,
250
+ end: DateLike | None = None,
251
+ ) -> Series:
252
+ """Time series for one country. `start`/`end`: 2000, "2000-06" or "2000-06-30"."""
253
+ url = f"{self._data}/timeseries/{self._code(indicator)}"
254
+ return self._one(Series, self._get(url, self._series_params(country, start, end)))
255
+
256
+ def list_countries(self) -> list[Country]:
257
+ return self._many(Country, self._get(f"{self._data}/countries"))
258
+
259
+ def top_countries(
260
+ self, indicator: str, *, year: int | None = None, limit: int = 10
261
+ ) -> TopCountries:
262
+ """Ranking by the latest value (or by `year`). Includes aggregates like WLD."""
263
+ params: dict[str, Any] = {"limit": limit}
264
+ if year is not None:
265
+ params["year"] = year
266
+ url = f"{self._data}/timeseries/{self._code(indicator)}/top-countries"
267
+ return self._one(TopCountries, self._get(url, params))
268
+
269
+ def cite(self, indicator: str, country: str) -> Citation:
270
+ """Latest value with ready citation strings (text, short, markdown). Free endpoint."""
271
+ return self._one(Citation, self._get(f"{self.base_url}/cite/{country}/{indicator}"))
272
+
273
+
274
+ class AsyncEconDash(_Base):
275
+ """Asynchronous client with the same methods as `EconDash`, all awaitable."""
276
+
277
+ def __init__(self, api_key: str | None = None, **kw: Any) -> None:
278
+ super().__init__(api_key, **kw)
279
+ self._http = httpx.AsyncClient(headers=self.headers, timeout=self.timeout)
280
+ self._x402 = self._x402_for(sync=False)
281
+
282
+ async def __aenter__(self) -> AsyncEconDash:
283
+ return self
284
+
285
+ async def __aexit__(self, *exc: object) -> None:
286
+ await self.aclose()
287
+
288
+ async def aclose(self) -> None:
289
+ await self._http.aclose()
290
+
291
+ async def _get(self, url: str, params: dict[str, Any] | None = None) -> Any:
292
+ resp = await self._http.get(url, params=params)
293
+ if resp.status_code == 402 and self._x402 is not None:
294
+ try:
295
+ pay, _ = await self._x402.handle_402_response(
296
+ dict(resp.headers), resp.content, str(resp.request.url)
297
+ )
298
+ except Exception as e:
299
+ raise _payment_required(resp, f"x402 payment not made: {e}") from e
300
+ resp = await self._http.get(url, params=params, headers=pay)
301
+ return _check(resp)
302
+
303
+ async def _code(self, indicator: str) -> str:
304
+ if _is_code(indicator):
305
+ return indicator
306
+ if indicator not in self._codes:
307
+ await self.get_indicator(indicator)
308
+ return self._codes[indicator]
309
+
310
+ async def list_indicators(self) -> list[Indicator]:
311
+ if self._indicators is None:
312
+ data = await self._get(f"{self._data}/indicators", {"limit": 1200})
313
+ self._indicators = [self._remember(i) for i in self._many(Indicator, data)]
314
+ return self._indicators
315
+
316
+ async def search_indicators(self, query: str, *, limit: int = 20) -> list[Indicator]:
317
+ return _search(await self.list_indicators(), query, limit)
318
+
319
+ async def get_indicator(self, indicator: str) -> Indicator:
320
+ path = indicator if _is_code(indicator) else f"by-slug/{indicator}"
321
+ data = await self._get(f"{self._data}/indicators/{path}")
322
+ return self._remember(self._one(Indicator, data))
323
+
324
+ async def get_series(
325
+ self,
326
+ indicator: str,
327
+ country: str,
328
+ *,
329
+ start: DateLike | None = None,
330
+ end: DateLike | None = None,
331
+ ) -> Series:
332
+ url = f"{self._data}/timeseries/{await self._code(indicator)}"
333
+ return self._one(Series, await self._get(url, self._series_params(country, start, end)))
334
+
335
+ async def list_countries(self) -> list[Country]:
336
+ return self._many(Country, await self._get(f"{self._data}/countries"))
337
+
338
+ async def top_countries(
339
+ self, indicator: str, *, year: int | None = None, limit: int = 10
340
+ ) -> TopCountries:
341
+ params: dict[str, Any] = {"limit": limit}
342
+ if year is not None:
343
+ params["year"] = year
344
+ url = f"{self._data}/timeseries/{await self._code(indicator)}/top-countries"
345
+ return self._one(TopCountries, await self._get(url, params))
346
+
347
+ async def cite(self, indicator: str, country: str) -> Citation:
348
+ data = await self._get(f"{self.base_url}/cite/{country}/{indicator}")
349
+ return self._one(Citation, data)
econdash/errors.py ADDED
@@ -0,0 +1,45 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+
6
+ class EconDashError(Exception):
7
+ """Base class. `status_code` and `body` carry the raw API answer."""
8
+
9
+ def __init__(self, message: str, *, status_code: int | None = None, body: Any = None) -> None:
10
+ super().__init__(message)
11
+ self.status_code = status_code
12
+ self.body = body
13
+
14
+
15
+ class EconDashAuthError(EconDashError):
16
+ """401/403: bad API key, inactive subscription, or data outside the free trial."""
17
+
18
+
19
+ class EconDashNotFound(EconDashError):
20
+ """404: unknown indicator, country or series."""
21
+
22
+
23
+ class EconDashRateLimitError(EconDashError):
24
+ """429: trial or key quota exhausted. `retry_after` is in seconds."""
25
+
26
+ def __init__(self, message: str, *, retry_after: int | None = None, **kw: Any) -> None:
27
+ super().__init__(message, **kw)
28
+ self.retry_after = retry_after
29
+
30
+
31
+ class EconDashPaymentRequired(EconDashError):
32
+ """402: the call needs payment. `challenge` is the decoded x402 PAYMENT-REQUIRED
33
+ object, `www_authenticate` the raw MPP challenges. Nothing was paid."""
34
+
35
+ def __init__(
36
+ self,
37
+ message: str,
38
+ *,
39
+ challenge: dict[str, Any] | None = None,
40
+ www_authenticate: list[str] | None = None,
41
+ **kw: Any,
42
+ ) -> None:
43
+ super().__init__(message, **kw)
44
+ self.challenge = challenge
45
+ self.www_authenticate = www_authenticate or []
econdash/models.py ADDED
@@ -0,0 +1,132 @@
1
+ """Response models. Field names and types follow the EconDash API schema
2
+ (`IndicatorResponse`, `TimeSeriesResponse`, `CountryResponse`, ... in the backend OpenAPI).
3
+ Unknown fields are ignored, so new API fields never break old clients."""
4
+
5
+ from __future__ import annotations
6
+
7
+ from datetime import date
8
+
9
+ from pydantic import BaseModel
10
+
11
+
12
+ class Indicator(BaseModel):
13
+ code: str
14
+ slug: str | None
15
+ name_ru: str
16
+ name_en: str | None
17
+ category: str | None
18
+ source: str | None
19
+ unit: str | None
20
+ fetch_method: str | None
21
+ data_points: int = 0
22
+ description: str | None = None
23
+ updated_at: str | None = None
24
+ frequency: str | None = None
25
+ first_observation: str | None = None
26
+ last_observation: str | None = None
27
+ data_changed_at: str | None = None
28
+ data_checked_at: str | None = None
29
+ source_series_id: str | None = None
30
+ source_url: str | None = None
31
+ source_url_template: str | None = None
32
+ source_publisher: str | None = None
33
+ license_name: str | None = None
34
+ license_url: str | None = None
35
+ attribution_text: str | None = None
36
+ is_based_on: list[str] | None = None
37
+
38
+ @property
39
+ def name(self) -> str:
40
+ return self.name_en or self.name_ru
41
+
42
+ @property
43
+ def url(self) -> str | None:
44
+ """Chart hub page on econdash.org."""
45
+ return f"https://econdash.org/chart/{self.slug}" if self.slug else None
46
+
47
+
48
+ class Point(BaseModel):
49
+ date: date
50
+ value: float
51
+ country: str
52
+ country_name: str | None = None
53
+ is_projection: bool | None = None
54
+
55
+
56
+ class Series(BaseModel):
57
+ indicator_code: str
58
+ indicator_name_ru: str
59
+ indicator_name_en: str | None
60
+ unit: str | None
61
+ frequency: str | None = None
62
+ first_observation: str | None = None
63
+ last_observation: str | None = None
64
+ data_points: int | None = None
65
+ data_changed_at: str | None = None
66
+ data_checked_at: str | None = None
67
+ data: list[Point]
68
+
69
+
70
+ class Country(BaseModel):
71
+ iso3_code: str
72
+ iso2_code: str | None
73
+ name: str
74
+ region: str | None
75
+ income_level: str | None
76
+ data_points: int = 0
77
+
78
+
79
+ class TopCountry(BaseModel):
80
+ iso3_code: str
81
+ country_name: str | None
82
+ value: float
83
+ date: date
84
+ yoy_change_pct: float | None = None
85
+
86
+
87
+ class TopCountries(BaseModel):
88
+ indicator_code: str
89
+ unit: str | None
90
+ data: list[TopCountry]
91
+
92
+
93
+ class CiteSource(BaseModel):
94
+ name: str
95
+ acronym: str | None = None
96
+ url: str | None = None
97
+ release_date: str | None = None
98
+
99
+
100
+ class CitationStrings(BaseModel):
101
+ text: str
102
+ short: str
103
+ markdown: str
104
+
105
+
106
+ class CiteMeta(BaseModel):
107
+ updated_at: str
108
+ next_update: str | None = None
109
+ frequency: str | None = None
110
+ canonical_url: str
111
+
112
+
113
+ class Citation(BaseModel):
114
+ type: str = "economic_indicator"
115
+ indicator: str
116
+ indicator_name: str
117
+ country: str
118
+ country_name: str
119
+ value: float
120
+ unit: str
121
+ change: str | None = None
122
+ period: str
123
+ previous_value: float | None = None
124
+ previous_period: str | None = None
125
+ trend: str | None = None
126
+ trend_magnitude: float | None = None
127
+ definition: str | None = None
128
+ source: CiteSource
129
+ citation: CitationStrings
130
+ meta: CiteMeta
131
+ is_projection: bool = False
132
+ projection_label: str | None = None
econdash/py.typed ADDED
File without changes
@@ -0,0 +1,162 @@
1
+ Metadata-Version: 2.5
2
+ Name: econdash
3
+ Version: 0.1.0
4
+ Summary: Python client for the EconDash macroeconomic data API: 750+ indicators, 290+ countries, sources and licences.
5
+ Project-URL: Homepage, https://econdash.org
6
+ Project-URL: Documentation, https://econdash.org/docs/python
7
+ Project-URL: Repository, https://github.com/econdash/econdash-python
8
+ Project-URL: Issues, https://github.com/econdash/econdash-python/issues
9
+ Project-URL: Changelog, https://github.com/econdash/econdash-python/blob/main/CHANGELOG.md
10
+ Author-email: EconDash <noderguru@gmail.com>
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Keywords: api,economics,gdp,imf,inflation,macroeconomics,time-series,world-bank,x402
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Intended Audience :: Science/Research
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Topic :: Office/Business :: Financial
24
+ Classifier: Topic :: Scientific/Engineering :: Information Analysis
25
+ Classifier: Typing :: Typed
26
+ Requires-Python: >=3.10
27
+ Requires-Dist: httpx>=0.27
28
+ Requires-Dist: pydantic>=2.5
29
+ Provides-Extra: x402
30
+ Requires-Dist: x402[evm]<3,>=2.25; extra == 'x402'
31
+ Description-Content-Type: text/markdown
32
+
33
+ # econdash
34
+
35
+ [![PyPI](https://img.shields.io/pypi/v/econdash)](https://pypi.org/project/econdash/)
36
+ [![CI](https://github.com/econdash/econdash-python/actions/workflows/ci.yml/badge.svg)](https://github.com/econdash/econdash-python/actions/workflows/ci.yml)
37
+ [![Python](https://img.shields.io/pypi/pyversions/econdash)](https://pypi.org/project/econdash/)
38
+
39
+ Python client for the [EconDash](https://econdash.org) macroeconomic data API: 750+ indicators
40
+ (GDP, inflation, debt, trade, labour, money supply, …) for 290+ countries and regions, with
41
+ source, licence and freshness metadata for every series.
42
+
43
+ ```bash
44
+ pip install econdash
45
+ ```
46
+
47
+ ```python
48
+ from econdash import EconDash
49
+
50
+ ed = EconDash() # free trial, no key needed
51
+
52
+ ed.search_indicators("inflation")[:3] # -> list[Indicator]
53
+ ed.get_indicator("gdp-nominal") # metadata, source, licence, freshness
54
+ s = ed.get_series("gdp-nominal", "USA", start=2000)
55
+ s.data[-1] # Point(date=2025-01-01, value=3.07e13, ...)
56
+ ed.list_countries() # -> list[Country]
57
+ ed.top_countries("gdp-nominal", year=2024, limit=10)
58
+ ed.cite(
59
+ "gdp-nominal", "USA"
60
+ ).citation.text # 'United States GDP nominal was $30.77 tn in 2025 (World Bank, ...)'
61
+ ```
62
+
63
+ Indicators can be addressed by slug (`gdp-nominal`, as in
64
+ `econdash.org/chart/gdp-nominal/USA`) or by code (`world_bank__gdp_nominal`).
65
+ `start`/`end` accept `2000`, `"2000-06"`, `"2000-06-30"` or a `date`.
66
+
67
+ ## Free trial vs API key
68
+
69
+ Without a key the client works on the **free trial**:
70
+
71
+ | Limit | Trial |
72
+ |---|---|
73
+ | Requests | 300 per day, 30 per minute (per IP) |
74
+ | Countries | 10: USA, CHN, DEU, JPN, GBR, FRA, IND, BRA, RUS, KOR |
75
+ | Indicators | 50 core series (GDP, CPI, money supply, debt, trade, …) |
76
+
77
+ Outside the trial the API answers `403` and the client raises `EconDashAuthError`
78
+ ("Country ZWE not available in trial"). That is the trial limit, not a bug.
79
+
80
+ For full access create a key at [econdash.org/dashboard/api-keys](https://econdash.org/dashboard/api-keys)
81
+ ([plans](https://econdash.org/api-access)) and pass it:
82
+
83
+ ```python
84
+ ed = EconDash(api_key="ed_...") # or set ECONDASH_API_KEY
85
+ ```
86
+
87
+ ## Async
88
+
89
+ ```python
90
+ from econdash import AsyncEconDash
91
+
92
+ async with AsyncEconDash() as ed:
93
+ series = await ed.get_series("cpi-inflation-rate-percent", "DEU", start=2010)
94
+ ```
95
+
96
+ ## Errors
97
+
98
+ | Exception | When |
99
+ |---|---|
100
+ | `EconDashAuthError` | 401/403: bad key, inactive subscription, data outside the trial |
101
+ | `EconDashNotFound` | 404: unknown indicator or country |
102
+ | `EconDashRateLimitError` | 429, `retry_after` in seconds |
103
+ | `EconDashPaymentRequired` | 402, `challenge` holds the x402 payment requirements |
104
+ | `EconDashError` | base class, anything else |
105
+
106
+ ## Pay per call with x402 (for AI agents)
107
+
108
+ Agents without an account can pay each call in USDC on Base via
109
+ [x402](https://www.x402.org) ($0.02 per call at the time of writing). It is **off by
110
+ default**: the client never spends money unless you turn it on and set a cap.
111
+
112
+ ```bash
113
+ pip install "econdash[x402]"
114
+ ```
115
+
116
+ ```python
117
+ import os
118
+
119
+ from eth_account import Account
120
+ from econdash import EconDash
121
+
122
+ ed = EconDash(
123
+ pay_with_x402=True,
124
+ x402_signer=Account.from_key(os.environ["AGENT_PRIVATE_KEY"]),
125
+ max_usd_per_call=0.05, # x402 refuses to sign anything more expensive
126
+ )
127
+ ed.get_series("gdp-nominal", "ZWE")
128
+ ```
129
+
130
+ If the price is above `max_usd_per_call`, nothing is signed and `EconDashPaymentRequired`
131
+ is raised. Keep agent wallets funded with small amounts only.
132
+
133
+ ## Examples
134
+
135
+ Notebooks in [`examples/`](examples/): search → series → pandas chart, comparing countries,
136
+ a LangChain agent, an agent paying with x402.
137
+
138
+ ## Data and licences
139
+
140
+ Every `Indicator` carries `source`, `source_url`, `license_name`, `license_url` and
141
+ `attribution_text`. Source licences differ (World Bank CC BY 4.0, IMF, OECD, FRED, …);
142
+ see [econdash.org/data-license](https://econdash.org/data-license). Please keep the
143
+ attribution when you publish charts or numbers.
144
+
145
+ ## Links
146
+
147
+ - Website: <https://econdash.org>
148
+ - API docs: <https://econdash.org/docs>, OpenAPI: <https://econdash.org/openapi.json>
149
+ - Python docs: <https://econdash.org/docs/python>
150
+
151
+ ## Development
152
+
153
+ ```bash
154
+ uv sync --all-extras
155
+ uv run pytest # offline tests on recorded API answers
156
+ uv run pytest -m live # contract tests against the real API
157
+ uv run ruff check . && uv run mypy
158
+ ```
159
+
160
+ Releases are published to PyPI from GitHub Actions on a GitHub Release (trusted publishing).
161
+
162
+ MIT licence. The data itself is licensed by its original sources.
@@ -0,0 +1,9 @@
1
+ econdash/__init__.py,sha256=TW5KfoN0_RmanupFibPEuiQ1_AhEaHu9h-P3NFvJ_JE,820
2
+ econdash/_client.py,sha256=kldWLfaBIiDc4hWA1sjsXWt2vf5phuQUwwQGG_ufxh0,13060
3
+ econdash/errors.py,sha256=vih-ycvwhGCrptrczKFKLfdJRzvIM53bJ7rd-cuaxrc,1419
4
+ econdash/models.py,sha256=B_DJVBBX48Ci_RL0LJwgEp5-h1_s3DsSBrc0-I94HsE,3258
5
+ econdash/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ econdash-0.1.0.dist-info/METADATA,sha256=c3DqwiRHwDe-wMEMkCBDK8HewkffIpPLBotFilU-sXs,5925
7
+ econdash-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
8
+ econdash-0.1.0.dist-info/licenses/LICENSE,sha256=OupCmP07CGvR28W_kudL9YPyrT_gBW09XduaA9eEkJc,1065
9
+ econdash-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 EconDash
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.