parsimony-eodhd 0.0.1__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.
- parsimony_eodhd/__init__.py +662 -0
- parsimony_eodhd/_http.py +112 -0
- parsimony_eodhd/outputs.py +200 -0
- parsimony_eodhd/py.typed +0 -0
- parsimony_eodhd-0.0.1.dist-info/METADATA +115 -0
- parsimony_eodhd-0.0.1.dist-info/RECORD +9 -0
- parsimony_eodhd-0.0.1.dist-info/WHEEL +4 -0
- parsimony_eodhd-0.0.1.dist-info/entry_points.txt +2 -0
- parsimony_eodhd-0.0.1.dist-info/licenses/LICENSE +190 -0
|
@@ -0,0 +1,662 @@
|
|
|
1
|
+
"""EODHD source: end-of-day, intraday, fundamentals, news, calendars, macro.
|
|
2
|
+
|
|
3
|
+
API docs: https://eodhd.com/financial-apis/
|
|
4
|
+
Authentication: ``api_token`` query parameter (in the transport sensitive-param
|
|
5
|
+
set, so it is redacted from every log line and never reaches a surfaced URL).
|
|
6
|
+
Base URL: https://eodhd.com/api
|
|
7
|
+
|
|
8
|
+
Provides 17 plain ``@connector`` verbs over the EODHD REST surface:
|
|
9
|
+
- Market data: EOD prices, live quotes, intraday, bulk EOD
|
|
10
|
+
- Corporate actions: dividends, splits
|
|
11
|
+
- Reference: search, exchanges, exchange symbol lists
|
|
12
|
+
- Fundamentals (raw nested dict)
|
|
13
|
+
- Calendars: earnings, IPO, trends, splits
|
|
14
|
+
- News
|
|
15
|
+
- Macro indicators (single + bulk)
|
|
16
|
+
- Technical indicators
|
|
17
|
+
- Insider transactions
|
|
18
|
+
- Screener
|
|
19
|
+
|
|
20
|
+
The API key is declared as a secret (stripped from provenance) and supplied
|
|
21
|
+
either by binding (``load(api_key=...)`` / ``Connector.bind``) or, as a dev
|
|
22
|
+
fallback, from the ``EODHD_API_KEY`` environment variable. A missing key fails
|
|
23
|
+
fast with :class:`UnauthorizedError` naming the env var.
|
|
24
|
+
|
|
25
|
+
Status semantics (verified live 2026-06-04): an invalid key returns 401
|
|
26
|
+
(→ :class:`UnauthorizedError`); a plan-restricted endpoint returns 403, and a
|
|
27
|
+
bulk plan-restriction returns 423 Locked — both surfaced as
|
|
28
|
+
:class:`PaymentRequiredError`. Many verbs require a paid EODHD plan; their
|
|
29
|
+
docstrings tag the minimum plan as ``[Free+]``, ``[EOD+Intraday+]``, or
|
|
30
|
+
``[Fundamentals+]`` and they return :class:`PaymentRequiredError` on a free key.
|
|
31
|
+
|
|
32
|
+
Internal layout (not part of the public contract):
|
|
33
|
+
|
|
34
|
+
* :mod:`parsimony_eodhd._http` — keyed client builder and error mapping.
|
|
35
|
+
* :mod:`parsimony_eodhd.outputs` — declarative :class:`OutputConfig` schemas.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
from __future__ import annotations
|
|
39
|
+
|
|
40
|
+
import json
|
|
41
|
+
import re
|
|
42
|
+
from typing import Annotated, Any, Literal
|
|
43
|
+
|
|
44
|
+
import pandas as pd
|
|
45
|
+
from parsimony import Namespace
|
|
46
|
+
from parsimony.connector import Connectors, connector
|
|
47
|
+
from parsimony.errors import EmptyDataError, InvalidParameterError, ParseError
|
|
48
|
+
|
|
49
|
+
from parsimony_eodhd._http import _client, eodhd_get
|
|
50
|
+
from parsimony_eodhd.outputs import BULK_EOD_OUTPUT as _BULK_EOD_OUTPUT
|
|
51
|
+
from parsimony_eodhd.outputs import CALENDAR_OUTPUT as _CALENDAR_OUTPUT
|
|
52
|
+
from parsimony_eodhd.outputs import DIVIDENDS_OUTPUT as _DIVIDENDS_OUTPUT
|
|
53
|
+
from parsimony_eodhd.outputs import EOD_OUTPUT as _EOD_OUTPUT
|
|
54
|
+
from parsimony_eodhd.outputs import EXCHANGE_SYMBOLS_OUTPUT as _EXCHANGE_SYMBOLS_OUTPUT
|
|
55
|
+
from parsimony_eodhd.outputs import EXCHANGES_OUTPUT as _EXCHANGES_OUTPUT
|
|
56
|
+
from parsimony_eodhd.outputs import INSIDER_OUTPUT as _INSIDER_OUTPUT
|
|
57
|
+
from parsimony_eodhd.outputs import INTRADAY_OUTPUT as _INTRADAY_OUTPUT
|
|
58
|
+
from parsimony_eodhd.outputs import LIVE_OUTPUT as _LIVE_OUTPUT
|
|
59
|
+
from parsimony_eodhd.outputs import MACRO_OUTPUT as _MACRO_OUTPUT
|
|
60
|
+
from parsimony_eodhd.outputs import NEWS_OUTPUT as _NEWS_OUTPUT
|
|
61
|
+
from parsimony_eodhd.outputs import SCREENER_OUTPUT as _SCREENER_OUTPUT
|
|
62
|
+
from parsimony_eodhd.outputs import SEARCH_OUTPUT as _SEARCH_OUTPUT
|
|
63
|
+
from parsimony_eodhd.outputs import SPLITS_OUTPUT as _SPLITS_OUTPUT
|
|
64
|
+
from parsimony_eodhd.outputs import TECHNICAL_OUTPUT as _TECHNICAL_OUTPUT
|
|
65
|
+
|
|
66
|
+
__all__ = ["CONNECTORS", "load"]
|
|
67
|
+
|
|
68
|
+
_PROVIDER = "eodhd"
|
|
69
|
+
|
|
70
|
+
_LATENCY_TIMEOUT: float = 10.0
|
|
71
|
+
_BULK_TIMEOUT: float = 60.0
|
|
72
|
+
|
|
73
|
+
# Technical-indicator function names accepted by eodhd_technical.
|
|
74
|
+
_TechnicalFunction = Literal[
|
|
75
|
+
"sma",
|
|
76
|
+
"ema",
|
|
77
|
+
"wma",
|
|
78
|
+
"volatility",
|
|
79
|
+
"stochastic",
|
|
80
|
+
"rsi",
|
|
81
|
+
"stddev",
|
|
82
|
+
"stochrsi",
|
|
83
|
+
"slope",
|
|
84
|
+
"dmi",
|
|
85
|
+
"adx",
|
|
86
|
+
"macd",
|
|
87
|
+
"atr",
|
|
88
|
+
"cci",
|
|
89
|
+
"sar",
|
|
90
|
+
"bbands",
|
|
91
|
+
"splitadjusted",
|
|
92
|
+
"avgvol",
|
|
93
|
+
"avgvolacave",
|
|
94
|
+
"williams_r",
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
# Guard for values interpolated directly into request paths
|
|
98
|
+
# (``/eod/<ticker>``, ``/exchange-symbol-list/<exchange>`` etc.).
|
|
99
|
+
_PATH_TOKEN_RE = re.compile(r"^[A-Za-z0-9._\-]+$")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _safe_path_token(value: str, name: str) -> str:
|
|
103
|
+
"""Validate and return a value that is interpolated into a request path."""
|
|
104
|
+
cleaned = value.strip()
|
|
105
|
+
if not cleaned:
|
|
106
|
+
raise InvalidParameterError(_PROVIDER, f"{name} must be non-empty")
|
|
107
|
+
if not _PATH_TOKEN_RE.match(cleaned):
|
|
108
|
+
raise InvalidParameterError(_PROVIDER, f"{name} contains unsafe characters for a URL path: {value!r}")
|
|
109
|
+
return cleaned
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _select_declared(df: pd.DataFrame, output: Any) -> pd.DataFrame:
|
|
113
|
+
"""Project a frame to the columns the schema declares, in declared order.
|
|
114
|
+
|
|
115
|
+
Drops provider extras not in the schema. Missing declared columns are filled
|
|
116
|
+
with ``NA`` so :class:`~parsimony.result.OutputConfig` can shape sparse
|
|
117
|
+
payloads (calendar types, dividend-adjusted prices, etc.) without folding
|
|
118
|
+
extras in as stray DATA columns. Wildcard (``"*"``) schemas keep unmapped
|
|
119
|
+
columns after the fixed prefix.
|
|
120
|
+
"""
|
|
121
|
+
names = [c.name for c in output.columns]
|
|
122
|
+
fixed = [n for n in names if n != "*"]
|
|
123
|
+
out = df.copy()
|
|
124
|
+
for n in fixed:
|
|
125
|
+
if n not in out.columns:
|
|
126
|
+
out[n] = pd.NA
|
|
127
|
+
if "*" in names:
|
|
128
|
+
extra = [c for c in out.columns if c not in fixed]
|
|
129
|
+
return out[fixed + extra]
|
|
130
|
+
return out[fixed]
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _rows_to_frame(data: Any, op_name: str, query_params: dict[str, Any]) -> pd.DataFrame:
|
|
134
|
+
"""Build a DataFrame from a JSON list of records; guard empty/parse failures."""
|
|
135
|
+
if not isinstance(data, list):
|
|
136
|
+
raise ParseError(_PROVIDER, f"{op_name} response was not a JSON array")
|
|
137
|
+
if not data:
|
|
138
|
+
raise EmptyDataError(_PROVIDER, query_params=query_params)
|
|
139
|
+
df = pd.DataFrame(data)
|
|
140
|
+
if df.empty:
|
|
141
|
+
raise EmptyDataError(_PROVIDER, query_params=query_params)
|
|
142
|
+
return df
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# ---------------------------------------------------------------------------
|
|
146
|
+
# Reference Data — search / exchanges / exchange symbols
|
|
147
|
+
# ---------------------------------------------------------------------------
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@connector(output=_SEARCH_OUTPUT, tags=["eodhd", "tool"], secrets=("api_key",))
|
|
151
|
+
def eodhd_search(
|
|
152
|
+
query: str,
|
|
153
|
+
limit: int = 50,
|
|
154
|
+
type: Literal["stock", "etf", "fund", "bond", "index"] | None = None,
|
|
155
|
+
api_key: str = "",
|
|
156
|
+
) -> pd.DataFrame:
|
|
157
|
+
"""[Free+] Search EODHD for instruments by company name or partial ticker.
|
|
158
|
+
Resolves names to EODHD ticker codes (format TICKER.EXCHANGE, e.g. AAPL.US).
|
|
159
|
+
Returns Code, Name, Exchange, Type, Country, Currency, ISIN. Optionally
|
|
160
|
+
filter by type: stock, etf, fund, bond, index.
|
|
161
|
+
"""
|
|
162
|
+
q = query.strip()
|
|
163
|
+
if not q:
|
|
164
|
+
raise InvalidParameterError(_PROVIDER, "query must be non-empty")
|
|
165
|
+
if limit < 1 or limit > 500:
|
|
166
|
+
raise InvalidParameterError(_PROVIDER, "limit must be between 1 and 500")
|
|
167
|
+
|
|
168
|
+
http = _client(api_key)
|
|
169
|
+
data = eodhd_get(
|
|
170
|
+
http,
|
|
171
|
+
path=f"search/{q}",
|
|
172
|
+
params={"limit": limit, "type": type},
|
|
173
|
+
op_name="eodhd_search",
|
|
174
|
+
)
|
|
175
|
+
df = _rows_to_frame(data, "eodhd_search", {"query": q})
|
|
176
|
+
return _select_declared(df, _SEARCH_OUTPUT)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@connector(output=_EXCHANGES_OUTPUT, tags=["eodhd", "tool"], secrets=("api_key",))
|
|
180
|
+
def eodhd_exchanges(api_key: str = "") -> pd.DataFrame:
|
|
181
|
+
"""[Free+] List all exchanges supported by EODHD. Returns Code, Name, country,
|
|
182
|
+
currency and operating MIC. Use the Code with eodhd_bulk_eod and
|
|
183
|
+
eodhd_exchange_symbols.
|
|
184
|
+
"""
|
|
185
|
+
http = _client(api_key)
|
|
186
|
+
data = eodhd_get(http, path="exchanges-list", op_name="eodhd_exchanges")
|
|
187
|
+
df = _rows_to_frame(data, "eodhd_exchanges", {})
|
|
188
|
+
return _select_declared(df, _EXCHANGES_OUTPUT)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@connector(output=_EXCHANGE_SYMBOLS_OUTPUT, tags=["eodhd"], secrets=("api_key",))
|
|
192
|
+
def eodhd_exchange_symbols(
|
|
193
|
+
exchange: str,
|
|
194
|
+
type: Literal["common_stock", "preferred_stock", "stock", "etf", "fund"] | None = None,
|
|
195
|
+
api_key: str = "",
|
|
196
|
+
) -> pd.DataFrame:
|
|
197
|
+
"""[Free+] List all symbols traded on an exchange (returns Code, Name, country,
|
|
198
|
+
exchange, currency, type, ISIN). Large response for major exchanges (US has
|
|
199
|
+
50 000+ symbols) — use the type filter to narrow. Empty result may indicate
|
|
200
|
+
an invalid exchange code.
|
|
201
|
+
"""
|
|
202
|
+
ex = _safe_path_token(exchange, "exchange")
|
|
203
|
+
http = _client(api_key, timeout=_BULK_TIMEOUT)
|
|
204
|
+
data = eodhd_get(
|
|
205
|
+
http,
|
|
206
|
+
path=f"exchange-symbol-list/{ex}",
|
|
207
|
+
params={"type": type},
|
|
208
|
+
op_name="eodhd_exchange_symbols",
|
|
209
|
+
)
|
|
210
|
+
df = _rows_to_frame(data, "eodhd_exchange_symbols", {"exchange": ex})
|
|
211
|
+
return _select_declared(df, _EXCHANGE_SYMBOLS_OUTPUT)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
# ---------------------------------------------------------------------------
|
|
215
|
+
# Market Data — EOD / live / intraday / bulk
|
|
216
|
+
# ---------------------------------------------------------------------------
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
@connector(output=_EOD_OUTPUT, tags=["eodhd", "equity"], secrets=("api_key",))
|
|
220
|
+
def eodhd_eod(
|
|
221
|
+
ticker: Annotated[str, Namespace("eodhd_symbols")],
|
|
222
|
+
from_date: str | None = None,
|
|
223
|
+
to_date: str | None = None,
|
|
224
|
+
period: Literal["d", "w", "m"] | None = None,
|
|
225
|
+
api_key: str = "",
|
|
226
|
+
) -> pd.DataFrame:
|
|
227
|
+
"""[Free+] Fetch end-of-day OHLCV prices for a ticker. Supports daily, weekly,
|
|
228
|
+
and monthly aggregation via period. Use from_date/to_date (ISO 8601) to limit
|
|
229
|
+
the range. Empty result may indicate an invalid ticker or exchange code —
|
|
230
|
+
verify with eodhd_search first. Free tier is limited to ~1 year of history.
|
|
231
|
+
"""
|
|
232
|
+
t = _safe_path_token(ticker, "ticker")
|
|
233
|
+
http = _client(api_key)
|
|
234
|
+
data = eodhd_get(
|
|
235
|
+
http,
|
|
236
|
+
path=f"eod/{t}",
|
|
237
|
+
params={"from": from_date, "to": to_date, "period": period},
|
|
238
|
+
op_name="eodhd_eod",
|
|
239
|
+
)
|
|
240
|
+
df = _rows_to_frame(data, "eodhd_eod", {"ticker": t})
|
|
241
|
+
return _select_declared(df, _EOD_OUTPUT)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
@connector(output=_LIVE_OUTPUT, tags=["eodhd", "equity", "tool"], secrets=("api_key",))
|
|
245
|
+
def eodhd_live(ticker: Annotated[str, Namespace("eodhd_symbols")], api_key: str = "") -> pd.DataFrame:
|
|
246
|
+
"""[Free+] Fetch the live (real-time or 15-min delayed) quote for a ticker:
|
|
247
|
+
code, timestamp, OHLC, volume, previous close, and change. Use eodhd_search
|
|
248
|
+
to resolve a company name to its EODHD ticker (e.g. AAPL.US).
|
|
249
|
+
"""
|
|
250
|
+
t = _safe_path_token(ticker, "ticker")
|
|
251
|
+
http = _client(api_key, timeout=_LATENCY_TIMEOUT)
|
|
252
|
+
data = eodhd_get(http, path=f"real-time/{t}", op_name="eodhd_live")
|
|
253
|
+
if not isinstance(data, dict) or not data.get("code"):
|
|
254
|
+
raise EmptyDataError(_PROVIDER, query_params={"ticker": t})
|
|
255
|
+
df = pd.DataFrame([data])
|
|
256
|
+
return _select_declared(df, _LIVE_OUTPUT)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
@connector(output=_INTRADAY_OUTPUT, tags=["eodhd", "equity"], secrets=("api_key",))
|
|
260
|
+
def eodhd_intraday(
|
|
261
|
+
ticker: Annotated[str, Namespace("eodhd_symbols")],
|
|
262
|
+
interval: Literal["1m", "5m", "1h"],
|
|
263
|
+
from_unix: int | None = None,
|
|
264
|
+
to_unix: int | None = None,
|
|
265
|
+
api_key: str = "",
|
|
266
|
+
) -> pd.DataFrame:
|
|
267
|
+
"""[EOD+Intraday+] Fetch intraday OHLCV data for a ticker at 1m, 5m, or 1h
|
|
268
|
+
intervals. Provide from_unix / to_unix (Unix timestamps in seconds) to bound
|
|
269
|
+
the range; otherwise the most recent points are returned. Requires a paid
|
|
270
|
+
EOD+Intraday plan — a free key returns PaymentRequiredError.
|
|
271
|
+
"""
|
|
272
|
+
t = _safe_path_token(ticker, "ticker")
|
|
273
|
+
http = _client(api_key, timeout=_LATENCY_TIMEOUT)
|
|
274
|
+
data = eodhd_get(
|
|
275
|
+
http,
|
|
276
|
+
path=f"intraday/{t}",
|
|
277
|
+
params={"interval": interval, "from": from_unix, "to": to_unix},
|
|
278
|
+
op_name="eodhd_intraday",
|
|
279
|
+
)
|
|
280
|
+
df = _rows_to_frame(data, "eodhd_intraday", {"ticker": t})
|
|
281
|
+
return _select_declared(df, _INTRADAY_OUTPUT)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
@connector(output=_BULK_EOD_OUTPUT, tags=["eodhd", "equity"], secrets=("api_key",))
|
|
285
|
+
def eodhd_bulk_eod(exchange: str, date: str | None = None, api_key: str = "") -> pd.DataFrame:
|
|
286
|
+
"""[EOD Historical+] Fetch end-of-day prices for every symbol on an exchange
|
|
287
|
+
in one request (returns code, name, date, OHLCV). Defaults to the last
|
|
288
|
+
trading day; pass date to fetch a specific day. Large response — use for
|
|
289
|
+
batch ingestion, not per-ticker lookups. Requires a paid plan; a free key
|
|
290
|
+
returns PaymentRequiredError.
|
|
291
|
+
"""
|
|
292
|
+
ex = _safe_path_token(exchange, "exchange")
|
|
293
|
+
http = _client(api_key, timeout=_BULK_TIMEOUT)
|
|
294
|
+
data = eodhd_get(
|
|
295
|
+
http,
|
|
296
|
+
path=f"eod-bulk-last-day/{ex}",
|
|
297
|
+
params={"date": date},
|
|
298
|
+
op_name="eodhd_bulk_eod",
|
|
299
|
+
)
|
|
300
|
+
df = _rows_to_frame(data, "eodhd_bulk_eod", {"exchange": ex})
|
|
301
|
+
return _select_declared(df, _BULK_EOD_OUTPUT)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
# ---------------------------------------------------------------------------
|
|
305
|
+
# Corporate Actions — dividends / splits
|
|
306
|
+
# ---------------------------------------------------------------------------
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
@connector(output=_DIVIDENDS_OUTPUT, tags=["eodhd", "equity"], secrets=("api_key",))
|
|
310
|
+
def eodhd_dividends(
|
|
311
|
+
ticker: Annotated[str, Namespace("eodhd_symbols")],
|
|
312
|
+
from_date: str | None = None,
|
|
313
|
+
to_date: str | None = None,
|
|
314
|
+
api_key: str = "",
|
|
315
|
+
) -> pd.DataFrame:
|
|
316
|
+
"""[Free+] Fetch dividend history for a ticker: ex-date, declaration/record/
|
|
317
|
+
payment dates, period, value, unadjusted value, and currency. Use from_date/
|
|
318
|
+
to_date (ISO 8601) to limit the range.
|
|
319
|
+
"""
|
|
320
|
+
t = _safe_path_token(ticker, "ticker")
|
|
321
|
+
http = _client(api_key)
|
|
322
|
+
data = eodhd_get(
|
|
323
|
+
http,
|
|
324
|
+
path=f"div/{t}",
|
|
325
|
+
params={"from": from_date, "to": to_date},
|
|
326
|
+
op_name="eodhd_dividends",
|
|
327
|
+
)
|
|
328
|
+
df = _rows_to_frame(data, "eodhd_dividends", {"ticker": t})
|
|
329
|
+
return _select_declared(df, _DIVIDENDS_OUTPUT)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
@connector(output=_SPLITS_OUTPUT, tags=["eodhd", "equity"], secrets=("api_key",))
|
|
333
|
+
def eodhd_splits(
|
|
334
|
+
ticker: Annotated[str, Namespace("eodhd_symbols")],
|
|
335
|
+
from_date: str | None = None,
|
|
336
|
+
to_date: str | None = None,
|
|
337
|
+
api_key: str = "",
|
|
338
|
+
) -> pd.DataFrame:
|
|
339
|
+
"""[Free+] Fetch stock split history for a ticker. The split column carries the
|
|
340
|
+
ratio string as returned by the API (e.g. "2.000000/1.000000" for a 2-for-1
|
|
341
|
+
split). Use from_date/to_date (ISO 8601) to limit the range.
|
|
342
|
+
"""
|
|
343
|
+
t = _safe_path_token(ticker, "ticker")
|
|
344
|
+
http = _client(api_key)
|
|
345
|
+
data = eodhd_get(
|
|
346
|
+
http,
|
|
347
|
+
path=f"splits/{t}",
|
|
348
|
+
params={"from": from_date, "to": to_date},
|
|
349
|
+
op_name="eodhd_splits",
|
|
350
|
+
)
|
|
351
|
+
df = _rows_to_frame(data, "eodhd_splits", {"ticker": t})
|
|
352
|
+
return _select_declared(df, _SPLITS_OUTPUT)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
# ---------------------------------------------------------------------------
|
|
356
|
+
# Fundamentals — raw nested dict (no output schema)
|
|
357
|
+
# ---------------------------------------------------------------------------
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
@connector(tags=["eodhd", "equity"], secrets=("api_key",))
|
|
361
|
+
def eodhd_fundamentals(ticker: Annotated[str, Namespace("eodhd_symbols")], api_key: str = "") -> dict[str, Any]:
|
|
362
|
+
"""[Fundamentals+] Fetch full fundamentals for a stock or ETF as a large nested
|
|
363
|
+
dict (not a DataFrame). Typical equity top-level keys: General, Highlights,
|
|
364
|
+
Valuation, SharesStats, Technicals, SplitsDividends, AnalystRatings, Holders,
|
|
365
|
+
InsiderTransactions, Financials, Earnings. ETFs differ (General, Technicals,
|
|
366
|
+
ETF_Data). Navigate via result.data, e.g.
|
|
367
|
+
result.data['Highlights']['MarketCapitalization']. Requires a paid plan; a
|
|
368
|
+
free key returns PaymentRequiredError.
|
|
369
|
+
"""
|
|
370
|
+
t = _safe_path_token(ticker, "ticker")
|
|
371
|
+
http = _client(api_key, timeout=_BULK_TIMEOUT)
|
|
372
|
+
data = eodhd_get(http, path=f"fundamentals/{t}", op_name="eodhd_fundamentals")
|
|
373
|
+
if not isinstance(data, dict) or not data:
|
|
374
|
+
raise EmptyDataError(_PROVIDER, query_params={"ticker": t})
|
|
375
|
+
return data
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
# ---------------------------------------------------------------------------
|
|
379
|
+
# Calendars — earnings / ipos / trends / splits
|
|
380
|
+
# ---------------------------------------------------------------------------
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
@connector(output=_CALENDAR_OUTPUT, tags=["eodhd", "equity"], secrets=("api_key",))
|
|
384
|
+
def eodhd_calendar(
|
|
385
|
+
type: Literal["earnings", "ipos", "trends", "splits"],
|
|
386
|
+
from_date: str | None = None,
|
|
387
|
+
to_date: str | None = None,
|
|
388
|
+
symbols: str | None = None,
|
|
389
|
+
api_key: str = "",
|
|
390
|
+
) -> pd.DataFrame:
|
|
391
|
+
"""[Fundamentals+] Fetch market calendar data. Types: earnings (upcoming EPS
|
|
392
|
+
announcements with estimates/actuals), ipos (upcoming and recent IPO
|
|
393
|
+
listings), trends (analyst recommendation trends), splits (upcoming splits).
|
|
394
|
+
Use from_date/to_date to narrow the window and symbols to filter. Requires a
|
|
395
|
+
paid plan; a free key returns PaymentRequiredError.
|
|
396
|
+
"""
|
|
397
|
+
http = _client(api_key)
|
|
398
|
+
data = eodhd_get(
|
|
399
|
+
http,
|
|
400
|
+
path=f"calendar/{type}",
|
|
401
|
+
params={"from": from_date, "to": to_date, "symbols": symbols},
|
|
402
|
+
op_name="eodhd_calendar",
|
|
403
|
+
)
|
|
404
|
+
# The calendar endpoints wrap rows under a type-specific key.
|
|
405
|
+
if isinstance(data, dict):
|
|
406
|
+
for key in ("earnings", "ipos", "trends", "splits", "data"):
|
|
407
|
+
rows = data.get(key)
|
|
408
|
+
if isinstance(rows, list):
|
|
409
|
+
data = rows
|
|
410
|
+
break
|
|
411
|
+
df = _rows_to_frame(data, "eodhd_calendar", {"type": type})
|
|
412
|
+
return _select_declared(df, _CALENDAR_OUTPUT)
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
# ---------------------------------------------------------------------------
|
|
416
|
+
# News
|
|
417
|
+
# ---------------------------------------------------------------------------
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
@connector(output=_NEWS_OUTPUT, tags=["eodhd", "tool"], secrets=("api_key",))
|
|
421
|
+
def eodhd_news(
|
|
422
|
+
ticker: str | None = None,
|
|
423
|
+
from_date: str | None = None,
|
|
424
|
+
to_date: str | None = None,
|
|
425
|
+
limit: int = 50,
|
|
426
|
+
offset: int = 0,
|
|
427
|
+
api_key: str = "",
|
|
428
|
+
) -> pd.DataFrame:
|
|
429
|
+
"""[Free+] Fetch financial news articles (date, title, content, link, related
|
|
430
|
+
symbols, tags). Filter by ticker (e.g. AAPL.US) or omit for broad market
|
|
431
|
+
news. Use from_date/to_date for date filtering and limit/offset to page.
|
|
432
|
+
Empty result may indicate no news in the range for the ticker.
|
|
433
|
+
"""
|
|
434
|
+
if limit < 1 or limit > 1000:
|
|
435
|
+
raise InvalidParameterError(_PROVIDER, "limit must be between 1 and 1000")
|
|
436
|
+
if offset < 0:
|
|
437
|
+
raise InvalidParameterError(_PROVIDER, "offset must be non-negative")
|
|
438
|
+
|
|
439
|
+
http = _client(api_key)
|
|
440
|
+
data = eodhd_get(
|
|
441
|
+
http,
|
|
442
|
+
path="news",
|
|
443
|
+
params={
|
|
444
|
+
"s": ticker, # EODHD uses ``s=`` for symbol filtering on the news endpoint
|
|
445
|
+
"from": from_date,
|
|
446
|
+
"to": to_date,
|
|
447
|
+
"limit": limit,
|
|
448
|
+
"offset": offset,
|
|
449
|
+
},
|
|
450
|
+
op_name="eodhd_news",
|
|
451
|
+
)
|
|
452
|
+
df = _rows_to_frame(data, "eodhd_news", {"ticker": ticker or ""})
|
|
453
|
+
return _select_declared(df, _NEWS_OUTPUT)
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
# ---------------------------------------------------------------------------
|
|
457
|
+
# Macro indicators — single / bulk
|
|
458
|
+
# ---------------------------------------------------------------------------
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
@connector(output=_MACRO_OUTPUT, tags=["eodhd", "macro"], secrets=("api_key",))
|
|
462
|
+
def eodhd_macro(country: str, indicator: str, api_key: str = "") -> pd.DataFrame:
|
|
463
|
+
"""[Fundamentals+] Fetch a macro indicator time series for a country (returns
|
|
464
|
+
Date, Value, Period). Country must be an ISO 3-letter code (e.g. USA, DEU).
|
|
465
|
+
Common indicators: gdp_current_usd, unemployment_total_percent,
|
|
466
|
+
inflation_consumer_prices_annual, real_interest_rate, population_total.
|
|
467
|
+
Requires a paid plan; a free key returns PaymentRequiredError.
|
|
468
|
+
"""
|
|
469
|
+
c = _safe_path_token(country, "country")
|
|
470
|
+
ind = indicator.strip()
|
|
471
|
+
if not ind:
|
|
472
|
+
raise InvalidParameterError(_PROVIDER, "indicator must be non-empty")
|
|
473
|
+
http = _client(api_key)
|
|
474
|
+
data = eodhd_get(
|
|
475
|
+
http,
|
|
476
|
+
path=f"macro-indicator/{c}",
|
|
477
|
+
params={"indicator": ind},
|
|
478
|
+
op_name="eodhd_macro",
|
|
479
|
+
)
|
|
480
|
+
df = _rows_to_frame(data, "eodhd_macro", {"country": c, "indicator": ind})
|
|
481
|
+
return _select_declared(df, _MACRO_OUTPUT)
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
@connector(output=_MACRO_OUTPUT, tags=["eodhd", "macro"], secrets=("api_key",))
|
|
485
|
+
def eodhd_macro_bulk(country: str, indicator: str | None = None, api_key: str = "") -> pd.DataFrame:
|
|
486
|
+
"""[Fundamentals+] Fetch macro indicator data for a country. The EODHD
|
|
487
|
+
macro-indicator endpoint requires an indicator; pass one explicitly or rely
|
|
488
|
+
on the default. Returns Date, Value, Period. Country must be an ISO 3-letter
|
|
489
|
+
code (e.g. USA). Requires a paid plan; a free key returns
|
|
490
|
+
PaymentRequiredError.
|
|
491
|
+
"""
|
|
492
|
+
c = _safe_path_token(country, "country")
|
|
493
|
+
http = _client(api_key, timeout=_BULK_TIMEOUT)
|
|
494
|
+
data = eodhd_get(
|
|
495
|
+
http,
|
|
496
|
+
path=f"macro-indicator/{c}",
|
|
497
|
+
params={"indicator": indicator},
|
|
498
|
+
op_name="eodhd_macro_bulk",
|
|
499
|
+
)
|
|
500
|
+
df = _rows_to_frame(data, "eodhd_macro_bulk", {"country": c})
|
|
501
|
+
return _select_declared(df, _MACRO_OUTPUT)
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
# ---------------------------------------------------------------------------
|
|
505
|
+
# Technical indicators
|
|
506
|
+
# ---------------------------------------------------------------------------
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
@connector(output=_TECHNICAL_OUTPUT, tags=["eodhd", "equity"], secrets=("api_key",))
|
|
510
|
+
def eodhd_technical(
|
|
511
|
+
ticker: Annotated[str, Namespace("eodhd_symbols")],
|
|
512
|
+
function: _TechnicalFunction,
|
|
513
|
+
period: int = 50,
|
|
514
|
+
from_date: str | None = None,
|
|
515
|
+
to_date: str | None = None,
|
|
516
|
+
order: Literal["a", "d"] = "d",
|
|
517
|
+
api_key: str = "",
|
|
518
|
+
) -> pd.DataFrame:
|
|
519
|
+
"""[EOD+Intraday+] Fetch technical indicator values for a ticker. The output
|
|
520
|
+
columns vary by function: sma/ema/wma → the indicator column; macd → macd,
|
|
521
|
+
macd_signal, macd_hist; bbands → uband, mband, lband; stochastic → k/d;
|
|
522
|
+
adx/dmi → adx, plusDI, minusDI. Use period for the lookback window (default
|
|
523
|
+
50). Requires a paid plan; a free key returns PaymentRequiredError.
|
|
524
|
+
"""
|
|
525
|
+
t = _safe_path_token(ticker, "ticker")
|
|
526
|
+
if period < 1:
|
|
527
|
+
raise InvalidParameterError(_PROVIDER, "period must be a positive integer")
|
|
528
|
+
http = _client(api_key)
|
|
529
|
+
data = eodhd_get(
|
|
530
|
+
http,
|
|
531
|
+
path=f"technical/{t}",
|
|
532
|
+
params={
|
|
533
|
+
"function": function,
|
|
534
|
+
"period": period,
|
|
535
|
+
"order": order,
|
|
536
|
+
"from": from_date,
|
|
537
|
+
"to": to_date,
|
|
538
|
+
},
|
|
539
|
+
op_name="eodhd_technical",
|
|
540
|
+
)
|
|
541
|
+
df = _rows_to_frame(data, "eodhd_technical", {"ticker": t, "function": function})
|
|
542
|
+
return _select_declared(df, _TECHNICAL_OUTPUT)
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
# ---------------------------------------------------------------------------
|
|
546
|
+
# Insider transactions
|
|
547
|
+
# ---------------------------------------------------------------------------
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
@connector(output=_INSIDER_OUTPUT, tags=["eodhd", "equity"], secrets=("api_key",))
|
|
551
|
+
def eodhd_insider(
|
|
552
|
+
ticker: Annotated[str, Namespace("eodhd_symbols")] | None = None,
|
|
553
|
+
limit: int = 100,
|
|
554
|
+
offset: int = 0,
|
|
555
|
+
api_key: str = "",
|
|
556
|
+
) -> pd.DataFrame:
|
|
557
|
+
"""[Fundamentals+] Fetch insider (executive and director) transactions. Filter
|
|
558
|
+
by ticker (e.g. AAPL.US) or omit for recent cross-market transactions. Use
|
|
559
|
+
limit/offset to page. Requires a paid plan; a free key returns
|
|
560
|
+
PaymentRequiredError.
|
|
561
|
+
"""
|
|
562
|
+
if limit < 1 or limit > 1000:
|
|
563
|
+
raise InvalidParameterError(_PROVIDER, "limit must be between 1 and 1000")
|
|
564
|
+
if offset < 0:
|
|
565
|
+
raise InvalidParameterError(_PROVIDER, "offset must be non-negative")
|
|
566
|
+
code = ticker.strip() if ticker else None
|
|
567
|
+
http = _client(api_key)
|
|
568
|
+
data = eodhd_get(
|
|
569
|
+
http,
|
|
570
|
+
path="insider-transactions",
|
|
571
|
+
params={"code": code, "limit": limit, "offset": offset},
|
|
572
|
+
op_name="eodhd_insider",
|
|
573
|
+
)
|
|
574
|
+
df = _rows_to_frame(data, "eodhd_insider", {"ticker": code or ""})
|
|
575
|
+
return _select_declared(df, _INSIDER_OUTPUT)
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
# ---------------------------------------------------------------------------
|
|
579
|
+
# Screener
|
|
580
|
+
# ---------------------------------------------------------------------------
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
@connector(output=_SCREENER_OUTPUT, tags=["eodhd", "equity", "tool"], secrets=("api_key",))
|
|
584
|
+
def eodhd_screener(
|
|
585
|
+
filters: list[tuple[str, str, str]] | None = None,
|
|
586
|
+
signals: str | None = None,
|
|
587
|
+
sort: str | None = None,
|
|
588
|
+
order: Literal["asc", "desc"] = "desc",
|
|
589
|
+
limit: int = 50,
|
|
590
|
+
offset: int = 0,
|
|
591
|
+
api_key: str = "",
|
|
592
|
+
) -> pd.DataFrame:
|
|
593
|
+
"""[EOD+Intraday+] Screen stocks by fundamental, price, and exchange criteria.
|
|
594
|
+
filters is a list of [field, operator, value] triples, e.g.
|
|
595
|
+
[["market_capitalization", ">", "1000000000"], ["exchange", "=", "us"]].
|
|
596
|
+
Operators: >, <, =, >=, <=. Common fields: market_capitalization,
|
|
597
|
+
earnings_share, dividend_yield, refund_1d_p, sector, exchange. Requires a
|
|
598
|
+
paid plan; a free key returns PaymentRequiredError.
|
|
599
|
+
"""
|
|
600
|
+
if limit < 1 or limit > 100:
|
|
601
|
+
raise InvalidParameterError(_PROVIDER, "limit must be between 1 and 100")
|
|
602
|
+
if offset < 0:
|
|
603
|
+
raise InvalidParameterError(_PROVIDER, "offset must be non-negative")
|
|
604
|
+
|
|
605
|
+
params: dict[str, Any] = {"limit": limit, "offset": offset, "sort": sort, "signals": signals}
|
|
606
|
+
if filters:
|
|
607
|
+
params["filters"] = json.dumps([[f[0], f[1], f[2]] for f in filters])
|
|
608
|
+
if sort and order:
|
|
609
|
+
# EODHD sort syntax is ``field.direction`` (e.g. market_capitalization.desc).
|
|
610
|
+
params["sort"] = f"{sort}.{order}"
|
|
611
|
+
|
|
612
|
+
http = _client(api_key)
|
|
613
|
+
data = eodhd_get(http, path="screener", params=params, op_name="eodhd_screener")
|
|
614
|
+
# The screener wraps rows under a top-level ``data`` key.
|
|
615
|
+
if isinstance(data, dict):
|
|
616
|
+
rows = data.get("data")
|
|
617
|
+
if isinstance(rows, list):
|
|
618
|
+
data = rows
|
|
619
|
+
df = _rows_to_frame(data, "eodhd_screener", {"limit": limit})
|
|
620
|
+
return df
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
# ---------------------------------------------------------------------------
|
|
624
|
+
# Connector collection
|
|
625
|
+
# ---------------------------------------------------------------------------
|
|
626
|
+
|
|
627
|
+
CONNECTORS = Connectors(
|
|
628
|
+
[
|
|
629
|
+
# Reference / discovery
|
|
630
|
+
eodhd_search,
|
|
631
|
+
eodhd_exchanges,
|
|
632
|
+
eodhd_exchange_symbols,
|
|
633
|
+
# Market data
|
|
634
|
+
eodhd_eod,
|
|
635
|
+
eodhd_live,
|
|
636
|
+
eodhd_intraday,
|
|
637
|
+
eodhd_bulk_eod,
|
|
638
|
+
# Corporate actions
|
|
639
|
+
eodhd_dividends,
|
|
640
|
+
eodhd_splits,
|
|
641
|
+
# Fundamentals
|
|
642
|
+
eodhd_fundamentals,
|
|
643
|
+
# Calendars
|
|
644
|
+
eodhd_calendar,
|
|
645
|
+
# News
|
|
646
|
+
eodhd_news,
|
|
647
|
+
# Macro
|
|
648
|
+
eodhd_macro,
|
|
649
|
+
eodhd_macro_bulk,
|
|
650
|
+
# Technical
|
|
651
|
+
eodhd_technical,
|
|
652
|
+
# Transactions
|
|
653
|
+
eodhd_insider,
|
|
654
|
+
# Screener
|
|
655
|
+
eodhd_screener,
|
|
656
|
+
]
|
|
657
|
+
)
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
def load(*, api_key: str) -> Connectors:
|
|
661
|
+
"""Return :data:`CONNECTORS` with ``api_key`` bound on every connector that accepts it."""
|
|
662
|
+
return CONNECTORS.bind(api_key=api_key)
|
parsimony_eodhd/_http.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""EODHD transport — keyed client builder and unified error mapping.
|
|
2
|
+
|
|
3
|
+
Every EODHD connector resolves its client through :func:`_client` (the
|
|
4
|
+
canonical §4.3 keyed template: arg → env fallback → fast-fail) and routes its
|
|
5
|
+
GET through :func:`eodhd_get` (the package error-mapping chokepoint).
|
|
6
|
+
|
|
7
|
+
EODHD's status semantics differ from the canonical transport table on two
|
|
8
|
+
points, which is why this package drops to a raw ``HttpClient`` plus a
|
|
9
|
+
hand-written mapper instead of :func:`parsimony.transport.helpers.fetch_json`
|
|
10
|
+
(verified live 2026-06-04):
|
|
11
|
+
|
|
12
|
+
* an **invalid / missing** key returns **401** (body ``Unauthenticated``), and
|
|
13
|
+
* a **plan restriction** (an endpoint or range not in the caller's plan, e.g.
|
|
14
|
+
fundamentals / intraday / macro on a free key) returns **403** (body
|
|
15
|
+
``Only EOD data allowed for free users``), and
|
|
16
|
+
* a **bulk** plan restriction returns **423 Locked** (body
|
|
17
|
+
``Bulk requests are prohibited for free users``).
|
|
18
|
+
|
|
19
|
+
The canonical mapper folds 403 into :class:`UnauthorizedError`; for EODHD a 403
|
|
20
|
+
(and a 423) means "your plan does not grant this," so both map to
|
|
21
|
+
:class:`PaymentRequiredError`. Because invalid-key is unambiguously 401, this is
|
|
22
|
+
a status-only disambiguation (the finnhub case), not a body-sniffing one (the
|
|
23
|
+
tiingo dual-403 case). The 401 path still maps to :class:`UnauthorizedError`.
|
|
24
|
+
Every other status flows through the canonical :func:`map_http_error` /
|
|
25
|
+
:func:`map_timeout_error`.
|
|
26
|
+
|
|
27
|
+
Auth rides as the ``api_token`` query parameter (alongside EODHD's ``fmt=json``
|
|
28
|
+
convention). ``api_token`` is in the transport layer's sensitive-param set, so
|
|
29
|
+
it is redacted from every log line and never appears in a request URL surfaced
|
|
30
|
+
to the agent. Error bodies are ``text/html`` even on failures, so this module
|
|
31
|
+
never parses an error body — it branches on the HTTP status alone.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
from typing import Any
|
|
37
|
+
|
|
38
|
+
import httpx
|
|
39
|
+
from parsimony.errors import PaymentRequiredError
|
|
40
|
+
from parsimony.transport import HttpClient, map_http_error, map_timeout_error
|
|
41
|
+
from parsimony.transport.helpers import make_http_client, require_key
|
|
42
|
+
|
|
43
|
+
_PROVIDER = "eodhd"
|
|
44
|
+
_BASE_URL = "https://eodhd.com/api"
|
|
45
|
+
_ENV_VAR = "EODHD_API_KEY"
|
|
46
|
+
|
|
47
|
+
# EODHD's REST endpoints are not streaming; 15s is a conservative ceiling.
|
|
48
|
+
# Bulk endpoints (bulk_eod, exchange_symbols, macro_bulk, fundamentals) override
|
|
49
|
+
# this with a longer value via ``timeout=``.
|
|
50
|
+
_DEFAULT_TIMEOUT_SECONDS = 15.0
|
|
51
|
+
|
|
52
|
+
# HTTP statuses EODHD uses for a plan-tier restriction (not a credential
|
|
53
|
+
# failure): 403 ("Only EOD data allowed for free users") and 423 Locked
|
|
54
|
+
# ("Bulk requests are prohibited for free users"). Both map to
|
|
55
|
+
# PaymentRequiredError. Invalid-key is 401, so this is unambiguous on status.
|
|
56
|
+
_PLAN_RESTRICTION_STATUSES: frozenset[int] = frozenset({403, 423})
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _client(api_key: str, *, timeout: float = _DEFAULT_TIMEOUT_SECONDS) -> HttpClient:
|
|
60
|
+
"""Resolve the API key (arg → env fallback) and build the EODHD client.
|
|
61
|
+
|
|
62
|
+
Fast-fails with :class:`UnauthorizedError` before any network call when no
|
|
63
|
+
key is available. Auth is the ``api_token`` query parameter (redacted by the
|
|
64
|
+
transport layer), carried alongside EODHD's ``fmt=json`` convention as a
|
|
65
|
+
fixed default param — hence ``make_http_client`` with explicit
|
|
66
|
+
``query_params`` rather than ``make_api_key_client`` (which can set only the
|
|
67
|
+
key and hardcodes ``apikey``).
|
|
68
|
+
"""
|
|
69
|
+
key = require_key(api_key, env_var=_ENV_VAR, provider=_PROVIDER)
|
|
70
|
+
return make_http_client(
|
|
71
|
+
_BASE_URL,
|
|
72
|
+
query_params={"api_token": key, "fmt": "json"},
|
|
73
|
+
timeout=timeout,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def eodhd_get(
|
|
78
|
+
http: HttpClient,
|
|
79
|
+
*,
|
|
80
|
+
path: str,
|
|
81
|
+
params: dict[str, Any] | None = None,
|
|
82
|
+
op_name: str,
|
|
83
|
+
) -> Any:
|
|
84
|
+
"""Shared EODHD GET with EODHD-specific error mapping; returns parsed JSON.
|
|
85
|
+
|
|
86
|
+
Drops ``None``-valued params, raises for status, then maps:
|
|
87
|
+
|
|
88
|
+
* 403 / 423 → :class:`PaymentRequiredError` (plan restriction — EODHD-specific),
|
|
89
|
+
* everything else → :func:`map_http_error` (401 → Unauthorized, 402 →
|
|
90
|
+
Payment, 429 → RateLimit, other → Provider),
|
|
91
|
+
* timeout → :func:`map_timeout_error` (→ ``ProviderError(408)``).
|
|
92
|
+
|
|
93
|
+
Error bodies are ``text/html`` and are never parsed (the EODHD API token is
|
|
94
|
+
a query param, redacted, and never reaches an exception message).
|
|
95
|
+
"""
|
|
96
|
+
filtered = {k: v for k, v in (params or {}).items() if v is not None}
|
|
97
|
+
try:
|
|
98
|
+
response = http.request("GET", f"/{path.lstrip('/')}", params=filtered or None)
|
|
99
|
+
response.raise_for_status()
|
|
100
|
+
except httpx.HTTPStatusError as exc:
|
|
101
|
+
if exc.response.status_code in _PLAN_RESTRICTION_STATUSES:
|
|
102
|
+
raise PaymentRequiredError(
|
|
103
|
+
_PROVIDER,
|
|
104
|
+
message=f"eodhd plan does not grant access to '{op_name}'",
|
|
105
|
+
) from exc
|
|
106
|
+
map_http_error(exc, provider=_PROVIDER, op_name=op_name)
|
|
107
|
+
except httpx.TimeoutException as exc:
|
|
108
|
+
map_timeout_error(exc, provider=_PROVIDER, op_name=op_name)
|
|
109
|
+
return response.json()
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
__all__ = ["_client", "eodhd_get"]
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""Declarative output schemas for the EODHD connectors.
|
|
2
|
+
|
|
3
|
+
One :class:`OutputConfig` per connector that projects a shaped DataFrame
|
|
4
|
+
out of EODHD's raw JSON. Columns declared here are the contract with the
|
|
5
|
+
MCP tool catalog — renaming or re-ordering them is a breaking change.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from parsimony.result import Column, ColumnRole, OutputConfig
|
|
11
|
+
|
|
12
|
+
EOD_OUTPUT = OutputConfig(
|
|
13
|
+
columns=[
|
|
14
|
+
Column(name="date", role=ColumnRole.KEY, dtype="date"),
|
|
15
|
+
Column(name="open", dtype="numeric"),
|
|
16
|
+
Column(name="high", dtype="numeric"),
|
|
17
|
+
Column(name="low", dtype="numeric"),
|
|
18
|
+
Column(name="close", dtype="numeric"),
|
|
19
|
+
Column(name="adjusted_close", dtype="numeric"),
|
|
20
|
+
Column(name="volume", dtype="numeric"),
|
|
21
|
+
]
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
LIVE_OUTPUT = OutputConfig(
|
|
25
|
+
columns=[
|
|
26
|
+
Column(name="code", role=ColumnRole.KEY),
|
|
27
|
+
# EODHD returns ``timestamp`` as a raw Unix epoch int; keep it un-coerced
|
|
28
|
+
# (dtype="auto") — a "timestamp" coercion would mis-read the seconds value.
|
|
29
|
+
Column(name="timestamp", role=ColumnRole.METADATA),
|
|
30
|
+
Column(name="gmtoffset", role=ColumnRole.METADATA),
|
|
31
|
+
Column(name="open", dtype="numeric"),
|
|
32
|
+
Column(name="high", dtype="numeric"),
|
|
33
|
+
Column(name="low", dtype="numeric"),
|
|
34
|
+
Column(name="close", dtype="numeric"),
|
|
35
|
+
Column(name="volume", dtype="numeric"),
|
|
36
|
+
Column(name="previousClose", dtype="numeric"),
|
|
37
|
+
Column(name="change", dtype="numeric"),
|
|
38
|
+
Column(name="change_p", dtype="numeric"),
|
|
39
|
+
]
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
INTRADAY_OUTPUT = OutputConfig(
|
|
43
|
+
columns=[
|
|
44
|
+
Column(name="timestamp", role=ColumnRole.KEY, dtype="timestamp"),
|
|
45
|
+
Column(name="datetime", role=ColumnRole.METADATA),
|
|
46
|
+
Column(name="open", dtype="numeric"),
|
|
47
|
+
Column(name="high", dtype="numeric"),
|
|
48
|
+
Column(name="low", dtype="numeric"),
|
|
49
|
+
Column(name="close", dtype="numeric"),
|
|
50
|
+
Column(name="volume", dtype="numeric"),
|
|
51
|
+
]
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
BULK_EOD_OUTPUT = OutputConfig(
|
|
55
|
+
columns=[
|
|
56
|
+
Column(name="code", role=ColumnRole.KEY),
|
|
57
|
+
Column(name="name", role=ColumnRole.TITLE),
|
|
58
|
+
Column(name="exchange_short_name", role=ColumnRole.METADATA),
|
|
59
|
+
Column(name="date", dtype="date"),
|
|
60
|
+
Column(name="open", dtype="numeric"),
|
|
61
|
+
Column(name="high", dtype="numeric"),
|
|
62
|
+
Column(name="low", dtype="numeric"),
|
|
63
|
+
Column(name="close", dtype="numeric"),
|
|
64
|
+
Column(name="adjusted_close", dtype="numeric"),
|
|
65
|
+
Column(name="volume", dtype="numeric"),
|
|
66
|
+
]
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
DIVIDENDS_OUTPUT = OutputConfig(
|
|
70
|
+
columns=[
|
|
71
|
+
Column(name="date", role=ColumnRole.KEY, dtype="date"),
|
|
72
|
+
Column(name="declarationDate", dtype="date"),
|
|
73
|
+
Column(name="recordDate", dtype="date"),
|
|
74
|
+
Column(name="paymentDate", dtype="date"),
|
|
75
|
+
Column(name="period", role=ColumnRole.METADATA),
|
|
76
|
+
Column(name="value", dtype="numeric"),
|
|
77
|
+
Column(name="unadjustedValue", dtype="numeric"),
|
|
78
|
+
Column(name="currency", role=ColumnRole.METADATA),
|
|
79
|
+
]
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
SPLITS_OUTPUT = OutputConfig(
|
|
83
|
+
columns=[
|
|
84
|
+
Column(name="date", role=ColumnRole.KEY, dtype="date"),
|
|
85
|
+
Column(name="split", dtype="auto"),
|
|
86
|
+
]
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
SEARCH_OUTPUT = OutputConfig(
|
|
90
|
+
columns=[
|
|
91
|
+
Column(name="Code", role=ColumnRole.KEY, namespace="eodhd_symbols"),
|
|
92
|
+
Column(name="Name", role=ColumnRole.TITLE),
|
|
93
|
+
Column(name="Exchange", role=ColumnRole.METADATA),
|
|
94
|
+
Column(name="Type", role=ColumnRole.METADATA),
|
|
95
|
+
Column(name="Country", role=ColumnRole.METADATA),
|
|
96
|
+
Column(name="Currency", role=ColumnRole.METADATA),
|
|
97
|
+
Column(name="ISIN", role=ColumnRole.METADATA),
|
|
98
|
+
]
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
EXCHANGES_OUTPUT = OutputConfig(
|
|
102
|
+
columns=[
|
|
103
|
+
Column(name="Code", role=ColumnRole.KEY),
|
|
104
|
+
Column(name="Name", role=ColumnRole.TITLE),
|
|
105
|
+
Column(name="OperatingMIC", role=ColumnRole.METADATA),
|
|
106
|
+
Column(name="Country", role=ColumnRole.METADATA),
|
|
107
|
+
Column(name="Currency", role=ColumnRole.METADATA),
|
|
108
|
+
Column(name="CountryISO2", role=ColumnRole.METADATA),
|
|
109
|
+
Column(name="CountryISO3", role=ColumnRole.METADATA),
|
|
110
|
+
]
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
EXCHANGE_SYMBOLS_OUTPUT = OutputConfig(
|
|
114
|
+
columns=[
|
|
115
|
+
Column(name="Code", role=ColumnRole.KEY, namespace="eodhd_symbols"),
|
|
116
|
+
Column(name="Name", role=ColumnRole.TITLE),
|
|
117
|
+
Column(name="Country", role=ColumnRole.METADATA),
|
|
118
|
+
Column(name="Exchange", role=ColumnRole.METADATA),
|
|
119
|
+
Column(name="Currency", role=ColumnRole.METADATA),
|
|
120
|
+
Column(name="Type", role=ColumnRole.METADATA),
|
|
121
|
+
# Live payload key is ``Isin`` (not ``ISIN`` as on the search endpoint).
|
|
122
|
+
Column(name="Isin", role=ColumnRole.METADATA),
|
|
123
|
+
]
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
CALENDAR_OUTPUT = OutputConfig(
|
|
127
|
+
columns=[
|
|
128
|
+
Column(name="code", role=ColumnRole.KEY),
|
|
129
|
+
Column(name="date", dtype="date"),
|
|
130
|
+
Column(name="report_date", dtype="date"),
|
|
131
|
+
Column(name="before_after_market", role=ColumnRole.METADATA),
|
|
132
|
+
Column(name="currency", role=ColumnRole.METADATA),
|
|
133
|
+
Column(name="actual", dtype="numeric"),
|
|
134
|
+
Column(name="estimate", dtype="numeric"),
|
|
135
|
+
Column(name="difference", dtype="numeric"),
|
|
136
|
+
Column(name="percent", dtype="numeric"),
|
|
137
|
+
]
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
NEWS_OUTPUT = OutputConfig(
|
|
141
|
+
columns=[
|
|
142
|
+
Column(name="date", role=ColumnRole.KEY, dtype="datetime"),
|
|
143
|
+
Column(name="title", role=ColumnRole.TITLE),
|
|
144
|
+
Column(name="content"),
|
|
145
|
+
Column(name="link", role=ColumnRole.METADATA),
|
|
146
|
+
Column(name="symbols", role=ColumnRole.METADATA),
|
|
147
|
+
Column(name="tags", role=ColumnRole.METADATA),
|
|
148
|
+
]
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
MACRO_OUTPUT = OutputConfig(
|
|
152
|
+
columns=[
|
|
153
|
+
# EODHD macro-indicator rows: CountryCode, CountryName, Indicator, Date,
|
|
154
|
+
# Period, Value. (No LastUpdated — do not declare a column the payload
|
|
155
|
+
# cannot populate.) CountryName/Indicator/CountryCode fold in as DATA.
|
|
156
|
+
Column(name="Date", role=ColumnRole.KEY, dtype="date"),
|
|
157
|
+
Column(name="Value", dtype="numeric"),
|
|
158
|
+
Column(name="Period", role=ColumnRole.METADATA),
|
|
159
|
+
]
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
TECHNICAL_OUTPUT = OutputConfig(
|
|
163
|
+
columns=[
|
|
164
|
+
Column(name="date", role=ColumnRole.KEY, dtype="date"),
|
|
165
|
+
Column(name="open", dtype="numeric"),
|
|
166
|
+
Column(name="high", dtype="numeric"),
|
|
167
|
+
Column(name="low", dtype="numeric"),
|
|
168
|
+
Column(name="close", dtype="numeric"),
|
|
169
|
+
Column(name="volume", dtype="numeric"),
|
|
170
|
+
Column(name="*"), # indicator-specific columns vary by function
|
|
171
|
+
]
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
INSIDER_OUTPUT = OutputConfig(
|
|
175
|
+
columns=[
|
|
176
|
+
Column(name="code", role=ColumnRole.KEY),
|
|
177
|
+
Column(name="date", dtype="date"),
|
|
178
|
+
Column(name="ownerName", role=ColumnRole.METADATA),
|
|
179
|
+
Column(name="ownerCik", role=ColumnRole.METADATA),
|
|
180
|
+
Column(name="transactionType", role=ColumnRole.METADATA),
|
|
181
|
+
Column(name="transactionDate", dtype="date"),
|
|
182
|
+
Column(name="value", dtype="numeric"),
|
|
183
|
+
Column(name="sharesOwned", dtype="numeric"),
|
|
184
|
+
Column(name="change", dtype="numeric"),
|
|
185
|
+
Column(name="*"),
|
|
186
|
+
]
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
SCREENER_OUTPUT = OutputConfig(
|
|
190
|
+
columns=[
|
|
191
|
+
Column(name="code", role=ColumnRole.KEY, namespace="eodhd_symbols"),
|
|
192
|
+
Column(name="name", role=ColumnRole.TITLE),
|
|
193
|
+
Column(name="exchange", role=ColumnRole.METADATA),
|
|
194
|
+
Column(name="currency", role=ColumnRole.METADATA),
|
|
195
|
+
Column(name="sector", role=ColumnRole.METADATA),
|
|
196
|
+
Column(name="industry", role=ColumnRole.METADATA),
|
|
197
|
+
Column(name="market_capitalization", dtype="numeric"),
|
|
198
|
+
Column(name="*"),
|
|
199
|
+
]
|
|
200
|
+
)
|
parsimony_eodhd/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: parsimony-eodhd
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: EODHD connector for the parsimony framework
|
|
5
|
+
Project-URL: Homepage, https://eodhd.com
|
|
6
|
+
Project-URL: Repository, https://github.com/ockham-sh/parsimony-connectors
|
|
7
|
+
Project-URL: Issues, https://github.com/ockham-sh/parsimony-connectors/issues
|
|
8
|
+
Author-email: "Ockham.sh" <team@ockham.sh>
|
|
9
|
+
License-Expression: Apache-2.0
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: connectors,data,eodhd,finance,parsimony
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
15
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.11
|
|
24
|
+
Requires-Dist: pandas<3,>=2.3.0
|
|
25
|
+
Requires-Dist: parsimony-core>=0.0.1
|
|
26
|
+
Requires-Dist: pydantic<3,>=2.11.1
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest>=9.0.3; extra == 'dev'
|
|
31
|
+
Requires-Dist: respx>=0.22.0; extra == 'dev'
|
|
32
|
+
Requires-Dist: ruff>=0.15.10; extra == 'dev'
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
|
|
35
|
+
# parsimony-eodhd
|
|
36
|
+
|
|
37
|
+
EODHD connector — end-of-day, intraday, fundamentals, news, calendars, macro and technical indicators from the EODHD REST API.
|
|
38
|
+
|
|
39
|
+
Part of the [parsimony-connectors](https://github.com/ockham-sh/parsimony-connectors) monorepo. Distributed standalone on PyPI as `parsimony-eodhd`.
|
|
40
|
+
|
|
41
|
+
## Connectors
|
|
42
|
+
|
|
43
|
+
17 connectors grouped by capability:
|
|
44
|
+
|
|
45
|
+
| Name | Kind | Description |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| `eodhd_search` | fetch | Resolve company names / partial tickers to EODHD ticker codes (`AAPL.US`). |
|
|
48
|
+
| `eodhd_exchanges` | fetch | List supported exchanges. |
|
|
49
|
+
| `eodhd_exchange_symbols` | fetch | List all symbols on an exchange. |
|
|
50
|
+
| `eodhd_eod` | fetch | End-of-day OHLCV for a ticker (daily/weekly/monthly). |
|
|
51
|
+
| `eodhd_live` | fetch | Live (real-time or 15-min delayed) quote. |
|
|
52
|
+
| `eodhd_intraday` | fetch | Intraday OHLCV at 1m / 5m / 1h. |
|
|
53
|
+
| `eodhd_bulk_eod` | fetch | EOD prices for every symbol on an exchange in one request. |
|
|
54
|
+
| `eodhd_dividends` | fetch | Dividend history for a ticker. |
|
|
55
|
+
| `eodhd_splits` | fetch | Stock split history for a ticker. |
|
|
56
|
+
| `eodhd_fundamentals` | fetch | Full fundamentals for a stock or ETF (raw nested dict). |
|
|
57
|
+
| `eodhd_calendar` | fetch | Earnings / IPO / analyst trends / splits calendars. |
|
|
58
|
+
| `eodhd_news` | fetch | Financial news, optionally filtered by ticker. |
|
|
59
|
+
| `eodhd_macro` | fetch | Single macro indicator time series for a country. |
|
|
60
|
+
| `eodhd_macro_bulk` | fetch | All available macro indicators for a country. |
|
|
61
|
+
| `eodhd_technical` | fetch | Technical indicators (SMA, EMA, MACD, BBANDS, ADX, etc.). |
|
|
62
|
+
| `eodhd_insider` | fetch | Insider (executive / director) transactions. |
|
|
63
|
+
| `eodhd_screener` | fetch | Screen stocks by structured filter triples. |
|
|
64
|
+
|
|
65
|
+
Several endpoints require paid EODHD plans (EOD+Intraday, Fundamentals); per-connector docstrings tag the minimum plan as `[Free+]`, `[EOD+Intraday+]`, or `[Fundamentals+]`. On a free key a plan-gated endpoint returns HTTP 403 (or 423 for bulk), surfaced as `PaymentRequiredError` — not an auth error.
|
|
66
|
+
|
|
67
|
+
## Install
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
pip install parsimony-eodhd
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Pulls in `parsimony-core>=0.7,<0.8` automatically. Verify discovery:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
python -c "from parsimony import discover; print([p.name for p in discover.iter_providers()])"
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Configuration
|
|
80
|
+
|
|
81
|
+
Set the following environment variable:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
export EODHD_API_KEY="<your-key>"
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Get a key at https://eodhd.com/register.
|
|
88
|
+
|
|
89
|
+
## Quick start
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
import os
|
|
93
|
+
from parsimony_eodhd import load
|
|
94
|
+
|
|
95
|
+
# load() binds the API key off the call surface (and out of provenance).
|
|
96
|
+
connectors = load(api_key=os.environ["EODHD_API_KEY"])
|
|
97
|
+
result = connectors["eodhd_eod"](ticker="AAPL.US")
|
|
98
|
+
print(result.data.head())
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
For multi-plugin composition:
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from parsimony import discover
|
|
105
|
+
connectors = discover.load_all()
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Provider
|
|
109
|
+
|
|
110
|
+
- Homepage: https://eodhd.com
|
|
111
|
+
- API docs: https://eodhd.com/financial-apis/
|
|
112
|
+
|
|
113
|
+
## License
|
|
114
|
+
|
|
115
|
+
See [LICENSE](./LICENSE).
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
parsimony_eodhd/__init__.py,sha256=VBy1BqF_gRiR_bJnM-9Ac90ve0vCAXCJeVKvBWt6PJU,26000
|
|
2
|
+
parsimony_eodhd/_http.py,sha256=cp0_Rqussav7SAVPujMO28WJbUaunBcir7A0th0NaRs,5010
|
|
3
|
+
parsimony_eodhd/outputs.py,sha256=pBC5rXbGdQujWSGKRHyv5Vc0KLurPNbOYQ6vl88pNjM,7526
|
|
4
|
+
parsimony_eodhd/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
parsimony_eodhd-0.0.1.dist-info/METADATA,sha256=_SIciiZ78MGlKEQFuW3Uv75mXMeUgdvM7Rr7LsrCYiY,4339
|
|
6
|
+
parsimony_eodhd-0.0.1.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
7
|
+
parsimony_eodhd-0.0.1.dist-info/entry_points.txt,sha256=FKkSMd44tFkRnvcAbuEnWR0V0pC1PM81CQtuIeKaBtU,46
|
|
8
|
+
parsimony_eodhd-0.0.1.dist-info/licenses/LICENSE,sha256=PtHUFTCSwal_QX2Ijk2cx_bpsPV6ooZUMCYAxKBHNu0,10760
|
|
9
|
+
parsimony_eodhd-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to the Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by the Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or
|
|
95
|
+
Derivative Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work,
|
|
103
|
+
excluding those notices that do not pertain to any part of
|
|
104
|
+
the Derivative Works; and
|
|
105
|
+
|
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding any notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and
|
|
117
|
+
do not modify the License. You may add Your own attribution
|
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
+
that such additional attribution notices cannot be construed
|
|
121
|
+
as modifying the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
129
|
+
|
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
+
this License, without any additional terms or conditions.
|
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
+
the terms of any separate license agreement you may have executed
|
|
136
|
+
with Licensor regarding such Contributions.
|
|
137
|
+
|
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
+
except as required for reasonable and customary use in describing the
|
|
141
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
142
|
+
|
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
|
152
|
+
|
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
164
|
+
|
|
165
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
166
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
167
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
168
|
+
or other liability obligations and/or rights consistent with this
|
|
169
|
+
License. However, in accepting such obligations, You may act only
|
|
170
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
171
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
172
|
+
defend, and hold each Contributor harmless for any liability
|
|
173
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
174
|
+
of your accepting any such warranty or additional liability.
|
|
175
|
+
|
|
176
|
+
END OF TERMS AND CONDITIONS
|
|
177
|
+
|
|
178
|
+
Copyright 2026 Ockham.sh
|
|
179
|
+
|
|
180
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
181
|
+
you may not use this file except in compliance with the License.
|
|
182
|
+
You may obtain a copy of the License at
|
|
183
|
+
|
|
184
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
185
|
+
|
|
186
|
+
Unless required by applicable law or agreed to in writing, software
|
|
187
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
188
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
189
|
+
See the License for the specific language governing permissions and
|
|
190
|
+
limitations under the License.
|