kensho-kfinance 2.0.1__py3-none-any.whl → 2.2.2__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.
- {kensho_kfinance-2.0.1.dist-info → kensho_kfinance-2.2.2.dist-info}/METADATA +9 -1
- kensho_kfinance-2.2.2.dist-info/RECORD +42 -0
- {kensho_kfinance-2.0.1.dist-info → kensho_kfinance-2.2.2.dist-info}/WHEEL +1 -1
- kfinance/CHANGELOG.md +21 -0
- kfinance/batch_request_handling.py +32 -27
- kfinance/constants.py +23 -7
- kfinance/fetch.py +106 -40
- kfinance/kfinance.py +164 -89
- kfinance/meta_classes.py +118 -9
- kfinance/tests/conftest.py +32 -0
- kfinance/tests/test_batch_requests.py +46 -8
- kfinance/tests/test_client.py +54 -0
- kfinance/tests/test_example_notebook.py +194 -0
- kfinance/tests/test_fetch.py +31 -2
- kfinance/tests/test_group_objects.py +32 -0
- kfinance/tests/test_objects.py +40 -0
- kfinance/tests/test_tools.py +13 -61
- kfinance/tool_calling/__init__.py +2 -6
- kfinance/tool_calling/get_business_relationship_from_identifier.py +2 -1
- kfinance/tool_calling/get_capitalization_from_identifier.py +2 -1
- kfinance/tool_calling/get_cusip_from_ticker.py +2 -0
- kfinance/tool_calling/get_earnings_call_datetimes_from_identifier.py +2 -0
- kfinance/tool_calling/get_financial_line_item_from_identifier.py +2 -1
- kfinance/tool_calling/get_financial_statement_from_identifier.py +2 -1
- kfinance/tool_calling/get_history_metadata_from_identifier.py +2 -1
- kfinance/tool_calling/get_info_from_identifier.py +3 -1
- kfinance/tool_calling/get_isin_from_ticker.py +2 -0
- kfinance/tool_calling/get_latest.py +2 -1
- kfinance/tool_calling/get_n_quarters_ago.py +2 -1
- kfinance/tool_calling/get_prices_from_identifier.py +2 -1
- kfinance/tool_calling/resolve_identifier.py +18 -0
- kfinance/tool_calling/shared_models.py +2 -0
- kfinance/version.py +2 -2
- kensho_kfinance-2.0.1.dist-info/RECORD +0 -40
- kfinance/tool_calling/get_company_id_from_identifier.py +0 -14
- kfinance/tool_calling/get_security_id_from_identifier.py +0 -14
- kfinance/tool_calling/get_trading_item_id_from_identifier.py +0 -14
- {kensho_kfinance-2.0.1.dist-info → kensho_kfinance-2.2.2.dist-info}/licenses/AUTHORS.md +0 -0
- {kensho_kfinance-2.0.1.dist-info → kensho_kfinance-2.2.2.dist-info}/licenses/LICENSE +0 -0
- {kensho_kfinance-2.0.1.dist-info → kensho_kfinance-2.2.2.dist-info}/top_level.txt +0 -0
kfinance/kfinance.py
CHANGED
|
@@ -22,6 +22,7 @@ from .batch_request_handling import add_methods_of_singular_class_to_iterable_cl
|
|
|
22
22
|
from .constants import (
|
|
23
23
|
HistoryMetadata,
|
|
24
24
|
IdentificationTriple,
|
|
25
|
+
IndustryClassification,
|
|
25
26
|
LatestPeriods,
|
|
26
27
|
Periodicity,
|
|
27
28
|
YearAndQuarter,
|
|
@@ -233,10 +234,9 @@ class Company(CompanyFunctionsMetaClass):
|
|
|
233
234
|
primary_security_id = self.kfinance_api_client.fetch_primary_security(self.company_id)[
|
|
234
235
|
"primary_security"
|
|
235
236
|
]
|
|
236
|
-
|
|
237
|
+
return Security(
|
|
237
238
|
kfinance_api_client=self.kfinance_api_client, security_id=primary_security_id
|
|
238
239
|
)
|
|
239
|
-
return self.primary_security
|
|
240
240
|
|
|
241
241
|
@cached_property
|
|
242
242
|
def securities(self) -> Securities:
|
|
@@ -246,10 +246,7 @@ class Company(CompanyFunctionsMetaClass):
|
|
|
246
246
|
:rtype: Securities
|
|
247
247
|
"""
|
|
248
248
|
security_ids = self.kfinance_api_client.fetch_securities(self.company_id)["securities"]
|
|
249
|
-
self.
|
|
250
|
-
kfinance_api_client=self.kfinance_api_client, security_ids=security_ids
|
|
251
|
-
)
|
|
252
|
-
return self.securities
|
|
249
|
+
return Securities(kfinance_api_client=self.kfinance_api_client, security_ids=security_ids)
|
|
253
250
|
|
|
254
251
|
@cached_property
|
|
255
252
|
def latest_earnings_call(self) -> None:
|
|
@@ -265,11 +262,10 @@ class Company(CompanyFunctionsMetaClass):
|
|
|
265
262
|
def info(self) -> dict:
|
|
266
263
|
"""Get the company info
|
|
267
264
|
|
|
268
|
-
:return: a dict with containing: name, status, type, simple industry, number of employees, founding date, webpage, address, city, zip code, state, country, & iso_country
|
|
265
|
+
:return: a dict with containing: name, status, type, simple industry, number of employees (if available), founding date, webpage, address, city, zip code, state, country, & iso_country
|
|
269
266
|
:rtype: dict
|
|
270
267
|
"""
|
|
271
|
-
|
|
272
|
-
return self.info
|
|
268
|
+
return self.kfinance_api_client.fetch_info(self.company_id)
|
|
273
269
|
|
|
274
270
|
@property
|
|
275
271
|
def name(self) -> str:
|
|
@@ -308,11 +304,11 @@ class Company(CompanyFunctionsMetaClass):
|
|
|
308
304
|
return self.info["simple_industry"]
|
|
309
305
|
|
|
310
306
|
@property
|
|
311
|
-
def number_of_employees(self) -> str:
|
|
312
|
-
"""Get the number of employees the company has
|
|
307
|
+
def number_of_employees(self) -> str | None:
|
|
308
|
+
"""Get the number of employees the company has (if available)
|
|
313
309
|
|
|
314
310
|
:return: how many employees the company has
|
|
315
|
-
:rtype: str
|
|
311
|
+
:rtype: str | None
|
|
316
312
|
"""
|
|
317
313
|
return self.info["number_of_employees"]
|
|
318
314
|
|
|
@@ -539,6 +535,44 @@ class Ticker(DelegatedCompanyFunctionsMetaClass):
|
|
|
539
535
|
"Neither an identifier nor an identification triple (company id, security id, & trading item id) were passed in"
|
|
540
536
|
)
|
|
541
537
|
|
|
538
|
+
@property
|
|
539
|
+
def id_triple(self) -> IdentificationTriple:
|
|
540
|
+
"""Returns a unique identification triple for the Ticker object.
|
|
541
|
+
|
|
542
|
+
:return: an identification triple consisting of company_id, security_id, and trading_item_id
|
|
543
|
+
:rtype: IdentificationTriple
|
|
544
|
+
"""
|
|
545
|
+
|
|
546
|
+
if self._company_id is None or self._security_id is None or self._trading_item_id is None:
|
|
547
|
+
if self._identifier is None:
|
|
548
|
+
raise RuntimeError(
|
|
549
|
+
"Fetching the id triple of a Ticker requires an identifier "
|
|
550
|
+
"(ticker, CUSIP, or ISIN)."
|
|
551
|
+
)
|
|
552
|
+
id_triple = self.kfinance_api_client.fetch_id_triple(
|
|
553
|
+
identifier=self._identifier, exchange_code=self.exchange_code
|
|
554
|
+
)
|
|
555
|
+
self._company_id = id_triple["company_id"]
|
|
556
|
+
self._security_id = id_triple["security_id"]
|
|
557
|
+
self._trading_item_id = id_triple["trading_item_id"]
|
|
558
|
+
assert self._company_id
|
|
559
|
+
assert self._security_id
|
|
560
|
+
assert self._trading_item_id
|
|
561
|
+
|
|
562
|
+
return IdentificationTriple(
|
|
563
|
+
company_id=self._company_id,
|
|
564
|
+
security_id=self._security_id,
|
|
565
|
+
trading_item_id=self._trading_item_id,
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
def __hash__(self) -> int:
|
|
569
|
+
return hash(self.id_triple)
|
|
570
|
+
|
|
571
|
+
def __eq__(self, other: Any) -> bool:
|
|
572
|
+
if not isinstance(other, Ticker):
|
|
573
|
+
return False
|
|
574
|
+
return self.id_triple == other.id_triple
|
|
575
|
+
|
|
542
576
|
def __str__(self) -> str:
|
|
543
577
|
"""String representation for the ticker object"""
|
|
544
578
|
str_attributes = []
|
|
@@ -557,79 +591,32 @@ class Ticker(DelegatedCompanyFunctionsMetaClass):
|
|
|
557
591
|
|
|
558
592
|
return f"{type(self).__module__}.{type(self).__qualname__} of {', '.join(str_attributes)}"
|
|
559
593
|
|
|
560
|
-
|
|
561
|
-
"""Get & set company_id, security_id, & trading_item_id for ticker with an exchange"""
|
|
562
|
-
if self._identifier is None:
|
|
563
|
-
raise RuntimeError(
|
|
564
|
-
"Ticker.set_identification_triple was called with a identifier set to None"
|
|
565
|
-
)
|
|
566
|
-
else:
|
|
567
|
-
id_triple = self.kfinance_api_client.fetch_id_triple(
|
|
568
|
-
self._identifier, self.exchange_code
|
|
569
|
-
)
|
|
570
|
-
self.company_id = id_triple["company_id"]
|
|
571
|
-
self.security_id = id_triple["security_id"]
|
|
572
|
-
self.trading_item_id = id_triple["trading_item_id"]
|
|
573
|
-
|
|
574
|
-
def set_company_id(self) -> int:
|
|
575
|
-
"""Set the company id for the object
|
|
576
|
-
|
|
577
|
-
:return: the CIQ company id
|
|
578
|
-
:rtype: int
|
|
579
|
-
"""
|
|
580
|
-
self.set_identification_triple()
|
|
581
|
-
return self.company_id
|
|
582
|
-
|
|
583
|
-
def set_security_id(self) -> int:
|
|
584
|
-
"""Set the security id for the object
|
|
585
|
-
|
|
586
|
-
:return: the CIQ security id
|
|
587
|
-
:rtype: int
|
|
588
|
-
"""
|
|
589
|
-
self.set_identification_triple()
|
|
590
|
-
return self.security_id
|
|
591
|
-
|
|
592
|
-
def set_trading_item_id(self) -> int:
|
|
593
|
-
"""Set the trading item id for the object
|
|
594
|
-
|
|
595
|
-
:return: the CIQ trading item id
|
|
596
|
-
:rtype: int
|
|
597
|
-
"""
|
|
598
|
-
self.set_identification_triple()
|
|
599
|
-
return self.trading_item_id
|
|
600
|
-
|
|
601
|
-
@cached_property
|
|
594
|
+
@property
|
|
602
595
|
def company_id(self) -> int:
|
|
603
596
|
"""Get the company id for the object
|
|
604
597
|
|
|
605
598
|
:return: the CIQ company id
|
|
606
599
|
:rtype: int
|
|
607
600
|
"""
|
|
608
|
-
|
|
609
|
-
return self._company_id
|
|
610
|
-
return self.set_company_id()
|
|
601
|
+
return self.id_triple.company_id
|
|
611
602
|
|
|
612
|
-
@
|
|
603
|
+
@property
|
|
613
604
|
def security_id(self) -> int:
|
|
614
605
|
"""Get the CIQ security id for the object
|
|
615
606
|
|
|
616
607
|
:return: the CIQ security id
|
|
617
608
|
:rtype: int
|
|
618
609
|
"""
|
|
619
|
-
|
|
620
|
-
return self._security_id
|
|
621
|
-
return self.set_security_id()
|
|
610
|
+
return self.id_triple.security_id
|
|
622
611
|
|
|
623
|
-
@
|
|
612
|
+
@property
|
|
624
613
|
def trading_item_id(self) -> int:
|
|
625
614
|
"""Get the CIQ trading item id for the object
|
|
626
615
|
|
|
627
616
|
:return: the CIQ trading item id
|
|
628
617
|
:rtype: int
|
|
629
618
|
"""
|
|
630
|
-
|
|
631
|
-
return self._trading_item_id
|
|
632
|
-
return self.set_trading_item_id()
|
|
619
|
+
return self.id_triple.trading_item_id
|
|
633
620
|
|
|
634
621
|
@cached_property
|
|
635
622
|
def primary_security(self) -> Security:
|
|
@@ -698,7 +685,7 @@ class Ticker(DelegatedCompanyFunctionsMetaClass):
|
|
|
698
685
|
def info(self) -> dict:
|
|
699
686
|
"""Get the company info for the ticker
|
|
700
687
|
|
|
701
|
-
:return: a dict with containing: name, status, type, simple industry, number of employees, founding date, webpage, address, city, zip code, state, country, & iso_country
|
|
688
|
+
:return: a dict with containing: name, status, type, simple industry, number of employees (if available), founding date, webpage, address, city, zip code, state, country, & iso_country
|
|
702
689
|
:rtype: dict
|
|
703
690
|
"""
|
|
704
691
|
return self.company.info
|
|
@@ -740,11 +727,11 @@ class Ticker(DelegatedCompanyFunctionsMetaClass):
|
|
|
740
727
|
return self.company.simple_industry
|
|
741
728
|
|
|
742
729
|
@property
|
|
743
|
-
def number_of_employees(self) -> str:
|
|
744
|
-
"""Get the number of employees the company has
|
|
730
|
+
def number_of_employees(self) -> str | None:
|
|
731
|
+
"""Get the number of employees the company has (if available)
|
|
745
732
|
|
|
746
733
|
:return: how many employees the company has
|
|
747
|
-
:rtype: str
|
|
734
|
+
:rtype: str | None
|
|
748
735
|
"""
|
|
749
736
|
return self.company.number_of_employees
|
|
750
737
|
|
|
@@ -991,13 +978,33 @@ class Tickers(set):
|
|
|
991
978
|
super().__init__(
|
|
992
979
|
Ticker(
|
|
993
980
|
kfinance_api_client=kfinance_api_client,
|
|
994
|
-
company_id=id_triple
|
|
995
|
-
security_id=id_triple
|
|
996
|
-
trading_item_id=id_triple
|
|
981
|
+
company_id=id_triple.company_id,
|
|
982
|
+
security_id=id_triple.security_id,
|
|
983
|
+
trading_item_id=id_triple.trading_item_id,
|
|
997
984
|
)
|
|
998
985
|
for id_triple in id_triples
|
|
999
986
|
)
|
|
1000
987
|
|
|
988
|
+
def intersection(self, *s: Iterable[Any]) -> Tickers:
|
|
989
|
+
"""Returns the intersection of Tickers objects"""
|
|
990
|
+
for obj in s:
|
|
991
|
+
if not isinstance(obj, Tickers):
|
|
992
|
+
raise ValueError("Can only intersect Tickers object with other Tickers object.")
|
|
993
|
+
|
|
994
|
+
self_triples = {t.id_triple for t in self}
|
|
995
|
+
set_triples = []
|
|
996
|
+
|
|
997
|
+
for ticker_set in s:
|
|
998
|
+
set_triples.append({t.id_triple for t in ticker_set})
|
|
999
|
+
common_triples = self_triples.intersection(*set_triples)
|
|
1000
|
+
|
|
1001
|
+
return Tickers(kfinance_api_client=self.kfinance_api_client, id_triples=common_triples)
|
|
1002
|
+
|
|
1003
|
+
def __and__(self, other: Any) -> "Tickers":
|
|
1004
|
+
if not isinstance(other, Tickers):
|
|
1005
|
+
raise ValueError("Can only combine Tickers objects with other Tickers objects.")
|
|
1006
|
+
return self.intersection(other)
|
|
1007
|
+
|
|
1001
1008
|
def companies(self) -> Companies:
|
|
1002
1009
|
"""Build a group of company objects from the group of tickers
|
|
1003
1010
|
|
|
@@ -1126,7 +1133,16 @@ class Client:
|
|
|
1126
1133
|
if self._tools is None:
|
|
1127
1134
|
from kfinance.tool_calling import ALL_TOOLS
|
|
1128
1135
|
|
|
1129
|
-
self._tools = [
|
|
1136
|
+
self._tools = []
|
|
1137
|
+
# Add tool to _tools if the user has permissions to use it.
|
|
1138
|
+
for tool_cls in ALL_TOOLS:
|
|
1139
|
+
tool = tool_cls(kfinance_client=self) # type: ignore[call-arg]
|
|
1140
|
+
if (
|
|
1141
|
+
tool.required_permission is None
|
|
1142
|
+
or tool.required_permission in self.kfinance_api_client.user_permissions
|
|
1143
|
+
):
|
|
1144
|
+
self._tools.append(tool)
|
|
1145
|
+
|
|
1130
1146
|
return self._tools
|
|
1131
1147
|
|
|
1132
1148
|
@property
|
|
@@ -1212,31 +1228,90 @@ class Client:
|
|
|
1212
1228
|
state_iso_code: Optional[str] = None,
|
|
1213
1229
|
simple_industry: Optional[str] = None,
|
|
1214
1230
|
exchange_code: Optional[str] = None,
|
|
1231
|
+
sic: Optional[str] = None,
|
|
1232
|
+
naics: Optional[str] = None,
|
|
1233
|
+
nace: Optional[str] = None,
|
|
1234
|
+
anzsic: Optional[str] = None,
|
|
1235
|
+
spcapiqetf: Optional[str] = None,
|
|
1236
|
+
spratings: Optional[str] = None,
|
|
1237
|
+
gics: Optional[str] = None,
|
|
1215
1238
|
) -> Tickers:
|
|
1216
|
-
"""Generate
|
|
1239
|
+
"""Generate a Tickers object representing the collection of Tickers that meet all the supplied parameters.
|
|
1217
1240
|
|
|
1218
|
-
One of country_iso_code, simple_industry, or exchange_code must be supplied
|
|
1241
|
+
One of country_iso_code, simple_industry, or exchange_code must be supplied, or one of sic, naics, nace, anzsic, spcapiqetf, spratings, or gics.
|
|
1219
1242
|
|
|
1220
|
-
:param country_iso_code: The ISO 3166-1 Alpha-2 or Alpha-3 code that represent the primary country the firm is based in. It
|
|
1243
|
+
:param country_iso_code: The ISO 3166-1 Alpha-2 or Alpha-3 code that represent the primary country the firm is based in. It defaults to None
|
|
1221
1244
|
:type country_iso_code: str, optional
|
|
1222
|
-
:param state_iso_code: The ISO 3166-2 Alpha-2 code that represents the primary subdivision of the country the firm the based in. Not all ISO 3166-2 codes are supported as S&P doesn't maintain the full list but a feature request for the full list is submitted to S&P product. Requires country_iso_code also to have a value other then None. It
|
|
1245
|
+
:param state_iso_code: The ISO 3166-2 Alpha-2 code that represents the primary subdivision of the country the firm the based in. Not all ISO 3166-2 codes are supported as S&P doesn't maintain the full list but a feature request for the full list is submitted to S&P product. Requires country_iso_code also to have a value other then None. It defaults to None
|
|
1223
1246
|
:type state_iso_code: str, optional
|
|
1224
|
-
:param simple_industry: The S&P CIQ Simple Industry defined in ciqSimpleIndustry in XPF. It
|
|
1247
|
+
:param simple_industry: The S&P CIQ Simple Industry defined in ciqSimpleIndustry in XPF. It defaults to None
|
|
1225
1248
|
:type simple_industry: str, optional
|
|
1226
|
-
:param exchange_code: The exchange code for the primary equity listing exchange of the firm. It
|
|
1249
|
+
:param exchange_code: The exchange code for the primary equity listing exchange of the firm. It defaults to None
|
|
1227
1250
|
:type exchange_code: str, optional
|
|
1228
|
-
:
|
|
1251
|
+
:param sic: The SIC industry code. It defaults to None
|
|
1252
|
+
:type sic: str, optional
|
|
1253
|
+
:param naics: The NAICS industry code. It defaults to None
|
|
1254
|
+
:type naics: str, optional
|
|
1255
|
+
:param nace: The NACE industry code. It defaults to None
|
|
1256
|
+
:type nace: str, optional
|
|
1257
|
+
:param anzsic: The ANZSIC industry code. It defaults to None
|
|
1258
|
+
:type anzsic: str, optional
|
|
1259
|
+
:param spcapiqetf: The S&P CapitalIQ ETF industry code. It defaults to None
|
|
1260
|
+
:type spcapiqetf: str, optional
|
|
1261
|
+
:param spratings: The S&P Ratings industry code. It defaults to None
|
|
1262
|
+
:type spratings: str, optional
|
|
1263
|
+
:param gics: The GICS code. It defaults to None
|
|
1264
|
+
:type gics: str, optional
|
|
1265
|
+
:return: A Tickers object that is the intersection of Ticker objects meeting all the supplied parameters.
|
|
1229
1266
|
:rtype: Tickers
|
|
1230
1267
|
"""
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1268
|
+
# Create a list to accumulate the fetched ticker sets
|
|
1269
|
+
ticker_sets: list[Tickers] = []
|
|
1270
|
+
|
|
1271
|
+
# Map the parameters to the industry_dict, pass the values in as the key.
|
|
1272
|
+
industry_dict = {
|
|
1273
|
+
"sic": sic,
|
|
1274
|
+
"naics": naics,
|
|
1275
|
+
"nace": nace,
|
|
1276
|
+
"anzsic": anzsic,
|
|
1277
|
+
"spcapiqetf": spcapiqetf,
|
|
1278
|
+
"spratings": spratings,
|
|
1279
|
+
"gics": gics,
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
if any(
|
|
1283
|
+
parameter is not None
|
|
1284
|
+
for parameter in [country_iso_code, state_iso_code, simple_industry, exchange_code]
|
|
1285
|
+
):
|
|
1286
|
+
ticker_sets.append(
|
|
1287
|
+
Tickers(
|
|
1288
|
+
kfinance_api_client=self.kfinance_api_client,
|
|
1289
|
+
id_triples=self.kfinance_api_client.fetch_ticker_combined(
|
|
1290
|
+
country_iso_code=country_iso_code,
|
|
1291
|
+
state_iso_code=state_iso_code,
|
|
1292
|
+
simple_industry=simple_industry,
|
|
1293
|
+
exchange_code=exchange_code,
|
|
1294
|
+
),
|
|
1295
|
+
)
|
|
1296
|
+
)
|
|
1297
|
+
|
|
1298
|
+
for key, value in industry_dict.items():
|
|
1299
|
+
if value is not None:
|
|
1300
|
+
ticker_sets.append(
|
|
1301
|
+
Tickers(
|
|
1302
|
+
kfinance_api_client=self.kfinance_api_client,
|
|
1303
|
+
id_triples=self.kfinance_api_client.fetch_ticker_from_industry_code(
|
|
1304
|
+
industry_code=value,
|
|
1305
|
+
industry_classification=IndustryClassification(key),
|
|
1306
|
+
),
|
|
1307
|
+
)
|
|
1308
|
+
)
|
|
1309
|
+
|
|
1310
|
+
if not ticker_sets:
|
|
1311
|
+
return Tickers(kfinance_api_client=self.kfinance_api_client, id_triples=set())
|
|
1312
|
+
|
|
1313
|
+
common_ticker_elements = Tickers.intersection(*ticker_sets)
|
|
1314
|
+
return common_ticker_elements
|
|
1240
1315
|
|
|
1241
1316
|
def company(self, company_id: int) -> Company:
|
|
1242
1317
|
"""Generate the Company object from company_id
|
kfinance/meta_classes.py
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
from datetime import datetime
|
|
2
|
-
from functools import
|
|
2
|
+
from functools import cached_property
|
|
3
3
|
import logging
|
|
4
4
|
from typing import TYPE_CHECKING, Any, Callable, Literal, Optional
|
|
5
5
|
|
|
6
|
+
from cachetools import LRUCache, cached
|
|
6
7
|
import numpy as np
|
|
7
8
|
import pandas as pd
|
|
8
9
|
|
|
9
|
-
from .constants import LINE_ITEMS, BusinessRelationshipType, PeriodType
|
|
10
|
+
from .constants import LINE_ITEMS, BusinessRelationshipType, PeriodType, SegmentType
|
|
10
11
|
from .fetch import KFinanceApiClient
|
|
11
12
|
|
|
12
13
|
|
|
@@ -46,7 +47,7 @@ class CompanyFunctionsMetaClass:
|
|
|
46
47
|
if end_quarter and not (1 <= end_quarter <= 4):
|
|
47
48
|
raise ValueError("end_qtr is out of range 1 to 4")
|
|
48
49
|
|
|
49
|
-
@cache
|
|
50
|
+
@cached(cache=LRUCache(maxsize=100))
|
|
50
51
|
def statement(
|
|
51
52
|
self,
|
|
52
53
|
statement_type: str,
|
|
@@ -85,7 +86,7 @@ class CompanyFunctionsMetaClass:
|
|
|
85
86
|
|
|
86
87
|
def income_statement(
|
|
87
88
|
self,
|
|
88
|
-
period_type: Optional[
|
|
89
|
+
period_type: Optional[PeriodType] = None,
|
|
89
90
|
start_year: Optional[int] = None,
|
|
90
91
|
end_year: Optional[int] = None,
|
|
91
92
|
start_quarter: Optional[int] = None,
|
|
@@ -103,7 +104,7 @@ class CompanyFunctionsMetaClass:
|
|
|
103
104
|
|
|
104
105
|
def income_stmt(
|
|
105
106
|
self,
|
|
106
|
-
period_type: Optional[
|
|
107
|
+
period_type: Optional[PeriodType] = None,
|
|
107
108
|
start_year: Optional[int] = None,
|
|
108
109
|
end_year: Optional[int] = None,
|
|
109
110
|
start_quarter: Optional[int] = None,
|
|
@@ -121,7 +122,7 @@ class CompanyFunctionsMetaClass:
|
|
|
121
122
|
|
|
122
123
|
def balance_sheet(
|
|
123
124
|
self,
|
|
124
|
-
period_type: Optional[
|
|
125
|
+
period_type: Optional[PeriodType] = None,
|
|
125
126
|
start_year: Optional[int] = None,
|
|
126
127
|
end_year: Optional[int] = None,
|
|
127
128
|
start_quarter: Optional[int] = None,
|
|
@@ -139,7 +140,7 @@ class CompanyFunctionsMetaClass:
|
|
|
139
140
|
|
|
140
141
|
def cash_flow(
|
|
141
142
|
self,
|
|
142
|
-
period_type: Optional[
|
|
143
|
+
period_type: Optional[PeriodType] = None,
|
|
143
144
|
start_year: Optional[int] = None,
|
|
144
145
|
end_year: Optional[int] = None,
|
|
145
146
|
start_quarter: Optional[int] = None,
|
|
@@ -157,7 +158,7 @@ class CompanyFunctionsMetaClass:
|
|
|
157
158
|
|
|
158
159
|
def cashflow(
|
|
159
160
|
self,
|
|
160
|
-
period_type: Optional[
|
|
161
|
+
period_type: Optional[PeriodType] = None,
|
|
161
162
|
start_year: Optional[int] = None,
|
|
162
163
|
end_year: Optional[int] = None,
|
|
163
164
|
start_quarter: Optional[int] = None,
|
|
@@ -173,7 +174,7 @@ class CompanyFunctionsMetaClass:
|
|
|
173
174
|
end_quarter=end_quarter,
|
|
174
175
|
)
|
|
175
176
|
|
|
176
|
-
@cache
|
|
177
|
+
@cached(cache=LRUCache(maxsize=100))
|
|
177
178
|
def line_item(
|
|
178
179
|
self,
|
|
179
180
|
line_item: str,
|
|
@@ -303,6 +304,114 @@ class CompanyFunctionsMetaClass:
|
|
|
303
304
|
)
|
|
304
305
|
return df.set_index("date")[[column_to_extract]].apply(pd.to_numeric).replace(np.nan, None)
|
|
305
306
|
|
|
307
|
+
def _segments(
|
|
308
|
+
self,
|
|
309
|
+
segment_type: SegmentType,
|
|
310
|
+
period_type: Optional[PeriodType] = None,
|
|
311
|
+
start_year: Optional[int] = None,
|
|
312
|
+
end_year: Optional[int] = None,
|
|
313
|
+
start_quarter: Optional[int] = None,
|
|
314
|
+
end_quarter: Optional[int] = None,
|
|
315
|
+
) -> pd.DataFrame:
|
|
316
|
+
"""Get the company's segments"""
|
|
317
|
+
try:
|
|
318
|
+
self.validate_inputs(
|
|
319
|
+
start_year=start_year,
|
|
320
|
+
end_year=end_year,
|
|
321
|
+
start_quarter=start_quarter,
|
|
322
|
+
end_quarter=end_quarter,
|
|
323
|
+
)
|
|
324
|
+
except ValueError:
|
|
325
|
+
return pd.DataFrame()
|
|
326
|
+
|
|
327
|
+
results = self.kfinance_api_client.fetch_segments(
|
|
328
|
+
company_id=self.company_id,
|
|
329
|
+
segment_type=segment_type,
|
|
330
|
+
period_type=period_type,
|
|
331
|
+
start_year=start_year,
|
|
332
|
+
end_year=end_year,
|
|
333
|
+
start_quarter=start_quarter,
|
|
334
|
+
end_quarter=end_quarter,
|
|
335
|
+
)["segments"]
|
|
336
|
+
|
|
337
|
+
period_name = (
|
|
338
|
+
"Year" if (period_type == PeriodType.annual or period_type is None) else "Period"
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
# flatten the nested dictionary and return as a DataFrame
|
|
342
|
+
rows = []
|
|
343
|
+
for period, segments in results.items():
|
|
344
|
+
for segment_name, line_items in segments.items():
|
|
345
|
+
for line_item, value in line_items.items():
|
|
346
|
+
rows.append([period, segment_name, line_item, value])
|
|
347
|
+
return pd.DataFrame(
|
|
348
|
+
rows, columns=[period_name, "Segment Name", "Line Item", "Value"]
|
|
349
|
+
).replace(np.nan, None)
|
|
350
|
+
|
|
351
|
+
def business_segments(
|
|
352
|
+
self,
|
|
353
|
+
period_type: Optional[PeriodType] = None,
|
|
354
|
+
start_year: Optional[int] = None,
|
|
355
|
+
end_year: Optional[int] = None,
|
|
356
|
+
start_quarter: Optional[int] = None,
|
|
357
|
+
end_quarter: Optional[int] = None,
|
|
358
|
+
) -> pd.DataFrame:
|
|
359
|
+
"""Retrieves the templated line of business segments for a given period_type, start_year, start_quarter, end_year and end_quarter.
|
|
360
|
+
|
|
361
|
+
:param period_type: The period_type requested for. Can be “annual”, “quarterly”, "ytd". Defaults to “annual” when start_quarter and end_quarter are None.
|
|
362
|
+
:type start_year: PeriodType, optional
|
|
363
|
+
:param start_year: The starting calendar year, defaults to None
|
|
364
|
+
:type start_year: int, optional
|
|
365
|
+
:param end_year: The ending calendar year, defaults to None
|
|
366
|
+
:type end_year: int, optional
|
|
367
|
+
:param start_quarter: The starting calendar quarter, defaults to None
|
|
368
|
+
:type start_quarter: int, optional
|
|
369
|
+
:param end_quarter: The ending calendar quarter, defaults to None
|
|
370
|
+
:type end_quarter: int, optional
|
|
371
|
+
:return: A DataFrame with `Year`/`Period`, `Segment Name`, `Line Item` and `Value` column.
|
|
372
|
+
:rtype: pd.DataFrame
|
|
373
|
+
"""
|
|
374
|
+
return self._segments(
|
|
375
|
+
segment_type=SegmentType.business,
|
|
376
|
+
period_type=period_type,
|
|
377
|
+
start_year=start_year,
|
|
378
|
+
end_year=end_year,
|
|
379
|
+
start_quarter=start_quarter,
|
|
380
|
+
end_quarter=end_quarter,
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
def geographic_segments(
|
|
384
|
+
self,
|
|
385
|
+
period_type: Optional[PeriodType] = None,
|
|
386
|
+
start_year: Optional[int] = None,
|
|
387
|
+
end_year: Optional[int] = None,
|
|
388
|
+
start_quarter: Optional[int] = None,
|
|
389
|
+
end_quarter: Optional[int] = None,
|
|
390
|
+
) -> pd.DataFrame:
|
|
391
|
+
"""Retrieves the templated geographic segments for a given company for a given period_type, start_year, start_quarter, end_year and end_quarter.
|
|
392
|
+
|
|
393
|
+
:param period_type: The period_type requested for. Can be “annual”, “quarterly”, "ytd". Defaults to “annual” when start_quarter and end_quarter are None.
|
|
394
|
+
:type start_year: PeriodType, optional
|
|
395
|
+
:param start_year: The starting calendar year, defaults to None
|
|
396
|
+
:type start_year: int, optional
|
|
397
|
+
:param end_year: The ending calendar year, defaults to None
|
|
398
|
+
:type end_year: int, optional
|
|
399
|
+
:param start_quarter: The starting calendar quarter, defaults to None
|
|
400
|
+
:type start_quarter: int, optional
|
|
401
|
+
:param end_quarter: The ending calendar quarter, defaults to None
|
|
402
|
+
:type end_quarter: int, optional
|
|
403
|
+
:return: A DataFrame with `Year`/`Period`, `Segment Name`, `Line Item` and `Value` column.
|
|
404
|
+
:rtype: pd.DataFrame
|
|
405
|
+
"""
|
|
406
|
+
return self._segments(
|
|
407
|
+
segment_type=SegmentType.geographic,
|
|
408
|
+
period_type=period_type,
|
|
409
|
+
start_year=start_year,
|
|
410
|
+
end_year=end_year,
|
|
411
|
+
start_quarter=start_quarter,
|
|
412
|
+
end_quarter=end_quarter,
|
|
413
|
+
)
|
|
414
|
+
|
|
306
415
|
|
|
307
416
|
for line_item in LINE_ITEMS:
|
|
308
417
|
line_item_name = line_item["name"]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from requests_mock import Mocker
|
|
5
|
+
|
|
6
|
+
from kfinance.kfinance import Client
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
SPGI_COMPANY_ID = 21719
|
|
10
|
+
SPGI_SECURITY_ID = 2629107
|
|
11
|
+
SPGI_TRADING_ITEM_ID = 2629108
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@pytest.fixture
|
|
15
|
+
def mock_client(requests_mock: Mocker) -> Client:
|
|
16
|
+
"""Create a KFinanceApiClient with a mock response for the SPGI id triple."""
|
|
17
|
+
|
|
18
|
+
client = Client(refresh_token="foo")
|
|
19
|
+
# Set access token so that the client doesn't try to fetch it.
|
|
20
|
+
client.kfinance_api_client._access_token = "foo" # noqa: SLF001
|
|
21
|
+
client.kfinance_api_client._access_token_expiry = datetime(2100, 1, 1).timestamp() # noqa: SLF001
|
|
22
|
+
|
|
23
|
+
# Create a mock for the SPGI id triple.
|
|
24
|
+
requests_mock.get(
|
|
25
|
+
url="https://kfinance.kensho.com/api/v1/id/SPGI",
|
|
26
|
+
json={
|
|
27
|
+
"trading_item_id": SPGI_TRADING_ITEM_ID,
|
|
28
|
+
"security_id": SPGI_SECURITY_ID,
|
|
29
|
+
"company_id": SPGI_COMPANY_ID,
|
|
30
|
+
},
|
|
31
|
+
)
|
|
32
|
+
return client
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
from concurrent.futures import ThreadPoolExecutor
|
|
2
|
+
import time
|
|
2
3
|
from typing import Any, Dict
|
|
3
4
|
from unittest import TestCase
|
|
4
|
-
from unittest.mock import patch
|
|
5
|
+
from unittest.mock import PropertyMock, patch
|
|
5
6
|
|
|
6
7
|
import numpy as np
|
|
7
8
|
import pandas as pd
|
|
@@ -9,6 +10,7 @@ import pytest
|
|
|
9
10
|
import requests
|
|
10
11
|
import requests_mock
|
|
11
12
|
|
|
13
|
+
from kfinance.batch_request_handling import MAX_WORKERS_CAP
|
|
12
14
|
from kfinance.fetch import KFinanceApiClient
|
|
13
15
|
from kfinance.kfinance import Companies, Company, Ticker, TradingItems
|
|
14
16
|
|
|
@@ -34,8 +36,13 @@ class TestTradingItem(TestCase):
|
|
|
34
36
|
def test_batch_request_property(self, m):
|
|
35
37
|
"""GIVEN a kfinance group object like Companies
|
|
36
38
|
WHEN we batch request a property for each object in the group
|
|
37
|
-
THEN the batch request completes successfully and we get back a mapping of
|
|
38
|
-
company objects to the corresponding values.
|
|
39
|
+
THEN the batch request completes successfully, and we get back a mapping of
|
|
40
|
+
company objects to the corresponding values.
|
|
41
|
+
|
|
42
|
+
Note: This test also checks that multiple tasks can be submitted. In the
|
|
43
|
+
first implementation, we used the threadpool context manager, which shuts down
|
|
44
|
+
the threadpool on __exit__ and prevented further tasks from getting submitted.
|
|
45
|
+
"""
|
|
39
46
|
|
|
40
47
|
m.get(
|
|
41
48
|
"https://kfinance.kensho.com/api/v1/info/1001",
|
|
@@ -52,12 +59,13 @@ class TestTradingItem(TestCase):
|
|
|
52
59
|
},
|
|
53
60
|
)
|
|
54
61
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
62
|
+
for _ in range(3):
|
|
63
|
+
companies = Companies(self.kfinance_api_client, [1001, 1002])
|
|
64
|
+
result = companies.city
|
|
65
|
+
id_based_result = self.company_object_keys_as_company_id(result)
|
|
58
66
|
|
|
59
|
-
|
|
60
|
-
|
|
67
|
+
expected_id_based_result = {1001: "Mock City A", 1002: "Mock City B"}
|
|
68
|
+
self.assertDictEqual(id_based_result, expected_id_based_result)
|
|
61
69
|
|
|
62
70
|
@requests_mock.Mocker()
|
|
63
71
|
def test_batch_request_cached_properties(self, m):
|
|
@@ -254,3 +262,33 @@ class TestTradingItem(TestCase):
|
|
|
254
262
|
|
|
255
263
|
expected_id_based_result = {1001: "Mock City A"}
|
|
256
264
|
self.assertDictEqual(id_based_result, expected_id_based_result)
|
|
265
|
+
|
|
266
|
+
@patch.object(Company, "info", new_callable=PropertyMock)
|
|
267
|
+
def test_batch_requests_processed_in_parallel(self, mock_value: PropertyMock):
|
|
268
|
+
"""
|
|
269
|
+
WHEN a batch request gets processed
|
|
270
|
+
THEN the requests are handled in parallel not sequentially.
|
|
271
|
+
"""
|
|
272
|
+
|
|
273
|
+
sleep_duration = 0.05
|
|
274
|
+
|
|
275
|
+
def mock_info_with_sleep() -> dict[str, str]:
|
|
276
|
+
"""Mock an info call with a short sleep"""
|
|
277
|
+
time.sleep(sleep_duration)
|
|
278
|
+
return {"city": "Cambridge"}
|
|
279
|
+
|
|
280
|
+
mock_value.side_effect = mock_info_with_sleep
|
|
281
|
+
|
|
282
|
+
# Create tasks up to the MAX_WORKERS_CAP (max number of parallel tasks)
|
|
283
|
+
companies = Companies(
|
|
284
|
+
self.kfinance_api_client_with_thread_pool, [i for i in range(MAX_WORKERS_CAP)]
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
start = time.perf_counter()
|
|
288
|
+
result = companies.city
|
|
289
|
+
end = time.perf_counter()
|
|
290
|
+
assert len(result) == MAX_WORKERS_CAP
|
|
291
|
+
# Check that the requests run faster than sequential.
|
|
292
|
+
# In practice, the requests should take barely more than the `sleep_duration` but timing
|
|
293
|
+
# based tests can be flaky, especially in CI.
|
|
294
|
+
assert end - start < MAX_WORKERS_CAP * sleep_duration
|