kensho-kfinance 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.

Potentially problematic release.


This version of kensho-kfinance might be problematic. Click here for more details.

kfinance/fetch.py ADDED
@@ -0,0 +1,374 @@
1
+ from time import time
2
+ from typing import Callable, Optional
3
+
4
+ import jwt
5
+ import requests
6
+
7
+ from .constants import BusinessRelationshipType, IdentificationTriple
8
+ from .version import __version__ as kfinance_version
9
+
10
+
11
+ DEFAULT_API_HOST: str = "https://kfinance.kensho.com"
12
+ DEFAULT_API_VERSION: int = 1
13
+ DEFAULT_OKTA_HOST: str = "https://kensho.okta.com"
14
+ DEFAULT_OKTA_AUTH_SERVER: str = "default"
15
+
16
+
17
+ class KFinanceApiClient:
18
+ def __init__(
19
+ self,
20
+ refresh_token: Optional[str] = None,
21
+ client_id: Optional[str] = None,
22
+ private_key: Optional[str] = None,
23
+ api_host: str = DEFAULT_API_HOST,
24
+ api_version: int = DEFAULT_API_VERSION,
25
+ okta_host: str = DEFAULT_OKTA_HOST,
26
+ okta_auth_server: str = DEFAULT_OKTA_AUTH_SERVER,
27
+ ):
28
+ """Configuration of KFinance Client."""
29
+ if refresh_token is not None:
30
+ self.refresh_token = refresh_token
31
+ self._access_token_refresh_func: Callable[..., str] = (
32
+ self._get_access_token_via_refresh_token
33
+ )
34
+ elif client_id is not None and private_key is not None:
35
+ self.client_id = client_id
36
+ self.private_key = private_key
37
+ self._access_token_refresh_func = self._get_access_token_via_keypair
38
+ else:
39
+ raise RuntimeError("No credentials for any authentication strategy were provided")
40
+ self.api_host = api_host
41
+ self.api_version = api_version
42
+ self.okta_host = okta_host
43
+ self.okta_auth_server = okta_auth_server
44
+ self.url_base = f"{self.api_host}/api/v{self.api_version}/"
45
+ self._access_token_expiry = 0
46
+ self._access_token: str | None = None
47
+ self.user_agent_source = "object_oriented"
48
+
49
+ @property
50
+ def access_token(self) -> str:
51
+ """Returns the client access token.
52
+
53
+ If the token is not set or has expired, a new token gets fetched and returned.
54
+ """
55
+ if self._access_token is None or time() + 60 > self._access_token_expiry:
56
+ self._access_token = self._access_token_refresh_func()
57
+ self._access_token_expiry = jwt.decode(
58
+ self._access_token,
59
+ # nosemgrep: python.jwt.security.unverified-jwt-decode.unverified-jwt-decode
60
+ options={"verify_signature": False},
61
+ ).get("exp")
62
+ return self._access_token
63
+
64
+ def _get_access_token_via_refresh_token(self) -> str:
65
+ """Get an access token via oauth by submitting a refresh token."""
66
+ response = requests.get(
67
+ f"{self.api_host}/oauth2/refresh?refresh_token={self.refresh_token}",
68
+ timeout=60,
69
+ )
70
+ response.raise_for_status()
71
+ return response.json().get("access_token")
72
+
73
+ def _get_access_token_via_keypair(self) -> str:
74
+ """Get an access token via okta by submitting a registered public key."""
75
+ iat = int(time())
76
+ encoded = jwt.encode(
77
+ {
78
+ "aud": f"{self.okta_host}/oauth2/{self.okta_auth_server}/v1/token",
79
+ "exp": iat + (60 * 60), # expire in 60 minutes
80
+ "iat": iat,
81
+ "sub": self.client_id,
82
+ "iss": self.client_id,
83
+ },
84
+ self.private_key,
85
+ algorithm="RS256",
86
+ )
87
+ response = requests.post(
88
+ f"{self.okta_host}/oauth2/{self.okta_auth_server}/v1/token",
89
+ headers={
90
+ "Content-Type": "application/x-www-form-urlencoded",
91
+ "Accept": "application/json",
92
+ },
93
+ data={
94
+ "scope": "kensho:app:kfinance",
95
+ "grant_type": "client_credentials",
96
+ "client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
97
+ "client_assertion": encoded,
98
+ },
99
+ timeout=60,
100
+ )
101
+ response.raise_for_status()
102
+ return response.json().get("access_token")
103
+
104
+ def fetch(self, url: str) -> dict:
105
+ """Does the request and auth"""
106
+ response = requests.get(
107
+ url,
108
+ headers={
109
+ "Content-Type": "application/json",
110
+ "Authorization": f"Bearer {self.access_token}",
111
+ "User-Agent": f"kfinance/{kfinance_version} {self.user_agent_source}",
112
+ },
113
+ timeout=60,
114
+ )
115
+ response.raise_for_status()
116
+ return response.json()
117
+
118
+ def fetch_id_triple(self, identifier: str, exchange_code: Optional[str] = None) -> dict:
119
+ """Get the ID triple from [identifier]."""
120
+ url = f"{self.url_base}id/{identifier}"
121
+ if exchange_code is not None:
122
+ url = url + f"/exchange_code/{exchange_code}"
123
+ return self.fetch(url)
124
+
125
+ def fetch_isin(self, security_id: int) -> dict:
126
+ """Get the ISIN."""
127
+ url = f"{self.url_base}isin/{security_id}"
128
+ return self.fetch(url)
129
+
130
+ def fetch_cusip(self, security_id: int) -> dict:
131
+ """Get the CUSIP."""
132
+ url = f"{self.url_base}cusip/{security_id}"
133
+ return self.fetch(url)
134
+
135
+ def fetch_primary_security(self, company_id: int) -> dict:
136
+ """Get the primary security of a company."""
137
+ url = f"{self.url_base}securities/{company_id}/primary"
138
+ return self.fetch(url)
139
+
140
+ def fetch_securities(self, company_id: int) -> dict:
141
+ """Get the list of securities of a company."""
142
+ url = f"{self.url_base}securities/{company_id}"
143
+ return self.fetch(url)
144
+
145
+ def fetch_primary_trading_item(self, security_id: int) -> dict:
146
+ """Get the primary trading item of a security."""
147
+ url = f"{self.url_base}trading_items/{security_id}/primary"
148
+ return self.fetch(url)
149
+
150
+ def fetch_trading_items(self, security_id: int) -> dict:
151
+ """Get the list of trading items of a security."""
152
+ url = f"{self.url_base}trading_items/{security_id}"
153
+ return self.fetch(url)
154
+
155
+ def fetch_history(
156
+ self,
157
+ trading_item_id: int,
158
+ is_adjusted: bool = True,
159
+ start_date: Optional[str] = None,
160
+ end_date: Optional[str] = None,
161
+ periodicity: Optional[str] = None,
162
+ ) -> dict:
163
+ """Get the pricing history."""
164
+ url = (
165
+ f"{self.url_base}pricing/{trading_item_id}/"
166
+ f"{start_date if start_date is not None else 'none'}/"
167
+ f"{end_date if end_date is not None else 'none'}/"
168
+ f"{periodicity if periodicity is not None else 'none'}/"
169
+ f"{'adjusted' if is_adjusted else 'unadjusted'}"
170
+ )
171
+ return self.fetch(url)
172
+
173
+ def fetch_history_metadata(self, trading_item_id: int) -> dict[str, str]:
174
+ """Get the pricing history metadata."""
175
+ url = f"{self.url_base}pricing/{trading_item_id}/metadata"
176
+ return self.fetch(url)
177
+
178
+ def fetch_price_chart(
179
+ self,
180
+ trading_item_id: int,
181
+ is_adjusted: bool = True,
182
+ start_date: Optional[str] = None,
183
+ end_date: Optional[str] = None,
184
+ periodicity: Optional[str] = "",
185
+ ) -> bytes:
186
+ """Get the price chart."""
187
+ url = (
188
+ f"{self.url_base}price_chart/{trading_item_id}/"
189
+ f"{start_date if start_date is not None else 'none'}/"
190
+ f"{end_date if end_date is not None else 'none'}/"
191
+ f"{periodicity if periodicity is not None else 'none'}/"
192
+ f"{'adjusted' if is_adjusted else 'unadjusted'}"
193
+ )
194
+
195
+ response = requests.get(
196
+ url,
197
+ headers={
198
+ "Content-Type": "image/png",
199
+ "Authorization": f"Bearer {self.access_token}",
200
+ },
201
+ timeout=60,
202
+ )
203
+ response.raise_for_status()
204
+ return response.content
205
+
206
+ def fetch_statement(
207
+ self,
208
+ company_id: int,
209
+ statement_type: str,
210
+ period_type: Optional[str] = None,
211
+ start_year: Optional[int] = None,
212
+ end_year: Optional[int] = None,
213
+ start_quarter: Optional[int] = None,
214
+ end_quarter: Optional[int] = None,
215
+ ) -> dict:
216
+ """Get a specified financial statement for a specified duration."""
217
+ url = (
218
+ f"{self.url_base}statements/{company_id}/{statement_type}/"
219
+ f"{period_type if period_type is not None else 'none'}/"
220
+ f"{start_year if start_year is not None else 'none'}/"
221
+ f"{end_year if end_year is not None else 'none'}/"
222
+ f"{start_quarter if start_quarter is not None else 'none'}/"
223
+ f"{end_quarter if end_quarter is not None else 'none'}"
224
+ )
225
+ return self.fetch(url)
226
+
227
+ def fetch_line_item(
228
+ self,
229
+ company_id: int,
230
+ line_item: str,
231
+ period_type: Optional[str] = None,
232
+ start_year: Optional[int] = None,
233
+ end_year: Optional[int] = None,
234
+ start_quarter: Optional[int] = None,
235
+ end_quarter: Optional[int] = None,
236
+ ) -> dict:
237
+ """Get a specified financial line item for a specified duration."""
238
+ url = (
239
+ f"{self.url_base}line_item/{company_id}/{line_item}/"
240
+ f"{period_type if period_type is not None else 'none'}/"
241
+ f"{start_year if start_year is not None else 'none'}/"
242
+ f"{end_year if end_year is not None else 'none'}/"
243
+ f"{start_quarter if start_quarter is not None else 'none'}/"
244
+ f"{end_quarter if end_quarter is not None else 'none'}"
245
+ )
246
+ return self.fetch(url)
247
+
248
+ def fetch_info(self, company_id: int) -> dict:
249
+ """Get the company info."""
250
+ url = f"{self.url_base}info/{company_id}"
251
+ return self.fetch(url)
252
+
253
+ def fetch_earnings_dates(self, company_id: int) -> dict:
254
+ """Get the earnings dates."""
255
+ url = f"{self.url_base}earnings/{company_id}/dates"
256
+ return self.fetch(url)
257
+
258
+ def fetch_geography_groups(
259
+ self, country_iso_code: str, state_iso_code: Optional[str] = None, fetch_ticker: bool = True
260
+ ) -> dict[str, list]:
261
+ """Fetch geography groups"""
262
+ url = f"{self.url_base}{'ticker_groups' if fetch_ticker else 'company_groups'}/geo/country/{country_iso_code}"
263
+ if state_iso_code:
264
+ url = url + f"/{state_iso_code}"
265
+ return self.fetch(url)
266
+
267
+ def fetch_ticker_geography_groups(
268
+ self,
269
+ country_iso_code: str,
270
+ state_iso_code: Optional[str] = None,
271
+ ) -> dict[str, list[IdentificationTriple]]:
272
+ """Fetch ticker geography groups"""
273
+ return self.fetch_geography_groups(
274
+ country_iso_code=country_iso_code, state_iso_code=state_iso_code, fetch_ticker=True
275
+ )
276
+
277
+ def fetch_company_geography_groups(
278
+ self,
279
+ country_iso_code: str,
280
+ state_iso_code: Optional[str] = None,
281
+ ) -> dict[str, list[int]]:
282
+ """Fetch company geography groups"""
283
+ return self.fetch_geography_groups(
284
+ country_iso_code=country_iso_code, state_iso_code=state_iso_code, fetch_ticker=False
285
+ )
286
+
287
+ def fetch_simple_industry_groups(
288
+ self, simple_industry: str, fetch_ticker: bool = True
289
+ ) -> dict[str, list]:
290
+ """Fetch simple industry groups"""
291
+ url = f"{self.url_base}{'ticker_groups' if fetch_ticker else 'company_groups'}/industry/simple/{simple_industry}"
292
+ return self.fetch(url)
293
+
294
+ def fetch_ticker_simple_industry_groups(
295
+ self, simple_industry: str
296
+ ) -> dict[str, list[IdentificationTriple]]:
297
+ """Fetch ticker simple industry groups"""
298
+ return self.fetch_simple_industry_groups(simple_industry=simple_industry, fetch_ticker=True)
299
+
300
+ def fetch_company_simple_industry_groups(self, simple_industry: str) -> dict[str, list[int]]:
301
+ """Fetch company simple industry groups"""
302
+ return self.fetch_simple_industry_groups(
303
+ simple_industry=simple_industry,
304
+ fetch_ticker=False,
305
+ )
306
+
307
+ def fetch_exchange_groups(
308
+ self, exchange_code: str, fetch_ticker: bool = True
309
+ ) -> dict[str, list]:
310
+ """Fetch exchange groups"""
311
+ url = f"{self.url_base}{'ticker_groups' if fetch_ticker else 'trading_item_groups'}/exchange/{exchange_code}"
312
+ return self.fetch(url)
313
+
314
+ def fetch_ticker_exchange_groups(
315
+ self, exchange_code: str
316
+ ) -> dict[str, list[IdentificationTriple]]:
317
+ """Fetch ticker exchange groups"""
318
+ return self.fetch_exchange_groups(
319
+ exchange_code=exchange_code,
320
+ fetch_ticker=True,
321
+ )
322
+
323
+ def fetch_trading_item_exchange_groups(self, exchange_code: str) -> dict[str, list[int]]:
324
+ """Fetch company exchange groups"""
325
+ return self.fetch_exchange_groups(
326
+ exchange_code=exchange_code,
327
+ fetch_ticker=False,
328
+ )
329
+
330
+ def fetch_ticker_combined(
331
+ self,
332
+ country_iso_code: Optional[str] = None,
333
+ state_iso_code: Optional[str] = None,
334
+ simple_industry: Optional[str] = None,
335
+ exchange_code: Optional[str] = None,
336
+ ) -> dict[str, list[IdentificationTriple]]:
337
+ """Fetch tickers using combined filters route"""
338
+ if (
339
+ country_iso_code is None
340
+ and state_iso_code is None
341
+ and simple_industry is None
342
+ and exchange_code is None
343
+ ):
344
+ raise RuntimeError("Invalid parameters: No parameters provided or all set to none")
345
+ elif country_iso_code is None and state_iso_code is not None:
346
+ raise RuntimeError(
347
+ "Invalid parameters: state_iso_code must be provided with a country_iso_code value"
348
+ )
349
+ else:
350
+ url = f"{self.url_base}ticker_groups/filters/geo/{str(country_iso_code).lower()}/{str(state_iso_code).lower()}/simple/{str(simple_industry).lower()}/exchange/{str(exchange_code).lower()}"
351
+ return self.fetch(url)
352
+
353
+ def fetch_companies_from_business_relationship(
354
+ self, company_id: int, relationship_type: BusinessRelationshipType
355
+ ) -> dict[str, list[int]]:
356
+ """Fetches a dictionary of current and previous company IDs associated with a given company ID based on the specified relationship type.
357
+
358
+ The returned dictionary has the following structure:
359
+ {
360
+ "current": List[int],
361
+ "previous": List[int]
362
+ }
363
+
364
+ Example: fetch_companies_from_business_relationship(company_id=1234, relationship_type="distributor") returns a dictionary of company 1234's current and previous distributors.
365
+
366
+ :param company_id: The ID of the company for which associated companies are being fetched.
367
+ :type company_id: int
368
+ :param relationship_type: The type of relationship to filter by. Valid relationship types are defined in the BusinessRelationshipType class.
369
+ :type relationship_type: BusinessRelationshipType
370
+ :return: A dictionary containing lists of current and previous company IDs that have the specified relationship with the given company_id.
371
+ :rtype: dict[str, list[int]]
372
+ """
373
+ url = f"{self.url_base}relationship/{company_id}/{relationship_type}"
374
+ return self.fetch(url)