kensho-kfinance 2.0.0__py3-none-any.whl → 2.1.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.0.dist-info → kensho_kfinance-2.1.2.dist-info}/METADATA +3 -1
- kensho_kfinance-2.1.2.dist-info/RECORD +43 -0
- {kensho_kfinance-2.0.0.dist-info → kensho_kfinance-2.1.2.dist-info}/WHEEL +1 -1
- kfinance/CHANGELOG.md +15 -0
- kfinance/batch_request_handling.py +10 -10
- kfinance/constants.py +16 -7
- kfinance/fetch.py +84 -40
- kfinance/kfinance.py +129 -28
- kfinance/meta_classes.py +9 -8
- kfinance/tests/conftest.py +32 -0
- kfinance/tests/test_batch_requests.py +13 -7
- kfinance/tests/test_client.py +54 -0
- kfinance/tests/test_fetch.py +8 -2
- kfinance/tests/test_group_objects.py +32 -0
- kfinance/tests/test_tools.py +1 -27
- 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_company_id_from_identifier.py +2 -0
- 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 +2 -0
- 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/get_security_id_from_identifier.py +2 -0
- kfinance/tool_calling/get_trading_item_id_from_identifier.py +2 -0
- kfinance/tool_calling/shared_models.py +2 -0
- kfinance/version.py +2 -2
- kensho_kfinance-2.0.0.dist-info/RECORD +0 -40
- {kensho_kfinance-2.0.0.dist-info → kensho_kfinance-2.1.2.dist-info}/licenses/AUTHORS.md +0 -0
- {kensho_kfinance-2.0.0.dist-info → kensho_kfinance-2.1.2.dist-info}/licenses/LICENSE +0 -0
- {kensho_kfinance-2.0.0.dist-info → kensho_kfinance-2.1.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:
|
|
@@ -268,8 +265,7 @@ class Company(CompanyFunctionsMetaClass):
|
|
|
268
265
|
:return: a dict with containing: name, status, type, simple industry, number of employees, 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:
|
|
@@ -539,6 +535,23 @@ 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 identifier triple for the Ticker object."""
|
|
541
|
+
return IdentificationTriple(
|
|
542
|
+
company_id=self.company_id,
|
|
543
|
+
security_id=self.security_id,
|
|
544
|
+
trading_item_id=self.trading_item_id,
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
def __hash__(self) -> int:
|
|
548
|
+
return hash(self.id_triple)
|
|
549
|
+
|
|
550
|
+
def __eq__(self, other: Any) -> bool:
|
|
551
|
+
if not isinstance(other, Ticker):
|
|
552
|
+
return False
|
|
553
|
+
return self.id_triple == other.id_triple
|
|
554
|
+
|
|
542
555
|
def __str__(self) -> str:
|
|
543
556
|
"""String representation for the ticker object"""
|
|
544
557
|
str_attributes = []
|
|
@@ -991,13 +1004,33 @@ class Tickers(set):
|
|
|
991
1004
|
super().__init__(
|
|
992
1005
|
Ticker(
|
|
993
1006
|
kfinance_api_client=kfinance_api_client,
|
|
994
|
-
company_id=id_triple
|
|
995
|
-
security_id=id_triple
|
|
996
|
-
trading_item_id=id_triple
|
|
1007
|
+
company_id=id_triple.company_id,
|
|
1008
|
+
security_id=id_triple.security_id,
|
|
1009
|
+
trading_item_id=id_triple.trading_item_id,
|
|
997
1010
|
)
|
|
998
1011
|
for id_triple in id_triples
|
|
999
1012
|
)
|
|
1000
1013
|
|
|
1014
|
+
def intersection(self, *s: Iterable[Any]) -> Tickers:
|
|
1015
|
+
"""Returns the intersection of Tickers objects"""
|
|
1016
|
+
for obj in s:
|
|
1017
|
+
if not isinstance(obj, Tickers):
|
|
1018
|
+
raise ValueError("Can only intersect Tickers object with other Tickers object.")
|
|
1019
|
+
|
|
1020
|
+
self_triples = {t.id_triple for t in self}
|
|
1021
|
+
set_triples = []
|
|
1022
|
+
|
|
1023
|
+
for ticker_set in s:
|
|
1024
|
+
set_triples.append({t.id_triple for t in ticker_set})
|
|
1025
|
+
common_triples = self_triples.intersection(*set_triples)
|
|
1026
|
+
|
|
1027
|
+
return Tickers(kfinance_api_client=self.kfinance_api_client, id_triples=common_triples)
|
|
1028
|
+
|
|
1029
|
+
def __and__(self, other: Any) -> "Tickers":
|
|
1030
|
+
if not isinstance(other, Tickers):
|
|
1031
|
+
raise ValueError("Can only combine Tickers objects with other Tickers objects.")
|
|
1032
|
+
return self.intersection(other)
|
|
1033
|
+
|
|
1001
1034
|
def companies(self) -> Companies:
|
|
1002
1035
|
"""Build a group of company objects from the group of tickers
|
|
1003
1036
|
|
|
@@ -1126,7 +1159,16 @@ class Client:
|
|
|
1126
1159
|
if self._tools is None:
|
|
1127
1160
|
from kfinance.tool_calling import ALL_TOOLS
|
|
1128
1161
|
|
|
1129
|
-
self._tools = [
|
|
1162
|
+
self._tools = []
|
|
1163
|
+
# Add tool to _tools if the user has permissions to use it.
|
|
1164
|
+
for tool_cls in ALL_TOOLS:
|
|
1165
|
+
tool = tool_cls(kfinance_client=self) # type: ignore[call-arg]
|
|
1166
|
+
if (
|
|
1167
|
+
tool.required_permission is None
|
|
1168
|
+
or tool.required_permission in self.kfinance_api_client.user_permissions
|
|
1169
|
+
):
|
|
1170
|
+
self._tools.append(tool)
|
|
1171
|
+
|
|
1130
1172
|
return self._tools
|
|
1131
1173
|
|
|
1132
1174
|
@property
|
|
@@ -1212,31 +1254,90 @@ class Client:
|
|
|
1212
1254
|
state_iso_code: Optional[str] = None,
|
|
1213
1255
|
simple_industry: Optional[str] = None,
|
|
1214
1256
|
exchange_code: Optional[str] = None,
|
|
1257
|
+
sic: Optional[str] = None,
|
|
1258
|
+
naics: Optional[str] = None,
|
|
1259
|
+
nace: Optional[str] = None,
|
|
1260
|
+
anzsic: Optional[str] = None,
|
|
1261
|
+
spcapiqetf: Optional[str] = None,
|
|
1262
|
+
spratings: Optional[str] = None,
|
|
1263
|
+
gics: Optional[str] = None,
|
|
1215
1264
|
) -> Tickers:
|
|
1216
|
-
"""Generate
|
|
1265
|
+
"""Generate a Tickers object representing the collection of Tickers that meet all the supplied parameters.
|
|
1217
1266
|
|
|
1218
|
-
One of country_iso_code, simple_industry, or exchange_code must be supplied
|
|
1267
|
+
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
1268
|
|
|
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
|
|
1269
|
+
: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
1270
|
: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
|
|
1271
|
+
: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
1272
|
:type state_iso_code: str, optional
|
|
1224
|
-
:param simple_industry: The S&P CIQ Simple Industry defined in ciqSimpleIndustry in XPF. It
|
|
1273
|
+
:param simple_industry: The S&P CIQ Simple Industry defined in ciqSimpleIndustry in XPF. It defaults to None
|
|
1225
1274
|
:type simple_industry: str, optional
|
|
1226
|
-
:param exchange_code: The exchange code for the primary equity listing exchange of the firm. It
|
|
1275
|
+
:param exchange_code: The exchange code for the primary equity listing exchange of the firm. It defaults to None
|
|
1227
1276
|
:type exchange_code: str, optional
|
|
1228
|
-
:
|
|
1277
|
+
:param sic: The SIC industry code. It defaults to None
|
|
1278
|
+
:type sic: str, optional
|
|
1279
|
+
:param naics: The NAICS industry code. It defaults to None
|
|
1280
|
+
:type naics: str, optional
|
|
1281
|
+
:param nace: The NACE industry code. It defaults to None
|
|
1282
|
+
:type nace: str, optional
|
|
1283
|
+
:param anzsic: The ANZSIC industry code. It defaults to None
|
|
1284
|
+
:type anzsic: str, optional
|
|
1285
|
+
:param spcapiqetf: The S&P CapitalIQ ETF industry code. It defaults to None
|
|
1286
|
+
:type spcapiqetf: str, optional
|
|
1287
|
+
:param spratings: The S&P Ratings industry code. It defaults to None
|
|
1288
|
+
:type spratings: str, optional
|
|
1289
|
+
:param gics: The GICS code. It defaults to None
|
|
1290
|
+
:type gics: str, optional
|
|
1291
|
+
:return: A Tickers object that is the intersection of Ticker objects meeting all the supplied parameters.
|
|
1229
1292
|
:rtype: Tickers
|
|
1230
1293
|
"""
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1294
|
+
# Create a list to accumulate the fetched ticker sets
|
|
1295
|
+
ticker_sets: list[Tickers] = []
|
|
1296
|
+
|
|
1297
|
+
# Map the parameters to the industry_dict, pass the values in as the key.
|
|
1298
|
+
industry_dict = {
|
|
1299
|
+
"sic": sic,
|
|
1300
|
+
"naics": naics,
|
|
1301
|
+
"nace": nace,
|
|
1302
|
+
"anzsic": anzsic,
|
|
1303
|
+
"spcapiqetf": spcapiqetf,
|
|
1304
|
+
"spratings": spratings,
|
|
1305
|
+
"gics": gics,
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
if any(
|
|
1309
|
+
parameter is not None
|
|
1310
|
+
for parameter in [country_iso_code, state_iso_code, simple_industry, exchange_code]
|
|
1311
|
+
):
|
|
1312
|
+
ticker_sets.append(
|
|
1313
|
+
Tickers(
|
|
1314
|
+
kfinance_api_client=self.kfinance_api_client,
|
|
1315
|
+
id_triples=self.kfinance_api_client.fetch_ticker_combined(
|
|
1316
|
+
country_iso_code=country_iso_code,
|
|
1317
|
+
state_iso_code=state_iso_code,
|
|
1318
|
+
simple_industry=simple_industry,
|
|
1319
|
+
exchange_code=exchange_code,
|
|
1320
|
+
),
|
|
1321
|
+
)
|
|
1322
|
+
)
|
|
1323
|
+
|
|
1324
|
+
for key, value in industry_dict.items():
|
|
1325
|
+
if value is not None:
|
|
1326
|
+
ticker_sets.append(
|
|
1327
|
+
Tickers(
|
|
1328
|
+
kfinance_api_client=self.kfinance_api_client,
|
|
1329
|
+
id_triples=self.kfinance_api_client.fetch_ticker_from_industry_code(
|
|
1330
|
+
industry_code=value,
|
|
1331
|
+
industry_classification=IndustryClassification(key),
|
|
1332
|
+
),
|
|
1333
|
+
)
|
|
1334
|
+
)
|
|
1335
|
+
|
|
1336
|
+
if not ticker_sets:
|
|
1337
|
+
return Tickers(kfinance_api_client=self.kfinance_api_client, id_triples=set())
|
|
1338
|
+
|
|
1339
|
+
common_ticker_elements = Tickers.intersection(*ticker_sets)
|
|
1340
|
+
return common_ticker_elements
|
|
1240
1341
|
|
|
1241
1342
|
def company(self, company_id: int) -> Company:
|
|
1242
1343
|
"""Generate the Company object from company_id
|
kfinance/meta_classes.py
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
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
|
|
|
@@ -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,
|
|
@@ -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
|
|
@@ -34,8 +34,13 @@ class TestTradingItem(TestCase):
|
|
|
34
34
|
def test_batch_request_property(self, m):
|
|
35
35
|
"""GIVEN a kfinance group object like Companies
|
|
36
36
|
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.
|
|
37
|
+
THEN the batch request completes successfully, and we get back a mapping of
|
|
38
|
+
company objects to the corresponding values.
|
|
39
|
+
|
|
40
|
+
Note: This test also checks that multiple tasks can be submitted. In the
|
|
41
|
+
first implementation, we used the threadpool context manager, which shuts down
|
|
42
|
+
the threadpool on __exit__ and prevented further tasks from getting submitted.
|
|
43
|
+
"""
|
|
39
44
|
|
|
40
45
|
m.get(
|
|
41
46
|
"https://kfinance.kensho.com/api/v1/info/1001",
|
|
@@ -52,12 +57,13 @@ class TestTradingItem(TestCase):
|
|
|
52
57
|
},
|
|
53
58
|
)
|
|
54
59
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
60
|
+
for _ in range(3):
|
|
61
|
+
companies = Companies(self.kfinance_api_client, [1001, 1002])
|
|
62
|
+
result = companies.city
|
|
63
|
+
id_based_result = self.company_object_keys_as_company_id(result)
|
|
58
64
|
|
|
59
|
-
|
|
60
|
-
|
|
65
|
+
expected_id_based_result = {1001: "Mock City A", 1002: "Mock City B"}
|
|
66
|
+
self.assertDictEqual(id_based_result, expected_id_based_result)
|
|
61
67
|
|
|
62
68
|
@requests_mock.Mocker()
|
|
63
69
|
def test_batch_request_cached_properties(self, m):
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from unittest.mock import Mock
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from kfinance.constants import Permission
|
|
6
|
+
from kfinance.kfinance import Client
|
|
7
|
+
from kfinance.tool_calling import (
|
|
8
|
+
GetBusinessRelationshipFromIdentifier,
|
|
9
|
+
GetFinancialStatementFromIdentifier,
|
|
10
|
+
GetLatest,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TestLangchainTools:
|
|
15
|
+
@pytest.mark.parametrize(
|
|
16
|
+
"fetch_value, parsed_permissions",
|
|
17
|
+
[
|
|
18
|
+
pytest.param(["RelationshipPermission"], {Permission.RelationshipPermission}),
|
|
19
|
+
pytest.param([], set(), id="empty permissions don't raise."),
|
|
20
|
+
pytest.param(
|
|
21
|
+
["InvalidPermission"], set(), id="invalid permissions get logged but don't raise."
|
|
22
|
+
),
|
|
23
|
+
],
|
|
24
|
+
)
|
|
25
|
+
def test_user_permissions(
|
|
26
|
+
self, fetch_value: list[str], parsed_permissions: set[Permission], mock_client: Client
|
|
27
|
+
) -> None:
|
|
28
|
+
"""
|
|
29
|
+
WHEN we fetch user permissions from the fetch_permissions endpoint
|
|
30
|
+
THEN we correctly parse those permission strings into Permission enums.
|
|
31
|
+
"""
|
|
32
|
+
mock_client.kfinance_api_client.fetch_permissions = Mock()
|
|
33
|
+
mock_client.kfinance_api_client.fetch_permissions.return_value = {
|
|
34
|
+
"permissions": fetch_value
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
assert mock_client.kfinance_api_client.user_permissions == parsed_permissions
|
|
38
|
+
|
|
39
|
+
def test_permission_filtering(self, mock_client: Client):
|
|
40
|
+
"""
|
|
41
|
+
GIVEN a user with limited permissions
|
|
42
|
+
WHEN we filter tools by permissions
|
|
43
|
+
THEN we only return tools that either don't require permissions or tools that the user
|
|
44
|
+
specifically has access to.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
mock_client.kfinance_api_client._user_permissions = {Permission.RelationshipPermission} # noqa: SLF001
|
|
48
|
+
tool_classes = [type(t) for t in mock_client.langchain_tools]
|
|
49
|
+
# User should have access to GetBusinessRelationshipFromIdentifier
|
|
50
|
+
assert GetBusinessRelationshipFromIdentifier in tool_classes
|
|
51
|
+
# User should have access to functions that don't require permissions
|
|
52
|
+
assert GetLatest in tool_classes
|
|
53
|
+
# User should not have access to functions that require statement permissions
|
|
54
|
+
assert GetFinancialStatementFromIdentifier not in tool_classes
|
kfinance/tests/test_fetch.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from unittest import TestCase
|
|
2
|
-
from unittest.mock import
|
|
2
|
+
from unittest.mock import MagicMock
|
|
3
3
|
|
|
4
4
|
import pytest
|
|
5
5
|
|
|
@@ -10,7 +10,7 @@ from kfinance.fetch import KFinanceApiClient
|
|
|
10
10
|
def build_mock_api_client() -> KFinanceApiClient:
|
|
11
11
|
"""Create a KFinanceApiClient with mocked-out fetch function."""
|
|
12
12
|
kfinance_api_client = KFinanceApiClient(refresh_token="fake_refresh_token")
|
|
13
|
-
kfinance_api_client.fetch =
|
|
13
|
+
kfinance_api_client.fetch = MagicMock()
|
|
14
14
|
return kfinance_api_client
|
|
15
15
|
|
|
16
16
|
|
|
@@ -250,3 +250,9 @@ class TestMarketCap:
|
|
|
250
250
|
company_id=company_id, start_date=start_date, end_date=end_date
|
|
251
251
|
)
|
|
252
252
|
client.fetch.assert_called_with(expected_fetch_url)
|
|
253
|
+
|
|
254
|
+
def test_fetch_permissions(self):
|
|
255
|
+
client = build_mock_api_client()
|
|
256
|
+
expected_fetch_url = f"{client.url_base}users/permissions"
|
|
257
|
+
client.fetch_permissions()
|
|
258
|
+
client.fetch.assert_called_with(expected_fetch_url)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from unittest.mock import Mock
|
|
2
|
+
|
|
3
|
+
from kfinance.kfinance import Client, IdentificationTriple, Tickers
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TestTickers:
|
|
7
|
+
def test_tickers(self, mock_client: Client):
|
|
8
|
+
"""
|
|
9
|
+
WHEN the client requests tickers using multiple filters
|
|
10
|
+
THEN only tickers matching all filter criteria are returned
|
|
11
|
+
"""
|
|
12
|
+
ticker_1 = IdentificationTriple(company_id=1, security_id=1, trading_item_id=1)
|
|
13
|
+
ticker_2 = IdentificationTriple(company_id=2, security_id=2, trading_item_id=2)
|
|
14
|
+
ticker_3 = IdentificationTriple(company_id=3, security_id=3, trading_item_id=3)
|
|
15
|
+
expected_intersection = Tickers(
|
|
16
|
+
kfinance_api_client=mock_client.kfinance_api_client, id_triples=[ticker_2]
|
|
17
|
+
)
|
|
18
|
+
# tickers() calls both fetch_ticker_combined and fetch_ticker_from_industry_code so we set two different return values to test intersection()
|
|
19
|
+
mock_client.kfinance_api_client.fetch_ticker_combined = Mock()
|
|
20
|
+
mock_client.kfinance_api_client.fetch_ticker_combined.return_value = [ticker_1, ticker_2]
|
|
21
|
+
mock_client.kfinance_api_client.fetch_ticker_from_industry_code = Mock()
|
|
22
|
+
mock_client.kfinance_api_client.fetch_ticker_from_industry_code.return_value = [
|
|
23
|
+
ticker_3,
|
|
24
|
+
ticker_2,
|
|
25
|
+
]
|
|
26
|
+
tickers_object = mock_client.tickers(
|
|
27
|
+
country_iso_code="USA", state_iso_code="FL", sic="6141", gics="2419512"
|
|
28
|
+
)
|
|
29
|
+
# fetch_ticker_from_industry_code should be called once for SIC and GICS
|
|
30
|
+
assert mock_client.kfinance_api_client.fetch_ticker_from_industry_code.call_count == 2
|
|
31
|
+
# Only the common tickers are returned
|
|
32
|
+
assert tickers_object == expected_intersection
|
kfinance/tests/test_tools.py
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
from datetime import date, datetime
|
|
2
2
|
|
|
3
3
|
from langchain_core.utils.function_calling import convert_to_openai_tool
|
|
4
|
-
import pytest
|
|
5
4
|
from requests_mock import Mocker
|
|
6
5
|
import time_machine
|
|
7
6
|
|
|
8
7
|
from kfinance.constants import BusinessRelationshipType, Capitalization, StatementType
|
|
9
8
|
from kfinance.kfinance import Client
|
|
9
|
+
from kfinance.tests.conftest import SPGI_COMPANY_ID, SPGI_SECURITY_ID, SPGI_TRADING_ITEM_ID
|
|
10
10
|
from kfinance.tool_calling import (
|
|
11
11
|
GetCompanyIdFromIdentifier,
|
|
12
12
|
GetEarningsCallDatetimesFromIdentifier,
|
|
@@ -43,32 +43,6 @@ from kfinance.tool_calling.get_prices_from_identifier import GetPricesFromIdenti
|
|
|
43
43
|
from kfinance.tool_calling.shared_models import ToolArgsWithIdentifier
|
|
44
44
|
|
|
45
45
|
|
|
46
|
-
SPGI_COMPANY_ID = 21719
|
|
47
|
-
SPGI_SECURITY_ID = 2629107
|
|
48
|
-
SPGI_TRADING_ITEM_ID = 2629108
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
@pytest.fixture
|
|
52
|
-
def mock_client(requests_mock: Mocker) -> Client:
|
|
53
|
-
"""Create a KFinanceApiClient with a mock response for the SPGI id triple."""
|
|
54
|
-
|
|
55
|
-
client = Client(refresh_token="foo")
|
|
56
|
-
# Set access token so that the client doesn't try to fetch it.
|
|
57
|
-
client.kfinance_api_client._access_token = "foo" # noqa: SLF001
|
|
58
|
-
client.kfinance_api_client._access_token_expiry = datetime(2100, 1, 1).timestamp() # noqa: SLF001
|
|
59
|
-
|
|
60
|
-
# Create a mock for the SPGI id triple.
|
|
61
|
-
requests_mock.get(
|
|
62
|
-
url="https://kfinance.kensho.com/api/v1/id/SPGI",
|
|
63
|
-
json={
|
|
64
|
-
"trading_item_id": SPGI_TRADING_ITEM_ID,
|
|
65
|
-
"security_id": SPGI_SECURITY_ID,
|
|
66
|
-
"company_id": SPGI_COMPANY_ID,
|
|
67
|
-
},
|
|
68
|
-
)
|
|
69
|
-
return client
|
|
70
|
-
|
|
71
|
-
|
|
72
46
|
class TestGetBusinessRelationshipFromIdentifier:
|
|
73
47
|
def test_get_business_relationship_from_identifier(
|
|
74
48
|
self, requests_mock: Mocker, mock_client: Client
|
|
@@ -2,7 +2,7 @@ from typing import Type
|
|
|
2
2
|
|
|
3
3
|
from pydantic import BaseModel
|
|
4
4
|
|
|
5
|
-
from kfinance.constants import BusinessRelationshipType
|
|
5
|
+
from kfinance.constants import BusinessRelationshipType, Permission
|
|
6
6
|
from kfinance.kfinance import BusinessRelationships
|
|
7
7
|
from kfinance.tool_calling.shared_models import KfinanceTool, ToolArgsWithIdentifier
|
|
8
8
|
|
|
@@ -16,6 +16,7 @@ class GetBusinessRelationshipFromIdentifier(KfinanceTool):
|
|
|
16
16
|
name: str = "get_business_relationship_from_identifier"
|
|
17
17
|
description: str = 'Get the current and previous company IDs that are relationship_type of a given identifier. For example, "What are the current distributors of SPGI?" or "What are the previous borrowers of JPM?"'
|
|
18
18
|
args_schema: Type[BaseModel] = GetBusinessRelationshipFromIdentifierArgs
|
|
19
|
+
required_permission: Permission | None = Permission.RelationshipPermission
|
|
19
20
|
|
|
20
21
|
def _run(self, identifier: str, business_relationship: BusinessRelationshipType) -> dict:
|
|
21
22
|
ticker = self.kfinance_client.ticker(identifier)
|
|
@@ -2,7 +2,7 @@ from datetime import date
|
|
|
2
2
|
|
|
3
3
|
from pydantic import Field
|
|
4
4
|
|
|
5
|
-
from kfinance.constants import Capitalization
|
|
5
|
+
from kfinance.constants import Capitalization, Permission
|
|
6
6
|
from kfinance.tool_calling.shared_models import KfinanceTool, ToolArgsWithIdentifier
|
|
7
7
|
|
|
8
8
|
|
|
@@ -21,6 +21,7 @@ class GetCapitalizationFromIdentifier(KfinanceTool):
|
|
|
21
21
|
name: str = "get_capitalization_from_identifier"
|
|
22
22
|
description: str = "Get the historical market cap, tev (Total Enterprise Value), or shares outstanding of an identifier between inclusive start_date and inclusive end date. When requesting the most recent values, leave start_date and end_date empty."
|
|
23
23
|
args_schema = GetCapitalizationFromIdentifierArgs
|
|
24
|
+
required_permission: Permission | None = Permission.PricingPermission
|
|
24
25
|
|
|
25
26
|
def _run(
|
|
26
27
|
self,
|
|
@@ -2,6 +2,7 @@ from typing import Type
|
|
|
2
2
|
|
|
3
3
|
from pydantic import BaseModel
|
|
4
4
|
|
|
5
|
+
from kfinance.constants import Permission
|
|
5
6
|
from kfinance.tool_calling.shared_models import KfinanceTool, ToolArgsWithIdentifier
|
|
6
7
|
|
|
7
8
|
|
|
@@ -9,6 +10,7 @@ class GetCompanyIdFromIdentifier(KfinanceTool):
|
|
|
9
10
|
name: str = "get_company_id_from_identifier"
|
|
10
11
|
description: str = "Get the company id associated with an identifier."
|
|
11
12
|
args_schema: Type[BaseModel] = ToolArgsWithIdentifier
|
|
13
|
+
required_permission: Permission | None = None
|
|
12
14
|
|
|
13
15
|
def _run(self, identifier: str) -> int:
|
|
14
16
|
return self.kfinance_client.ticker(identifier).company_id
|
|
@@ -2,6 +2,7 @@ from typing import Type
|
|
|
2
2
|
|
|
3
3
|
from pydantic import BaseModel, Field
|
|
4
4
|
|
|
5
|
+
from kfinance.constants import Permission
|
|
5
6
|
from kfinance.tool_calling.shared_models import KfinanceTool
|
|
6
7
|
|
|
7
8
|
|
|
@@ -13,6 +14,7 @@ class GetCusipFromTicker(KfinanceTool):
|
|
|
13
14
|
name: str = "get_cusip_from_ticker"
|
|
14
15
|
description: str = "Get the CUSIP associated with a ticker."
|
|
15
16
|
args_schema: Type[BaseModel] = GetCusipFromTickerArgs
|
|
17
|
+
required_permission: Permission | None = Permission.IDPermission
|
|
16
18
|
|
|
17
19
|
def _run(self, ticker_str: str) -> str:
|
|
18
20
|
return self.kfinance_client.ticker(ticker_str).cusip
|
|
@@ -3,6 +3,7 @@ from typing import Type
|
|
|
3
3
|
|
|
4
4
|
from pydantic import BaseModel
|
|
5
5
|
|
|
6
|
+
from kfinance.constants import Permission
|
|
6
7
|
from kfinance.tool_calling.shared_models import KfinanceTool, ToolArgsWithIdentifier
|
|
7
8
|
|
|
8
9
|
|
|
@@ -10,6 +11,7 @@ class GetEarningsCallDatetimesFromIdentifier(KfinanceTool):
|
|
|
10
11
|
name: str = "get_earnings_call_datetimes_from_identifier"
|
|
11
12
|
description: str = "Get earnings call datetimes associated with an identifier."
|
|
12
13
|
args_schema: Type[BaseModel] = ToolArgsWithIdentifier
|
|
14
|
+
required_permission: Permission | None = Permission.EarningsPermission
|
|
13
15
|
|
|
14
16
|
def _run(self, identifier: str) -> str:
|
|
15
17
|
ticker = self.kfinance_client.ticker(identifier)
|
|
@@ -2,7 +2,7 @@ from typing import Literal, Type
|
|
|
2
2
|
|
|
3
3
|
from pydantic import BaseModel, Field
|
|
4
4
|
|
|
5
|
-
from kfinance.constants import LINE_ITEM_NAMES_AND_ALIASES, PeriodType
|
|
5
|
+
from kfinance.constants import LINE_ITEM_NAMES_AND_ALIASES, PeriodType, Permission
|
|
6
6
|
from kfinance.tool_calling.shared_models import KfinanceTool, ToolArgsWithIdentifier
|
|
7
7
|
|
|
8
8
|
|
|
@@ -24,6 +24,7 @@ class GetFinancialLineItemFromIdentifier(KfinanceTool):
|
|
|
24
24
|
name: str = "get_financial_line_item_from_identifier"
|
|
25
25
|
description: str = "Get the financial line item associated with an identifier."
|
|
26
26
|
args_schema: Type[BaseModel] = GetFinancialLineItemFromIdentifierArgs
|
|
27
|
+
required_permission: Permission | None = Permission.StatementsPermission
|
|
27
28
|
|
|
28
29
|
def _run(
|
|
29
30
|
self,
|
|
@@ -2,7 +2,7 @@ from typing import Literal, Type
|
|
|
2
2
|
|
|
3
3
|
from pydantic import BaseModel, Field
|
|
4
4
|
|
|
5
|
-
from kfinance.constants import PeriodType, StatementType
|
|
5
|
+
from kfinance.constants import PeriodType, Permission, StatementType
|
|
6
6
|
from kfinance.tool_calling.shared_models import KfinanceTool, ToolArgsWithIdentifier
|
|
7
7
|
|
|
8
8
|
|
|
@@ -20,6 +20,7 @@ class GetFinancialStatementFromIdentifier(KfinanceTool):
|
|
|
20
20
|
name: str = "get_financial_statement_from_identifier"
|
|
21
21
|
description: str = "Get the financial statement associated with an identifier."
|
|
22
22
|
args_schema: Type[BaseModel] = GetFinancialStatementFromIdentifierArgs
|
|
23
|
+
required_permission: Permission | None = Permission.StatementsPermission
|
|
23
24
|
|
|
24
25
|
def _run(
|
|
25
26
|
self,
|
|
@@ -2,7 +2,7 @@ from typing import Type
|
|
|
2
2
|
|
|
3
3
|
from pydantic import BaseModel
|
|
4
4
|
|
|
5
|
-
from kfinance.constants import HistoryMetadata
|
|
5
|
+
from kfinance.constants import HistoryMetadata, Permission
|
|
6
6
|
from kfinance.tool_calling.shared_models import KfinanceTool, ToolArgsWithIdentifier
|
|
7
7
|
|
|
8
8
|
|
|
@@ -10,6 +10,7 @@ class GetHistoryMetadataFromIdentifier(KfinanceTool):
|
|
|
10
10
|
name: str = "get_history_metadata_from_identifier"
|
|
11
11
|
description: str = "Get the history metadata associated with an identifier. History metadata includes currency, symbol, exchange name, instrument type, and first trade date."
|
|
12
12
|
args_schema: Type[BaseModel] = ToolArgsWithIdentifier
|
|
13
|
+
required_permission: Permission | None = None
|
|
13
14
|
|
|
14
15
|
def _run(self, identifier: str) -> HistoryMetadata:
|
|
15
16
|
return self.kfinance_client.ticker(identifier).history_metadata
|