eulerpool 1.0.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.
- eulerpool/__init__.py +281 -0
- eulerpool/_client.py +250 -0
- eulerpool/_version.py +3 -0
- eulerpool/errors.py +46 -0
- eulerpool/py.typed +0 -0
- eulerpool/resources/__init__.py +0 -0
- eulerpool/resources/_base.py +34 -0
- eulerpool/resources/aaq.py +18 -0
- eulerpool/resources/alternative.py +116 -0
- eulerpool/resources/analytics.py +50 -0
- eulerpool/resources/backtest.py +66 -0
- eulerpool/resources/bonds.py +36 -0
- eulerpool/resources/calendar.py +78 -0
- eulerpool/resources/certificates.py +30 -0
- eulerpool/resources/charting.py +42 -0
- eulerpool/resources/commodity.py +64 -0
- eulerpool/resources/crypto.py +30 -0
- eulerpool/resources/crypto_extended.py +246 -0
- eulerpool/resources/data.py +24 -0
- eulerpool/resources/datasets.py +100 -0
- eulerpool/resources/deals.py +28 -0
- eulerpool/resources/derivatives.py +68 -0
- eulerpool/resources/dex.py +54 -0
- eulerpool/resources/earning_calls.py +24 -0
- eulerpool/resources/ecb.py +34 -0
- eulerpool/resources/economic_forecasts.py +28 -0
- eulerpool/resources/energy.py +94 -0
- eulerpool/resources/equity.py +420 -0
- eulerpool/resources/equity_extended.py +150 -0
- eulerpool/resources/etf.py +60 -0
- eulerpool/resources/fair_value.py +18 -0
- eulerpool/resources/fixed_income.py +46 -0
- eulerpool/resources/forex.py +24 -0
- eulerpool/resources/fundamentals.py +60 -0
- eulerpool/resources/funds.py +32 -0
- eulerpool/resources/government.py +42 -0
- eulerpool/resources/ice_swap.py +18 -0
- eulerpool/resources/index.py +34 -0
- eulerpool/resources/institutional.py +60 -0
- eulerpool/resources/interest_rates.py +34 -0
- eulerpool/resources/macro.py +156 -0
- eulerpool/resources/market.py +214 -0
- eulerpool/resources/mutual_fund.py +42 -0
- eulerpool/resources/news.py +18 -0
- eulerpool/resources/nft.py +42 -0
- eulerpool/resources/partner.py +18 -0
- eulerpool/resources/patents.py +24 -0
- eulerpool/resources/peer_comparison.py +58 -0
- eulerpool/resources/portfolio.py +96 -0
- eulerpool/resources/portfolio_risk.py +58 -0
- eulerpool/resources/private_markets.py +24 -0
- eulerpool/resources/research.py +30 -0
- eulerpool/resources/risk_models.py +52 -0
- eulerpool/resources/screener.py +46 -0
- eulerpool/resources/sentiment.py +60 -0
- eulerpool/resources/shipping.py +58 -0
- eulerpool/resources/singapore.py +82 -0
- eulerpool/resources/transcripts.py +40 -0
- eulerpool/resources/trends.py +18 -0
- eulerpool/resources/vendor.py +28 -0
- eulerpool-1.0.0.dist-info/METADATA +146 -0
- eulerpool-1.0.0.dist-info/RECORD +64 -0
- eulerpool-1.0.0.dist-info/WHEEL +4 -0
- eulerpool-1.0.0.dist-info/licenses/LICENSE +21 -0
eulerpool/__init__.py
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
"""Eulerpool Financial Data API SDK for Python."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from typing import Any, Optional
|
|
7
|
+
|
|
8
|
+
from ._client import AsyncHttpClient, HttpClient
|
|
9
|
+
from ._version import __version__
|
|
10
|
+
from .errors import (
|
|
11
|
+
AuthenticationError,
|
|
12
|
+
BadRequestError,
|
|
13
|
+
EulerpoolError,
|
|
14
|
+
NotFoundError,
|
|
15
|
+
RateLimitError,
|
|
16
|
+
ServerError,
|
|
17
|
+
)
|
|
18
|
+
from .resources.aaq import Aaq, AsyncAaq
|
|
19
|
+
from .resources.alternative import Alternative, AsyncAlternative
|
|
20
|
+
from .resources.analytics import Analytics, AsyncAnalytics
|
|
21
|
+
from .resources.backtest import Backtest, AsyncBacktest
|
|
22
|
+
from .resources.bonds import Bonds, AsyncBonds
|
|
23
|
+
from .resources.calendar import Calendar, AsyncCalendar
|
|
24
|
+
from .resources.certificates import Certificates, AsyncCertificates
|
|
25
|
+
from .resources.charting import Charting, AsyncCharting
|
|
26
|
+
from .resources.commodity import Commodity, AsyncCommodity
|
|
27
|
+
from .resources.crypto import Crypto, AsyncCrypto
|
|
28
|
+
from .resources.crypto_extended import CryptoExtended, AsyncCryptoExtended
|
|
29
|
+
from .resources.data import Data, AsyncData
|
|
30
|
+
from .resources.datasets import Datasets, AsyncDatasets
|
|
31
|
+
from .resources.deals import Deals, AsyncDeals
|
|
32
|
+
from .resources.derivatives import Derivatives, AsyncDerivatives
|
|
33
|
+
from .resources.dex import Dex, AsyncDex
|
|
34
|
+
from .resources.earning_calls import EarningCalls, AsyncEarningCalls
|
|
35
|
+
from .resources.ecb import Ecb, AsyncEcb
|
|
36
|
+
from .resources.economic_forecasts import EconomicForecasts, AsyncEconomicForecasts
|
|
37
|
+
from .resources.energy import Energy, AsyncEnergy
|
|
38
|
+
from .resources.equity import Equity, AsyncEquity
|
|
39
|
+
from .resources.equity_extended import EquityExtended, AsyncEquityExtended
|
|
40
|
+
from .resources.etf import Etf, AsyncEtf
|
|
41
|
+
from .resources.fair_value import FairValue, AsyncFairValue
|
|
42
|
+
from .resources.fixed_income import FixedIncome, AsyncFixedIncome
|
|
43
|
+
from .resources.forex import Forex, AsyncForex
|
|
44
|
+
from .resources.fundamentals import Fundamentals, AsyncFundamentals
|
|
45
|
+
from .resources.funds import Funds, AsyncFunds
|
|
46
|
+
from .resources.government import Government, AsyncGovernment
|
|
47
|
+
from .resources.ice_swap import IceSwap, AsyncIceSwap
|
|
48
|
+
from .resources.index import Index, AsyncIndex
|
|
49
|
+
from .resources.institutional import Institutional, AsyncInstitutional
|
|
50
|
+
from .resources.interest_rates import InterestRates, AsyncInterestRates
|
|
51
|
+
from .resources.macro import Macro, AsyncMacro
|
|
52
|
+
from .resources.market import Market, AsyncMarket
|
|
53
|
+
from .resources.mutual_fund import MutualFund, AsyncMutualFund
|
|
54
|
+
from .resources.news import News, AsyncNews
|
|
55
|
+
from .resources.nft import Nft, AsyncNft
|
|
56
|
+
from .resources.partner import Partner, AsyncPartner
|
|
57
|
+
from .resources.patents import Patents, AsyncPatents
|
|
58
|
+
from .resources.peer_comparison import PeerComparison, AsyncPeerComparison
|
|
59
|
+
from .resources.portfolio import Portfolio, AsyncPortfolio
|
|
60
|
+
from .resources.portfolio_risk import PortfolioRisk, AsyncPortfolioRisk
|
|
61
|
+
from .resources.private_markets import PrivateMarkets, AsyncPrivateMarkets
|
|
62
|
+
from .resources.research import Research, AsyncResearch
|
|
63
|
+
from .resources.risk_models import RiskModels, AsyncRiskModels
|
|
64
|
+
from .resources.screener import Screener, AsyncScreener
|
|
65
|
+
from .resources.sentiment import Sentiment, AsyncSentiment
|
|
66
|
+
from .resources.shipping import Shipping, AsyncShipping
|
|
67
|
+
from .resources.singapore import Singapore, AsyncSingapore
|
|
68
|
+
from .resources.transcripts import Transcripts, AsyncTranscripts
|
|
69
|
+
from .resources.trends import Trends, AsyncTrends
|
|
70
|
+
from .resources.vendor import Vendor, AsyncVendor
|
|
71
|
+
|
|
72
|
+
__all__ = [
|
|
73
|
+
"Eulerpool",
|
|
74
|
+
"AsyncEulerpool",
|
|
75
|
+
"EulerpoolError",
|
|
76
|
+
"AuthenticationError",
|
|
77
|
+
"NotFoundError",
|
|
78
|
+
"RateLimitError",
|
|
79
|
+
"BadRequestError",
|
|
80
|
+
"ServerError",
|
|
81
|
+
"__version__",
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _require_key(api_key: Optional[str]) -> str:
|
|
86
|
+
key = api_key or os.environ.get("EULERPOOL_API_KEY")
|
|
87
|
+
if not key:
|
|
88
|
+
raise AuthenticationError(
|
|
89
|
+
"Pass an API key or set the EULERPOOL_API_KEY environment variable. "
|
|
90
|
+
"Get a free key at https://eulerpool.com/developers/register"
|
|
91
|
+
)
|
|
92
|
+
return key
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class Eulerpool:
|
|
96
|
+
"""Synchronous client for the Eulerpool Financial Data API."""
|
|
97
|
+
|
|
98
|
+
def __init__(
|
|
99
|
+
self,
|
|
100
|
+
api_key: Optional[str] = None,
|
|
101
|
+
*,
|
|
102
|
+
base_url: Optional[str] = None,
|
|
103
|
+
use_auth_header: bool = False,
|
|
104
|
+
max_retries: int = 2,
|
|
105
|
+
timeout: float = 30.0,
|
|
106
|
+
) -> None:
|
|
107
|
+
self._client = HttpClient(
|
|
108
|
+
_require_key(api_key),
|
|
109
|
+
base_url=base_url,
|
|
110
|
+
use_auth_header=use_auth_header,
|
|
111
|
+
max_retries=max_retries,
|
|
112
|
+
timeout=timeout,
|
|
113
|
+
)
|
|
114
|
+
self.aaq = Aaq(self._client)
|
|
115
|
+
self.alternative = Alternative(self._client)
|
|
116
|
+
self.analytics = Analytics(self._client)
|
|
117
|
+
self.backtest = Backtest(self._client)
|
|
118
|
+
self.bonds = Bonds(self._client)
|
|
119
|
+
self.calendar = Calendar(self._client)
|
|
120
|
+
self.certificates = Certificates(self._client)
|
|
121
|
+
self.charting = Charting(self._client)
|
|
122
|
+
self.commodity = Commodity(self._client)
|
|
123
|
+
self.crypto = Crypto(self._client)
|
|
124
|
+
self.crypto_extended = CryptoExtended(self._client)
|
|
125
|
+
self.data = Data(self._client)
|
|
126
|
+
self.datasets = Datasets(self._client)
|
|
127
|
+
self.deals = Deals(self._client)
|
|
128
|
+
self.derivatives = Derivatives(self._client)
|
|
129
|
+
self.dex = Dex(self._client)
|
|
130
|
+
self.earning_calls = EarningCalls(self._client)
|
|
131
|
+
self.ecb = Ecb(self._client)
|
|
132
|
+
self.economic_forecasts = EconomicForecasts(self._client)
|
|
133
|
+
self.energy = Energy(self._client)
|
|
134
|
+
self.equity = Equity(self._client)
|
|
135
|
+
self.equity_extended = EquityExtended(self._client)
|
|
136
|
+
self.etf = Etf(self._client)
|
|
137
|
+
self.fair_value = FairValue(self._client)
|
|
138
|
+
self.fixed_income = FixedIncome(self._client)
|
|
139
|
+
self.forex = Forex(self._client)
|
|
140
|
+
self.fundamentals = Fundamentals(self._client)
|
|
141
|
+
self.funds = Funds(self._client)
|
|
142
|
+
self.government = Government(self._client)
|
|
143
|
+
self.ice_swap = IceSwap(self._client)
|
|
144
|
+
self.index = Index(self._client)
|
|
145
|
+
self.institutional = Institutional(self._client)
|
|
146
|
+
self.interest_rates = InterestRates(self._client)
|
|
147
|
+
self.macro = Macro(self._client)
|
|
148
|
+
self.market = Market(self._client)
|
|
149
|
+
self.mutual_fund = MutualFund(self._client)
|
|
150
|
+
self.news = News(self._client)
|
|
151
|
+
self.nft = Nft(self._client)
|
|
152
|
+
self.partner = Partner(self._client)
|
|
153
|
+
self.patents = Patents(self._client)
|
|
154
|
+
self.peer_comparison = PeerComparison(self._client)
|
|
155
|
+
self.portfolio = Portfolio(self._client)
|
|
156
|
+
self.portfolio_risk = PortfolioRisk(self._client)
|
|
157
|
+
self.private_markets = PrivateMarkets(self._client)
|
|
158
|
+
self.research = Research(self._client)
|
|
159
|
+
self.risk_models = RiskModels(self._client)
|
|
160
|
+
self.screener = Screener(self._client)
|
|
161
|
+
self.sentiment = Sentiment(self._client)
|
|
162
|
+
self.shipping = Shipping(self._client)
|
|
163
|
+
self.singapore = Singapore(self._client)
|
|
164
|
+
self.transcripts = Transcripts(self._client)
|
|
165
|
+
self.trends = Trends(self._client)
|
|
166
|
+
self.vendor = Vendor(self._client)
|
|
167
|
+
self.aaqs = self.aaq
|
|
168
|
+
|
|
169
|
+
def get(self, path: str, **params: Any) -> Any:
|
|
170
|
+
"""Call any GET endpoint by path, e.g. client.get("/equity/profile/AAPL")."""
|
|
171
|
+
if not path.startswith("/"):
|
|
172
|
+
path = "/" + path
|
|
173
|
+
return self._client.get(path, params or None)
|
|
174
|
+
|
|
175
|
+
def post(self, path: str, body: Any = None, **params: Any) -> Any:
|
|
176
|
+
if not path.startswith("/"):
|
|
177
|
+
path = "/" + path
|
|
178
|
+
return self._client.post(path, body, params or None)
|
|
179
|
+
|
|
180
|
+
def close(self) -> None:
|
|
181
|
+
self._client.close()
|
|
182
|
+
|
|
183
|
+
def __enter__(self) -> "Eulerpool":
|
|
184
|
+
return self
|
|
185
|
+
|
|
186
|
+
def __exit__(self, *exc: object) -> None:
|
|
187
|
+
self.close()
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class AsyncEulerpool:
|
|
191
|
+
"""Async client for the Eulerpool Financial Data API."""
|
|
192
|
+
|
|
193
|
+
def __init__(
|
|
194
|
+
self,
|
|
195
|
+
api_key: Optional[str] = None,
|
|
196
|
+
*,
|
|
197
|
+
base_url: Optional[str] = None,
|
|
198
|
+
use_auth_header: bool = False,
|
|
199
|
+
max_retries: int = 2,
|
|
200
|
+
timeout: float = 30.0,
|
|
201
|
+
) -> None:
|
|
202
|
+
self._client = AsyncHttpClient(
|
|
203
|
+
_require_key(api_key),
|
|
204
|
+
base_url=base_url,
|
|
205
|
+
use_auth_header=use_auth_header,
|
|
206
|
+
max_retries=max_retries,
|
|
207
|
+
timeout=timeout,
|
|
208
|
+
)
|
|
209
|
+
self.aaq = AsyncAaq(self._client)
|
|
210
|
+
self.alternative = AsyncAlternative(self._client)
|
|
211
|
+
self.analytics = AsyncAnalytics(self._client)
|
|
212
|
+
self.backtest = AsyncBacktest(self._client)
|
|
213
|
+
self.bonds = AsyncBonds(self._client)
|
|
214
|
+
self.calendar = AsyncCalendar(self._client)
|
|
215
|
+
self.certificates = AsyncCertificates(self._client)
|
|
216
|
+
self.charting = AsyncCharting(self._client)
|
|
217
|
+
self.commodity = AsyncCommodity(self._client)
|
|
218
|
+
self.crypto = AsyncCrypto(self._client)
|
|
219
|
+
self.crypto_extended = AsyncCryptoExtended(self._client)
|
|
220
|
+
self.data = AsyncData(self._client)
|
|
221
|
+
self.datasets = AsyncDatasets(self._client)
|
|
222
|
+
self.deals = AsyncDeals(self._client)
|
|
223
|
+
self.derivatives = AsyncDerivatives(self._client)
|
|
224
|
+
self.dex = AsyncDex(self._client)
|
|
225
|
+
self.earning_calls = AsyncEarningCalls(self._client)
|
|
226
|
+
self.ecb = AsyncEcb(self._client)
|
|
227
|
+
self.economic_forecasts = AsyncEconomicForecasts(self._client)
|
|
228
|
+
self.energy = AsyncEnergy(self._client)
|
|
229
|
+
self.equity = AsyncEquity(self._client)
|
|
230
|
+
self.equity_extended = AsyncEquityExtended(self._client)
|
|
231
|
+
self.etf = AsyncEtf(self._client)
|
|
232
|
+
self.fair_value = AsyncFairValue(self._client)
|
|
233
|
+
self.fixed_income = AsyncFixedIncome(self._client)
|
|
234
|
+
self.forex = AsyncForex(self._client)
|
|
235
|
+
self.fundamentals = AsyncFundamentals(self._client)
|
|
236
|
+
self.funds = AsyncFunds(self._client)
|
|
237
|
+
self.government = AsyncGovernment(self._client)
|
|
238
|
+
self.ice_swap = AsyncIceSwap(self._client)
|
|
239
|
+
self.index = AsyncIndex(self._client)
|
|
240
|
+
self.institutional = AsyncInstitutional(self._client)
|
|
241
|
+
self.interest_rates = AsyncInterestRates(self._client)
|
|
242
|
+
self.macro = AsyncMacro(self._client)
|
|
243
|
+
self.market = AsyncMarket(self._client)
|
|
244
|
+
self.mutual_fund = AsyncMutualFund(self._client)
|
|
245
|
+
self.news = AsyncNews(self._client)
|
|
246
|
+
self.nft = AsyncNft(self._client)
|
|
247
|
+
self.partner = AsyncPartner(self._client)
|
|
248
|
+
self.patents = AsyncPatents(self._client)
|
|
249
|
+
self.peer_comparison = AsyncPeerComparison(self._client)
|
|
250
|
+
self.portfolio = AsyncPortfolio(self._client)
|
|
251
|
+
self.portfolio_risk = AsyncPortfolioRisk(self._client)
|
|
252
|
+
self.private_markets = AsyncPrivateMarkets(self._client)
|
|
253
|
+
self.research = AsyncResearch(self._client)
|
|
254
|
+
self.risk_models = AsyncRiskModels(self._client)
|
|
255
|
+
self.screener = AsyncScreener(self._client)
|
|
256
|
+
self.sentiment = AsyncSentiment(self._client)
|
|
257
|
+
self.shipping = AsyncShipping(self._client)
|
|
258
|
+
self.singapore = AsyncSingapore(self._client)
|
|
259
|
+
self.transcripts = AsyncTranscripts(self._client)
|
|
260
|
+
self.trends = AsyncTrends(self._client)
|
|
261
|
+
self.vendor = AsyncVendor(self._client)
|
|
262
|
+
self.aaqs = self.aaq
|
|
263
|
+
|
|
264
|
+
async def get(self, path: str, **params: Any) -> Any:
|
|
265
|
+
if not path.startswith("/"):
|
|
266
|
+
path = "/" + path
|
|
267
|
+
return await self._client.get(path, params or None)
|
|
268
|
+
|
|
269
|
+
async def post(self, path: str, body: Any = None, **params: Any) -> Any:
|
|
270
|
+
if not path.startswith("/"):
|
|
271
|
+
path = "/" + path
|
|
272
|
+
return await self._client.post(path, body, params or None)
|
|
273
|
+
|
|
274
|
+
async def close(self) -> None:
|
|
275
|
+
await self._client.close()
|
|
276
|
+
|
|
277
|
+
async def __aenter__(self) -> "AsyncEulerpool":
|
|
278
|
+
return self
|
|
279
|
+
|
|
280
|
+
async def __aexit__(self, *exc: object) -> None:
|
|
281
|
+
await self.close()
|
eulerpool/_client.py
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from typing import Any, Dict, Optional
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
from ._version import __version__
|
|
9
|
+
from .errors import (
|
|
10
|
+
AuthenticationError,
|
|
11
|
+
BadRequestError,
|
|
12
|
+
EulerpoolError,
|
|
13
|
+
NotFoundError,
|
|
14
|
+
RateLimitError,
|
|
15
|
+
ServerError,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
_PARAM_ALIASES = {"from_": "from", "class_": "class", "global_": "global"}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _normalize_params(params: Optional[Dict[str, Any]]) -> Dict[str, Any]:
|
|
22
|
+
q: Dict[str, Any] = {}
|
|
23
|
+
if not params:
|
|
24
|
+
return q
|
|
25
|
+
for key, value in params.items():
|
|
26
|
+
if value is None:
|
|
27
|
+
continue
|
|
28
|
+
q[_PARAM_ALIASES.get(key, key)] = value
|
|
29
|
+
return q
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class HttpClient:
|
|
33
|
+
"""Low-level synchronous HTTP client for the Eulerpool API."""
|
|
34
|
+
|
|
35
|
+
_BASE_URL = "https://api.eulerpool.com/api/1"
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
api_key: str,
|
|
40
|
+
*,
|
|
41
|
+
base_url: Optional[str] = None,
|
|
42
|
+
use_auth_header: bool = False,
|
|
43
|
+
max_retries: int = 2,
|
|
44
|
+
timeout: float = 30.0,
|
|
45
|
+
) -> None:
|
|
46
|
+
self._api_key = api_key
|
|
47
|
+
self._base_url = (base_url or self._BASE_URL).rstrip("/")
|
|
48
|
+
self._use_auth_header = use_auth_header
|
|
49
|
+
self._max_retries = max_retries
|
|
50
|
+
self._http = httpx.Client(
|
|
51
|
+
timeout=timeout,
|
|
52
|
+
follow_redirects=True,
|
|
53
|
+
headers={
|
|
54
|
+
"Accept": "application/json",
|
|
55
|
+
"User-Agent": f"eulerpool-python/{__version__}",
|
|
56
|
+
},
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
def get(self, path: str, params: Optional[Dict[str, Any]] = None) -> Any:
|
|
60
|
+
return self._request("GET", path, params=params)
|
|
61
|
+
|
|
62
|
+
def post(self, path: str, body: Any = None, params: Optional[Dict[str, Any]] = None) -> Any:
|
|
63
|
+
return self._request("POST", path, params=params, json_body=body)
|
|
64
|
+
|
|
65
|
+
def delete(self, path: str, params: Optional[Dict[str, Any]] = None) -> Any:
|
|
66
|
+
return self._request("DELETE", path, params=params)
|
|
67
|
+
|
|
68
|
+
def close(self) -> None:
|
|
69
|
+
self._http.close()
|
|
70
|
+
|
|
71
|
+
def __enter__(self) -> "HttpClient":
|
|
72
|
+
return self
|
|
73
|
+
|
|
74
|
+
def __exit__(self, *exc: Any) -> None:
|
|
75
|
+
self.close()
|
|
76
|
+
|
|
77
|
+
def _request(
|
|
78
|
+
self,
|
|
79
|
+
method: str,
|
|
80
|
+
path: str,
|
|
81
|
+
*,
|
|
82
|
+
params: Optional[Dict[str, Any]] = None,
|
|
83
|
+
json_body: Any = None,
|
|
84
|
+
) -> Any:
|
|
85
|
+
url = f"{self._base_url}{path}"
|
|
86
|
+
query = self._build_query(params)
|
|
87
|
+
headers: Dict[str, str] = {
|
|
88
|
+
"Authorization": f"Bearer {self._api_key}",
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
last_exc: Optional[Exception] = None
|
|
92
|
+
for attempt in range(self._max_retries + 1):
|
|
93
|
+
if attempt > 0:
|
|
94
|
+
time.sleep(min(2 ** (attempt - 1), 8))
|
|
95
|
+
try:
|
|
96
|
+
resp = self._http.request(
|
|
97
|
+
method,
|
|
98
|
+
url,
|
|
99
|
+
params=query,
|
|
100
|
+
json=json_body,
|
|
101
|
+
headers=headers,
|
|
102
|
+
)
|
|
103
|
+
if resp.is_success:
|
|
104
|
+
if not resp.content:
|
|
105
|
+
return None
|
|
106
|
+
try:
|
|
107
|
+
return resp.json()
|
|
108
|
+
except Exception:
|
|
109
|
+
return resp.text
|
|
110
|
+
error = self._make_error(resp)
|
|
111
|
+
if not self._is_retryable(resp.status_code):
|
|
112
|
+
raise error
|
|
113
|
+
last_exc = error
|
|
114
|
+
except EulerpoolError:
|
|
115
|
+
raise
|
|
116
|
+
except Exception as exc:
|
|
117
|
+
last_exc = exc
|
|
118
|
+
|
|
119
|
+
raise last_exc or EulerpoolError("Request failed after retries.")
|
|
120
|
+
|
|
121
|
+
def _build_query(self, params: Optional[Dict[str, Any]]) -> Dict[str, Any]:
|
|
122
|
+
q: Dict[str, Any] = {}
|
|
123
|
+
if not self._use_auth_header:
|
|
124
|
+
q["token"] = self._api_key
|
|
125
|
+
q.update(_normalize_params(params))
|
|
126
|
+
return q
|
|
127
|
+
|
|
128
|
+
@staticmethod
|
|
129
|
+
def _make_error(resp: httpx.Response) -> EulerpoolError:
|
|
130
|
+
try:
|
|
131
|
+
body = resp.json()
|
|
132
|
+
msg = body.get("message") or body.get("error") or resp.reason_phrase
|
|
133
|
+
except Exception:
|
|
134
|
+
msg = resp.text or resp.reason_phrase
|
|
135
|
+
status = resp.status_code
|
|
136
|
+
if status == 400:
|
|
137
|
+
return BadRequestError(msg)
|
|
138
|
+
if status == 401:
|
|
139
|
+
return AuthenticationError(msg)
|
|
140
|
+
if status == 404:
|
|
141
|
+
return NotFoundError(msg)
|
|
142
|
+
if status == 429:
|
|
143
|
+
retry = resp.headers.get("Retry-After")
|
|
144
|
+
return RateLimitError(msg, retry_after=int(retry) if retry else None)
|
|
145
|
+
if status >= 500:
|
|
146
|
+
return ServerError(msg)
|
|
147
|
+
return EulerpoolError(msg, status=status)
|
|
148
|
+
|
|
149
|
+
@staticmethod
|
|
150
|
+
def _is_retryable(status: int) -> bool:
|
|
151
|
+
return status == 429 or status >= 500
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class AsyncHttpClient:
|
|
155
|
+
"""Low-level async HTTP client for the Eulerpool API."""
|
|
156
|
+
|
|
157
|
+
_BASE_URL = "https://api.eulerpool.com/api/1"
|
|
158
|
+
|
|
159
|
+
def __init__(
|
|
160
|
+
self,
|
|
161
|
+
api_key: str,
|
|
162
|
+
*,
|
|
163
|
+
base_url: Optional[str] = None,
|
|
164
|
+
use_auth_header: bool = False,
|
|
165
|
+
max_retries: int = 2,
|
|
166
|
+
timeout: float = 30.0,
|
|
167
|
+
) -> None:
|
|
168
|
+
self._api_key = api_key
|
|
169
|
+
self._base_url = (base_url or self._BASE_URL).rstrip("/")
|
|
170
|
+
self._use_auth_header = use_auth_header
|
|
171
|
+
self._max_retries = max_retries
|
|
172
|
+
self._http = httpx.AsyncClient(
|
|
173
|
+
timeout=timeout,
|
|
174
|
+
follow_redirects=True,
|
|
175
|
+
headers={
|
|
176
|
+
"Accept": "application/json",
|
|
177
|
+
"User-Agent": f"eulerpool-python/{__version__}",
|
|
178
|
+
},
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
async def get(self, path: str, params: Optional[Dict[str, Any]] = None) -> Any:
|
|
182
|
+
return await self._request("GET", path, params=params)
|
|
183
|
+
|
|
184
|
+
async def post(self, path: str, body: Any = None, params: Optional[Dict[str, Any]] = None) -> Any:
|
|
185
|
+
return await self._request("POST", path, params=params, json_body=body)
|
|
186
|
+
|
|
187
|
+
async def delete(self, path: str, params: Optional[Dict[str, Any]] = None) -> Any:
|
|
188
|
+
return await self._request("DELETE", path, params=params)
|
|
189
|
+
|
|
190
|
+
async def close(self) -> None:
|
|
191
|
+
await self._http.aclose()
|
|
192
|
+
|
|
193
|
+
async def __aenter__(self) -> "AsyncHttpClient":
|
|
194
|
+
return self
|
|
195
|
+
|
|
196
|
+
async def __aexit__(self, *exc: Any) -> None:
|
|
197
|
+
await self.close()
|
|
198
|
+
|
|
199
|
+
async def _request(
|
|
200
|
+
self,
|
|
201
|
+
method: str,
|
|
202
|
+
path: str,
|
|
203
|
+
*,
|
|
204
|
+
params: Optional[Dict[str, Any]] = None,
|
|
205
|
+
json_body: Any = None,
|
|
206
|
+
) -> Any:
|
|
207
|
+
import asyncio
|
|
208
|
+
|
|
209
|
+
url = f"{self._base_url}{path}"
|
|
210
|
+
query = self._build_query(params)
|
|
211
|
+
headers: Dict[str, str] = {
|
|
212
|
+
"Authorization": f"Bearer {self._api_key}",
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
last_exc: Optional[Exception] = None
|
|
216
|
+
for attempt in range(self._max_retries + 1):
|
|
217
|
+
if attempt > 0:
|
|
218
|
+
await asyncio.sleep(min(2 ** (attempt - 1), 8))
|
|
219
|
+
try:
|
|
220
|
+
resp = await self._http.request(
|
|
221
|
+
method,
|
|
222
|
+
url,
|
|
223
|
+
params=query,
|
|
224
|
+
json=json_body,
|
|
225
|
+
headers=headers,
|
|
226
|
+
)
|
|
227
|
+
if resp.is_success:
|
|
228
|
+
if not resp.content:
|
|
229
|
+
return None
|
|
230
|
+
try:
|
|
231
|
+
return resp.json()
|
|
232
|
+
except Exception:
|
|
233
|
+
return resp.text
|
|
234
|
+
error = HttpClient._make_error(resp)
|
|
235
|
+
if not HttpClient._is_retryable(resp.status_code):
|
|
236
|
+
raise error
|
|
237
|
+
last_exc = error
|
|
238
|
+
except EulerpoolError:
|
|
239
|
+
raise
|
|
240
|
+
except Exception as exc:
|
|
241
|
+
last_exc = exc
|
|
242
|
+
|
|
243
|
+
raise last_exc or EulerpoolError("Request failed after retries.")
|
|
244
|
+
|
|
245
|
+
def _build_query(self, params: Optional[Dict[str, Any]]) -> Dict[str, Any]:
|
|
246
|
+
q: Dict[str, Any] = {}
|
|
247
|
+
if not self._use_auth_header:
|
|
248
|
+
q["token"] = self._api_key
|
|
249
|
+
q.update(_normalize_params(params))
|
|
250
|
+
return q
|
eulerpool/_version.py
ADDED
eulerpool/errors.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
class EulerpoolError(Exception):
|
|
4
|
+
"""Base exception for Eulerpool API errors."""
|
|
5
|
+
|
|
6
|
+
def __init__(self, message: str, status: int = 0, code: str = "api_error"):
|
|
7
|
+
super().__init__(message)
|
|
8
|
+
self.message = message
|
|
9
|
+
self.status = status
|
|
10
|
+
self.code = code
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AuthenticationError(EulerpoolError):
|
|
14
|
+
"""Raised when the API key is invalid or missing."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, message: str = "Invalid or missing API key."):
|
|
17
|
+
super().__init__(message, status=401, code="authentication_error")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class NotFoundError(EulerpoolError):
|
|
21
|
+
"""Raised when the requested resource is not found."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, message: str = "The requested resource was not found."):
|
|
24
|
+
super().__init__(message, status=404, code="not_found")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class RateLimitError(EulerpoolError):
|
|
28
|
+
"""Raised when rate limit is exceeded."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, message: str = "Rate limit exceeded.", retry_after=None):
|
|
31
|
+
super().__init__(message, status=429, code="rate_limit_exceeded")
|
|
32
|
+
self.retry_after = retry_after
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class BadRequestError(EulerpoolError):
|
|
36
|
+
"""Raised on 400 bad request."""
|
|
37
|
+
|
|
38
|
+
def __init__(self, message: str = "Bad request."):
|
|
39
|
+
super().__init__(message, status=400, code="bad_request")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ServerError(EulerpoolError):
|
|
43
|
+
"""Raised on 5xx server errors."""
|
|
44
|
+
|
|
45
|
+
def __init__(self, message: str = "Internal server error."):
|
|
46
|
+
super().__init__(message, status=500, code="server_error")
|
eulerpool/py.typed
ADDED
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, Optional, TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from .._client import AsyncHttpClient, HttpClient
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SyncResource:
|
|
10
|
+
def __init__(self, client: "HttpClient") -> None:
|
|
11
|
+
self._client = client
|
|
12
|
+
|
|
13
|
+
def _get(self, path: str, params: Optional[Dict[str, Any]] = None) -> Any:
|
|
14
|
+
return self._client.get(path, params)
|
|
15
|
+
|
|
16
|
+
def _post(self, path: str, body: Any = None, params: Optional[Dict[str, Any]] = None) -> Any:
|
|
17
|
+
return self._client.post(path, body, params)
|
|
18
|
+
|
|
19
|
+
def _delete(self, path: str, params: Optional[Dict[str, Any]] = None) -> Any:
|
|
20
|
+
return self._client.delete(path, params)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AsyncResource:
|
|
24
|
+
def __init__(self, client: "AsyncHttpClient") -> None:
|
|
25
|
+
self._client = client
|
|
26
|
+
|
|
27
|
+
async def _get(self, path: str, params: Optional[Dict[str, Any]] = None) -> Any:
|
|
28
|
+
return await self._client.get(path, params)
|
|
29
|
+
|
|
30
|
+
async def _post(self, path: str, body: Any = None, params: Optional[Dict[str, Any]] = None) -> Any:
|
|
31
|
+
return await self._client.post(path, body, params)
|
|
32
|
+
|
|
33
|
+
async def _delete(self, path: str, params: Optional[Dict[str, Any]] = None) -> Any:
|
|
34
|
+
return await self._client.delete(path, params)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
from urllib.parse import quote
|
|
5
|
+
|
|
6
|
+
from ._base import AsyncResource, SyncResource
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Aaq(SyncResource):
|
|
10
|
+
def by_isin(self, identifier: str, **params: Any) -> Any:
|
|
11
|
+
"""AAQS Score"""
|
|
12
|
+
return self._get(f"/aaqs/by-isin/{quote(str(identifier))}", params)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AsyncAaq(AsyncResource):
|
|
16
|
+
async def by_isin(self, identifier: str, **params: Any) -> Any:
|
|
17
|
+
"""AAQS Score"""
|
|
18
|
+
return await self._get(f"/aaqs/by-isin/{quote(str(identifier))}", params)
|