parsimony-eodhd 0.4.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.
@@ -0,0 +1,479 @@
1
+ """EODHD source: typed connectors per endpoint.
2
+
3
+ API docs: https://eodhd.com/financial-apis/api-for-historical-data-and-volumes/
4
+ Authentication: API token via ``?api_token=<key>`` query param.
5
+ Base URL: https://eodhd.com/api
6
+
7
+ Provides 17 connectors covering the full EODHD REST surface:
8
+ - Market data: EOD prices, live quotes, intraday, bulk EOD
9
+ - Corporate actions: dividends, splits
10
+ - Reference: search, exchanges, exchange symbol lists
11
+ - Fundamentals (raw dict — nested JSON blob)
12
+ - Calendars: earnings, IPO, trends
13
+ - News
14
+ - Macro indicators
15
+ - Technical indicators
16
+ - Insider transactions
17
+ - Screener
18
+
19
+ Internal layout (not part of the public contract):
20
+
21
+ * :mod:`parsimony_eodhd._http` — shared transport, unified error mapping,
22
+ URL redaction, Retry-After parsing, JSON fetch helper.
23
+ * :mod:`parsimony_eodhd.params` — Pydantic parameter models.
24
+ * :mod:`parsimony_eodhd.outputs` — declarative :class:`OutputConfig` schemas.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import json
30
+ from typing import Any
31
+
32
+ from parsimony.connector import Connectors, connector
33
+ from parsimony.result import Result
34
+
35
+ from parsimony_eodhd._http import eodhd_fetch as _eodhd_fetch
36
+ from parsimony_eodhd._http import make_http as _make_http
37
+ from parsimony_eodhd.outputs import BULK_EOD_OUTPUT as _BULK_EOD_OUTPUT
38
+ from parsimony_eodhd.outputs import CALENDAR_OUTPUT as _CALENDAR_OUTPUT
39
+ from parsimony_eodhd.outputs import DIVIDENDS_OUTPUT as _DIVIDENDS_OUTPUT
40
+ from parsimony_eodhd.outputs import EOD_OUTPUT as _EOD_OUTPUT
41
+ from parsimony_eodhd.outputs import EXCHANGE_SYMBOLS_OUTPUT as _EXCHANGE_SYMBOLS_OUTPUT
42
+ from parsimony_eodhd.outputs import EXCHANGES_OUTPUT as _EXCHANGES_OUTPUT
43
+ from parsimony_eodhd.outputs import INSIDER_OUTPUT as _INSIDER_OUTPUT
44
+ from parsimony_eodhd.outputs import INTRADAY_OUTPUT as _INTRADAY_OUTPUT
45
+ from parsimony_eodhd.outputs import LIVE_OUTPUT as _LIVE_OUTPUT
46
+ from parsimony_eodhd.outputs import MACRO_OUTPUT as _MACRO_OUTPUT
47
+ from parsimony_eodhd.outputs import NEWS_OUTPUT as _NEWS_OUTPUT
48
+ from parsimony_eodhd.outputs import SCREENER_OUTPUT as _SCREENER_OUTPUT
49
+ from parsimony_eodhd.outputs import SEARCH_OUTPUT as _SEARCH_OUTPUT
50
+ from parsimony_eodhd.outputs import SPLITS_OUTPUT as _SPLITS_OUTPUT
51
+ from parsimony_eodhd.outputs import TECHNICAL_OUTPUT as _TECHNICAL_OUTPUT
52
+ from parsimony_eodhd.params import (
53
+ EodhdBulkEodParams,
54
+ EodhdCalendarParams,
55
+ EodhdDividendsParams,
56
+ EodhdEodParams,
57
+ EodhdExchangesParams,
58
+ EodhdExchangeSymbolsParams,
59
+ EodhdFundamentalsParams,
60
+ EodhdInsiderParams,
61
+ EodhdIntradayParams,
62
+ EodhdLiveParams,
63
+ EodhdMacroBulkParams,
64
+ EodhdMacroParams,
65
+ EodhdNewsParams,
66
+ EodhdScreenerParams,
67
+ EodhdSearchParams,
68
+ EodhdSplitsParams,
69
+ EodhdTechnicalParams,
70
+ )
71
+
72
+ _LATENCY_TIMEOUT: float = 10.0
73
+ _BULK_TIMEOUT: float = 60.0
74
+
75
+
76
+ # ---------------------------------------------------------------------------
77
+ # Market Data — Connectors
78
+ # ---------------------------------------------------------------------------
79
+
80
+
81
+ @connector(env={"api_key": "EODHD_API_KEY"}, output=_EOD_OUTPUT, tags=["eodhd", "equity"])
82
+ async def eodhd_eod(params: EodhdEodParams, *, api_key: str) -> Result:
83
+ """[Free+] Fetch end-of-day OHLCV prices for a ticker. Supports daily, weekly, and monthly
84
+ aggregation. Use from/to to limit the date range (ISO 8601). Empty result may indicate an
85
+ invalid ticker or exchange code — verify with eodhd_search first."""
86
+ http = _make_http(api_key)
87
+ p: dict[str, Any] = {"ticker": params.ticker}
88
+ if params.from_date:
89
+ p["from"] = params.from_date
90
+ if params.to_date:
91
+ p["to"] = params.to_date
92
+ if params.period:
93
+ p["period"] = params.period
94
+ return await _eodhd_fetch(http, path="/eod/{ticker}", params=p, op_name="eodhd_eod", output_config=_EOD_OUTPUT)
95
+
96
+
97
+ @connector(env={"api_key": "EODHD_API_KEY"}, output=_LIVE_OUTPUT, tags=["eodhd", "equity", "tool"])
98
+ async def eodhd_live(params: EodhdLiveParams, *, api_key: str) -> Result:
99
+ """[Free+] Fetch live (real-time or 15-min delayed) quote for a ticker. Use eodhd_search
100
+ to resolve a company name to its EODHD ticker format (e.g. AAPL.US)."""
101
+ http = _make_http(api_key, timeout=_LATENCY_TIMEOUT)
102
+ return await _eodhd_fetch(
103
+ http,
104
+ path="/real-time/{ticker}",
105
+ params={"ticker": params.ticker},
106
+ op_name="eodhd_live",
107
+ output_config=_LIVE_OUTPUT,
108
+ )
109
+
110
+
111
+ @connector(env={"api_key": "EODHD_API_KEY"}, output=_INTRADAY_OUTPUT, tags=["eodhd", "equity"])
112
+ async def eodhd_intraday(params: EodhdIntradayParams, *, api_key: str) -> Result:
113
+ """[EOD+Intraday+] Fetch intraday OHLCV data for a ticker. Intervals: 1m, 5m, 1h.
114
+ Provide from_unix / to_unix as Unix timestamps (seconds) to bound the range.
115
+ Returns at most the last 100 data points when no range is specified."""
116
+ http = _make_http(api_key, timeout=_LATENCY_TIMEOUT)
117
+ p: dict[str, Any] = {"ticker": params.ticker, "interval": params.interval}
118
+ if params.from_unix is not None:
119
+ p["from"] = params.from_unix
120
+ if params.to_unix is not None:
121
+ p["to"] = params.to_unix
122
+ return await _eodhd_fetch(
123
+ http, path="/intraday/{ticker}", params=p, op_name="eodhd_intraday", output_config=_INTRADAY_OUTPUT
124
+ )
125
+
126
+
127
+ @connector(env={"api_key": "EODHD_API_KEY"}, output=_BULK_EOD_OUTPUT, tags=["eodhd", "equity"])
128
+ async def eodhd_bulk_eod(params: EodhdBulkEodParams, *, api_key: str) -> Result:
129
+ """[EOD Historical+] Fetch end-of-day prices for all symbols on an exchange in a single request.
130
+ Returns the last trading day by default; pass date to fetch a specific day.
131
+ Large response — use for batch ingestion, not per-ticker lookups."""
132
+ http = _make_http(api_key, timeout=_BULK_TIMEOUT)
133
+ p: dict[str, Any] = {"exchange": params.exchange}
134
+ if params.date:
135
+ p["date"] = params.date
136
+ return await _eodhd_fetch(
137
+ http, path="/eod/bulk_last_day/{exchange}", params=p, op_name="eodhd_bulk_eod", output_config=_BULK_EOD_OUTPUT
138
+ )
139
+
140
+
141
+ # ---------------------------------------------------------------------------
142
+ # Corporate Actions — Connectors
143
+ # ---------------------------------------------------------------------------
144
+
145
+
146
+ @connector(env={"api_key": "EODHD_API_KEY"}, output=_DIVIDENDS_OUTPUT, tags=["eodhd", "equity"])
147
+ async def eodhd_dividends(params: EodhdDividendsParams, *, api_key: str) -> Result:
148
+ """[Free+] Fetch dividend history for a ticker. Use from/to to limit the range."""
149
+ http = _make_http(api_key)
150
+ p: dict[str, Any] = {"ticker": params.ticker}
151
+ if params.from_date:
152
+ p["from"] = params.from_date
153
+ if params.to_date:
154
+ p["to"] = params.to_date
155
+ return await _eodhd_fetch(
156
+ http, path="/div/{ticker}", params=p, op_name="eodhd_dividends", output_config=_DIVIDENDS_OUTPUT
157
+ )
158
+
159
+
160
+ @connector(env={"api_key": "EODHD_API_KEY"}, output=_SPLITS_OUTPUT, tags=["eodhd", "equity"])
161
+ async def eodhd_splits(params: EodhdSplitsParams, *, api_key: str) -> Result:
162
+ """[Free+] Fetch stock split history for a ticker. The split ratio column contains the
163
+ ratio string as returned by the API (e.g. "4/1" for a 4-for-1 split). Use from/to to limit the range."""
164
+ http = _make_http(api_key)
165
+ p: dict[str, Any] = {"ticker": params.ticker}
166
+ if params.from_date:
167
+ p["from"] = params.from_date
168
+ if params.to_date:
169
+ p["to"] = params.to_date
170
+ return await _eodhd_fetch(
171
+ http, path="/splits/{ticker}", params=p, op_name="eodhd_splits", output_config=_SPLITS_OUTPUT
172
+ )
173
+
174
+
175
+ # ---------------------------------------------------------------------------
176
+ # Reference Data — Connectors
177
+ # ---------------------------------------------------------------------------
178
+
179
+
180
+ @connector(env={"api_key": "EODHD_API_KEY"}, output=_SEARCH_OUTPUT, tags=["eodhd", "tool"])
181
+ async def eodhd_search(params: EodhdSearchParams, *, api_key: str) -> Result:
182
+ """[Free+] Search for instruments by company name or partial ticker. Use to resolve company
183
+ names to EODHD ticker codes (format: TICKER.EXCHANGE, e.g. AAPL.US). Filter by type to
184
+ narrow results."""
185
+ http = _make_http(api_key)
186
+ p: dict[str, Any] = {"query": params.query, "limit": params.limit}
187
+ if params.type:
188
+ p["type"] = params.type
189
+ return await _eodhd_fetch(
190
+ http, path="/search/{query}", params=p, op_name="eodhd_search", output_config=_SEARCH_OUTPUT
191
+ )
192
+
193
+
194
+ @connector(env={"api_key": "EODHD_API_KEY"}, output=_EXCHANGES_OUTPUT, tags=["eodhd", "tool"])
195
+ async def eodhd_exchanges(params: EodhdExchangesParams, *, api_key: str) -> Result:
196
+ """[Free+] List all exchanges supported by EODHD. Use to find valid exchange codes for
197
+ eodhd_bulk_eod and eodhd_exchange_symbols."""
198
+ http = _make_http(api_key)
199
+ return await _eodhd_fetch(
200
+ http, path="/exchanges-list", params={}, op_name="eodhd_exchanges", output_config=_EXCHANGES_OUTPUT
201
+ )
202
+
203
+
204
+ @connector(env={"api_key": "EODHD_API_KEY"}, output=_EXCHANGE_SYMBOLS_OUTPUT, tags=["eodhd"])
205
+ async def eodhd_exchange_symbols(params: EodhdExchangeSymbolsParams, *, api_key: str) -> Result:
206
+ """[Free+] List all symbols traded on an exchange. Large response for major exchanges
207
+ (US has 20 000+ symbols) — use type filter to limit. Empty result may indicate an
208
+ invalid exchange code."""
209
+ http = _make_http(api_key, timeout=_BULK_TIMEOUT)
210
+ p: dict[str, Any] = {"exchange": params.exchange}
211
+ if params.type:
212
+ p["type"] = params.type
213
+ return await _eodhd_fetch(
214
+ http,
215
+ path="/exchange-symbol-list/{exchange}",
216
+ params=p,
217
+ op_name="eodhd_exchange_symbols",
218
+ output_config=_EXCHANGE_SYMBOLS_OUTPUT,
219
+ )
220
+
221
+
222
+ # ---------------------------------------------------------------------------
223
+ # Fundamentals — Connector
224
+ # ---------------------------------------------------------------------------
225
+
226
+
227
+ @connector(env={"api_key": "EODHD_API_KEY"}, tags=["eodhd", "equity"])
228
+ async def eodhd_fundamentals(params: EodhdFundamentalsParams, *, api_key: str) -> Result:
229
+ """[Fundamentals+] Fetch full fundamentals for a stock or ETF. Returns a large nested dict
230
+ (not a DataFrame). Typical top-level keys for equities: General, Highlights, Valuation,
231
+ SharesStats, Technicals, SplitsDividends, AnalystRatings, Holders, InsiderTransactions,
232
+ Financials, Earnings. ETF top-level keys differ: General, Technicals, ETF_Data.
233
+
234
+ Navigate by key path, e.g.:
235
+ result.data['Highlights']['MarketCapitalization']
236
+ result.data['Financials']['Income_Statement']['annual']
237
+
238
+ Returns raw dict — use result.data to access the nested structure."""
239
+ http = _make_http(api_key, timeout=_BULK_TIMEOUT)
240
+ return await _eodhd_fetch(
241
+ http,
242
+ path="/fundamentals/{ticker}",
243
+ params={"ticker": params.ticker},
244
+ op_name="eodhd_fundamentals",
245
+ raw=True,
246
+ )
247
+
248
+
249
+ # ---------------------------------------------------------------------------
250
+ # Calendars — Dispatch map + Connector
251
+ # ---------------------------------------------------------------------------
252
+
253
+ _CALENDAR_PATHS: dict[str, str] = {
254
+ "earnings": "calendar/earnings",
255
+ "ipo": "calendar/ipo",
256
+ "trends": "calendar/trends",
257
+ }
258
+
259
+
260
+ @connector(env={"api_key": "EODHD_API_KEY"}, output=_CALENDAR_OUTPUT, tags=["eodhd", "equity"])
261
+ async def eodhd_calendar(params: EodhdCalendarParams, *, api_key: str) -> Result:
262
+ """[Fundamentals+] Fetch market calendar data. Three types available:
263
+ - earnings: upcoming earnings announcements with EPS estimates and actuals
264
+ - ipo: upcoming and recent IPO listings
265
+ - trends: analyst recommendation trends by sector
266
+
267
+ Use from/to to narrow the date window (max 90 days recommended for earnings)."""
268
+ http = _make_http(api_key)
269
+ path = _CALENDAR_PATHS[params.type]
270
+ p: dict[str, Any] = {}
271
+ if params.from_date:
272
+ p["from"] = params.from_date
273
+ if params.to_date:
274
+ p["to"] = params.to_date
275
+ if params.symbols:
276
+ p["symbols"] = params.symbols
277
+ return await _eodhd_fetch(http, path=path, params=p, op_name="eodhd_calendar", output_config=_CALENDAR_OUTPUT)
278
+
279
+
280
+ # ---------------------------------------------------------------------------
281
+ # News — Connector
282
+ # ---------------------------------------------------------------------------
283
+
284
+
285
+ @connector(env={"api_key": "EODHD_API_KEY"}, output=_NEWS_OUTPUT, tags=["eodhd", "tool"])
286
+ async def eodhd_news(params: EodhdNewsParams, *, api_key: str) -> Result:
287
+ """[Free+] Fetch financial news articles. Filter by ticker (e.g. AAPL.US) or leave
288
+ empty for broad market news. Use from/to for date filtering and limit/offset for pagination.
289
+ Empty result may indicate no news in the date range for the specified ticker."""
290
+ http = _make_http(api_key)
291
+ p: dict[str, Any] = {"limit": params.limit, "offset": params.offset}
292
+ if params.ticker:
293
+ p["s"] = params.ticker # EODHD uses 's=' for symbol filtering on news endpoint
294
+ if params.from_date:
295
+ p["from"] = params.from_date
296
+ if params.to_date:
297
+ p["to"] = params.to_date
298
+ return await _eodhd_fetch(http, path="/news", params=p, op_name="eodhd_news", output_config=_NEWS_OUTPUT)
299
+
300
+
301
+ # ---------------------------------------------------------------------------
302
+ # Macro Indicators — Connectors
303
+ # ---------------------------------------------------------------------------
304
+
305
+
306
+ @connector(env={"api_key": "EODHD_API_KEY"}, output=_MACRO_OUTPUT, tags=["eodhd", "macro"])
307
+ async def eodhd_macro(params: EodhdMacroParams, *, api_key: str) -> Result:
308
+ """[Fundamentals+] Fetch a macro indicator time series for a country.
309
+ Country must be an ISO 3-letter code (e.g. USA, DEU). Common indicators:
310
+ gdp_current_usd, unemployment_total_percent, inflation_consumer_prices_annual,
311
+ real_interest_rate, population_total, exports_of_goods_and_services_usd."""
312
+ http = _make_http(api_key)
313
+ return await _eodhd_fetch(
314
+ http,
315
+ path="/macro-indicator/{country}",
316
+ params={"country": params.country, "indicator": params.indicator},
317
+ op_name="eodhd_macro",
318
+ output_config=_MACRO_OUTPUT,
319
+ )
320
+
321
+
322
+ @connector(env={"api_key": "EODHD_API_KEY"}, output=_MACRO_OUTPUT, tags=["eodhd", "macro"])
323
+ async def eodhd_macro_bulk(params: EodhdMacroBulkParams, *, api_key: str) -> Result:
324
+ """[Fundamentals+] Fetch all available macro indicators for a country in a single request.
325
+ Large response — use eodhd_macro for a specific indicator.
326
+ Country must be an ISO 3-letter code (e.g. USA)."""
327
+ http = _make_http(api_key, timeout=_BULK_TIMEOUT)
328
+ p: dict[str, Any] = {"country": params.country}
329
+ if params.topic:
330
+ p["topic"] = params.topic
331
+ return await _eodhd_fetch(
332
+ http, path="/macro-indicator/{country}", params=p, op_name="eodhd_macro_bulk", output_config=_MACRO_OUTPUT
333
+ )
334
+
335
+
336
+ # ---------------------------------------------------------------------------
337
+ # Technical Indicators — Connector
338
+ # ---------------------------------------------------------------------------
339
+
340
+
341
+ @connector(env={"api_key": "EODHD_API_KEY"}, output=_TECHNICAL_OUTPUT, tags=["eodhd", "equity"])
342
+ async def eodhd_technical(params: EodhdTechnicalParams, *, api_key: str) -> Result:
343
+ """[EOD+Intraday+] Fetch technical indicator values for a ticker alongside OHLCV data.
344
+ Indicator-specific output columns vary by function:
345
+ - sma/ema/wma → sma/ema/wma column
346
+ - macd → macd, macd_signal, macd_hist
347
+ - bbands → uband, mband, lband
348
+ - stochastic → stoch_kd, stoch_d
349
+ - adx/dmi → adx, plusDI, minusDI
350
+
351
+ Use period to control the lookback window (default 50)."""
352
+ http = _make_http(api_key)
353
+ p: dict[str, Any] = {
354
+ "ticker": params.ticker,
355
+ "function": params.function,
356
+ "period": params.period,
357
+ "order": params.order,
358
+ }
359
+ if params.from_date:
360
+ p["from"] = params.from_date
361
+ if params.to_date:
362
+ p["to"] = params.to_date
363
+ return await _eodhd_fetch(
364
+ http, path="/technicals/{ticker}", params=p, op_name="eodhd_technical", output_config=_TECHNICAL_OUTPUT
365
+ )
366
+
367
+
368
+ # ---------------------------------------------------------------------------
369
+ # Insider Transactions & Screener — Connectors
370
+ # ---------------------------------------------------------------------------
371
+
372
+
373
+ @connector(env={"api_key": "EODHD_API_KEY"}, output=_INSIDER_OUTPUT, tags=["eodhd", "equity"])
374
+ async def eodhd_insider(params: EodhdInsiderParams, *, api_key: str) -> Result:
375
+ """[Fundamentals+] Fetch insider (executive and director) transactions. Filter by ticker
376
+ or omit for recent cross-market transactions. Use limit/offset to page."""
377
+ http = _make_http(api_key)
378
+ p: dict[str, Any] = {"limit": params.limit, "offset": params.offset}
379
+ if params.ticker:
380
+ p["code"] = params.ticker
381
+ return await _eodhd_fetch(
382
+ http, path="/insider-transactions", params=p, op_name="eodhd_insider", output_config=_INSIDER_OUTPUT
383
+ )
384
+
385
+
386
+ @connector(env={"api_key": "EODHD_API_KEY"}, output=_SCREENER_OUTPUT, tags=["eodhd", "equity", "tool"])
387
+ async def eodhd_screener(params: EodhdScreenerParams, *, api_key: str) -> Result:
388
+ """[EOD+Intraday+] Screen stocks by fundamental, price, and exchange criteria.
389
+ Filters are structured triples [field, operator, value] — see EodhdScreenerParams.filters.
390
+ Empty result may indicate invalid filter field or operator — verify against the EODHD
391
+ screener field list in their documentation."""
392
+ http = _make_http(api_key)
393
+ p: dict[str, Any] = {"limit": params.limit, "offset": params.offset, "order": params.order}
394
+ if params.filters:
395
+ p["filters"] = json.dumps([[f[0], f[1], f[2]] for f in params.filters])
396
+ if params.signals:
397
+ p["signals"] = params.signals
398
+ if params.sort:
399
+ p["sort"] = params.sort
400
+ return await _eodhd_fetch(
401
+ http, path="/screener", params=p, op_name="eodhd_screener", output_config=_SCREENER_OUTPUT
402
+ )
403
+
404
+
405
+ # ---------------------------------------------------------------------------
406
+ # Connector collections
407
+ # ---------------------------------------------------------------------------
408
+
409
+ CONNECTORS = Connectors(
410
+ [
411
+ # Discovery
412
+ eodhd_search,
413
+ eodhd_exchanges,
414
+ eodhd_news,
415
+ eodhd_screener,
416
+ # Market data
417
+ eodhd_eod,
418
+ eodhd_live,
419
+ eodhd_intraday,
420
+ eodhd_bulk_eod,
421
+ # Corporate actions
422
+ eodhd_dividends,
423
+ eodhd_splits,
424
+ # Reference
425
+ eodhd_exchange_symbols,
426
+ # Fundamentals
427
+ eodhd_fundamentals,
428
+ # Calendars
429
+ eodhd_calendar,
430
+ # Macro
431
+ eodhd_macro,
432
+ eodhd_macro_bulk,
433
+ # Technical
434
+ eodhd_technical,
435
+ # Transactions
436
+ eodhd_insider,
437
+ ]
438
+ )
439
+
440
+
441
+ __all__ = [
442
+ "CONNECTORS",
443
+ # Connectors
444
+ "eodhd_bulk_eod",
445
+ "eodhd_calendar",
446
+ "eodhd_dividends",
447
+ "eodhd_eod",
448
+ "eodhd_exchange_symbols",
449
+ "eodhd_exchanges",
450
+ "eodhd_fundamentals",
451
+ "eodhd_insider",
452
+ "eodhd_intraday",
453
+ "eodhd_live",
454
+ "eodhd_macro",
455
+ "eodhd_macro_bulk",
456
+ "eodhd_news",
457
+ "eodhd_screener",
458
+ "eodhd_search",
459
+ "eodhd_splits",
460
+ "eodhd_technical",
461
+ # Param classes
462
+ "EodhdBulkEodParams",
463
+ "EodhdCalendarParams",
464
+ "EodhdDividendsParams",
465
+ "EodhdEodParams",
466
+ "EodhdExchangeSymbolsParams",
467
+ "EodhdExchangesParams",
468
+ "EodhdFundamentalsParams",
469
+ "EodhdInsiderParams",
470
+ "EodhdIntradayParams",
471
+ "EodhdLiveParams",
472
+ "EodhdMacroBulkParams",
473
+ "EodhdMacroParams",
474
+ "EodhdNewsParams",
475
+ "EodhdScreenerParams",
476
+ "EodhdSearchParams",
477
+ "EodhdSplitsParams",
478
+ "EodhdTechnicalParams",
479
+ ]
@@ -0,0 +1,172 @@
1
+ """EODHD transport — shared HTTP helpers and timeout handling.
2
+
3
+ Error mapping and Retry-After parsing are delegated to the kernel
4
+ (:func:`parsimony.transport.map_http_error`,
5
+ :func:`parsimony.transport.parse_retry_after`). API-token redaction is
6
+ delegated to :func:`parsimony.transport.redact_url` — the ``api_token``
7
+ query param is already in the kernel's sensitive-name set.
8
+
9
+ This module only owns what is EODHD-specific: bracket-syntax query
10
+ parameter encoding (``filter_x`` → ``filter[x]``) and 200-body error
11
+ detection.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import re
17
+ from typing import Any
18
+
19
+ import httpx
20
+ import pandas as pd
21
+ from parsimony.errors import (
22
+ EmptyDataError,
23
+ ParseError,
24
+ ProviderError,
25
+ )
26
+ from parsimony.result import OutputConfig, Provenance, Result
27
+ from parsimony.transport import HttpClient, map_http_error, map_timeout_error
28
+
29
+ # Per-request timeout. 15s is defensible for EODHD's REST endpoints, which
30
+ # are not streaming. Bulk endpoints (fundamentals, macro_bulk, bulk_eod,
31
+ # exchange_symbols) override this via the ``timeout`` kwarg on ``make_http``.
32
+ _DEFAULT_TIMEOUT_SECONDS: float = 15.0
33
+
34
+ _DEFAULT_BASE_URL: str = "https://eodhd.com/api"
35
+
36
+ _PROVIDER: str = "eodhd"
37
+
38
+
39
+ def make_http(
40
+ api_key: str,
41
+ base_url: str = _DEFAULT_BASE_URL,
42
+ timeout: float = _DEFAULT_TIMEOUT_SECONDS,
43
+ ) -> HttpClient:
44
+ """Construct the standard EODHD transport.
45
+
46
+ The API token rides as a default query parameter (``api_token=<key>``),
47
+ alongside EODHD's ``fmt=json`` convention. Timeout defaults to 15s;
48
+ bulk endpoints pass a larger value explicitly.
49
+ """
50
+ return HttpClient(
51
+ base_url,
52
+ query_params={"api_token": api_key, "fmt": "json"},
53
+ timeout=timeout,
54
+ )
55
+
56
+
57
+ def _to_bracket_params(params: dict[str, Any]) -> dict[str, Any]:
58
+ """Transform ``filter_x`` → ``filter[x]`` and ``page_x`` → ``page[x]`` for EODHD bracket syntax.
59
+
60
+ Pure function: does not mutate input. None values are dropped.
61
+ """
62
+ result: dict[str, Any] = {}
63
+ for k, v in params.items():
64
+ if v is None:
65
+ continue
66
+ if k.startswith("filter_"):
67
+ result[f"filter[{k[7:]}]"] = v
68
+ elif k.startswith("page_"):
69
+ result[f"page[{k[5:]}]"] = v
70
+ else:
71
+ result[k] = v
72
+ return result
73
+
74
+
75
+ async def eodhd_fetch(
76
+ http: HttpClient,
77
+ *,
78
+ path: str,
79
+ params: dict[str, Any],
80
+ op_name: str,
81
+ output_config: OutputConfig | None = None,
82
+ raw: bool = False,
83
+ ) -> Result:
84
+ """Shared EODHD fetch: path interpolation, bracket params, JSON extraction, Result building.
85
+
86
+ Error mapping is delegated to :func:`~parsimony.transport.map_http_error`:
87
+ 401/403 → UnauthorizedError
88
+ 402 → PaymentRequiredError
89
+ 429 → RateLimitError (Retry-After parsed when present)
90
+ else → ProviderError
91
+
92
+ ``httpx.TimeoutException`` is mapped to ``ProviderError(status_code=408)``.
93
+ The EODHD API token is never included in exception messages.
94
+ ``asyncio.CancelledError`` propagates unchanged.
95
+
96
+ ``raw=True`` bypasses the DataFrame pipeline and returns the parsed JSON
97
+ verbatim (used by ``eodhd_fundamentals``, which returns a nested dict).
98
+ """
99
+ # Path template substitution: {key} → value; remainder → query params
100
+ rendered = path
101
+ query_params: dict[str, Any] = {}
102
+
103
+ for key, value in params.items():
104
+ if value is None:
105
+ continue
106
+ placeholder = f"{{{key}}}"
107
+ if placeholder in rendered:
108
+ rendered = rendered.replace(placeholder, str(value))
109
+ else:
110
+ query_params[key] = value
111
+
112
+ # Remove any unfilled optional placeholders
113
+ rendered = re.sub(r"\{[^}]+\}", "", rendered)
114
+
115
+ # Apply EODHD bracket syntax transformation (filter_x → filter[x], page_x → page[x])
116
+ query_params = _to_bracket_params(query_params)
117
+
118
+ try:
119
+ response = await http.request("GET", f"/{rendered.lstrip('/')}", params=query_params or None)
120
+ response.raise_for_status()
121
+ except httpx.HTTPStatusError as exc:
122
+ map_http_error(exc, provider=_PROVIDER, op_name=op_name)
123
+ except httpx.TimeoutException as exc:
124
+ map_timeout_error(exc, provider=_PROVIDER, op_name=op_name)
125
+
126
+ data = response.json()
127
+ prov = Provenance(source=op_name, params=dict(params))
128
+
129
+ # 200-body error detection (EODHD returns error strings in the body on some endpoints)
130
+ if isinstance(data, dict) and "error" in data and isinstance(data["error"], str):
131
+ raise ProviderError(
132
+ provider=_PROVIDER,
133
+ status_code=200,
134
+ message=f"EODHD error on '{op_name}': {data['error']}",
135
+ )
136
+
137
+ # Raw return path (fundamentals): bypass DataFrame pipeline entirely
138
+ if raw:
139
+ return Result(data=data, provenance=prov)
140
+
141
+ # DataFrame construction
142
+ if isinstance(data, list):
143
+ df = pd.DataFrame(data)
144
+ elif isinstance(data, dict):
145
+ for key in ("earnings", "ipos", "splits", "trends", "data", "results"):
146
+ if key in data and isinstance(data[key], list):
147
+ df = pd.DataFrame(data[key])
148
+ break
149
+ else:
150
+ df = pd.DataFrame([data])
151
+ else:
152
+ raise ParseError(
153
+ provider=_PROVIDER,
154
+ message=f"Unexpected response type from EODHD '{op_name}': {type(data).__name__}",
155
+ )
156
+
157
+ if df.empty:
158
+ raise EmptyDataError(
159
+ provider=_PROVIDER,
160
+ message=f"No data returned from EODHD endpoint '{op_name}'",
161
+ query_params=dict(params),
162
+ )
163
+
164
+ if output_config is not None:
165
+ return output_config.build_table_result(df, provenance=prov, params=dict(params))
166
+ return Result.from_dataframe(df, prov)
167
+
168
+
169
+ __all__ = [
170
+ "eodhd_fetch",
171
+ "make_http",
172
+ ]