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

Files changed (36) hide show
  1. {kensho_kfinance-2.0.1.dist-info → kensho_kfinance-2.1.2.dist-info}/METADATA +3 -1
  2. kensho_kfinance-2.1.2.dist-info/RECORD +43 -0
  3. {kensho_kfinance-2.0.1.dist-info → kensho_kfinance-2.1.2.dist-info}/WHEEL +1 -1
  4. kfinance/CHANGELOG.md +12 -0
  5. kfinance/batch_request_handling.py +10 -10
  6. kfinance/constants.py +16 -7
  7. kfinance/fetch.py +84 -40
  8. kfinance/kfinance.py +129 -28
  9. kfinance/meta_classes.py +9 -8
  10. kfinance/tests/conftest.py +32 -0
  11. kfinance/tests/test_batch_requests.py +13 -7
  12. kfinance/tests/test_client.py +54 -0
  13. kfinance/tests/test_fetch.py +8 -2
  14. kfinance/tests/test_group_objects.py +32 -0
  15. kfinance/tests/test_tools.py +1 -27
  16. kfinance/tool_calling/get_business_relationship_from_identifier.py +2 -1
  17. kfinance/tool_calling/get_capitalization_from_identifier.py +2 -1
  18. kfinance/tool_calling/get_company_id_from_identifier.py +2 -0
  19. kfinance/tool_calling/get_cusip_from_ticker.py +2 -0
  20. kfinance/tool_calling/get_earnings_call_datetimes_from_identifier.py +2 -0
  21. kfinance/tool_calling/get_financial_line_item_from_identifier.py +2 -1
  22. kfinance/tool_calling/get_financial_statement_from_identifier.py +2 -1
  23. kfinance/tool_calling/get_history_metadata_from_identifier.py +2 -1
  24. kfinance/tool_calling/get_info_from_identifier.py +2 -0
  25. kfinance/tool_calling/get_isin_from_ticker.py +2 -0
  26. kfinance/tool_calling/get_latest.py +2 -1
  27. kfinance/tool_calling/get_n_quarters_ago.py +2 -1
  28. kfinance/tool_calling/get_prices_from_identifier.py +2 -1
  29. kfinance/tool_calling/get_security_id_from_identifier.py +2 -0
  30. kfinance/tool_calling/get_trading_item_id_from_identifier.py +2 -0
  31. kfinance/tool_calling/shared_models.py +2 -0
  32. kfinance/version.py +2 -2
  33. kensho_kfinance-2.0.1.dist-info/RECORD +0 -40
  34. {kensho_kfinance-2.0.1.dist-info → kensho_kfinance-2.1.2.dist-info}/licenses/AUTHORS.md +0 -0
  35. {kensho_kfinance-2.0.1.dist-info → kensho_kfinance-2.1.2.dist-info}/licenses/LICENSE +0 -0
  36. {kensho_kfinance-2.0.1.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
- self.primary_security = Security(
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.securities = Securities(
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
- self.info = self.kfinance_api_client.fetch_info(self.company_id)
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["company_id"],
995
- security_id=id_triple["security_id"],
996
- trading_item_id=id_triple["trading_item_id"],
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 = [t(kfinance_client=self) for t in ALL_TOOLS] # type: ignore[call-arg]
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 tickers object representing the collection of Tickers that meet all the supplied parameters
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. A parameter set to None is not used to filter on
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 default None
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 default None
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 default None
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 default None
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
- :return: A tickers object that is the group of Ticker objects meeting all the supplied parameters
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
- return Tickers(
1232
- kfinance_api_client=self.kfinance_api_client,
1233
- id_triples=self.kfinance_api_client.fetch_ticker_combined(
1234
- country_iso_code=country_iso_code,
1235
- state_iso_code=state_iso_code,
1236
- simple_industry=simple_industry,
1237
- exchange_code=exchange_code,
1238
- )["tickers"],
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 cache, cached_property
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[str] = None,
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[str] = None,
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[str] = None,
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[str] = None,
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[str] = None,
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
- companies = Companies(self.kfinance_api_client, [1001, 1002])
56
- result = companies.city
57
- id_based_result = self.company_object_keys_as_company_id(result)
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
- expected_id_based_result = {1001: "Mock City A", 1002: "Mock City B"}
60
- self.assertDictEqual(id_based_result, expected_id_based_result)
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
@@ -1,5 +1,5 @@
1
1
  from unittest import TestCase
2
- from unittest.mock import Mock
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 = Mock()
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
@@ -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