kensho-kfinance 2.0.1__tar.gz → 2.1.2__tar.gz

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 (69) hide show
  1. {kensho_kfinance-2.0.1/kensho_kfinance.egg-info → kensho_kfinance-2.1.2}/PKG-INFO +3 -1
  2. {kensho_kfinance-2.0.1 → kensho_kfinance-2.1.2/kensho_kfinance.egg-info}/PKG-INFO +3 -1
  3. {kensho_kfinance-2.0.1 → kensho_kfinance-2.1.2}/kensho_kfinance.egg-info/SOURCES.txt +3 -0
  4. {kensho_kfinance-2.0.1 → kensho_kfinance-2.1.2}/kensho_kfinance.egg-info/requires.txt +2 -0
  5. {kensho_kfinance-2.0.1 → kensho_kfinance-2.1.2}/kfinance/CHANGELOG.md +12 -0
  6. {kensho_kfinance-2.0.1 → kensho_kfinance-2.1.2}/kfinance/batch_request_handling.py +10 -10
  7. {kensho_kfinance-2.0.1 → kensho_kfinance-2.1.2}/kfinance/constants.py +16 -7
  8. {kensho_kfinance-2.0.1 → kensho_kfinance-2.1.2}/kfinance/fetch.py +84 -40
  9. {kensho_kfinance-2.0.1 → kensho_kfinance-2.1.2}/kfinance/kfinance.py +129 -28
  10. {kensho_kfinance-2.0.1 → kensho_kfinance-2.1.2}/kfinance/meta_classes.py +9 -8
  11. kensho_kfinance-2.1.2/kfinance/tests/conftest.py +32 -0
  12. {kensho_kfinance-2.0.1 → kensho_kfinance-2.1.2}/kfinance/tests/test_batch_requests.py +13 -7
  13. kensho_kfinance-2.1.2/kfinance/tests/test_client.py +54 -0
  14. {kensho_kfinance-2.0.1 → kensho_kfinance-2.1.2}/kfinance/tests/test_fetch.py +8 -2
  15. kensho_kfinance-2.1.2/kfinance/tests/test_group_objects.py +32 -0
  16. {kensho_kfinance-2.0.1 → kensho_kfinance-2.1.2}/kfinance/tests/test_tools.py +1 -27
  17. {kensho_kfinance-2.0.1 → kensho_kfinance-2.1.2}/kfinance/tool_calling/get_business_relationship_from_identifier.py +2 -1
  18. {kensho_kfinance-2.0.1 → kensho_kfinance-2.1.2}/kfinance/tool_calling/get_capitalization_from_identifier.py +2 -1
  19. {kensho_kfinance-2.0.1 → kensho_kfinance-2.1.2}/kfinance/tool_calling/get_company_id_from_identifier.py +2 -0
  20. {kensho_kfinance-2.0.1 → kensho_kfinance-2.1.2}/kfinance/tool_calling/get_cusip_from_ticker.py +2 -0
  21. {kensho_kfinance-2.0.1 → kensho_kfinance-2.1.2}/kfinance/tool_calling/get_earnings_call_datetimes_from_identifier.py +2 -0
  22. {kensho_kfinance-2.0.1 → kensho_kfinance-2.1.2}/kfinance/tool_calling/get_financial_line_item_from_identifier.py +2 -1
  23. {kensho_kfinance-2.0.1 → kensho_kfinance-2.1.2}/kfinance/tool_calling/get_financial_statement_from_identifier.py +2 -1
  24. {kensho_kfinance-2.0.1 → kensho_kfinance-2.1.2}/kfinance/tool_calling/get_history_metadata_from_identifier.py +2 -1
  25. {kensho_kfinance-2.0.1 → kensho_kfinance-2.1.2}/kfinance/tool_calling/get_info_from_identifier.py +2 -0
  26. {kensho_kfinance-2.0.1 → kensho_kfinance-2.1.2}/kfinance/tool_calling/get_isin_from_ticker.py +2 -0
  27. {kensho_kfinance-2.0.1 → kensho_kfinance-2.1.2}/kfinance/tool_calling/get_latest.py +2 -1
  28. {kensho_kfinance-2.0.1 → kensho_kfinance-2.1.2}/kfinance/tool_calling/get_n_quarters_ago.py +2 -1
  29. {kensho_kfinance-2.0.1 → kensho_kfinance-2.1.2}/kfinance/tool_calling/get_prices_from_identifier.py +2 -1
  30. {kensho_kfinance-2.0.1 → kensho_kfinance-2.1.2}/kfinance/tool_calling/get_security_id_from_identifier.py +2 -0
  31. {kensho_kfinance-2.0.1 → kensho_kfinance-2.1.2}/kfinance/tool_calling/get_trading_item_id_from_identifier.py +2 -0
  32. {kensho_kfinance-2.0.1 → kensho_kfinance-2.1.2}/kfinance/tool_calling/shared_models.py +2 -0
  33. {kensho_kfinance-2.0.1 → kensho_kfinance-2.1.2}/kfinance/version.py +2 -2
  34. {kensho_kfinance-2.0.1 → kensho_kfinance-2.1.2}/pyproject.toml +3 -1
  35. {kensho_kfinance-2.0.1 → kensho_kfinance-2.1.2}/.coveragerc +0 -0
  36. {kensho_kfinance-2.0.1 → kensho_kfinance-2.1.2}/.github/workflows/ci-lint.yml +0 -0
  37. {kensho_kfinance-2.0.1 → kensho_kfinance-2.1.2}/.github/workflows/ci-test.yml +0 -0
  38. {kensho_kfinance-2.0.1 → kensho_kfinance-2.1.2}/.github/workflows/python-publish.yml +0 -0
  39. {kensho_kfinance-2.0.1 → kensho_kfinance-2.1.2}/.gitignore +0 -0
  40. {kensho_kfinance-2.0.1 → kensho_kfinance-2.1.2}/.readthedocs.yaml +0 -0
  41. {kensho_kfinance-2.0.1 → kensho_kfinance-2.1.2}/AUTHORS.md +0 -0
  42. {kensho_kfinance-2.0.1 → kensho_kfinance-2.1.2}/CODE_OF_CONDUCT.md +0 -0
  43. {kensho_kfinance-2.0.1 → kensho_kfinance-2.1.2}/CONTRIBUTING.md +0 -0
  44. {kensho_kfinance-2.0.1 → kensho_kfinance-2.1.2}/LICENSE +0 -0
  45. {kensho_kfinance-2.0.1 → kensho_kfinance-2.1.2}/README.md +0 -0
  46. {kensho_kfinance-2.0.1 → kensho_kfinance-2.1.2}/docs/build_tool_calling_documentation.py +0 -0
  47. {kensho_kfinance-2.0.1 → kensho_kfinance-2.1.2}/docs/conf.py +0 -0
  48. {kensho_kfinance-2.0.1 → kensho_kfinance-2.1.2}/docs/index.rst +0 -0
  49. {kensho_kfinance-2.0.1 → kensho_kfinance-2.1.2}/docs/kfinance.rst +0 -0
  50. {kensho_kfinance-2.0.1 → kensho_kfinance-2.1.2}/docs/requirements.txt +0 -0
  51. {kensho_kfinance-2.0.1 → kensho_kfinance-2.1.2}/docs/templates/apidoc/package.rst_t +0 -0
  52. {kensho_kfinance-2.0.1 → kensho_kfinance-2.1.2}/docs/templates/apidoc/toc.rst_t +0 -0
  53. {kensho_kfinance-2.0.1 → kensho_kfinance-2.1.2}/docs/tool_calling.rst +0 -0
  54. {kensho_kfinance-2.0.1 → kensho_kfinance-2.1.2}/justfile +0 -0
  55. {kensho_kfinance-2.0.1 → kensho_kfinance-2.1.2}/kensho_kfinance.egg-info/dependency_links.txt +0 -0
  56. {kensho_kfinance-2.0.1 → kensho_kfinance-2.1.2}/kensho_kfinance.egg-info/top_level.txt +0 -0
  57. {kensho_kfinance-2.0.1 → kensho_kfinance-2.1.2}/kfinance/__init__.py +0 -0
  58. {kensho_kfinance-2.0.1 → kensho_kfinance-2.1.2}/kfinance/prompt.py +0 -0
  59. {kensho_kfinance-2.0.1 → kensho_kfinance-2.1.2}/kfinance/py.typed +0 -0
  60. {kensho_kfinance-2.0.1 → kensho_kfinance-2.1.2}/kfinance/server_thread.py +0 -0
  61. {kensho_kfinance-2.0.1 → kensho_kfinance-2.1.2}/kfinance/tests/__init__.py +0 -0
  62. {kensho_kfinance-2.0.1 → kensho_kfinance-2.1.2}/kfinance/tests/test_objects.py +0 -0
  63. {kensho_kfinance-2.0.1 → kensho_kfinance-2.1.2}/kfinance/tool_calling/README.md +0 -0
  64. {kensho_kfinance-2.0.1 → kensho_kfinance-2.1.2}/kfinance/tool_calling/__init__.py +0 -0
  65. {kensho_kfinance-2.0.1 → kensho_kfinance-2.1.2}/scripts/copyright_line_check.sh +0 -0
  66. {kensho_kfinance-2.0.1 → kensho_kfinance-2.1.2}/scripts/lint.sh +0 -0
  67. {kensho_kfinance-2.0.1 → kensho_kfinance-2.1.2}/scripts/test.sh +0 -0
  68. {kensho_kfinance-2.0.1 → kensho_kfinance-2.1.2}/setup.cfg +0 -0
  69. {kensho_kfinance-2.0.1 → kensho_kfinance-2.1.2}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kensho-kfinance
3
- Version: 2.0.1
3
+ Version: 2.1.2
4
4
  Summary: Python CLI for kFinance
5
5
  Author-email: Luke Brown <luke.brown@kensho.com>, Michelle Keoy <michelle.keoy@kensho.com>, Keith Page <keith.page@kensho.com>, Matthew Rosen <matthew.rosen@kensho.com>, Nick Roshdieh <nick.roshdieh@kensho.com>
6
6
  Project-URL: source, https://github.com/kensho-technologies/kfinance
@@ -12,6 +12,7 @@ Requires-Python: >=3.10
12
12
  Description-Content-Type: text/markdown
13
13
  License-File: LICENSE
14
14
  License-File: AUTHORS.md
15
+ Requires-Dist: cachetools<6,>=5.5
15
16
  Requires-Dist: langchain-core>=0.3.15
16
17
  Requires-Dist: langchain-google-genai<3,>=2.1.0
17
18
  Requires-Dist: numpy>=1.22.4
@@ -33,6 +34,7 @@ Requires-Dist: pytest-cov<7,>=6.0.0; extra == "dev"
33
34
  Requires-Dist: requests_mock<2,>=1.12; extra == "dev"
34
35
  Requires-Dist: ruff<1,>=0.9.4; extra == "dev"
35
36
  Requires-Dist: time_machine<3,>=2.1; extra == "dev"
37
+ Requires-Dist: types-cachetools<6,>=5.5; extra == "dev"
36
38
  Dynamic: license-file
37
39
 
38
40
  # kFinance
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kensho-kfinance
3
- Version: 2.0.1
3
+ Version: 2.1.2
4
4
  Summary: Python CLI for kFinance
5
5
  Author-email: Luke Brown <luke.brown@kensho.com>, Michelle Keoy <michelle.keoy@kensho.com>, Keith Page <keith.page@kensho.com>, Matthew Rosen <matthew.rosen@kensho.com>, Nick Roshdieh <nick.roshdieh@kensho.com>
6
6
  Project-URL: source, https://github.com/kensho-technologies/kfinance
@@ -12,6 +12,7 @@ Requires-Python: >=3.10
12
12
  Description-Content-Type: text/markdown
13
13
  License-File: LICENSE
14
14
  License-File: AUTHORS.md
15
+ Requires-Dist: cachetools<6,>=5.5
15
16
  Requires-Dist: langchain-core>=0.3.15
16
17
  Requires-Dist: langchain-google-genai<3,>=2.1.0
17
18
  Requires-Dist: numpy>=1.22.4
@@ -33,6 +34,7 @@ Requires-Dist: pytest-cov<7,>=6.0.0; extra == "dev"
33
34
  Requires-Dist: requests_mock<2,>=1.12; extra == "dev"
34
35
  Requires-Dist: ruff<1,>=0.9.4; extra == "dev"
35
36
  Requires-Dist: time_machine<3,>=2.1; extra == "dev"
37
+ Requires-Dist: types-cachetools<6,>=5.5; extra == "dev"
36
38
  Dynamic: license-file
37
39
 
38
40
  # kFinance
@@ -37,8 +37,11 @@ kfinance/py.typed
37
37
  kfinance/server_thread.py
38
38
  kfinance/version.py
39
39
  kfinance/tests/__init__.py
40
+ kfinance/tests/conftest.py
40
41
  kfinance/tests/test_batch_requests.py
42
+ kfinance/tests/test_client.py
41
43
  kfinance/tests/test_fetch.py
44
+ kfinance/tests/test_group_objects.py
42
45
  kfinance/tests/test_objects.py
43
46
  kfinance/tests/test_tools.py
44
47
  kfinance/tool_calling/README.md
@@ -1,3 +1,4 @@
1
+ cachetools<6,>=5.5
1
2
  langchain-core>=0.3.15
2
3
  langchain-google-genai<3,>=2.1.0
3
4
  numpy>=1.22.4
@@ -20,3 +21,4 @@ pytest-cov<7,>=6.0.0
20
21
  requests_mock<2,>=1.12
21
22
  ruff<1,>=0.9.4
22
23
  time_machine<3,>=2.1
24
+ types-cachetools<6,>=5.5
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## v2.1.2
4
+ - Allow batch executor to handle multiple requests
5
+
6
+ ## v2.1.1
7
+ - Use cachetools cache
8
+
9
+ ## v2.1.0
10
+ - Filter llm tools by user permissions
11
+
12
+ ## v2.0.2
13
+ - Add Client.tickers() method for ticker industry filters for ISRCS and GICS
14
+
3
15
  ## v2.0.1
4
16
  - Fix readthedocs integration for llm tools.
5
17
 
@@ -72,19 +72,19 @@ def add_methods_of_singular_class_to_iterable_class(singular_cls: Type[T]) -> Ca
72
72
  def process_in_batch(
73
73
  method: Callable, self: IterableKfinanceClass, *args: Any, **kwargs: Any
74
74
  ) -> dict:
75
- results = {}
76
75
  kfinance_api_client = self.kfinance_api_client
77
76
  with kfinance_api_client.batch_request_header(batch_size=len(self)):
78
- with kfinance_api_client.thread_pool as executor:
79
- results = dict(
80
- zip(
81
- self,
82
- [
83
- process_in_thread_pool(executor, method, obj, *args, **kwargs)
84
- for obj in self
85
- ],
86
- )
77
+ results = dict(
78
+ zip(
79
+ self,
80
+ [
81
+ process_in_thread_pool(
82
+ kfinance_api_client.thread_pool, method, obj, *args, **kwargs
83
+ )
84
+ for obj in self
85
+ ],
87
86
  )
87
+ )
88
88
 
89
89
  return results
90
90
 
@@ -1,7 +1,6 @@
1
1
  from datetime import date
2
- from enum import Enum
3
2
  from itertools import chain
4
- from typing import TypedDict
3
+ from typing import NamedTuple, TypedDict
5
4
 
6
5
  from strenum import StrEnum
7
6
 
@@ -21,13 +20,13 @@ class HistoryMetadata(TypedDict):
21
20
  first_trade_date: date
22
21
 
23
22
 
24
- class IdentificationTriple(TypedDict):
23
+ class IdentificationTriple(NamedTuple):
25
24
  trading_item_id: int
26
25
  security_id: int
27
26
  company_id: int
28
27
 
29
28
 
30
- class Capitalization(Enum):
29
+ class Capitalization(StrEnum):
31
30
  """The capitalization type"""
32
31
 
33
32
  market_cap = "market_cap"
@@ -35,7 +34,7 @@ class Capitalization(Enum):
35
34
  shares_outstanding = "shares_outstanding"
36
35
 
37
36
 
38
- class PeriodType(Enum):
37
+ class PeriodType(StrEnum):
39
38
  """The period type"""
40
39
 
41
40
  annual = "annual"
@@ -44,7 +43,7 @@ class PeriodType(Enum):
44
43
  ytd = "ytd"
45
44
 
46
45
 
47
- class Periodicity(Enum):
46
+ class Periodicity(StrEnum):
48
47
  """The frequency or interval at which the historical data points are sampled or aggregated. Periodicity is not the same as the date range. The date range specifies the time span over which the data is retrieved, while periodicity determines how the data within that date range is aggregated."""
49
48
 
50
49
  day = "day"
@@ -53,7 +52,7 @@ class Periodicity(Enum):
53
52
  year = "year"
54
53
 
55
54
 
56
- class StatementType(Enum):
55
+ class StatementType(StrEnum):
57
56
  """The type of financial statement"""
58
57
 
59
58
  balance_sheet = "balance_sheet"
@@ -88,6 +87,16 @@ class BusinessRelationshipType(StrEnum):
88
87
  client_services = "client_services"
89
88
 
90
89
 
90
+ class Permission(StrEnum):
91
+ EarningsPermission = "EarningsPermission"
92
+ GICSPermission = "GICSPermission"
93
+ IDPermission = "IDPermission"
94
+ ISCRSPermission = "ISCRSPermission"
95
+ PricingPermission = "PricingPermission"
96
+ RelationshipPermission = "RelationshipPermission"
97
+ StatementsPermission = "StatementsPermission"
98
+
99
+
91
100
  class YearAndQuarter(TypedDict):
92
101
  year: int
93
102
  quarter: int
@@ -1,5 +1,6 @@
1
1
  from concurrent.futures import ThreadPoolExecutor
2
2
  from contextlib import contextmanager
3
+ import logging
3
4
  from time import time
4
5
  from typing import Callable, Generator, Optional
5
6
  from uuid import uuid4
@@ -13,6 +14,7 @@ from .constants import (
13
14
  IndustryClassification,
14
15
  Periodicity,
15
16
  PeriodType,
17
+ Permission,
16
18
  )
17
19
 
18
20
 
@@ -23,6 +25,8 @@ try:
23
25
  except ImportError:
24
26
  kfinance_version = "dev"
25
27
 
28
+ logger = logging.getLogger(__name__)
29
+
26
30
 
27
31
  DEFAULT_API_HOST: str = "https://kfinance.kensho.com"
28
32
  DEFAULT_API_VERSION: int = 1
@@ -86,6 +90,7 @@ class KFinanceApiClient:
86
90
  self.user_agent_source = "object_oriented"
87
91
  self._batch_id: str | None = None
88
92
  self._batch_size: str | None = None
93
+ self._user_permissions: set[Permission] | None = None
89
94
 
90
95
  @contextmanager
91
96
  def batch_request_header(self, batch_size: int) -> Generator:
@@ -127,6 +132,9 @@ class KFinanceApiClient:
127
132
  # nosemgrep: python.jwt.security.unverified-jwt-decode.unverified-jwt-decode
128
133
  options={"verify_signature": False},
129
134
  ).get("exp")
135
+ # When the access token gets refreshed, also refresh user permissions in case they
136
+ # have been updated.
137
+ self._refresh_user_permissions()
130
138
  return self._access_token
131
139
 
132
140
  def _get_access_token_via_refresh_token(self) -> str:
@@ -169,6 +177,33 @@ class KFinanceApiClient:
169
177
  response.raise_for_status()
170
178
  return response.json().get("access_token")
171
179
 
180
+ @property
181
+ def user_permissions(self) -> set[Permission]:
182
+ """Return the permissions that the current user holds."""
183
+
184
+ if self._user_permissions is None:
185
+ self._refresh_user_permissions()
186
+ # _refresh_user_permissions updates self._user_permissions in place
187
+ assert self._user_permissions is not None
188
+ return self._user_permissions
189
+
190
+ def _refresh_user_permissions(self) -> None:
191
+ """Fetches user permissions and stores them as KfinanceApiClient._user_permissions."""
192
+
193
+ user_permission_dict = self.fetch_permissions()
194
+ self._user_permissions = set()
195
+ for permission_str in user_permission_dict["permissions"]:
196
+ try:
197
+ self._user_permissions.add(Permission[permission_str])
198
+ except KeyError:
199
+ logger.warning(
200
+ "Could not resolve %s to a member of the Permission enum. "
201
+ "%s may be a new type of permission that still needs to be "
202
+ "added to the enum.",
203
+ permission_str,
204
+ permission_str,
205
+ )
206
+
172
207
  def fetch(self, url: str) -> dict:
173
208
  """Does the request and auth"""
174
209
 
@@ -191,6 +226,11 @@ class KFinanceApiClient:
191
226
  response.raise_for_status()
192
227
  return response.json()
193
228
 
229
+ def fetch_permissions(self) -> dict[str, list[str]]:
230
+ """Return the permissions of the user."""
231
+ url = f"{self.url_base}users/permissions"
232
+ return self.fetch(url)
233
+
194
234
  def fetch_id_triple(self, identifier: str, exchange_code: Optional[str] = None) -> dict:
195
235
  """Get the ID triple from [identifier]."""
196
236
  url = f"{self.url_base}id/{identifier}"
@@ -241,7 +281,7 @@ class KFinanceApiClient:
241
281
  f"{self.url_base}pricing/{trading_item_id}/"
242
282
  f"{start_date if start_date is not None else 'none'}/"
243
283
  f"{end_date if end_date is not None else 'none'}/"
244
- f"{periodicity.value if periodicity else 'none'}/"
284
+ f"{periodicity if periodicity else 'none'}/"
245
285
  f"{'adjusted' if is_adjusted else 'unadjusted'}"
246
286
  )
247
287
  return self.fetch(url)
@@ -278,7 +318,7 @@ class KFinanceApiClient:
278
318
  f"{self.url_base}price_chart/{trading_item_id}/"
279
319
  f"{start_date if start_date is not None else 'none'}/"
280
320
  f"{end_date if end_date is not None else 'none'}/"
281
- f"{periodicity.value if periodicity else 'none'}/"
321
+ f"{periodicity if periodicity else 'none'}/"
282
322
  f"{'adjusted' if is_adjusted else 'unadjusted'}"
283
323
  )
284
324
 
@@ -306,7 +346,7 @@ class KFinanceApiClient:
306
346
  """Get a specified financial statement for a specified duration."""
307
347
  url = (
308
348
  f"{self.url_base}statements/{company_id}/{statement_type}/"
309
- f"{period_type.value if period_type else 'none'}/"
349
+ f"{period_type if period_type else 'none'}/"
310
350
  f"{start_year if start_year is not None else 'none'}/"
311
351
  f"{end_year if end_year is not None else 'none'}/"
312
352
  f"{start_quarter if start_quarter is not None else 'none'}/"
@@ -327,7 +367,7 @@ class KFinanceApiClient:
327
367
  """Get a specified financial line item for a specified duration."""
328
368
  url = (
329
369
  f"{self.url_base}line_item/{company_id}/{line_item}/"
330
- f"{period_type.value if period_type else 'none'}/"
370
+ f"{period_type if period_type else 'none'}/"
331
371
  f"{start_year if start_year is not None else 'none'}/"
332
372
  f"{end_year if end_year is not None else 'none'}/"
333
373
  f"{start_quarter if start_quarter is not None else 'none'}/"
@@ -358,10 +398,12 @@ class KFinanceApiClient:
358
398
  self,
359
399
  country_iso_code: str,
360
400
  state_iso_code: Optional[str] = None,
361
- ) -> dict[str, list[IdentificationTriple]]:
401
+ ) -> list[IdentificationTriple]:
362
402
  """Fetch ticker geography groups"""
363
- return self.fetch_geography_groups(
364
- country_iso_code=country_iso_code, state_iso_code=state_iso_code, fetch_ticker=True
403
+ return self._tickers_response_to_id_triple(
404
+ self.fetch_geography_groups(
405
+ country_iso_code=country_iso_code, state_iso_code=state_iso_code, fetch_ticker=True
406
+ )
365
407
  )
366
408
 
367
409
  def fetch_company_geography_groups(
@@ -381,13 +423,13 @@ class KFinanceApiClient:
381
423
  url = f"{self.url_base}{'ticker_groups' if fetch_ticker else 'trading_item_groups'}/exchange/{exchange_code}"
382
424
  return self.fetch(url)
383
425
 
384
- def fetch_ticker_exchange_groups(
385
- self, exchange_code: str
386
- ) -> dict[str, list[IdentificationTriple]]:
426
+ def fetch_ticker_exchange_groups(self, exchange_code: str) -> list[IdentificationTriple]:
387
427
  """Fetch ticker exchange groups"""
388
- return self.fetch_exchange_groups(
389
- exchange_code=exchange_code,
390
- fetch_ticker=True,
428
+ return self._tickers_response_to_id_triple(
429
+ self.fetch_exchange_groups(
430
+ exchange_code=exchange_code,
431
+ fetch_ticker=True,
432
+ )
391
433
  )
392
434
 
393
435
  def fetch_trading_item_exchange_groups(self, exchange_code: str) -> dict[str, list[int]]:
@@ -397,13 +439,32 @@ class KFinanceApiClient:
397
439
  fetch_ticker=False,
398
440
  )
399
441
 
442
+ @staticmethod
443
+ def _tickers_response_to_id_triple(
444
+ tickers_response: dict[str, list[dict]],
445
+ ) -> list[IdentificationTriple]:
446
+ """For fetch ticker cases with a dict[str, list[dict]] response, return a list[IdentificationTriple].
447
+
448
+ For example, with a given fetch tickers response:
449
+ {"tickers" : [{"trading_item_id": 1, "security_id": 1, "company_id": 1}, {"trading_item_id": 2,"security_id": 2,"company_id": 2}]},
450
+ return [[1, 1, 1], [2, 2, 2]].
451
+ """
452
+ return [
453
+ IdentificationTriple(
454
+ trading_item_id=ticker["trading_item_id"],
455
+ security_id=ticker["security_id"],
456
+ company_id=ticker["company_id"],
457
+ )
458
+ for ticker in tickers_response["tickers"]
459
+ ]
460
+
400
461
  def fetch_ticker_combined(
401
462
  self,
402
463
  country_iso_code: Optional[str] = None,
403
464
  state_iso_code: Optional[str] = None,
404
465
  simple_industry: Optional[str] = None,
405
466
  exchange_code: Optional[str] = None,
406
- ) -> dict[str, list[IdentificationTriple]]:
467
+ ) -> list[IdentificationTriple]:
407
468
  """Fetch tickers using combined filters route"""
408
469
  if (
409
470
  country_iso_code is None
@@ -414,11 +475,11 @@ class KFinanceApiClient:
414
475
  raise RuntimeError("Invalid parameters: No parameters provided or all set to none")
415
476
  elif country_iso_code is None and state_iso_code is not None:
416
477
  raise RuntimeError(
417
- "Invalid parameters: state_iso_code must be provided with a country_iso_code value"
478
+ "Invalid parameters: country_iso_code must be provided with a state_iso_code value"
418
479
  )
419
480
  else:
420
481
  url = f"{self.url_base}ticker_groups/filters/geo/{str(country_iso_code).lower()}/{str(state_iso_code).lower()}/simple/{str(simple_industry).lower()}/exchange/{str(exchange_code).lower()}"
421
- return self.fetch(url)
482
+ return self._tickers_response_to_id_triple(self.fetch(url))
422
483
 
423
484
  def fetch_companies_from_business_relationship(
424
485
  self, company_id: int, relationship_type: BusinessRelationshipType
@@ -443,22 +504,11 @@ class KFinanceApiClient:
443
504
  url = f"{self.url_base}relationship/{company_id}/{relationship_type}"
444
505
  return self.fetch(url)
445
506
 
446
- def fetch_from_industry_code(
447
- self,
448
- industry_code: str,
449
- industry_classification: IndustryClassification,
450
- fetch_ticker: bool = True,
451
- ) -> dict[str, list]:
452
- """Fetches a list of companies or identification triples that are classified in the given industry_code and industry_classification."""
453
-
454
- url = f"{self.url_base}{'ticker_groups' if fetch_ticker else 'company_groups'}/industry/{industry_classification}/{industry_code}"
455
- return self.fetch(url)
456
-
457
507
  def fetch_ticker_from_industry_code(
458
508
  self,
459
509
  industry_code: str,
460
510
  industry_classification: IndustryClassification,
461
- ) -> dict[str, list[IdentificationTriple]]:
511
+ ) -> list[IdentificationTriple]:
462
512
  """Fetches a list of identification triples that are classified in the given industry_code and industry_classification.
463
513
 
464
514
  Returns a dictionary of shape {"tickers": List[{“company_id”: <company_id>, “security_id”: <security_id>, “trading_item_id”: <trading_item_id>}]}.
@@ -466,14 +516,11 @@ class KFinanceApiClient:
466
516
  :type industry_code: str
467
517
  :param industry_classification: The type of industry_classification to filter on.
468
518
  :type industry_classification: IndustryClassification
469
- :return: A dictionary containing the list of identification triple [company_id, security_id, trading_item_id] that are classified in the given industry_code and industry_classification.
470
- :rtype: dict[str, list[IdentificationTriple]]
519
+ :return: A list of identification triples [company_id, security_id, trading_item_id] that are classified in the given industry_code and industry_classification.
520
+ :rtype: list[IdentificationTriple]
471
521
  """
472
- return self.fetch_from_industry_code(
473
- industry_code=industry_code,
474
- industry_classification=industry_classification,
475
- fetch_ticker=True,
476
- )
522
+ url = f"{self.url_base}ticker_groups/industry/{industry_classification}/{industry_code}"
523
+ return self._tickers_response_to_id_triple(self.fetch(url))
477
524
 
478
525
  def fetch_company_from_industry_code(
479
526
  self,
@@ -490,8 +537,5 @@ class KFinanceApiClient:
490
537
  :return: A dictionary containing the list of companies that are classified in the given industry_code and industry_classification.
491
538
  :rtype: dict[str, list[int]]
492
539
  """
493
- return self.fetch_from_industry_code(
494
- industry_code=industry_code,
495
- industry_classification=industry_classification,
496
- fetch_ticker=False,
497
- )
540
+ url = f"{self.url_base}company_groups/industry/{industry_classification}/{industry_code}"
541
+ return self.fetch(url)
@@ -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