kensho-kfinance 2.1.2__py3-none-any.whl → 2.2.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kensho-kfinance
3
- Version: 2.1.2
3
+ Version: 2.2.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
@@ -28,7 +28,11 @@ Requires-Dist: requests<3,>=2.22.0
28
28
  Requires-Dist: urllib3>=1.21.1
29
29
  Provides-Extra: dev
30
30
  Requires-Dist: coverage<8,>=7.6.10; extra == "dev"
31
+ Requires-Dist: ipykernel<7,>=6.29; extra == "dev"
31
32
  Requires-Dist: mypy<2,>=1.15.0; extra == "dev"
33
+ Requires-Dist: nbconvert<8,>=7.16; extra == "dev"
34
+ Requires-Dist: nbformat<6,>5.10; extra == "dev"
35
+ Requires-Dist: nbqa<2,>1.9; extra == "dev"
32
36
  Requires-Dist: pytest<7,>=6.1.2; extra == "dev"
33
37
  Requires-Dist: pytest-cov<7,>=6.0.0; extra == "dev"
34
38
  Requires-Dist: requests_mock<2,>=1.12; extra == "dev"
@@ -57,6 +61,8 @@ To receive access, please email [S&P Global Market Intelligence](market.intellig
57
61
 
58
62
  Once access is obtained, get started using the [Authentication Guide](https://docs.kensho.com/llmreadyapi/kf-authentication) and [Usage Guide](https://docs.kensho.com/llmreadyapi/usage).
59
63
 
64
+ We also provide an [interactive notebook](usage_examples.ipynb) that demonstrates some usage examples.
65
+
60
66
  # Versioning
61
67
  The kFinance uses semantic versioning (major, minor, patch).
62
68
  To bump the version, add a new entry in [CHANGELOG.md](kfinance%2FCHANGELOG.md).
@@ -1,43 +1,42 @@
1
- kensho_kfinance-2.1.2.dist-info/licenses/AUTHORS.md,sha256=0h9ClbI0pu1oKj1M28ROUsaxrbZg-6ukQGl6X4y9noI,68
2
- kensho_kfinance-2.1.2.dist-info/licenses/LICENSE,sha256=bsY4blvSgq6o0FMQ3RXa2NCgco--nHCCchLXzxr6kms,83
3
- kfinance/CHANGELOG.md,sha256=kXKUnP2knjAi0OrJ-mptiTjuXsPqI8S3e2SQ96CCibs,931
1
+ kensho_kfinance-2.2.2.dist-info/licenses/AUTHORS.md,sha256=0h9ClbI0pu1oKj1M28ROUsaxrbZg-6ukQGl6X4y9noI,68
2
+ kensho_kfinance-2.2.2.dist-info/licenses/LICENSE,sha256=bsY4blvSgq6o0FMQ3RXa2NCgco--nHCCchLXzxr6kms,83
3
+ kfinance/CHANGELOG.md,sha256=eX36Pj6mDG2B6AtnHpP5FAJ14X-8XqF0fNRTYOGClXw,1140
4
4
  kfinance/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
- kfinance/batch_request_handling.py,sha256=8J7rUPu1AepqWWdRr3ucJQLnZUjitirjjv5L5YG4nFg,5489
6
- kfinance/constants.py,sha256=_MwkxWSqMKhBDvmPk4kL3tfWm-z54dVlXeVvbaiHEqg,48650
7
- kfinance/fetch.py,sha256=rl7WI8WSphA6XPt2DLBVdHc5aU9gPNdIiYNDkKPVrUE,22063
8
- kfinance/kfinance.py,sha256=b86qjy_hbxQ7sKDwcOPfXipaBiXgYsK4nJO2nJouTDs,53294
9
- kfinance/meta_classes.py,sha256=tGZb_6GOLiWJXMx4MYDSHzcxTnBGNp3lLdXlsxat_Mw,15858
5
+ kfinance/batch_request_handling.py,sha256=p6p_G4_BL06GgeKlh7P1k9CUqOMahWCLEw1NoBwbLvU,5698
6
+ kfinance/constants.py,sha256=4vj0WA0xupEJexvnI-ap6LFHXEf6dn8jQwJlxz8cGTs,48767
7
+ kfinance/fetch.py,sha256=fguxYy5rjLQKNkmP7gsvZUJtmt9uDNxg6LN9FRJTAVQ,22936
8
+ kfinance/kfinance.py,sha256=_U69k0Dcwkw1B_lzaUZy8N2-c-v93ZKoUAVVZb6wBUM,52758
9
+ kfinance/meta_classes.py,sha256=nwqHRi8IFa4WLFAZpX4a6htR47doK3_qVLnYBOylQV0,20482
10
10
  kfinance/prompt.py,sha256=PtVB8c_FcSlVdyGgByAnIFGzuUuBaEjciCqnBJl1hSQ,25133
11
11
  kfinance/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
12
  kfinance/server_thread.py,sha256=jUnt1YGoYDkqqz1MbCwd44zJs1T_Z2BCgvj75bdtLgA,2574
13
- kfinance/version.py,sha256=R_mUEqlkeWeuDiw8DRbHmYUvRxDtmQQJqhTpV8pds-Y,511
13
+ kfinance/version.py,sha256=YDBduKhVnvbZfZTbXbsjQMbxTwgIIXIZ646Mj9YV074,511
14
14
  kfinance/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
15
  kfinance/tests/conftest.py,sha256=voB-w8P_6L3Nel3rdgylXKe5WWaS1q7nCFt1O04uqoY,948
16
- kfinance/tests/test_batch_requests.py,sha256=hL3VfbmyVgL2KJ5blGRHhYKrnlIfICjCvXmvtx2zsV8,10149
16
+ kfinance/tests/test_batch_requests.py,sha256=uXJF2IcRdyBm5SthwIUHMKtkGZ21MY84pg_k1JeSNOY,11430
17
17
  kfinance/tests/test_client.py,sha256=O7icZCSDhlQ9WGhzoXlpiSvbuA-mQNJHBYVsilyP_dE,2209
18
- kfinance/tests/test_fetch.py,sha256=YeehfsXHpEBgyLdaBTJ6dE1bGNhq_A_JvZmy-iQ-gp4,12441
18
+ kfinance/tests/test_example_notebook.py,sha256=XhDAJp3H5Y6usLAt3k4Ug4W3H6gLyh68nzmoWgOOLn4,6441
19
+ kfinance/tests/test_fetch.py,sha256=O9qOsffQYJPSAR1X_P6CNoA0Buyq2kx4vfS0FOc2Goc,13564
19
20
  kfinance/tests/test_group_objects.py,sha256=SoMEZmkG4RYdgWOAwxLHHtzIQho92KM01YbQXPUg578,1689
20
- kfinance/tests/test_objects.py,sha256=egXkhfoK2MepZdXtrBxzWxWni7f-zVCefUbnyDnWDOE,22554
21
- kfinance/tests/test_tools.py,sha256=h5bScYQBYD9IQ8eAz2L6YB2rLHixqM4SXPfWjm17jB0,16187
21
+ kfinance/tests/test_objects.py,sha256=PpB3q4UPi9ctRznq48Fy6sJUsOEqdAWzMzee1BYORX4,24001
22
+ kfinance/tests/test_tools.py,sha256=fTN43Y8fX8l8Yyw65XJmn_xUIbrTLaPvrsuBEdKPqK0,15234
22
23
  kfinance/tool_calling/README.md,sha256=omJq7Us6r4U45QB7hRpLjRJ5BMalCkZkh4uXBjTbJXc,2022
23
- kfinance/tool_calling/__init__.py,sha256=GDEeK2zM2tOfYam6ejwYACXecoRdlkRve_ZC5VTKugo,2038
24
+ kfinance/tool_calling/__init__.py,sha256=ewTzpR4S9mzCtSRpfK7WldDX78VSRKFta-s_zBsmamA,1744
24
25
  kfinance/tool_calling/get_business_relationship_from_identifier.py,sha256=CipXvyqEjPm6BXYP0CA9Kp1BIyiIEm7abp85x1zXRV4,1472
25
26
  kfinance/tool_calling/get_capitalization_from_identifier.py,sha256=TdWdJDeI-jSL-1YfhnnwIA9D1SXobidvoHrjK42QmqQ,1521
26
- kfinance/tool_calling/get_company_id_from_identifier.py,sha256=GPakqIA27-RoqKFjM7W2nd3xp4v3utIPpH932BNNAlk,577
27
27
  kfinance/tool_calling/get_cusip_from_ticker.py,sha256=houhGCYXoSzaaTtCvOBf3pPsYiSbcV1Ej5nAyGuMWcU,644
28
28
  kfinance/tool_calling/get_earnings_call_datetimes_from_identifier.py,sha256=Zp7gHP-z2ePu484wWx4xw89hl0z7NLcG3qliJT-6rUo,782
29
29
  kfinance/tool_calling/get_financial_line_item_from_identifier.py,sha256=bMK-GUMz_wmJ-7iFS72sM8AvvUvR83JYRWzYtr9Fofw,2153
30
30
  kfinance/tool_calling/get_financial_statement_from_identifier.py,sha256=9ouzC43skA78uI3dh3bvtM86db5RGxuWQAAJ24y-kBM,1906
31
31
  kfinance/tool_calling/get_history_metadata_from_identifier.py,sha256=kKIInfRC1Plf-Wnx9cmaJ46RRABm6zn3F5_8qAdqrBg,728
32
- kfinance/tool_calling/get_info_from_identifier.py,sha256=MtcC3RSbKrbOML381rkRcPXcAVXOmhqltpF_XVT-r-w,751
32
+ kfinance/tool_calling/get_info_from_identifier.py,sha256=PRhSYpYs_iUcIVFGYCMN_7QJyhewua7c7pqngPIp-qg,766
33
33
  kfinance/tool_calling/get_isin_from_ticker.py,sha256=2fJBcA-rNGbVOQmQ7qJEYxqejQwJ6nyWOBSFlzxG7dY,638
34
34
  kfinance/tool_calling/get_latest.py,sha256=btGeVBmvX5QJutzrKfE6spatGGejDuHxtq53NoAaGNk,786
35
35
  kfinance/tool_calling/get_n_quarters_ago.py,sha256=A0ilwPKUqU0YYQSz3gNsVF0Jy4YttXrSaDhYj7y8GHA,713
36
36
  kfinance/tool_calling/get_prices_from_identifier.py,sha256=ViJkwLDvStB7grc8RuoKSDXQM399Wru4-OY3E8k1l_U,1882
37
- kfinance/tool_calling/get_security_id_from_identifier.py,sha256=oxFVc7YgKMug8VofLGt-w2ZxN_Z8VaRnHC5YdaIoFNU,581
38
- kfinance/tool_calling/get_trading_item_id_from_identifier.py,sha256=BYVb061vIj2S40tS7pkjxAzbVIDU6feMCwr0DNdA_4k,596
37
+ kfinance/tool_calling/resolve_identifier.py,sha256=npslr6bBCu0qEDV1-8d24F5OC3nQ1KBMphuMbHVC1AU,626
39
38
  kfinance/tool_calling/shared_models.py,sha256=K-NPQyE_7Ew6Cs0zxG1xO2O47gp5uDHdHtWD7wUDZX4,2132
40
- kensho_kfinance-2.1.2.dist-info/METADATA,sha256=S_IUpoLLbFir0jT_-L7opp8WVNG21-eW6Vmf9AOCgSE,3141
41
- kensho_kfinance-2.1.2.dist-info/WHEEL,sha256=DnLRTWE75wApRYVsjgc6wsVswC54sMSJhAEd4xhDpBk,91
42
- kensho_kfinance-2.1.2.dist-info/top_level.txt,sha256=kT_kNwVhfQoOAecY8W7uYah5xaHMoHoAdBIvXh6DaKM,9
43
- kensho_kfinance-2.1.2.dist-info/RECORD,,
39
+ kensho_kfinance-2.2.2.dist-info/METADATA,sha256=VOmB9CZIcpr2DI5tJBZuWnR039oK_iRTN4cvDFco4RQ,3436
40
+ kensho_kfinance-2.2.2.dist-info/WHEEL,sha256=zaaOINJESkSfm_4HQVc5ssNzHCPXhJm0kEUakpsEHaU,91
41
+ kensho_kfinance-2.2.2.dist-info/top_level.txt,sha256=kT_kNwVhfQoOAecY8W7uYah5xaHMoHoAdBIvXh6DaKM,9
42
+ kensho_kfinance-2.2.2.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.4.0)
2
+ Generator: setuptools (80.8.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
kfinance/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## v2.2.2
4
+ - Add segments
5
+
6
+ ## v2.2.1
7
+ - Make number of employees optional to reflect backend changes
8
+
9
+ ## v2.2.0
10
+ - Replace get company id, get security id, get trading item id tools with resolve identifier tool
11
+
3
12
  ## v2.1.2
4
13
  - Allow batch executor to handle multiple requests
5
14
 
@@ -1,4 +1,4 @@
1
- from concurrent.futures import ThreadPoolExecutor
1
+ from concurrent.futures import Future
2
2
  import functools
3
3
  from functools import cached_property
4
4
  import threading
@@ -55,36 +55,26 @@ def add_methods_of_singular_class_to_iterable_class(singular_cls: Type[T]) -> Ca
55
55
  instances of [singular_cls].
56
56
  """
57
57
 
58
- def process_in_thread_pool(
59
- executor: ThreadPoolExecutor, method: Callable, obj: T, *args: Any, **kwargs: Any
60
- ) -> Any:
61
- with throttle:
62
- future = executor.submit(method, obj, *args, **kwargs)
63
- try:
64
- return future.result()
65
- except HTTPError as http_err:
66
- error_code = http_err.response.status_code
67
- if error_code == 404:
68
- return None
69
- else:
70
- raise http_err
71
-
72
58
  def process_in_batch(
73
59
  method: Callable, self: IterableKfinanceClass, *args: Any, **kwargs: Any
74
60
  ) -> dict:
75
- kfinance_api_client = self.kfinance_api_client
76
- with kfinance_api_client.batch_request_header(batch_size=len(self)):
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
- ],
61
+ with self.kfinance_api_client.batch_request_header(batch_size=len(self)):
62
+ futures = []
63
+ for obj in self:
64
+ # Acquire throttle before submitting the task
65
+ throttle.acquire()
66
+ future = self.kfinance_api_client.thread_pool.submit(
67
+ method, obj, *args, **kwargs
86
68
  )
87
- )
69
+ # On success or failure, release the throttle.
70
+ # This releases the throttle before the
71
+ # `resolve_future_with_error_handling` call.
72
+ future.add_done_callback(lambda f: throttle.release())
73
+ futures.append(future)
74
+
75
+ results = {}
76
+ for obj, future in zip(self, futures):
77
+ results[obj] = resolve_future_with_error_handling(future)
88
78
 
89
79
  return results
90
80
 
@@ -135,3 +125,18 @@ def add_methods_of_singular_class_to_iterable_class(singular_cls: Type[T]) -> Ca
135
125
  return iterable_cls
136
126
 
137
127
  return decorator
128
+
129
+
130
+ def resolve_future_with_error_handling(future: Future) -> Any:
131
+ """Return the result of a future with error handling for non-200 status codes.
132
+
133
+ If request returned a 404, return None. Otherwise, raise the error.
134
+ """
135
+ try:
136
+ return future.result()
137
+ except HTTPError as http_err:
138
+ error_code = http_err.response.status_code
139
+ if error_code == 404:
140
+ return None
141
+ else:
142
+ raise http_err
kfinance/constants.py CHANGED
@@ -62,6 +62,13 @@ class StatementType(StrEnum):
62
62
  cf = "cashflow"
63
63
 
64
64
 
65
+ class SegmentType(StrEnum):
66
+ """The type of segment"""
67
+
68
+ business = "business"
69
+ geographic = "geographic"
70
+
71
+
65
72
  class BusinessRelationshipType(StrEnum):
66
73
  """The type of business relationship"""
67
74
 
kfinance/fetch.py CHANGED
@@ -15,6 +15,7 @@ from .constants import (
15
15
  Periodicity,
16
16
  PeriodType,
17
17
  Permission,
18
+ SegmentType,
18
19
  )
19
20
 
20
21
 
@@ -305,6 +306,27 @@ class KFinanceApiClient:
305
306
  )
306
307
  return self.fetch(url)
307
308
 
309
+ def fetch_segments(
310
+ self,
311
+ company_id: int,
312
+ segment_type: SegmentType,
313
+ period_type: Optional[PeriodType] = None,
314
+ start_year: Optional[int] = None,
315
+ end_year: Optional[int] = None,
316
+ start_quarter: Optional[int] = None,
317
+ end_quarter: Optional[int] = None,
318
+ ) -> dict:
319
+ """Get a specified segment type for a specified duration."""
320
+ url = (
321
+ f"{self.url_base}segments/{company_id}/{segment_type}/"
322
+ f"{period_type if period_type else 'none'}/"
323
+ f"{start_year if start_year is not None else 'none'}/"
324
+ f"{end_year if end_year is not None else 'none'}/"
325
+ f"{start_quarter if start_quarter is not None else 'none'}/"
326
+ f"{end_quarter if end_quarter is not None else 'none'}"
327
+ )
328
+ return self.fetch(url)
329
+
308
330
  def fetch_price_chart(
309
331
  self,
310
332
  trading_item_id: int,
kfinance/kfinance.py CHANGED
@@ -262,7 +262,7 @@ class Company(CompanyFunctionsMetaClass):
262
262
  def info(self) -> dict:
263
263
  """Get the company info
264
264
 
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
265
+ :return: a dict with containing: name, status, type, simple industry, number of employees (if available), founding date, webpage, address, city, zip code, state, country, & iso_country
266
266
  :rtype: dict
267
267
  """
268
268
  return self.kfinance_api_client.fetch_info(self.company_id)
@@ -304,11 +304,11 @@ class Company(CompanyFunctionsMetaClass):
304
304
  return self.info["simple_industry"]
305
305
 
306
306
  @property
307
- def number_of_employees(self) -> str:
308
- """Get the number of employees the company has
307
+ def number_of_employees(self) -> str | None:
308
+ """Get the number of employees the company has (if available)
309
309
 
310
310
  :return: how many employees the company has
311
- :rtype: str
311
+ :rtype: str | None
312
312
  """
313
313
  return self.info["number_of_employees"]
314
314
 
@@ -537,11 +537,32 @@ class Ticker(DelegatedCompanyFunctionsMetaClass):
537
537
 
538
538
  @property
539
539
  def id_triple(self) -> IdentificationTriple:
540
- """Returns a unique identifier triple for the Ticker object."""
540
+ """Returns a unique identification triple for the Ticker object.
541
+
542
+ :return: an identification triple consisting of company_id, security_id, and trading_item_id
543
+ :rtype: IdentificationTriple
544
+ """
545
+
546
+ if self._company_id is None or self._security_id is None or self._trading_item_id is None:
547
+ if self._identifier is None:
548
+ raise RuntimeError(
549
+ "Fetching the id triple of a Ticker requires an identifier "
550
+ "(ticker, CUSIP, or ISIN)."
551
+ )
552
+ id_triple = self.kfinance_api_client.fetch_id_triple(
553
+ identifier=self._identifier, exchange_code=self.exchange_code
554
+ )
555
+ self._company_id = id_triple["company_id"]
556
+ self._security_id = id_triple["security_id"]
557
+ self._trading_item_id = id_triple["trading_item_id"]
558
+ assert self._company_id
559
+ assert self._security_id
560
+ assert self._trading_item_id
561
+
541
562
  return IdentificationTriple(
542
- company_id=self.company_id,
543
- security_id=self.security_id,
544
- trading_item_id=self.trading_item_id,
563
+ company_id=self._company_id,
564
+ security_id=self._security_id,
565
+ trading_item_id=self._trading_item_id,
545
566
  )
546
567
 
547
568
  def __hash__(self) -> int:
@@ -570,79 +591,32 @@ class Ticker(DelegatedCompanyFunctionsMetaClass):
570
591
 
571
592
  return f"{type(self).__module__}.{type(self).__qualname__} of {', '.join(str_attributes)}"
572
593
 
573
- def set_identification_triple(self) -> None:
574
- """Get & set company_id, security_id, & trading_item_id for ticker with an exchange"""
575
- if self._identifier is None:
576
- raise RuntimeError(
577
- "Ticker.set_identification_triple was called with a identifier set to None"
578
- )
579
- else:
580
- id_triple = self.kfinance_api_client.fetch_id_triple(
581
- self._identifier, self.exchange_code
582
- )
583
- self.company_id = id_triple["company_id"]
584
- self.security_id = id_triple["security_id"]
585
- self.trading_item_id = id_triple["trading_item_id"]
586
-
587
- def set_company_id(self) -> int:
588
- """Set the company id for the object
589
-
590
- :return: the CIQ company id
591
- :rtype: int
592
- """
593
- self.set_identification_triple()
594
- return self.company_id
595
-
596
- def set_security_id(self) -> int:
597
- """Set the security id for the object
598
-
599
- :return: the CIQ security id
600
- :rtype: int
601
- """
602
- self.set_identification_triple()
603
- return self.security_id
604
-
605
- def set_trading_item_id(self) -> int:
606
- """Set the trading item id for the object
607
-
608
- :return: the CIQ trading item id
609
- :rtype: int
610
- """
611
- self.set_identification_triple()
612
- return self.trading_item_id
613
-
614
- @cached_property
594
+ @property
615
595
  def company_id(self) -> int:
616
596
  """Get the company id for the object
617
597
 
618
598
  :return: the CIQ company id
619
599
  :rtype: int
620
600
  """
621
- if self._company_id:
622
- return self._company_id
623
- return self.set_company_id()
601
+ return self.id_triple.company_id
624
602
 
625
- @cached_property
603
+ @property
626
604
  def security_id(self) -> int:
627
605
  """Get the CIQ security id for the object
628
606
 
629
607
  :return: the CIQ security id
630
608
  :rtype: int
631
609
  """
632
- if self._security_id:
633
- return self._security_id
634
- return self.set_security_id()
610
+ return self.id_triple.security_id
635
611
 
636
- @cached_property
612
+ @property
637
613
  def trading_item_id(self) -> int:
638
614
  """Get the CIQ trading item id for the object
639
615
 
640
616
  :return: the CIQ trading item id
641
617
  :rtype: int
642
618
  """
643
- if self._trading_item_id:
644
- return self._trading_item_id
645
- return self.set_trading_item_id()
619
+ return self.id_triple.trading_item_id
646
620
 
647
621
  @cached_property
648
622
  def primary_security(self) -> Security:
@@ -711,7 +685,7 @@ class Ticker(DelegatedCompanyFunctionsMetaClass):
711
685
  def info(self) -> dict:
712
686
  """Get the company info for the ticker
713
687
 
714
- :return: a dict with containing: name, status, type, simple industry, number of employees, founding date, webpage, address, city, zip code, state, country, & iso_country
688
+ :return: a dict with containing: name, status, type, simple industry, number of employees (if available), founding date, webpage, address, city, zip code, state, country, & iso_country
715
689
  :rtype: dict
716
690
  """
717
691
  return self.company.info
@@ -753,11 +727,11 @@ class Ticker(DelegatedCompanyFunctionsMetaClass):
753
727
  return self.company.simple_industry
754
728
 
755
729
  @property
756
- def number_of_employees(self) -> str:
757
- """Get the number of employees the company has
730
+ def number_of_employees(self) -> str | None:
731
+ """Get the number of employees the company has (if available)
758
732
 
759
733
  :return: how many employees the company has
760
- :rtype: str
734
+ :rtype: str | None
761
735
  """
762
736
  return self.company.number_of_employees
763
737
 
kfinance/meta_classes.py CHANGED
@@ -7,7 +7,7 @@ from cachetools import LRUCache, cached
7
7
  import numpy as np
8
8
  import pandas as pd
9
9
 
10
- from .constants import LINE_ITEMS, BusinessRelationshipType, PeriodType
10
+ from .constants import LINE_ITEMS, BusinessRelationshipType, PeriodType, SegmentType
11
11
  from .fetch import KFinanceApiClient
12
12
 
13
13
 
@@ -304,6 +304,114 @@ class CompanyFunctionsMetaClass:
304
304
  )
305
305
  return df.set_index("date")[[column_to_extract]].apply(pd.to_numeric).replace(np.nan, None)
306
306
 
307
+ def _segments(
308
+ self,
309
+ segment_type: SegmentType,
310
+ period_type: Optional[PeriodType] = None,
311
+ start_year: Optional[int] = None,
312
+ end_year: Optional[int] = None,
313
+ start_quarter: Optional[int] = None,
314
+ end_quarter: Optional[int] = None,
315
+ ) -> pd.DataFrame:
316
+ """Get the company's segments"""
317
+ try:
318
+ self.validate_inputs(
319
+ start_year=start_year,
320
+ end_year=end_year,
321
+ start_quarter=start_quarter,
322
+ end_quarter=end_quarter,
323
+ )
324
+ except ValueError:
325
+ return pd.DataFrame()
326
+
327
+ results = self.kfinance_api_client.fetch_segments(
328
+ company_id=self.company_id,
329
+ segment_type=segment_type,
330
+ period_type=period_type,
331
+ start_year=start_year,
332
+ end_year=end_year,
333
+ start_quarter=start_quarter,
334
+ end_quarter=end_quarter,
335
+ )["segments"]
336
+
337
+ period_name = (
338
+ "Year" if (period_type == PeriodType.annual or period_type is None) else "Period"
339
+ )
340
+
341
+ # flatten the nested dictionary and return as a DataFrame
342
+ rows = []
343
+ for period, segments in results.items():
344
+ for segment_name, line_items in segments.items():
345
+ for line_item, value in line_items.items():
346
+ rows.append([period, segment_name, line_item, value])
347
+ return pd.DataFrame(
348
+ rows, columns=[period_name, "Segment Name", "Line Item", "Value"]
349
+ ).replace(np.nan, None)
350
+
351
+ def business_segments(
352
+ self,
353
+ period_type: Optional[PeriodType] = None,
354
+ start_year: Optional[int] = None,
355
+ end_year: Optional[int] = None,
356
+ start_quarter: Optional[int] = None,
357
+ end_quarter: Optional[int] = None,
358
+ ) -> pd.DataFrame:
359
+ """Retrieves the templated line of business segments for a given period_type, start_year, start_quarter, end_year and end_quarter.
360
+
361
+ :param period_type: The period_type requested for. Can be “annual”, “quarterly”, "ytd". Defaults to “annual” when start_quarter and end_quarter are None.
362
+ :type start_year: PeriodType, optional
363
+ :param start_year: The starting calendar year, defaults to None
364
+ :type start_year: int, optional
365
+ :param end_year: The ending calendar year, defaults to None
366
+ :type end_year: int, optional
367
+ :param start_quarter: The starting calendar quarter, defaults to None
368
+ :type start_quarter: int, optional
369
+ :param end_quarter: The ending calendar quarter, defaults to None
370
+ :type end_quarter: int, optional
371
+ :return: A DataFrame with `Year`/`Period`, `Segment Name`, `Line Item` and `Value` column.
372
+ :rtype: pd.DataFrame
373
+ """
374
+ return self._segments(
375
+ segment_type=SegmentType.business,
376
+ period_type=period_type,
377
+ start_year=start_year,
378
+ end_year=end_year,
379
+ start_quarter=start_quarter,
380
+ end_quarter=end_quarter,
381
+ )
382
+
383
+ def geographic_segments(
384
+ self,
385
+ period_type: Optional[PeriodType] = None,
386
+ start_year: Optional[int] = None,
387
+ end_year: Optional[int] = None,
388
+ start_quarter: Optional[int] = None,
389
+ end_quarter: Optional[int] = None,
390
+ ) -> pd.DataFrame:
391
+ """Retrieves the templated geographic segments for a given company for a given period_type, start_year, start_quarter, end_year and end_quarter.
392
+
393
+ :param period_type: The period_type requested for. Can be “annual”, “quarterly”, "ytd". Defaults to “annual” when start_quarter and end_quarter are None.
394
+ :type start_year: PeriodType, optional
395
+ :param start_year: The starting calendar year, defaults to None
396
+ :type start_year: int, optional
397
+ :param end_year: The ending calendar year, defaults to None
398
+ :type end_year: int, optional
399
+ :param start_quarter: The starting calendar quarter, defaults to None
400
+ :type start_quarter: int, optional
401
+ :param end_quarter: The ending calendar quarter, defaults to None
402
+ :type end_quarter: int, optional
403
+ :return: A DataFrame with `Year`/`Period`, `Segment Name`, `Line Item` and `Value` column.
404
+ :rtype: pd.DataFrame
405
+ """
406
+ return self._segments(
407
+ segment_type=SegmentType.geographic,
408
+ period_type=period_type,
409
+ start_year=start_year,
410
+ end_year=end_year,
411
+ start_quarter=start_quarter,
412
+ end_quarter=end_quarter,
413
+ )
414
+
307
415
 
308
416
  for line_item in LINE_ITEMS:
309
417
  line_item_name = line_item["name"]
@@ -1,7 +1,8 @@
1
1
  from concurrent.futures import ThreadPoolExecutor
2
+ import time
2
3
  from typing import Any, Dict
3
4
  from unittest import TestCase
4
- from unittest.mock import patch
5
+ from unittest.mock import PropertyMock, patch
5
6
 
6
7
  import numpy as np
7
8
  import pandas as pd
@@ -9,6 +10,7 @@ import pytest
9
10
  import requests
10
11
  import requests_mock
11
12
 
13
+ from kfinance.batch_request_handling import MAX_WORKERS_CAP
12
14
  from kfinance.fetch import KFinanceApiClient
13
15
  from kfinance.kfinance import Companies, Company, Ticker, TradingItems
14
16
 
@@ -260,3 +262,33 @@ class TestTradingItem(TestCase):
260
262
 
261
263
  expected_id_based_result = {1001: "Mock City A"}
262
264
  self.assertDictEqual(id_based_result, expected_id_based_result)
265
+
266
+ @patch.object(Company, "info", new_callable=PropertyMock)
267
+ def test_batch_requests_processed_in_parallel(self, mock_value: PropertyMock):
268
+ """
269
+ WHEN a batch request gets processed
270
+ THEN the requests are handled in parallel not sequentially.
271
+ """
272
+
273
+ sleep_duration = 0.05
274
+
275
+ def mock_info_with_sleep() -> dict[str, str]:
276
+ """Mock an info call with a short sleep"""
277
+ time.sleep(sleep_duration)
278
+ return {"city": "Cambridge"}
279
+
280
+ mock_value.side_effect = mock_info_with_sleep
281
+
282
+ # Create tasks up to the MAX_WORKERS_CAP (max number of parallel tasks)
283
+ companies = Companies(
284
+ self.kfinance_api_client_with_thread_pool, [i for i in range(MAX_WORKERS_CAP)]
285
+ )
286
+
287
+ start = time.perf_counter()
288
+ result = companies.city
289
+ end = time.perf_counter()
290
+ assert len(result) == MAX_WORKERS_CAP
291
+ # Check that the requests run faster than sequential.
292
+ # In practice, the requests should take barely more than the `sleep_duration` but timing
293
+ # based tests can be flaky, especially in CI.
294
+ assert end - start < MAX_WORKERS_CAP * sleep_duration
@@ -0,0 +1,194 @@
1
+ from pathlib import Path
2
+ import subprocess
3
+ import sys
4
+ from textwrap import dedent
5
+ import uuid
6
+
7
+ from nbconvert.preprocessors import ExecutePreprocessor
8
+ import nbformat
9
+ import pytest
10
+
11
+
12
+ @pytest.fixture(scope="session")
13
+ def jupyter_kernel_name() -> str:
14
+ """Create a jupyter kernel for a test run and yield its name.
15
+
16
+ The kernel gets removed at the end of the test run.
17
+ """
18
+
19
+ kernel_name = f"test-kfinance-kernel-{uuid.uuid4().hex[:8]}"
20
+
21
+ # Install a new kernel pointing to the current interpreter.
22
+ subprocess.run(
23
+ [
24
+ sys.executable,
25
+ "-m",
26
+ "ipykernel",
27
+ "install",
28
+ "--user",
29
+ "--name",
30
+ kernel_name,
31
+ "--display-name",
32
+ f"Python ({kernel_name})",
33
+ ],
34
+ check=True,
35
+ )
36
+
37
+ yield kernel_name
38
+
39
+ # Remove the kernel after the test.
40
+ subprocess.run(
41
+ [sys.executable, "-m", "jupyter", "kernelspec", "uninstall", "-f", kernel_name], check=True
42
+ )
43
+
44
+
45
+ def test_run_notebook(jupyter_kernel_name: str):
46
+ """
47
+ GIVEN the usage_examples.ipynb notebook
48
+ WHEN the notebook gets run with a mock client and mock responses
49
+ THEN all cells of the notebook complete without errors.
50
+ """
51
+
52
+ # Create a replacement startup cell for the normal notebook client init.
53
+ # This cell contains:
54
+ # - setup of a mock client
55
+ # - mocks for all calls made by the client while executing the notebook
56
+ startup_cell_code = dedent("""
57
+ from datetime import datetime
58
+ from kfinance.kfinance import Client
59
+ kfinance_client = Client(refresh_token="foo")
60
+ api_client = kfinance_client.kfinance_api_client
61
+ # Set access token so that the client doesn't try to fetch it.
62
+ api_client._access_token = "foo"
63
+ api_client._access_token_expiry = datetime(2100, 1, 1).timestamp()
64
+
65
+ # Mock out all necessary requests with requests_mock
66
+ import requests_mock
67
+ mocker = requests_mock.Mocker()
68
+ mocker.start()
69
+
70
+ id_triple_resp = {
71
+ "trading_item_id": 2629108,
72
+ "security_id": 2629107,
73
+ "company_id": 21719
74
+ }
75
+
76
+ # spgi = kfinance_client.ticker("SPGI")
77
+ mocker.get(
78
+ url="https://kfinance.kensho.com/api/v1/id/SPGI",
79
+ json=id_triple_resp
80
+ )
81
+
82
+ mocker.get(
83
+ url="https://kfinance.kensho.com/api/v1/info/21719",
84
+ json={"name": "S&P Global Inc."}
85
+ )
86
+
87
+ balance_sheet_resp = {
88
+ "statements": {
89
+ "2022Q3": {"Cash And Equivalents": "1387000000.000000"},
90
+ "2022Q4": {"Cash And Equivalents": "1286000000.000000"}
91
+ }
92
+ }
93
+
94
+ # spgi.balance_sheet()
95
+ mocker.get(
96
+ url="https://kfinance.kensho.com/api/v1/statements/21719/balance_sheet/none/none/none/none/none",
97
+ json=balance_sheet_resp
98
+ )
99
+
100
+ # spgi.balance_sheet(period_type=PeriodType.annual, start_year=2010, end_year=2019)
101
+ mocker.get(
102
+ url="https://kfinance.kensho.com/api/v1/statements/21719/balance_sheet/annual/2010/2019/none/none",
103
+ json=balance_sheet_resp
104
+ )
105
+
106
+ # kfinance_client.ticker("JPM").balance_sheet()
107
+ # (leads to fetching SPGI balance sheet when requesting JPM because they return the same
108
+ # company id)
109
+ mocker.get(
110
+ url="https://kfinance.kensho.com/api/v1/id/JPM",
111
+ json=id_triple_resp
112
+ )
113
+
114
+ # spgi.net_income(period_type=PeriodType.annual, start_year=2010, end_year=2019)
115
+ mocker.get(
116
+ url="https://kfinance.kensho.com/api/v1/line_item/21719/net_income/annual/2010/2019/none/none",
117
+ json={"line_item": {"2010": "828000000.000000"}}
118
+ )
119
+
120
+ prices_resp = {
121
+ "prices": [
122
+ {
123
+ "date": "2024-05-20",
124
+ "open": "439.540000",
125
+ "high": "441.570000",
126
+ "low": "437.000000",
127
+ "close": "437.740000",
128
+ "volume": "1080006"
129
+ }
130
+ ]
131
+ }
132
+
133
+ # spgi.history()
134
+ mocker.get(
135
+ url="https://kfinance.kensho.com/api/v1/pricing/2629108/none/none/day/adjusted",
136
+ json=prices_resp
137
+ )
138
+
139
+ # spgi.history(
140
+ # periodicity=Periodicity.month,
141
+ # adjusted=False,
142
+ # start_date="2010-01-01",
143
+ # end_date="2019-12-31"
144
+ # )
145
+ mocker.get(
146
+ url="https://kfinance.kensho.com/api/v1/pricing/2629108/2010-01-01/2019-12-31/month/unadjusted",
147
+ json=prices_resp
148
+ )
149
+
150
+ # spgi.price_chart(
151
+ # periodicity=Periodicity.month,
152
+ # adjusted=False,
153
+ # start_date="2010-01-01",
154
+ # end_date="2019-12-31"
155
+ # )
156
+ mocker.get(
157
+ url='https://kfinance.kensho.com/api/v1/price_chart/2629108/2010-01-01/2019-12-31/month/unadjusted',
158
+ content=b"",
159
+ headers={'Content-Type': 'image/png'}
160
+ )
161
+ # Mock out image_open so that we don't have to return an actual png.
162
+ from unittest.mock import MagicMock
163
+ import kfinance.kfinance
164
+ kfinance.kfinance.image_open = MagicMock()
165
+ """)
166
+
167
+ # Load the notebook
168
+ notebook_path = Path(Path(__file__).parent.parent.parent, "usage_examples.ipynb")
169
+ with notebook_path.open() as f:
170
+ nb = nbformat.read(f, as_version=4)
171
+
172
+ # Set up the notebook executor
173
+ ep = ExecutePreprocessor(timeout=600, kernel_name=jupyter_kernel_name)
174
+
175
+ # Identify the start of the example section
176
+ example_sections_heading = "## Example functions"
177
+ examples_start_cell_id = None
178
+ for idx, cell in enumerate(nb.cells):
179
+ if cell["source"] == example_sections_heading:
180
+ examples_start_cell_id = idx
181
+
182
+ if not examples_start_cell_id:
183
+ raise ValueError(
184
+ f"Did not find a cell with content {example_sections_heading} that "
185
+ f"indicates the start of the examples."
186
+ )
187
+
188
+ # Combine the startup cell with the start of the examples
189
+ # i.e. toss everything before the examples start
190
+ nb.cells = [nbformat.v4.new_code_cell(startup_cell_code)] + nb.cells[examples_start_cell_id:]
191
+
192
+ # Run the notebook.
193
+ # The test passes if the notebook runs without errors.
194
+ ep.preprocess(nb, {"metadata": {"path": notebook_path.parent}})
@@ -229,6 +229,29 @@ class TestFetchItem(TestCase):
229
229
  )
230
230
  self.kfinance_api_client.fetch.assert_called_once_with(expected_fetch_url)
231
231
 
232
+ def test_fetch_segments(self) -> None:
233
+ company_id = 21719
234
+ segment_type = "business"
235
+ expected_fetch_url = f"{self.kfinance_api_client.url_base}segments/{company_id}/{segment_type}/none/none/none/none/none"
236
+ self.kfinance_api_client.fetch_segments(company_id=company_id, segment_type=segment_type)
237
+ self.kfinance_api_client.fetch.assert_called_with(expected_fetch_url)
238
+ period_type = PeriodType.quarterly
239
+ start_year = 2023
240
+ end_year = 2023
241
+ start_quarter = 1
242
+ end_quarter = 4
243
+ expected_fetch_url = f"{self.kfinance_api_client.url_base}segments/{company_id}/{segment_type}/{period_type.value}/{start_year}/{end_year}/{start_quarter}/{end_quarter}"
244
+ self.kfinance_api_client.fetch_segments(
245
+ company_id=company_id,
246
+ segment_type=segment_type,
247
+ period_type=period_type,
248
+ start_year=start_year,
249
+ end_year=end_year,
250
+ start_quarter=start_quarter,
251
+ end_quarter=end_quarter,
252
+ )
253
+ self.kfinance_api_client.fetch.assert_called_with(expected_fetch_url)
254
+
232
255
 
233
256
  class TestMarketCap:
234
257
  @pytest.mark.parametrize(
@@ -75,6 +75,19 @@ MOCK_COMPANY_DB = {
75
75
  }
76
76
  }
77
77
  },
78
+ "segments": {
79
+ "2024": {
80
+ "Intelligent Cloud": {"Operating Income": 49584000000.0, "Revenue": 105362000000.0},
81
+ "More Personal Computing": {
82
+ "Operating Income": 19309000000.0,
83
+ "Revenue": 62032000000.0,
84
+ },
85
+ "Productivity and Business Processes": {
86
+ "Operating Income": 40540000000.0,
87
+ "Revenue": 77728000000.0,
88
+ },
89
+ }
90
+ },
78
91
  }
79
92
  }
80
93
 
@@ -189,6 +202,19 @@ class MockKFinanceApiClient:
189
202
  ]
190
203
  }
191
204
 
205
+ def fetch_segments(
206
+ self,
207
+ company_id,
208
+ segment_type,
209
+ period_type,
210
+ start_year,
211
+ end_year,
212
+ start_quarter,
213
+ end_quarter,
214
+ ):
215
+ """Get a segment"""
216
+ return MOCK_COMPANY_DB[company_id]
217
+
192
218
 
193
219
  class TestTradingItem(TestCase):
194
220
  def setUp(self):
@@ -309,6 +335,20 @@ class TestCompany(TestCase):
309
335
  revenue = self.msft_company.revenue()
310
336
  pd.testing.assert_frame_equal(expected_revenue, revenue)
311
337
 
338
+ def test_business_segments(self) -> None:
339
+ """test business statement"""
340
+ rows = []
341
+ for period, segments in MOCK_COMPANY_DB[msft_company_id]["segments"].items():
342
+ for segment_name, line_items in segments.items():
343
+ for line_item, value in line_items.items():
344
+ rows.append([period, segment_name, line_item, value])
345
+ expected_segments = pd.DataFrame(
346
+ rows, columns=["Year", "Segment Name", "Line Item", "Value"]
347
+ ).replace(np.nan, None)
348
+
349
+ business_segment = self.msft_company.business_segments()
350
+ pd.testing.assert_frame_equal(expected_segments, business_segment)
351
+
312
352
 
313
353
  class TestSecurity(TestCase):
314
354
  def setUp(self):
@@ -8,7 +8,6 @@ from kfinance.constants import BusinessRelationshipType, Capitalization, Stateme
8
8
  from kfinance.kfinance import Client
9
9
  from kfinance.tests.conftest import SPGI_COMPANY_ID, SPGI_SECURITY_ID, SPGI_TRADING_ITEM_ID
10
10
  from kfinance.tool_calling import (
11
- GetCompanyIdFromIdentifier,
12
11
  GetEarningsCallDatetimesFromIdentifier,
13
12
  GetFinancialLineItemFromIdentifier,
14
13
  GetFinancialStatementFromIdentifier,
@@ -18,8 +17,7 @@ from kfinance.tool_calling import (
18
17
  GetLatest,
19
18
  GetNQuartersAgo,
20
19
  GetPricesFromIdentifier,
21
- GetSecurityIdFromIdentifier,
22
- GetTradingItemIdFromIdentifier,
20
+ ResolveIdentifier,
23
21
  )
24
22
  from kfinance.tool_calling.get_business_relationship_from_identifier import (
25
23
  GetBusinessRelationshipFromIdentifier,
@@ -107,18 +105,6 @@ class TestGetCapitalizationFromIdentifier:
107
105
  assert response == expected_response
108
106
 
109
107
 
110
- class TestGetCompanyIdFromIdentifier:
111
- def test_get_company_id_from_identifier(self, mock_client: Client):
112
- """
113
- GIVEN the GetCompanyIdFromIdentifier tool
114
- WHEN request the company id for SPGI
115
- THEN we get back the SPGI company id
116
- """
117
- tool = GetCompanyIdFromIdentifier(kfinance_client=mock_client)
118
- resp = tool.run(ToolArgsWithIdentifier(identifier="SPGI").model_dump(mode="json"))
119
- assert resp == SPGI_COMPANY_ID
120
-
121
-
122
108
  class TestGetCusipFromTicker:
123
109
  def test_get_cusip_from_ticker(self, requests_mock: Mocker, mock_client: Client):
124
110
  """
@@ -380,25 +366,17 @@ class TestPricesFromIdentifier:
380
366
  assert response == expected_response
381
367
 
382
368
 
383
- class TestGetSecurityIdFromIdentifier:
384
- def test_get_security_id_from_identifier(self, mock_client: Client):
369
+ class TestResolveIdentifier:
370
+ def test_resolve_identifier(self, mock_client: Client):
385
371
  """
386
- GIVEN the GetSecurityIdFromIdentifier tool
387
- WHEN we request the security id for SPGI
388
- THEN we get back the SPGI primary security id
372
+ GIVEN the ResolveIdentifier tool
373
+ WHEN request to resolve SPGI
374
+ THEN we get back a dict with the SPGI company id, security id, and trading item id
389
375
  """
390
- tool = GetSecurityIdFromIdentifier(kfinance_client=mock_client)
376
+ tool = ResolveIdentifier(kfinance_client=mock_client)
391
377
  resp = tool.run(ToolArgsWithIdentifier(identifier="SPGI").model_dump(mode="json"))
392
- assert resp == SPGI_SECURITY_ID
393
-
394
-
395
- class TestGetTradingItemIdFromIdentifier:
396
- def test_get_security_id_from_identifier(self, mock_client: Client):
397
- """
398
- GIVEN the GetTradingItemIdFromIdentifier tool
399
- WHEN we request the trading item id for SPGI
400
- THEN we get back the SPGI primary trading item id
401
- """
402
- tool = GetTradingItemIdFromIdentifier(kfinance_client=mock_client)
403
- resp = tool.run(ToolArgsWithIdentifier(identifier="SPGI").model_dump(mode="json"))
404
- assert resp == SPGI_TRADING_ITEM_ID
378
+ assert resp == {
379
+ "company_id": SPGI_COMPANY_ID,
380
+ "security_id": SPGI_SECURITY_ID,
381
+ "trading_item_id": SPGI_TRADING_ITEM_ID,
382
+ }
@@ -4,7 +4,6 @@ from kfinance.tool_calling.get_business_relationship_from_identifier import (
4
4
  GetBusinessRelationshipFromIdentifier,
5
5
  )
6
6
  from kfinance.tool_calling.get_capitalization_from_identifier import GetCapitalizationFromIdentifier
7
- from kfinance.tool_calling.get_company_id_from_identifier import GetCompanyIdFromIdentifier
8
7
  from kfinance.tool_calling.get_cusip_from_ticker import GetCusipFromTicker
9
8
  from kfinance.tool_calling.get_earnings_call_datetimes_from_identifier import (
10
9
  GetEarningsCallDatetimesFromIdentifier,
@@ -23,17 +22,13 @@ from kfinance.tool_calling.get_isin_from_ticker import GetIsinFromTicker
23
22
  from kfinance.tool_calling.get_latest import GetLatest
24
23
  from kfinance.tool_calling.get_n_quarters_ago import GetNQuartersAgo
25
24
  from kfinance.tool_calling.get_prices_from_identifier import GetPricesFromIdentifier
26
- from kfinance.tool_calling.get_security_id_from_identifier import GetSecurityIdFromIdentifier
27
- from kfinance.tool_calling.get_trading_item_id_from_identifier import GetTradingItemIdFromIdentifier
25
+ from kfinance.tool_calling.resolve_identifier import ResolveIdentifier
28
26
  from kfinance.tool_calling.shared_models import KfinanceTool
29
27
 
30
28
 
31
29
  ALL_TOOLS: list[Type[KfinanceTool]] = [
32
30
  GetLatest,
33
31
  GetNQuartersAgo,
34
- GetCompanyIdFromIdentifier,
35
- GetSecurityIdFromIdentifier,
36
- GetTradingItemIdFromIdentifier,
37
32
  GetIsinFromTicker,
38
33
  GetCusipFromTicker,
39
34
  GetInfoFromIdentifier,
@@ -44,4 +39,5 @@ ALL_TOOLS: list[Type[KfinanceTool]] = [
44
39
  GetFinancialStatementFromIdentifier,
45
40
  GetFinancialLineItemFromIdentifier,
46
41
  GetBusinessRelationshipFromIdentifier,
42
+ ResolveIdentifier,
47
43
  ]
@@ -8,7 +8,7 @@ from kfinance.tool_calling.shared_models import KfinanceTool, ToolArgsWithIdenti
8
8
 
9
9
  class GetInfoFromIdentifier(KfinanceTool):
10
10
  name: str = "get_info_from_identifier"
11
- description: str = "Get the information associated with an identifier. Info includes company name, status, type, simple industry, number of employees, founding date, webpage, HQ address, HQ city, HQ zip code, HQ state, HQ country, and HQ country iso code."
11
+ description: str = "Get the information associated with an identifier. Info includes company name, status, type, simple industry, number of employees (if available), founding date, webpage, HQ address, HQ city, HQ zip code, HQ state, HQ country, and HQ country iso code."
12
12
  args_schema: Type[BaseModel] = ToolArgsWithIdentifier
13
13
  required_permission: Permission | None = None
14
14
 
@@ -0,0 +1,18 @@
1
+ from typing import Type
2
+
3
+ from pydantic import BaseModel
4
+
5
+ from kfinance.constants import Permission
6
+ from kfinance.tool_calling.shared_models import KfinanceTool, ToolArgsWithIdentifier
7
+
8
+
9
+ class ResolveIdentifier(KfinanceTool):
10
+ name: str = "resolve_identifier"
11
+ description: str = (
12
+ "Get the company_id, security_id, and trading_item_id associated with an identifier."
13
+ )
14
+ args_schema: Type[BaseModel] = ToolArgsWithIdentifier
15
+ required_permission: Permission | None = None
16
+
17
+ def _run(self, identifier: str) -> dict[str, int]:
18
+ return self.kfinance_client.ticker(identifier).id_triple._asdict()
kfinance/version.py CHANGED
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '2.1.2'
21
- __version_tuple__ = version_tuple = (2, 1, 2)
20
+ __version__ = version = '2.2.2'
21
+ __version_tuple__ = version_tuple = (2, 2, 2)
@@ -1,16 +0,0 @@
1
- from typing import Type
2
-
3
- from pydantic import BaseModel
4
-
5
- from kfinance.constants import Permission
6
- from kfinance.tool_calling.shared_models import KfinanceTool, ToolArgsWithIdentifier
7
-
8
-
9
- class GetCompanyIdFromIdentifier(KfinanceTool):
10
- name: str = "get_company_id_from_identifier"
11
- description: str = "Get the company id associated with an identifier."
12
- args_schema: Type[BaseModel] = ToolArgsWithIdentifier
13
- required_permission: Permission | None = None
14
-
15
- def _run(self, identifier: str) -> int:
16
- return self.kfinance_client.ticker(identifier).company_id
@@ -1,16 +0,0 @@
1
- from typing import Type
2
-
3
- from pydantic import BaseModel
4
-
5
- from kfinance.constants import Permission
6
- from kfinance.tool_calling.shared_models import KfinanceTool, ToolArgsWithIdentifier
7
-
8
-
9
- class GetSecurityIdFromIdentifier(KfinanceTool):
10
- name: str = "get_security_id_from_identifier"
11
- description: str = "Get the security id associated with an identifier."
12
- args_schema: Type[BaseModel] = ToolArgsWithIdentifier
13
- required_permission: Permission | None = None
14
-
15
- def _run(self, identifier: str) -> int:
16
- return self.kfinance_client.ticker(identifier).security_id
@@ -1,16 +0,0 @@
1
- from typing import Type
2
-
3
- from pydantic import BaseModel
4
-
5
- from kfinance.constants import Permission
6
- from kfinance.tool_calling.shared_models import KfinanceTool, ToolArgsWithIdentifier
7
-
8
-
9
- class GetTradingItemIdFromIdentifier(KfinanceTool):
10
- name: str = "get_trading_item_id_from_identifier"
11
- description: str = "Get the trading item id associated with an identifier."
12
- args_schema: Type[BaseModel] = ToolArgsWithIdentifier
13
- required_permission: Permission | None = None
14
-
15
- def _run(self, identifier: str) -> int:
16
- return self.kfinance_client.ticker(identifier).trading_item_id