kensho-kfinance 2.2.5__py3-none-any.whl → 2.4.0__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.2.5
3
+ Version: 2.4.0
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
@@ -13,8 +13,10 @@ Description-Content-Type: text/markdown
13
13
  License-File: LICENSE
14
14
  License-File: AUTHORS.md
15
15
  Requires-Dist: cachetools<6,>=5.5
16
+ Requires-Dist: click<=9,>=8.2.1
17
+ Requires-Dist: fastmcp<3,>=2
16
18
  Requires-Dist: langchain-core>=0.3.15
17
- Requires-Dist: langchain-google-genai<3,>=2.1.0
19
+ Requires-Dist: langchain-google-genai<3,>=2.1.5
18
20
  Requires-Dist: numpy>=1.22.4
19
21
  Requires-Dist: pandas>=2.0.0
20
22
  Requires-Dist: pillow>=10
@@ -29,7 +31,8 @@ Requires-Dist: urllib3>=1.21.1
29
31
  Provides-Extra: dev
30
32
  Requires-Dist: coverage<8,>=7.6.10; extra == "dev"
31
33
  Requires-Dist: ipykernel<7,>=6.29; extra == "dev"
32
- Requires-Dist: mypy<2,>=1.15.0; extra == "dev"
34
+ Requires-Dist: langchain-anthropic<1,>=0.3.10; extra == "dev"
35
+ Requires-Dist: mypy<2,>=1.16.0; extra == "dev"
33
36
  Requires-Dist: nbconvert<8,>=7.16; extra == "dev"
34
37
  Requires-Dist: nbformat<6,>5.10; extra == "dev"
35
38
  Requires-Dist: nbqa<2,>1.9; extra == "dev"
@@ -51,7 +54,7 @@ Any questions or suggestions can be sent to the [kFinance Maintainers](kfinance-
51
54
 
52
55
  # Setup
53
56
 
54
- You can install kFinance on [PyPI](https://pypi.org/project/kensho-kfinance/) via
57
+ You can install kFinance on [PyPI](https://pypi.org/project/kensho-kfinance/) via
55
58
 
56
59
  `pip install kensho-kfinance`
57
60
 
@@ -61,11 +64,41 @@ To receive access, please email [S&P Global Market Intelligence](market.intellig
61
64
 
62
65
  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).
63
66
 
64
- We also provide an [interactive notebook](usage_examples.ipynb) that demonstrates some usage examples.
67
+ To get started, we provide some notebooks:
68
+
69
+ - The [LLM-ready API Basic Usage](example_notebooks%2Fbasic_usage.ipynb) notebook demonstrates how
70
+ fetch data with the kFinance client.
71
+ - The [tool_calling notebooks](example_notebooks%2Ftool_calling) show how the kFinance library can
72
+ be used for tool calling. We provide notebooks for OpenAI (GPT), Anthropic (Claude), and Google
73
+ (Gemini). Each of these integrations comes in a langchain version, which uses langchain as a
74
+ wrapper to simplify the integration, and as a lower level non-langchain version.
75
+
76
+ We also provide an [interactive notebook](example_notebooks/basic_usage.ipynb) that demonstrates some usage examples.
77
+
78
+ # MCP (Model Context Protocol)
79
+
80
+ To run the kFinance MCP server use:
81
+
82
+ `python -m kfinance.mcp`
83
+
84
+ This function initializes and starts an MCP server that exposes the kFinance tools. The server supports multiple authentication methods and transport protocols to accommodate different deployment scenarios.
85
+
86
+ The server's full signature is as follows:
87
+
88
+ `kfinance.mcp [--stdio,-s]/[--sse, ] --refresh-token <refresh-token> --client-id <client-id> --private-key <private-key>`
89
+
90
+ Authentication Methods (in order of precedence):
91
+ 1. Refresh Token: Uses an existing refresh token for authentication. The `--refresh-token <refresh-token>` argument must be provided.
92
+ 2. Key Pair: Uses client ID and private key for authentication. Both the `--client-id <client-id>` and `--private-key <private-key>` arguments must be provided.
93
+ 3. Browser: Falls back to browser-based authentication flow. This occurs if no auth arguments are provided.
94
+
95
+ Transport Layers:
96
+ - stdio can be set by passing either `--stdio` or `-s`
97
+ - sse can be set by passing `--sse` or no other transport related flag
65
98
 
66
99
  # Versioning
67
- The kFinance uses semantic versioning (major, minor, patch).
68
- To bump the version, add a new entry in [CHANGELOG.md](kfinance%2FCHANGELOG.md).
100
+ The kFinance uses semantic versioning (major, minor, patch).
101
+ To bump the version, add a new entry in [CHANGELOG.md](kfinance%2FCHANGELOG.md).
69
102
  This will generate a new version of the library as part of the release process.
70
103
 
71
104
  # License
@@ -1,31 +1,34 @@
1
- kensho_kfinance-2.2.5.dist-info/licenses/AUTHORS.md,sha256=0h9ClbI0pu1oKj1M28ROUsaxrbZg-6ukQGl6X4y9noI,68
2
- kensho_kfinance-2.2.5.dist-info/licenses/LICENSE,sha256=bsY4blvSgq6o0FMQ3RXa2NCgco--nHCCchLXzxr6kms,83
3
- kfinance/CHANGELOG.md,sha256=Nlkx-7DFWd9kyeKUdm7pqHW7bFi8ZdruQxCmOo88TpY,1302
1
+ kensho_kfinance-2.4.0.dist-info/licenses/AUTHORS.md,sha256=0h9ClbI0pu1oKj1M28ROUsaxrbZg-6ukQGl6X4y9noI,68
2
+ kensho_kfinance-2.4.0.dist-info/licenses/LICENSE,sha256=bsY4blvSgq6o0FMQ3RXa2NCgco--nHCCchLXzxr6kms,83
3
+ kfinance/CHANGELOG.md,sha256=snsLVUBbhNjIau_BsoWKdxxCAPbPHM_rUQ3pNsseF_k,1446
4
4
  kfinance/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
5
  kfinance/batch_request_handling.py,sha256=p6p_G4_BL06GgeKlh7P1k9CUqOMahWCLEw1NoBwbLvU,5698
6
6
  kfinance/constants.py,sha256=UuFzqL253-2tRQfma785K9tfaZGv-o821tO2tVLwc5Q,48813
7
- kfinance/fetch.py,sha256=y4B-xPKrGNDVkicSf904q-M1hXHtNN8AuJ0Y55x_r2s,23464
8
- kfinance/kfinance.py,sha256=_U69k0Dcwkw1B_lzaUZy8N2-c-v93ZKoUAVVZb6wBUM,52758
7
+ kfinance/fetch.py,sha256=yaCih8PAkOhVHb3tvmBW0x2w4QmXJiyUATu6Yx-xzP4,23851
8
+ kfinance/kfinance.py,sha256=9lcarUW4fLHJA_U7_1ihCMbNJWs0AztvKf_XEd0qupk,59889
9
+ kfinance/mcp.py,sha256=57OoTrxo5x_v7u1s3WFQNG0LySXyLEGc6CKSVpJLJVw,3717
9
10
  kfinance/meta_classes.py,sha256=3V0nSXDDoake5o7kXnrqXuqNIiwI75KR4IYxFqSPhTE,20736
10
11
  kfinance/prompt.py,sha256=PtVB8c_FcSlVdyGgByAnIFGzuUuBaEjciCqnBJl1hSQ,25133
11
12
  kfinance/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
- kfinance/pydantic_models.py,sha256=WWLjcxBvVOpW2Wzpq1zKKju4uMlIeH4nKvM2GqLsjEE,597
13
+ kfinance/pydantic_models.py,sha256=avpbPqwrAyLqsCbrmFpK_B8_fj1nPlBHrnPxRcBaSkE,774
13
14
  kfinance/server_thread.py,sha256=jUnt1YGoYDkqqz1MbCwd44zJs1T_Z2BCgvj75bdtLgA,2574
14
- kfinance/version.py,sha256=VXEVDzLsFHRu1B1J9TV6pbjt3sWduv6BDj43YpV5xIM,511
15
+ kfinance/version.py,sha256=KyHVhKcn4Ob8_JP09Buz8x5uB4b7RJZLl3FBlZ8sWSY,511
15
16
  kfinance/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
17
  kfinance/tests/conftest.py,sha256=voB-w8P_6L3Nel3rdgylXKe5WWaS1q7nCFt1O04uqoY,948
17
18
  kfinance/tests/test_batch_requests.py,sha256=uXJF2IcRdyBm5SthwIUHMKtkGZ21MY84pg_k1JeSNOY,11430
18
19
  kfinance/tests/test_client.py,sha256=O7icZCSDhlQ9WGhzoXlpiSvbuA-mQNJHBYVsilyP_dE,2209
19
- kfinance/tests/test_example_notebook.py,sha256=XhDAJp3H5Y6usLAt3k4Ug4W3H6gLyh68nzmoWgOOLn4,6441
20
- kfinance/tests/test_fetch.py,sha256=LbE8JLS4OsByfQ_GsGxYwSX5Z4Dgfr-T6bFaZo1k7Oo,16079
20
+ kfinance/tests/test_example_notebook.py,sha256=XHwDKw2avyMonTmi3snCcFWNfZhEJOkpBGOZNrMLrhk,6470
21
+ kfinance/tests/test_fetch.py,sha256=nfnz_ZxE-W3KMzpDaRClX55fQJrRjwLTha-rHTallmE,16713
21
22
  kfinance/tests/test_group_objects.py,sha256=SoMEZmkG4RYdgWOAwxLHHtzIQho92KM01YbQXPUg578,1689
22
- kfinance/tests/test_objects.py,sha256=CSk3iN-uDt-E6gaOX4jExuYPT8Up-YNC3hafpenadcA,23614
23
- kfinance/tests/test_tools.py,sha256=ne4XhebhoxDHFl4kRLl10j4K-SXLrxS6jv6Ypt0Gv_E,16846
23
+ kfinance/tests/test_mcp.py,sha256=-pmeZ0PDp8GmXWcrWmaNNqMe7OVZONZLrjA0Y4b82os,594
24
+ kfinance/tests/test_objects.py,sha256=0nDmCFrVcfI8VBo1Ph3YqXNo3uPLsSUiQkjEEHsax1M,30416
25
+ kfinance/tests/test_tools.py,sha256=EWWWyPlGGES8Cn43_VaDAT7Vdp1nlIETE5dtOrP163o,24447
24
26
  kfinance/tool_calling/README.md,sha256=omJq7Us6r4U45QB7hRpLjRJ5BMalCkZkh4uXBjTbJXc,2022
25
- kfinance/tool_calling/__init__.py,sha256=Fb30Snu09B63NscSUJGGnFAdlXSDrTAOJlzjYvMyZXk,1873
27
+ kfinance/tool_calling/__init__.py,sha256=V5BVcJkLT1zC0QKZIQjLb-cZiQYV4T9Egj64uH807WE,2215
26
28
  kfinance/tool_calling/get_business_relationship_from_identifier.py,sha256=CipXvyqEjPm6BXYP0CA9Kp1BIyiIEm7abp85x1zXRV4,1472
27
29
  kfinance/tool_calling/get_capitalization_from_identifier.py,sha256=TdWdJDeI-jSL-1YfhnnwIA9D1SXobidvoHrjK42QmqQ,1521
28
30
  kfinance/tool_calling/get_cusip_from_ticker.py,sha256=houhGCYXoSzaaTtCvOBf3pPsYiSbcV1Ej5nAyGuMWcU,644
31
+ kfinance/tool_calling/get_earnings.py,sha256=7xavYvqwq4fO0vpks25eDM87bCYwZxEQSwgT0I8jAok,1165
29
32
  kfinance/tool_calling/get_earnings_call_datetimes_from_identifier.py,sha256=Zp7gHP-z2ePu484wWx4xw89hl0z7NLcG3qliJT-6rUo,782
30
33
  kfinance/tool_calling/get_financial_line_item_from_identifier.py,sha256=bMK-GUMz_wmJ-7iFS72sM8AvvUvR83JYRWzYtr9Fofw,2153
31
34
  kfinance/tool_calling/get_financial_statement_from_identifier.py,sha256=9ouzC43skA78uI3dh3bvtM86db5RGxuWQAAJ24y-kBM,1906
@@ -33,12 +36,16 @@ kfinance/tool_calling/get_history_metadata_from_identifier.py,sha256=kKIInfRC1Pl
33
36
  kfinance/tool_calling/get_info_from_identifier.py,sha256=PRhSYpYs_iUcIVFGYCMN_7QJyhewua7c7pqngPIp-qg,766
34
37
  kfinance/tool_calling/get_isin_from_ticker.py,sha256=2fJBcA-rNGbVOQmQ7qJEYxqejQwJ6nyWOBSFlzxG7dY,638
35
38
  kfinance/tool_calling/get_latest.py,sha256=btGeVBmvX5QJutzrKfE6spatGGejDuHxtq53NoAaGNk,786
39
+ kfinance/tool_calling/get_latest_earnings.py,sha256=pQdExGdztGY3pHcak0bd6ULNf_ROPc91bJ7zAVSKQkk,1117
36
40
  kfinance/tool_calling/get_n_quarters_ago.py,sha256=A0ilwPKUqU0YYQSz3gNsVF0Jy4YttXrSaDhYj7y8GHA,713
41
+ kfinance/tool_calling/get_next_earnings.py,sha256=knzQw-m-hscCvTuDUXG9v_dObKJBGn5BbDZWGKHKQcw,1097
37
42
  kfinance/tool_calling/get_prices_from_identifier.py,sha256=ViJkwLDvStB7grc8RuoKSDXQM399Wru4-OY3E8k1l_U,1882
38
43
  kfinance/tool_calling/get_segments_from_identifier.py,sha256=WIqJ1wWE6Z87VBREGu42nRc6_eJqUbGKcE9elzqBQJE,1867
44
+ kfinance/tool_calling/get_transcript.py,sha256=eB-IsRwD-mllsMOYRZbH35caQ1Y3teKft0tmI9nVL-A,756
45
+ kfinance/tool_calling/prompts.py,sha256=Yw1DJIMh90cjL-8q6_RMRiSjCtFDXvJAy7QiV5_uAU8,911
39
46
  kfinance/tool_calling/resolve_identifier.py,sha256=npslr6bBCu0qEDV1-8d24F5OC3nQ1KBMphuMbHVC1AU,626
40
47
  kfinance/tool_calling/shared_models.py,sha256=K-NPQyE_7Ew6Cs0zxG1xO2O47gp5uDHdHtWD7wUDZX4,2132
41
- kensho_kfinance-2.2.5.dist-info/METADATA,sha256=ilytuQ2DVw1h-i2Qo3EV-VKP6_p0OgiuF-Fzdvo5R9M,3436
42
- kensho_kfinance-2.2.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
43
- kensho_kfinance-2.2.5.dist-info/top_level.txt,sha256=kT_kNwVhfQoOAecY8W7uYah5xaHMoHoAdBIvXh6DaKM,9
44
- kensho_kfinance-2.2.5.dist-info/RECORD,,
48
+ kensho_kfinance-2.4.0.dist-info/METADATA,sha256=yWX6RmluCSz_qoG7EVTJnKJcsOByeZYXmu3M88-ch2A,5191
49
+ kensho_kfinance-2.4.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
50
+ kensho_kfinance-2.4.0.dist-info/top_level.txt,sha256=kT_kNwVhfQoOAecY8W7uYah5xaHMoHoAdBIvXh6DaKM,9
51
+ kensho_kfinance-2.4.0.dist-info/RECORD,,
kfinance/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## v2.4.0
4
+ - Add MCP server
5
+
6
+ ## v2.3.1
7
+ - Add llm tools for retrieving earnings and transcripts
8
+
9
+ ## v2.3.0
10
+ - Add earnings and transcript objects
11
+
3
12
  ## v2.2.5
4
13
  - Add parsing for relationship response with name
5
14
 
kfinance/fetch.py CHANGED
@@ -567,3 +567,13 @@ class KFinanceApiClient:
567
567
  """
568
568
  url = f"{self.url_base}company_groups/industry/{industry_classification}/{industry_code}"
569
569
  return self.fetch(url)
570
+
571
+ def fetch_earnings(self, company_id: int) -> dict:
572
+ """Get the earnings for a company."""
573
+ url = f"{self.url_base}earnings/{company_id}"
574
+ return self.fetch(url)
575
+
576
+ def fetch_transcript(self, key_dev_id: int) -> dict:
577
+ """Get the transcript for an earnings item."""
578
+ url = f"{self.url_base}transcript/{key_dev_id}"
579
+ return self.fetch(url)
kfinance/kfinance.py CHANGED
@@ -1,13 +1,15 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from collections.abc import Sequence
3
4
  from concurrent.futures import ThreadPoolExecutor
5
+ from copy import deepcopy
4
6
  from datetime import date, datetime, timezone
5
7
  from functools import cached_property
6
8
  from io import BytesIO
7
9
  import logging
8
10
  import re
9
11
  from sys import stdout
10
- from typing import TYPE_CHECKING, Any, Callable, Iterable, NamedTuple, Optional
12
+ from typing import TYPE_CHECKING, Any, Callable, Iterable, NamedTuple, Optional, overload
11
13
  from urllib.parse import urljoin
12
14
  import webbrowser
13
15
 
@@ -39,6 +41,7 @@ from .meta_classes import (
39
41
  DelegatedCompanyFunctionsMetaClass,
40
42
  )
41
43
  from .prompt import PROMPT
44
+ from .pydantic_models import TranscriptComponent
42
45
  from .server_thread import ServerThread
43
46
 
44
47
 
@@ -48,6 +51,12 @@ if TYPE_CHECKING:
48
51
  logger = logging.getLogger(__name__)
49
52
 
50
53
 
54
+ class NoEarningsDataError(Exception):
55
+ """Exception raised when no earnings data is found for a company."""
56
+
57
+ pass
58
+
59
+
51
60
  class TradingItem:
52
61
  """Trading Class
53
62
 
@@ -199,6 +208,96 @@ class TradingItem:
199
208
  return image
200
209
 
201
210
 
211
+ class Transcript(Sequence[TranscriptComponent]):
212
+ """Transcript class that represents earnings item transcript components"""
213
+
214
+ def __init__(self, transcript_components: list[dict[str, str]]):
215
+ """Initialize the Transcript object
216
+
217
+ :param transcript_components: List of transcript component dictionaries
218
+ :type transcript_components: list[dict[str, str]]
219
+ """
220
+ self._components = [TranscriptComponent(**component) for component in transcript_components]
221
+ self._raw_transcript: str | None = None
222
+
223
+ @overload
224
+ def __getitem__(self, index: int) -> TranscriptComponent: ...
225
+
226
+ @overload
227
+ def __getitem__(self, index: slice) -> list[TranscriptComponent]: ...
228
+
229
+ def __getitem__(self, index: int | slice) -> TranscriptComponent | list[TranscriptComponent]:
230
+ return self._components[index]
231
+
232
+ def __len__(self) -> int:
233
+ return len(self._components)
234
+
235
+ @property
236
+ def raw(self) -> str:
237
+ """Get the raw transcript as a single string
238
+
239
+ :return: Raw transcript text with speaker names and double newlines between components
240
+ :rtype: str
241
+ """
242
+ if self._raw_transcript is not None:
243
+ return self._raw_transcript
244
+
245
+ raw_components = []
246
+ for component in self._components:
247
+ speaker = component.person_name
248
+ text = component.text
249
+ raw_components.append(f"{speaker}: {text}")
250
+
251
+ self._raw_transcript = "\n\n".join(raw_components)
252
+ return self._raw_transcript
253
+
254
+
255
+ class Earnings:
256
+ """Earnings class that represents an earnings item"""
257
+
258
+ def __init__(
259
+ self,
260
+ kfinance_api_client: "KFinanceApiClient",
261
+ name: str,
262
+ datetime: datetime,
263
+ key_dev_id: int,
264
+ ):
265
+ """Initialize the Earnings object
266
+
267
+ :param kfinance_api_client: The KFinanceApiClient used to fetch data
268
+ :type kfinance_api_client: KFinanceApiClient
269
+ :param name: The earnings name
270
+ :type name: str
271
+ :param datetime: The earnings datetime
272
+ :type datetime: datetime
273
+ :param key_dev_id: The key dev ID for the earnings
274
+ :type key_dev_id: int
275
+ """
276
+ self.kfinance_api_client = kfinance_api_client
277
+ self.name = name
278
+ self.datetime = datetime
279
+ self.key_dev_id = key_dev_id
280
+ self._transcript: Transcript | None = None
281
+
282
+ def __str__(self) -> str:
283
+ """String representation for the earnings object"""
284
+ return f"{type(self).__module__}.{type(self).__qualname__} of {self.key_dev_id}"
285
+
286
+ @property
287
+ def transcript(self) -> Transcript:
288
+ """Get the transcript for this earnings
289
+
290
+ :return: The transcript object containing all components
291
+ :rtype: Transcript
292
+ """
293
+ if self._transcript is not None:
294
+ return self._transcript
295
+
296
+ transcript_data = self.kfinance_api_client.fetch_transcript(self.key_dev_id)
297
+ self._transcript = Transcript(transcript_data["transcript"])
298
+ return self._transcript
299
+
300
+
202
301
  class Company(CompanyFunctionsMetaClass):
203
302
  """Company class
204
303
 
@@ -219,6 +318,7 @@ class Company(CompanyFunctionsMetaClass):
219
318
  super().__init__()
220
319
  self.kfinance_api_client = kfinance_api_client
221
320
  self.company_id = company_id
321
+ self._all_earnings: list[Earnings] | None = None
222
322
 
223
323
  def __str__(self) -> str:
224
324
  """String representation for the company object"""
@@ -248,16 +348,6 @@ class Company(CompanyFunctionsMetaClass):
248
348
  security_ids = self.kfinance_api_client.fetch_securities(self.company_id)["securities"]
249
349
  return Securities(kfinance_api_client=self.kfinance_api_client, security_ids=security_ids)
250
350
 
251
- @cached_property
252
- def latest_earnings_call(self) -> None:
253
- """Set and return the latest earnings call item for the object
254
-
255
- :raises NotImplementedError: This function is not yet implemented
256
- """
257
- raise NotImplementedError(
258
- "The latest earnings call property of company class not implemented yet"
259
- )
260
-
261
351
  @cached_property
262
352
  def info(self) -> dict:
263
353
  """Get the company info
@@ -398,6 +488,121 @@ class Company(CompanyFunctionsMetaClass):
398
488
  ]
399
489
  ]
400
490
 
491
+ @property
492
+ def all_earnings(self) -> list[Earnings]:
493
+ """Retrieve and cache all earnings items for this company"""
494
+ if self._all_earnings is not None:
495
+ return self._all_earnings
496
+
497
+ earnings_data = self.kfinance_api_client.fetch_earnings(self.company_id)
498
+ self._all_earnings = []
499
+
500
+ for earnings in earnings_data["earnings"]:
501
+ if "keydevid" in earnings:
502
+ earnings["key_dev_id"] = deepcopy(earnings["keydevid"])
503
+ del earnings["keydevid"]
504
+ earnings_datetime = datetime.fromisoformat(
505
+ earnings["datetime"].replace("Z", "+00:00")
506
+ ).replace(tzinfo=timezone.utc)
507
+
508
+ self._all_earnings.append(
509
+ Earnings(
510
+ kfinance_api_client=self.kfinance_api_client,
511
+ name=earnings["name"],
512
+ datetime=earnings_datetime,
513
+ key_dev_id=earnings["key_dev_id"],
514
+ )
515
+ )
516
+
517
+ return self._all_earnings
518
+
519
+ def earnings(
520
+ self, start_date: date | None = None, end_date: date | None = None
521
+ ) -> list[Earnings]:
522
+ """Get earnings for the company within date range sorted in descending order by date
523
+
524
+ :param start_date: Start date filter, defaults to None
525
+ :type start_date: date, optional
526
+ :param end_date: End date filter, defaults to None
527
+ :type end_date: date, optional
528
+ :return: List of earnings objects
529
+ :rtype: list[Earnings]
530
+ """
531
+ if not self.all_earnings:
532
+ return []
533
+
534
+ if start_date is not None:
535
+ start_date_utc = datetime.combine(start_date, datetime.min.time()).replace(
536
+ tzinfo=timezone.utc
537
+ )
538
+
539
+ else:
540
+ start_date_utc = None
541
+
542
+ if end_date is not None:
543
+ end_date_utc = datetime.combine(end_date, datetime.max.time()).replace(
544
+ tzinfo=timezone.utc
545
+ )
546
+
547
+ else:
548
+ end_date_utc = None
549
+
550
+ filtered_earnings = []
551
+
552
+ for earnings in self.all_earnings:
553
+ # Apply date filtering if provided
554
+ if start_date_utc is not None and earnings.datetime < start_date_utc:
555
+ continue
556
+
557
+ if end_date_utc is not None and earnings.datetime > end_date_utc:
558
+ continue
559
+
560
+ filtered_earnings.append(earnings)
561
+
562
+ return filtered_earnings
563
+
564
+ @property
565
+ def latest_earnings(self) -> Earnings | None:
566
+ """Get the most recent past earnings
567
+
568
+ :return: The most recent earnings or None if no data available
569
+ :rtype: Earnings | None
570
+ """
571
+ if not self.all_earnings:
572
+ return None
573
+
574
+ now = datetime.now(timezone.utc)
575
+ past_earnings = [
576
+ earnings_item for earnings_item in self.all_earnings if earnings_item.datetime <= now
577
+ ]
578
+
579
+ if not past_earnings:
580
+ return None
581
+
582
+ # Sort by datetime descending and get the most recent
583
+ return max(past_earnings, key=lambda x: x.datetime)
584
+
585
+ @property
586
+ def next_earnings(self) -> Earnings | None:
587
+ """Get the next upcoming earnings
588
+
589
+ :return: The next earnings or None if no data available
590
+ :rtype: Earnings | None
591
+ """
592
+ if not self.all_earnings:
593
+ return None
594
+
595
+ now = datetime.now(timezone.utc)
596
+ future_earnings = [
597
+ earnings_item for earnings_item in self.all_earnings if earnings_item.datetime > now
598
+ ]
599
+
600
+ if not future_earnings:
601
+ return None
602
+
603
+ # Sort by datetime ascending and get the earliest
604
+ return min(future_earnings, key=lambda x: x.datetime)
605
+
401
606
 
402
607
  class Security:
403
608
  """Security class
@@ -1345,6 +1550,17 @@ class Client:
1345
1550
  kfinance_api_client=self.kfinance_api_client, trading_item_id=trading_item_id
1346
1551
  )
1347
1552
 
1553
+ def transcript(self, key_dev_id: int) -> Transcript:
1554
+ """Generate Transcript object from key_dev_id
1555
+
1556
+ :param key_dev_id: The key dev ID for the earnings
1557
+ :type key_dev_id: int
1558
+ :return: The transcript specified by the key dev id
1559
+ :rtype: Transcript
1560
+ """
1561
+ transcript_data = self.kfinance_api_client.fetch_transcript(key_dev_id)
1562
+ return Transcript(transcript_data["transcript"])
1563
+
1348
1564
  @staticmethod
1349
1565
  def get_latest(use_local_timezone: bool = True) -> LatestPeriods:
1350
1566
  """Get the latest annual reporting year, latest quarterly reporting quarter and year, and current date.
kfinance/mcp.py ADDED
@@ -0,0 +1,101 @@
1
+ from textwrap import dedent
2
+ from typing import Literal, Optional
3
+
4
+ import click
5
+ from fastmcp import FastMCP
6
+ from fastmcp.utilities.logging import get_logger
7
+
8
+ from kfinance.kfinance import Client
9
+ from kfinance.tool_calling.shared_models import KfinanceTool
10
+
11
+
12
+ logger = get_logger(__name__)
13
+
14
+
15
+ def build_doc_string(tool: KfinanceTool) -> str:
16
+ """Build a formatted documentation string for a Kfinance tool.
17
+
18
+ This function takes a KfinanceTool object and constructs a comprehensive
19
+ documentation string that includes the tool's description and detailed
20
+ information about its arguments, including default values and descriptions.
21
+
22
+ :param tool: The Kfinance tool object containing metadata about the tool's functionality, description, and argument schema.
23
+ :type tool: KfinanceTool
24
+ :return: A formatted documentation string containing for the tool description with detailed argument information.
25
+ :rtype: str
26
+ """
27
+
28
+ description = dedent(f"""
29
+ {tool.description}
30
+
31
+ Args:
32
+ """).strip()
33
+
34
+ for arg_name, arg_field in tool.args_schema.model_fields.items():
35
+ default_value_description = (
36
+ f"Default: {arg_field.default}. " if not arg_field.is_required() else ""
37
+ )
38
+ param_description = f"\n {arg_name}: {default_value_description}{arg_field.description}"
39
+ description += param_description
40
+
41
+ return description
42
+
43
+
44
+ @click.command()
45
+ @click.option("--stdio/--sse", "-s/ ", default=False)
46
+ @click.option("--refresh-token", required=False)
47
+ @click.option("--client-id", required=False)
48
+ @click.option("--private-key", required=False)
49
+ def run_mcp(
50
+ stdio: bool,
51
+ refresh_token: Optional[str] = None,
52
+ client_id: Optional[str] = None,
53
+ private_key: Optional[str] = None,
54
+ ) -> None:
55
+ """Run the Kfinance MCP server with specified configuration.
56
+
57
+ This function initializes and starts an MCP server that exposes Kfinance
58
+ tools. The server supports multiple authentication methods and
59
+ transport protocols to accommodate different deployment scenarios.
60
+
61
+ Authentication Methods (in order of precedence):
62
+ 1. Refresh Token: Uses an existing refresh token for authentication
63
+ 2. Key Pair: Uses client ID and private key for authentication
64
+ 3. Browser: Falls back to browser-based authentication flow
65
+
66
+ :param stdio: If True, use STDIO transport; if False, use SSE transport.
67
+ :type stdio: bool
68
+ :param refresh_token: OAuth refresh token for authentication
69
+ :type refresh_token: str
70
+ :param client_id: Client id for key-pair authentication
71
+ :type client_id: str
72
+ :param private_key: Private key for key-pair authentication.
73
+ :type private_key: str
74
+ """
75
+ transport: Literal["stdio", "sse"] = "stdio" if stdio else "sse"
76
+ logger.info("Sever will run with %s transport", transport)
77
+ if refresh_token:
78
+ logger.info("The client will be authenticated using a refresh token")
79
+ kfinance_client = Client(refresh_token=refresh_token)
80
+ elif client_id and private_key:
81
+ logger.info("The client will be authenticated using a key pair")
82
+ kfinance_client = Client(client_id=client_id, private_key=private_key)
83
+ else:
84
+ logger.info("The client will be authenticated using a browser")
85
+ kfinance_client = Client()
86
+
87
+ kfinance_mcp: FastMCP = FastMCP("Kfinance")
88
+ for tool in kfinance_client.langchain_tools:
89
+ logger.info("Adding %s to server", tool.name)
90
+ kfinance_mcp.tool(
91
+ name_or_fn=getattr(tool, "_run"),
92
+ name=tool.name,
93
+ description=build_doc_string(tool),
94
+ )
95
+
96
+ logger.info("Server starting")
97
+ kfinance_mcp.run(transport=transport)
98
+
99
+
100
+ if __name__ == "__main__":
101
+ run_mcp()
@@ -1,6 +1,14 @@
1
1
  from pydantic import BaseModel
2
2
 
3
3
 
4
+ class TranscriptComponent(BaseModel):
5
+ """A transcript component with person name, text, and component type."""
6
+
7
+ person_name: str
8
+ text: str
9
+ component_type: str
10
+
11
+
4
12
  class RelationshipResponseNoName(BaseModel):
5
13
  """A response from the relationship endpoint before adding the company name.
6
14
 
@@ -44,7 +44,7 @@ def jupyter_kernel_name() -> str:
44
44
 
45
45
  def test_run_notebook(jupyter_kernel_name: str):
46
46
  """
47
- GIVEN the usage_examples.ipynb notebook
47
+ GIVEN the basic_usage.ipynb notebook
48
48
  WHEN the notebook gets run with a mock client and mock responses
49
49
  THEN all cells of the notebook complete without errors.
50
50
  """
@@ -165,7 +165,9 @@ def test_run_notebook(jupyter_kernel_name: str):
165
165
  """)
166
166
 
167
167
  # Load the notebook
168
- notebook_path = Path(Path(__file__).parent.parent.parent, "usage_examples.ipynb")
168
+ notebook_path = Path(
169
+ Path(__file__).parent.parent.parent, "example_notebooks", "basic_usage.ipynb"
170
+ )
169
171
  with notebook_path.open() as f:
170
172
  nb = nbformat.read(f, as_version=4)
171
173
 
@@ -138,6 +138,18 @@ class TestFetchItem(TestCase):
138
138
  self.kfinance_api_client.fetch_earnings_dates(company_id=company_id)
139
139
  self.kfinance_api_client.fetch.assert_called_once_with(expected_fetch_url)
140
140
 
141
+ def test_fetch_earnings(self) -> None:
142
+ company_id = 21719
143
+ expected_fetch_url = f"{self.kfinance_api_client.url_base}earnings/{company_id}"
144
+ self.kfinance_api_client.fetch_earnings(company_id=company_id)
145
+ self.kfinance_api_client.fetch.assert_called_once_with(expected_fetch_url)
146
+
147
+ def test_fetch_transcript(self) -> None:
148
+ key_dev_id = 12345
149
+ expected_fetch_url = f"{self.kfinance_api_client.url_base}transcript/{key_dev_id}"
150
+ self.kfinance_api_client.fetch_transcript(key_dev_id=key_dev_id)
151
+ self.kfinance_api_client.fetch.assert_called_once_with(expected_fetch_url)
152
+
141
153
  def test_fetch_ticker_geography_groups(self) -> None:
142
154
  country_iso_code = "USA"
143
155
  expected_fetch_url = (
@@ -0,0 +1,16 @@
1
+ from typing import Type
2
+
3
+ import pytest
4
+
5
+ from kfinance.kfinance import Client
6
+ from kfinance.mcp import build_doc_string
7
+ from kfinance.tool_calling import ALL_TOOLS
8
+ from kfinance.tool_calling.shared_models import KfinanceTool
9
+
10
+
11
+ class TestDocStringBuilding:
12
+ @pytest.mark.parametrize("tool_class", ALL_TOOLS)
13
+ def test_build_doc_string(self, mock_client: Client, tool_class: Type[KfinanceTool]):
14
+ """This test build the docstring for each tool. A success is considered if no exception is raised"""
15
+ tool = tool_class(kfinance_client=mock_client)
16
+ build_doc_string(tool)
@@ -1,4 +1,4 @@
1
- from datetime import datetime, timezone
1
+ from datetime import date, datetime, timezone
2
2
  from io import BytesIO
3
3
  import re
4
4
  from typing import Optional
@@ -7,8 +7,9 @@ from unittest import TestCase
7
7
  import numpy as np
8
8
  import pandas as pd
9
9
  from PIL.Image import open as image_open
10
+ import time_machine
10
11
 
11
- from kfinance.kfinance import Company, Security, Ticker, TradingItem
12
+ from kfinance.kfinance import Company, Earnings, Security, Ticker, TradingItem, Transcript
12
13
 
13
14
 
14
15
  msft_company_id = "21835"
@@ -54,6 +55,25 @@ MOCK_COMPANY_DB = {
54
55
  "iso_country": "USA",
55
56
  },
56
57
  "earnings_call_dates": {"earnings": ["2004-07-22T21:30:00"]},
58
+ "earnings": {
59
+ "earnings": [
60
+ {
61
+ "name": "Microsoft Corporation, Q4 2024 Earnings Call, Jul 25, 2024",
62
+ "key_dev_id": 1916266380,
63
+ "datetime": "2024-07-25T21:30:00",
64
+ },
65
+ {
66
+ "name": "Microsoft Corporation, Q1 2025 Earnings Call, Oct 24, 2024",
67
+ "keydevid": 1916266381,
68
+ "datetime": "2024-10-24T21:30:00",
69
+ },
70
+ {
71
+ "name": "Microsoft Corporation, Q2 2025 Earnings Call, Jan 25, 2025",
72
+ "keydevid": 1916266382,
73
+ "datetime": "2025-01-25T21:30:00",
74
+ },
75
+ ]
76
+ },
57
77
  "statements": {
58
78
  "income_statement": {
59
79
  "statements": {
@@ -91,6 +111,32 @@ MOCK_COMPANY_DB = {
91
111
  }
92
112
  }
93
113
 
114
+ MOCK_TRANSCRIPT_DB = {
115
+ 1916266380: {
116
+ "transcript": [
117
+ {
118
+ "component_type": "Presentation Operator Message",
119
+ "person_name": "Operator",
120
+ "text": "Good morning, and welcome to Microsoft's Fourth Quarter 2024 Earnings Conference Call.",
121
+ },
122
+ {
123
+ "component_type": "Presenter Speech",
124
+ "person_name": "Satya Nadella",
125
+ "text": "Thank you for joining us today. We had an exceptional quarter with strong growth across all segments.",
126
+ },
127
+ ]
128
+ },
129
+ 1916266381: {
130
+ "transcript": [
131
+ {
132
+ "component_type": "Presentation Operator Message",
133
+ "person_name": "Operator",
134
+ "text": "Good morning, and welcome to Microsoft's First Quarter 2025 Earnings Conference Call.",
135
+ }
136
+ ]
137
+ },
138
+ }
139
+
94
140
 
95
141
  MOCK_SECURITY_DB = {msft_security_id: {"isin": msft_isin, "cusip": msft_cusip}}
96
142
 
@@ -215,6 +261,14 @@ class MockKFinanceApiClient:
215
261
  """Get a segment"""
216
262
  return MOCK_COMPANY_DB[company_id]
217
263
 
264
+ def fetch_earnings(self, company_id: int) -> dict:
265
+ """Get the earnings for a company."""
266
+ return MOCK_COMPANY_DB[company_id]["earnings"]
267
+
268
+ def fetch_transcript(self, key_dev_id: int) -> dict:
269
+ """Get the transcript for an earnings item."""
270
+ return MOCK_TRANSCRIPT_DB[key_dev_id]
271
+
218
272
 
219
273
  class TestTradingItem(TestCase):
220
274
  def setUp(self):
@@ -619,3 +673,112 @@ class TestTicker(TestCase):
619
673
  expected_dataframe.index.name = "date"
620
674
  market_caps = self.msft_ticker_from_ticker.market_cap()
621
675
  pd.testing.assert_frame_equal(expected_dataframe, market_caps)
676
+
677
+
678
+ class TestTranscript(TestCase):
679
+ def setUp(self):
680
+ """setup tests"""
681
+ self.transcript_components = [
682
+ {
683
+ "component_type": "Presentation Operator Message",
684
+ "person_name": "Operator",
685
+ "text": "Good morning, and welcome to Microsoft's Fourth Quarter 2024 Earnings Conference Call.",
686
+ },
687
+ {
688
+ "component_type": "Presenter Speech",
689
+ "person_name": "Satya Nadella",
690
+ "text": "Thank you for joining us today. We had an exceptional quarter with strong growth across all segments.",
691
+ },
692
+ ]
693
+ self.transcript = Transcript(self.transcript_components)
694
+
695
+ def test_transcript_length(self):
696
+ """test transcript length"""
697
+ self.assertEqual(len(self.transcript), 2)
698
+
699
+ def test_transcript_indexing(self):
700
+ """test transcript indexing"""
701
+ self.assertEqual(
702
+ self.transcript[0].person_name, self.transcript_components[0]["person_name"]
703
+ )
704
+ self.assertEqual(self.transcript[0].text, self.transcript_components[0]["text"])
705
+ self.assertEqual(
706
+ self.transcript[0].component_type, self.transcript_components[0]["component_type"]
707
+ )
708
+ self.assertEqual(
709
+ self.transcript[1].person_name, self.transcript_components[1]["person_name"]
710
+ )
711
+ self.assertEqual(self.transcript[1].text, self.transcript_components[1]["text"])
712
+ self.assertEqual(
713
+ self.transcript[1].component_type, self.transcript_components[1]["component_type"]
714
+ )
715
+
716
+ def test_transcript_raw(self):
717
+ """test transcript raw property"""
718
+ expected_raw = "Operator: Good morning, and welcome to Microsoft's Fourth Quarter 2024 Earnings Conference Call.\n\nSatya Nadella: Thank you for joining us today. We had an exceptional quarter with strong growth across all segments."
719
+ self.assertEqual(self.transcript.raw, expected_raw)
720
+
721
+
722
+ class TestEarnings(TestCase):
723
+ def setUp(self):
724
+ """setup tests"""
725
+ self.kfinance_api_client = MockKFinanceApiClient()
726
+ self.earnings = Earnings(
727
+ kfinance_api_client=self.kfinance_api_client,
728
+ name="Microsoft Corporation, Q4 2024 Earnings Call, Jul 25, 2024",
729
+ datetime=datetime.fromisoformat("2024-07-25T21:30:00").replace(tzinfo=timezone.utc),
730
+ key_dev_id=1916266380,
731
+ )
732
+
733
+ def test_earnings_attributes(self):
734
+ """test earnings attributes"""
735
+ self.assertEqual(
736
+ self.earnings.name, "Microsoft Corporation, Q4 2024 Earnings Call, Jul 25, 2024"
737
+ )
738
+ self.assertEqual(self.earnings.key_dev_id, 1916266380)
739
+ expected_datetime = datetime.fromisoformat("2024-07-25T21:30:00").replace(
740
+ tzinfo=timezone.utc
741
+ )
742
+ self.assertEqual(self.earnings.datetime, expected_datetime)
743
+
744
+ def test_earnings_transcript(self):
745
+ """test earnings transcript property"""
746
+ transcript = self.earnings.transcript
747
+ self.assertIsInstance(transcript, Transcript)
748
+ self.assertEqual(len(transcript), 2)
749
+ self.assertEqual(transcript[0].person_name, "Operator")
750
+ self.assertEqual(transcript[1].person_name, "Satya Nadella")
751
+
752
+
753
+ class TestCompanyEarnings(TestCase):
754
+ def setUp(self):
755
+ """setup tests"""
756
+ self.kfinance_api_client = MockKFinanceApiClient()
757
+ self.msft_company = Company(self.kfinance_api_client, msft_company_id)
758
+
759
+ def test_company_earnings(self):
760
+ """test company earnings method"""
761
+ earnings_list = self.msft_company.earnings()
762
+ self.assertEqual(len(earnings_list), 3)
763
+ self.assertIsInstance(earnings_list[0], Earnings)
764
+ self.assertEqual(earnings_list[0].key_dev_id, 1916266380)
765
+
766
+ def test_company_earnings_with_date_filter(self):
767
+ """test company earnings method with date filtering"""
768
+ start_date = date(2024, 8, 1)
769
+ end_date = date(2024, 12, 31)
770
+ earnings_list = self.msft_company.earnings(start_date=start_date, end_date=end_date)
771
+ self.assertEqual(len(earnings_list), 1)
772
+ self.assertEqual(earnings_list[0].key_dev_id, 1916266381)
773
+
774
+ @time_machine.travel(datetime(2025, 2, 1, 12, tzinfo=timezone.utc))
775
+ def test_company_latest_earnings(self):
776
+ """test company latest_earnings property"""
777
+ latest_earnings = self.msft_company.latest_earnings
778
+ self.assertEqual(latest_earnings.key_dev_id, 1916266382)
779
+
780
+ @time_machine.travel(datetime(2024, 6, 1, 12, tzinfo=timezone.utc))
781
+ def test_company_next_earnings(self):
782
+ """test company next_earnings property"""
783
+ next_earnings = self.msft_company.next_earnings
784
+ self.assertEqual(next_earnings.key_dev_id, 1916266380)
@@ -1,13 +1,15 @@
1
1
  from datetime import date, datetime
2
2
 
3
3
  from langchain_core.utils.function_calling import convert_to_openai_tool
4
+ from pytest import raises
4
5
  from requests_mock import Mocker
5
6
  import time_machine
6
7
 
7
8
  from kfinance.constants import BusinessRelationshipType, Capitalization, SegmentType, StatementType
8
- from kfinance.kfinance import Client
9
+ from kfinance.kfinance import Client, NoEarningsDataError
9
10
  from kfinance.tests.conftest import SPGI_COMPANY_ID, SPGI_SECURITY_ID, SPGI_TRADING_ITEM_ID
10
11
  from kfinance.tool_calling import (
12
+ GetEarnings,
11
13
  GetEarningsCallDatetimesFromIdentifier,
12
14
  GetFinancialLineItemFromIdentifier,
13
15
  GetFinancialStatementFromIdentifier,
@@ -15,8 +17,11 @@ from kfinance.tool_calling import (
15
17
  GetInfoFromIdentifier,
16
18
  GetIsinFromTicker,
17
19
  GetLatest,
20
+ GetLatestEarnings,
21
+ GetNextEarnings,
18
22
  GetNQuartersAgo,
19
23
  GetPricesFromIdentifier,
24
+ GetTranscript,
20
25
  ResolveIdentifier,
21
26
  )
22
27
  from kfinance.tool_calling.get_business_relationship_from_identifier import (
@@ -42,6 +47,7 @@ from kfinance.tool_calling.get_segments_from_identifier import (
42
47
  GetSegmentsFromIdentifier,
43
48
  GetSegmentsFromIdentifierArgs,
44
49
  )
50
+ from kfinance.tool_calling.get_transcript import GetTranscriptArgs
45
51
  from kfinance.tool_calling.shared_models import ToolArgsWithIdentifier
46
52
 
47
53
 
@@ -422,3 +428,213 @@ class TestResolveIdentifier:
422
428
  "security_id": SPGI_SECURITY_ID,
423
429
  "trading_item_id": SPGI_TRADING_ITEM_ID,
424
430
  }
431
+
432
+
433
+ class TestGetLatestEarnings:
434
+ def test_get_latest_earnings(self, requests_mock: Mocker, mock_client: Client):
435
+ """
436
+ GIVEN the GetLatestEarnings tool
437
+ WHEN we request the latest earnings for SPGI
438
+ THEN we get back the latest SPGI earnings
439
+ """
440
+ earnings_data = {
441
+ "earnings": [
442
+ {
443
+ "name": "SPGI Q4 2024 Earnings Call",
444
+ "datetime": "2025-02-11T13:30:00Z",
445
+ "keydevid": 12345,
446
+ },
447
+ {
448
+ "name": "SPGI Q3 2024 Earnings Call",
449
+ "datetime": "2024-10-30T12:30:00Z",
450
+ "keydevid": 12344,
451
+ },
452
+ ]
453
+ }
454
+
455
+ requests_mock.get(
456
+ url=f"https://kfinance.kensho.com/api/v1/earnings/{SPGI_COMPANY_ID}",
457
+ json=earnings_data,
458
+ )
459
+
460
+ expected_response = {
461
+ "name": "SPGI Q4 2024 Earnings Call",
462
+ "key_dev_id": 12345,
463
+ "datetime": "2025-02-11T13:30:00+00:00",
464
+ }
465
+
466
+ tool = GetLatestEarnings(kfinance_client=mock_client)
467
+ response = tool.run(ToolArgsWithIdentifier(identifier="SPGI").model_dump(mode="json"))
468
+ assert response == expected_response
469
+
470
+ def test_get_latest_earnings_no_data(self, requests_mock: Mocker, mock_client: Client):
471
+ """
472
+ GIVEN the GetLatestEarnings tool
473
+ WHEN we request the latest earnings for a company with no data
474
+ THEN we get a NoEarningsDataError exception
475
+ """
476
+ earnings_data = {"earnings": []}
477
+
478
+ requests_mock.get(
479
+ url=f"https://kfinance.kensho.com/api/v1/earnings/{SPGI_COMPANY_ID}",
480
+ json=earnings_data,
481
+ )
482
+
483
+ tool = GetLatestEarnings(kfinance_client=mock_client)
484
+ with raises(NoEarningsDataError, match="Latest earnings for SPGI not found"):
485
+ tool.run(ToolArgsWithIdentifier(identifier="SPGI").model_dump(mode="json"))
486
+
487
+
488
+ class TestGetNextEarnings:
489
+ def test_get_next_earnings_(self, requests_mock: Mocker, mock_client: Client):
490
+ """
491
+ GIVEN the GetNextEarnings tool
492
+ WHEN we request the next earnings for SPGI
493
+ THEN we get back the next SPGI earnings
494
+ """
495
+ earnings_data = {
496
+ "earnings": [
497
+ {
498
+ "name": "SPGI Q1 2025 Earnings Call",
499
+ "datetime": "2025-04-29T12:30:00Z",
500
+ "keydevid": 12346,
501
+ },
502
+ {
503
+ "name": "SPGI Q4 2024 Earnings Call",
504
+ "datetime": "2025-02-11T13:30:00Z",
505
+ "keydevid": 12345,
506
+ },
507
+ ]
508
+ }
509
+
510
+ requests_mock.get(
511
+ url=f"https://kfinance.kensho.com/api/v1/earnings/{SPGI_COMPANY_ID}",
512
+ json=earnings_data,
513
+ )
514
+
515
+ expected_response = {
516
+ "name": "SPGI Q1 2025 Earnings Call",
517
+ "key_dev_id": 12346,
518
+ "datetime": "2025-04-29T12:30:00+00:00",
519
+ }
520
+
521
+ with time_machine.travel("2025-03-01T00:00:00+00:00"):
522
+ tool = GetNextEarnings(kfinance_client=mock_client)
523
+ response = tool.run(ToolArgsWithIdentifier(identifier="SPGI").model_dump(mode="json"))
524
+ assert response == expected_response
525
+
526
+ def test_get_next_earnings_no_data(self, requests_mock: Mocker, mock_client: Client):
527
+ """
528
+ GIVEN the GetNextEarnings tool
529
+ WHEN we request the next earnings for a company with no data
530
+ THEN we get a NoEarningsDataError exception
531
+ """
532
+ earnings_data = {"earnings": []}
533
+
534
+ requests_mock.get(
535
+ url=f"https://kfinance.kensho.com/api/v1/earnings/{SPGI_COMPANY_ID}",
536
+ json=earnings_data,
537
+ )
538
+
539
+ with time_machine.travel("2025-03-01T00:00:00+00:00"):
540
+ tool = GetNextEarnings(kfinance_client=mock_client)
541
+ with raises(NoEarningsDataError, match="Next earnings for SPGI not found"):
542
+ tool.run(ToolArgsWithIdentifier(identifier="SPGI").model_dump(mode="json"))
543
+
544
+
545
+ class TestGetEarnings:
546
+ def test_get_earnings(self, requests_mock: Mocker, mock_client: Client):
547
+ """
548
+ GIVEN the GetEarnings tool
549
+ WHEN we request all earnings for SPGI
550
+ THEN we get back all SPGI earnings
551
+ """
552
+ earnings_data = {
553
+ "earnings": [
554
+ {
555
+ "name": "SPGI Q1 2025 Earnings Call",
556
+ "datetime": "2025-04-29T12:30:00Z",
557
+ "keydevid": 12346,
558
+ },
559
+ {
560
+ "name": "SPGI Q4 2024 Earnings Call",
561
+ "datetime": "2025-02-11T13:30:00Z",
562
+ "keydevid": 12345,
563
+ },
564
+ ]
565
+ }
566
+
567
+ requests_mock.get(
568
+ url=f"https://kfinance.kensho.com/api/v1/earnings/{SPGI_COMPANY_ID}",
569
+ json=earnings_data,
570
+ )
571
+
572
+ expected_response = [
573
+ {
574
+ "name": "SPGI Q1 2025 Earnings Call",
575
+ "key_dev_id": 12346,
576
+ "datetime": "2025-04-29T12:30:00+00:00",
577
+ },
578
+ {
579
+ "name": "SPGI Q4 2024 Earnings Call",
580
+ "key_dev_id": 12345,
581
+ "datetime": "2025-02-11T13:30:00+00:00",
582
+ },
583
+ ]
584
+
585
+ tool = GetEarnings(kfinance_client=mock_client)
586
+ response = tool.run(ToolArgsWithIdentifier(identifier="SPGI").model_dump(mode="json"))
587
+ assert response == expected_response
588
+
589
+ def test_get_earnings_no_data(self, requests_mock: Mocker, mock_client: Client):
590
+ """
591
+ GIVEN the GetEarnings tool
592
+ WHEN we request all earnings for a company with no data
593
+ THEN we get a NoEarningslDataError exception
594
+ """
595
+ earnings_data = {"earnings": []}
596
+
597
+ requests_mock.get(
598
+ url=f"https://kfinance.kensho.com/api/v1/earnings/{SPGI_COMPANY_ID}",
599
+ json=earnings_data,
600
+ )
601
+
602
+ tool = GetEarnings(kfinance_client=mock_client)
603
+ with raises(NoEarningsDataError, match="Earnings for SPGI not found"):
604
+ tool.run(ToolArgsWithIdentifier(identifier="SPGI").model_dump(mode="json"))
605
+
606
+
607
+ class TestGetTranscript:
608
+ def test_get_transcript(self, requests_mock: Mocker, mock_client: Client):
609
+ """
610
+ GIVEN the GetTranscript tool
611
+ WHEN we request a transcript by key_dev_id
612
+ THEN we get back the transcript text
613
+ """
614
+ transcript_data = {
615
+ "transcript": [
616
+ {
617
+ "person_name": "Operator",
618
+ "text": "Good morning, everyone.",
619
+ "component_type": "speech",
620
+ },
621
+ {
622
+ "person_name": "CEO",
623
+ "text": "Thank you for joining us today.",
624
+ "component_type": "speech",
625
+ },
626
+ ]
627
+ }
628
+
629
+ requests_mock.get(
630
+ url="https://kfinance.kensho.com/api/v1/transcript/12345",
631
+ json=transcript_data,
632
+ )
633
+
634
+ expected_response = (
635
+ "Operator: Good morning, everyone.\n\nCEO: Thank you for joining us today."
636
+ )
637
+
638
+ tool = GetTranscript(kfinance_client=mock_client)
639
+ response = tool.run(GetTranscriptArgs(key_dev_id=12345).model_dump(mode="json"))
640
+ assert response == expected_response
@@ -5,6 +5,7 @@ from kfinance.tool_calling.get_business_relationship_from_identifier import (
5
5
  )
6
6
  from kfinance.tool_calling.get_capitalization_from_identifier import GetCapitalizationFromIdentifier
7
7
  from kfinance.tool_calling.get_cusip_from_ticker import GetCusipFromTicker
8
+ from kfinance.tool_calling.get_earnings import GetEarnings
8
9
  from kfinance.tool_calling.get_earnings_call_datetimes_from_identifier import (
9
10
  GetEarningsCallDatetimesFromIdentifier,
10
11
  )
@@ -20,11 +21,14 @@ from kfinance.tool_calling.get_history_metadata_from_identifier import (
20
21
  from kfinance.tool_calling.get_info_from_identifier import GetInfoFromIdentifier
21
22
  from kfinance.tool_calling.get_isin_from_ticker import GetIsinFromTicker
22
23
  from kfinance.tool_calling.get_latest import GetLatest
24
+ from kfinance.tool_calling.get_latest_earnings import GetLatestEarnings
23
25
  from kfinance.tool_calling.get_n_quarters_ago import GetNQuartersAgo
26
+ from kfinance.tool_calling.get_next_earnings import GetNextEarnings
24
27
  from kfinance.tool_calling.get_prices_from_identifier import GetPricesFromIdentifier
25
28
  from kfinance.tool_calling.get_segments_from_identifier import (
26
29
  GetSegmentsFromIdentifier,
27
30
  )
31
+ from kfinance.tool_calling.get_transcript import GetTranscript
28
32
  from kfinance.tool_calling.resolve_identifier import ResolveIdentifier
29
33
  from kfinance.tool_calling.shared_models import KfinanceTool
30
34
 
@@ -36,6 +40,10 @@ ALL_TOOLS: list[Type[KfinanceTool]] = [
36
40
  GetCusipFromTicker,
37
41
  GetInfoFromIdentifier,
38
42
  GetEarningsCallDatetimesFromIdentifier,
43
+ GetEarnings,
44
+ GetLatestEarnings,
45
+ GetNextEarnings,
46
+ GetTranscript,
39
47
  GetHistoryMetadataFromIdentifier,
40
48
  GetPricesFromIdentifier,
41
49
  GetCapitalizationFromIdentifier,
@@ -0,0 +1,30 @@
1
+ from typing import Type
2
+
3
+ from pydantic import BaseModel
4
+
5
+ from kfinance.constants import Permission
6
+ from kfinance.kfinance import NoEarningsDataError
7
+ from kfinance.tool_calling.shared_models import KfinanceTool, ToolArgsWithIdentifier
8
+
9
+
10
+ class GetEarnings(KfinanceTool):
11
+ name: str = "get_earnings"
12
+ description: str = "Get all earnings for a given identifier. Returns a list of dictionaries, each with 'name' (str), 'key_dev_id' (int), and 'datetime' (str in ISO 8601 format with UTC timezone) attributes."
13
+ args_schema: Type[BaseModel] = ToolArgsWithIdentifier
14
+ required_permission: Permission | None = Permission.EarningsPermission
15
+
16
+ def _run(self, identifier: str) -> list[dict]:
17
+ ticker = self.kfinance_client.ticker(identifier)
18
+ earnings = ticker.company.earnings()
19
+
20
+ if not earnings:
21
+ raise NoEarningsDataError(f"Earnings for {identifier} not found")
22
+
23
+ return [
24
+ {
25
+ "name": earnings_item.name,
26
+ "key_dev_id": earnings_item.key_dev_id,
27
+ "datetime": earnings_item.datetime.isoformat(),
28
+ }
29
+ for earnings_item in earnings
30
+ ]
@@ -0,0 +1,27 @@
1
+ from typing import Type
2
+
3
+ from pydantic import BaseModel
4
+
5
+ from kfinance.constants import Permission
6
+ from kfinance.kfinance import NoEarningsDataError
7
+ from kfinance.tool_calling.shared_models import KfinanceTool, ToolArgsWithIdentifier
8
+
9
+
10
+ class GetLatestEarnings(KfinanceTool):
11
+ name: str = "get_latest_earnings"
12
+ description: str = "Get the latest earnings for a given identifier. Returns a dictionary with 'name' (str), 'key_dev_id' (int), and 'datetime' (str in ISO 8601 format with UTC timezone) attributes."
13
+ args_schema: Type[BaseModel] = ToolArgsWithIdentifier
14
+ required_permission: Permission | None = Permission.EarningsPermission
15
+
16
+ def _run(self, identifier: str) -> dict:
17
+ ticker = self.kfinance_client.ticker(identifier)
18
+ latest_earnings = ticker.company.latest_earnings
19
+
20
+ if latest_earnings is None:
21
+ raise NoEarningsDataError(f"Latest earnings for {identifier} not found")
22
+
23
+ return {
24
+ "name": latest_earnings.name,
25
+ "key_dev_id": latest_earnings.key_dev_id,
26
+ "datetime": latest_earnings.datetime.isoformat(),
27
+ }
@@ -0,0 +1,27 @@
1
+ from typing import Type
2
+
3
+ from pydantic import BaseModel
4
+
5
+ from kfinance.constants import Permission
6
+ from kfinance.kfinance import NoEarningsDataError
7
+ from kfinance.tool_calling.shared_models import KfinanceTool, ToolArgsWithIdentifier
8
+
9
+
10
+ class GetNextEarnings(KfinanceTool):
11
+ name: str = "get_next_earnings"
12
+ description: str = "Get the next earnings for a given identifier. Returns a dictionary with 'name' (str), 'key_dev_id' (int), and 'datetime' (str in ISO 8601 format with UTC timezone) attributes."
13
+ args_schema: Type[BaseModel] = ToolArgsWithIdentifier
14
+ required_permission: Permission | None = Permission.EarningsPermission
15
+
16
+ def _run(self, identifier: str) -> dict:
17
+ ticker = self.kfinance_client.ticker(identifier)
18
+ next_earnings = ticker.company.next_earnings
19
+
20
+ if next_earnings is None:
21
+ raise NoEarningsDataError(f"Next earnings for {identifier} not found")
22
+
23
+ return {
24
+ "name": next_earnings.name,
25
+ "key_dev_id": next_earnings.key_dev_id,
26
+ "datetime": next_earnings.datetime.isoformat(),
27
+ }
@@ -0,0 +1,23 @@
1
+ from typing import Type
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+ from kfinance.constants import Permission
6
+ from kfinance.tool_calling.shared_models import KfinanceTool
7
+
8
+
9
+ class GetTranscriptArgs(BaseModel):
10
+ """Tool argument with a key_dev_id."""
11
+
12
+ key_dev_id: int = Field(description="The key dev ID for the earnings call")
13
+
14
+
15
+ class GetTranscript(KfinanceTool):
16
+ name: str = "get_transcript"
17
+ description: str = "Get the raw transcript text for an earnings call by key dev ID."
18
+ args_schema: Type[BaseModel] = GetTranscriptArgs
19
+ required_permission: Permission | None = Permission.EarningsPermission
20
+
21
+ def _run(self, key_dev_id: int) -> str:
22
+ transcript = self.kfinance_client.transcript(key_dev_id)
23
+ return transcript.raw
@@ -0,0 +1,16 @@
1
+ from kfinance.tool_calling import GetFinancialLineItemFromIdentifier, GetLatest
2
+
3
+
4
+ BASE_PROMPT = f"""
5
+ You are an agent that calls one or more tools to retrieve data to answer questions from
6
+ financial analysts. Use the supplied tools to answer the user's questions.
7
+
8
+ - Always use the `{GetLatest.model_fields["name"].default}` function when asked about the last or most recent quarter or
9
+ when the time is unspecified in the question.
10
+ - Try to use `{GetFinancialLineItemFromIdentifier.model_fields["name"].default}` for questions about a company's
11
+ finances.
12
+ - If the tools do not respond with data that answers the question, then respond by saying that
13
+ you don't have the data available.
14
+ - Keep calling tools until you have the answer or the tool says the data is not available.
15
+ - Label large numbers with "million" or "billion" and currency symbols if appropriate.
16
+ """
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.2.5'
21
- __version_tuple__ = version_tuple = (2, 2, 5)
20
+ __version__ = version = '2.4.0'
21
+ __version_tuple__ = version_tuple = (2, 4, 0)