kensho-kfinance 2.8.0__py3-none-any.whl → 3.0.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.

Files changed (134) hide show
  1. {kensho_kfinance-2.8.0.dist-info → kensho_kfinance-3.0.0.dist-info}/METADATA +2 -2
  2. kensho_kfinance-3.0.0.dist-info/RECORD +110 -0
  3. kfinance/CHANGELOG.md +6 -0
  4. kfinance/__init__.py +1 -0
  5. kfinance/client/README.md +9 -0
  6. kfinance/{batch_request_handling.py → client/batch_request_handling.py} +63 -27
  7. kfinance/{fetch.py → client/fetch.py} +23 -29
  8. kfinance/{kfinance.py → client/kfinance.py} +105 -111
  9. kfinance/{meta_classes.py → client/meta_classes.py} +26 -35
  10. kfinance/{decimal_with_unit.py → client/models/decimal_with_unit.py} +1 -1
  11. kfinance/{tests → client/models/tests}/test_decimal_with_unit.py +1 -1
  12. kfinance/client/tests/__init__.py +0 -0
  13. kfinance/{tests → client/tests}/test_batch_requests.py +8 -6
  14. kfinance/{tests → client/tests}/test_client.py +25 -19
  15. kfinance/{tests → client/tests}/test_fetch.py +11 -29
  16. kfinance/{tests → client/tests}/test_group_objects.py +1 -1
  17. kfinance/{tests → client/tests}/test_objects.py +111 -63
  18. kfinance/{tests/conftest.py → conftest.py} +14 -2
  19. kfinance/domains/README.md +14 -0
  20. kfinance/domains/__init__.py +0 -0
  21. kfinance/domains/business_relationships/__init__.py +0 -0
  22. kfinance/{models → domains/business_relationships}/business_relationship_models.py +10 -0
  23. kfinance/domains/business_relationships/business_relationship_tools.py +74 -0
  24. kfinance/domains/business_relationships/tests/__init__.py +0 -0
  25. kfinance/domains/business_relationships/tests/test_business_relationship_tools.py +55 -0
  26. kfinance/domains/capitalizations/__init__.py +0 -0
  27. kfinance/{models → domains/capitalizations}/capitalization_models.py +24 -17
  28. kfinance/domains/capitalizations/capitalization_tools.py +89 -0
  29. kfinance/domains/capitalizations/tests/__init__.py +0 -0
  30. kfinance/{tests/test_models → domains/capitalizations/tests}/test_capitalization_models.py +8 -10
  31. kfinance/domains/capitalizations/tests/test_capitalization_tools.py +85 -0
  32. kfinance/domains/companies/__init__.py +0 -0
  33. kfinance/domains/companies/company_identifiers.py +175 -0
  34. kfinance/domains/companies/company_models.py +27 -0
  35. kfinance/domains/companies/company_tools.py +66 -0
  36. kfinance/domains/companies/tests/__init__.py +0 -0
  37. kfinance/domains/companies/tests/test_company_tools.py +26 -0
  38. kfinance/domains/competitors/__init__.py +0 -0
  39. kfinance/{models → domains/competitors}/competitor_models.py +7 -0
  40. kfinance/domains/competitors/competitor_tools.py +62 -0
  41. kfinance/domains/competitors/tests/__init__.py +0 -0
  42. kfinance/domains/competitors/tests/test_competitor_tools.py +45 -0
  43. kfinance/domains/cusip_and_isin/__init__.py +0 -0
  44. kfinance/domains/cusip_and_isin/cusip_and_isin_tools.py +80 -0
  45. kfinance/domains/cusip_and_isin/tests/__init__.py +0 -0
  46. kfinance/domains/cusip_and_isin/tests/test_cusip_and_isin_tools.py +57 -0
  47. kfinance/domains/earnings/__init__.py +0 -0
  48. kfinance/domains/earnings/earning_models.py +41 -0
  49. kfinance/domains/earnings/earning_tools.py +174 -0
  50. kfinance/domains/earnings/tests/__init__.py +0 -0
  51. kfinance/domains/earnings/tests/test_earnings_tools.py +195 -0
  52. kfinance/domains/line_items/__init__.py +0 -0
  53. kfinance/domains/line_items/line_item_tools.py +114 -0
  54. kfinance/domains/line_items/tests/__init__.py +0 -0
  55. kfinance/domains/line_items/tests/test_line_item_tools.py +86 -0
  56. kfinance/domains/mergers_and_acquisitions/__init__.py +0 -0
  57. kfinance/domains/mergers_and_acquisitions/merger_and_acquisition_tools.py +176 -0
  58. kfinance/domains/mergers_and_acquisitions/tests/__init__.py +0 -0
  59. kfinance/domains/mergers_and_acquisitions/tests/test_merger_and_acquisition_tools.py +124 -0
  60. kfinance/domains/prices/__init__.py +0 -0
  61. kfinance/{models → domains/prices}/price_models.py +1 -1
  62. kfinance/domains/prices/price_tools.py +165 -0
  63. kfinance/domains/prices/tests/__init__.py +0 -0
  64. kfinance/{tests/test_models → domains/prices/tests}/test_price_models.py +2 -2
  65. kfinance/domains/prices/tests/test_price_tools.py +141 -0
  66. kfinance/domains/segments/__init__.py +0 -0
  67. kfinance/domains/segments/segment_tools.py +91 -0
  68. kfinance/domains/segments/tests/__init__.py +0 -0
  69. kfinance/domains/segments/tests/test_segment_tools.py +80 -0
  70. kfinance/domains/statements/__init__.py +0 -0
  71. kfinance/domains/statements/statement_tools.py +113 -0
  72. kfinance/domains/statements/tests/__init__.py +0 -0
  73. kfinance/domains/statements/tests/test_statement_tools.py +73 -0
  74. kfinance/integrations/README.md +8 -0
  75. kfinance/integrations/__init__.py +0 -0
  76. kfinance/integrations/mcp/__init__.py +0 -0
  77. kfinance/{mcp.py → integrations/mcp/mcp.py} +2 -2
  78. kfinance/integrations/tests/__init__.py +0 -0
  79. kfinance/{tests → integrations/tests}/test_example_notebook.py +4 -4
  80. kfinance/{tool_calling → integrations/tool_calling}/README.md +2 -2
  81. kfinance/integrations/tool_calling/__init__.py +0 -0
  82. kfinance/integrations/tool_calling/all_tools.py +55 -0
  83. kfinance/{tool_calling → integrations/tool_calling}/prompts.py +3 -2
  84. kfinance/integrations/tool_calling/static_tools/README.md +4 -0
  85. kfinance/integrations/tool_calling/static_tools/__init__.py +0 -0
  86. kfinance/{tool_calling → integrations/tool_calling/static_tools}/get_latest.py +3 -3
  87. kfinance/{tool_calling → integrations/tool_calling/static_tools}/get_n_quarters_ago.py +3 -3
  88. kfinance/integrations/tool_calling/static_tools/tests/__init__.py +0 -0
  89. kfinance/integrations/tool_calling/static_tools/tests/test_get_lastest.py +30 -0
  90. kfinance/integrations/tool_calling/static_tools/tests/test_get_n_quarters_ago.py +24 -0
  91. kfinance/integrations/tool_calling/tests/__init__.py +0 -0
  92. kfinance/integrations/tool_calling/tests/test_tool_calling_models.py +69 -0
  93. kfinance/{tool_calling/shared_models.py → integrations/tool_calling/tool_calling_models.py} +37 -7
  94. kfinance/version.py +2 -2
  95. kensho_kfinance-2.8.0.dist-info/RECORD +0 -70
  96. kfinance/models/id_models.py +0 -7
  97. kfinance/prompt.py +0 -526
  98. kfinance/pydantic_models.py +0 -33
  99. kfinance/tests/test_tools.py +0 -804
  100. kfinance/tool_calling/__init__.py +0 -53
  101. kfinance/tool_calling/get_advisors_for_company_in_transaction_from_identifier.py +0 -39
  102. kfinance/tool_calling/get_business_relationship_from_identifier.py +0 -30
  103. kfinance/tool_calling/get_capitalization_from_identifier.py +0 -35
  104. kfinance/tool_calling/get_competitors_from_identifier.py +0 -25
  105. kfinance/tool_calling/get_cusip_from_ticker.py +0 -20
  106. kfinance/tool_calling/get_earnings.py +0 -33
  107. kfinance/tool_calling/get_financial_line_item_from_identifier.py +0 -48
  108. kfinance/tool_calling/get_financial_statement_from_identifier.py +0 -44
  109. kfinance/tool_calling/get_history_metadata_from_identifier.py +0 -17
  110. kfinance/tool_calling/get_info_from_identifier.py +0 -16
  111. kfinance/tool_calling/get_isin_from_ticker.py +0 -20
  112. kfinance/tool_calling/get_latest_earnings.py +0 -30
  113. kfinance/tool_calling/get_merger_info_from_transaction_id.py +0 -68
  114. kfinance/tool_calling/get_mergers_from_identifier.py +0 -41
  115. kfinance/tool_calling/get_next_earnings.py +0 -30
  116. kfinance/tool_calling/get_prices_from_identifier.py +0 -46
  117. kfinance/tool_calling/get_segments_from_identifier.py +0 -44
  118. kfinance/tool_calling/get_transcript.py +0 -23
  119. kfinance/tool_calling/resolve_identifier.py +0 -18
  120. {kensho_kfinance-2.8.0.dist-info → kensho_kfinance-3.0.0.dist-info}/WHEEL +0 -0
  121. {kensho_kfinance-2.8.0.dist-info → kensho_kfinance-3.0.0.dist-info}/licenses/AUTHORS.md +0 -0
  122. {kensho_kfinance-2.8.0.dist-info → kensho_kfinance-3.0.0.dist-info}/licenses/LICENSE +0 -0
  123. {kensho_kfinance-2.8.0.dist-info → kensho_kfinance-3.0.0.dist-info}/top_level.txt +0 -0
  124. /kfinance/{models → client}/__init__.py +0 -0
  125. /kfinance/{models → client}/industry_models.py +0 -0
  126. /kfinance/{tests → client/models}/__init__.py +0 -0
  127. /kfinance/{models → client/models}/currency_models.py +0 -0
  128. /kfinance/{models → client/models}/date_and_period_models.py +0 -0
  129. /kfinance/{tests/test_models → client/models/tests}/__init__.py +0 -0
  130. /kfinance/{models → client}/permission_models.py +0 -0
  131. /kfinance/{server_thread.py → client/server_thread.py} +0 -0
  132. /kfinance/{models → domains/line_items}/line_item_models.py +0 -0
  133. /kfinance/{models → domains/segments}/segment_models.py +0 -0
  134. /kfinance/{models → domains/statements}/statement_models.py +0 -0
@@ -0,0 +1,80 @@
1
+ from typing import Type
2
+
3
+ from pydantic import BaseModel
4
+
5
+ from kfinance.client.batch_request_handling import Task, process_tasks_in_thread_pool_executor
6
+ from kfinance.client.permission_models import Permission
7
+ from kfinance.domains.companies.company_identifiers import (
8
+ fetch_security_ids_from_identifiers,
9
+ parse_identifiers,
10
+ )
11
+ from kfinance.integrations.tool_calling.tool_calling_models import (
12
+ KfinanceTool,
13
+ ToolArgsWithIdentifiers,
14
+ )
15
+
16
+
17
+ class GetCusipFromIdentifiers(KfinanceTool):
18
+ name: str = "get_cusip_from_identifiers"
19
+ description: str = "Get the CUSIPs for a group of identifiers."
20
+ args_schema: Type[BaseModel] = ToolArgsWithIdentifiers
21
+ accepted_permissions: set[Permission] | None = {Permission.IDPermission}
22
+
23
+ def _run(self, identifiers: list[str]) -> dict[str, str]:
24
+ """Sample response:
25
+
26
+ {"SPGI": "78409V104"}
27
+ """
28
+ api_client = self.kfinance_client.kfinance_api_client
29
+ parsed_identifiers = parse_identifiers(identifiers=identifiers, api_client=api_client)
30
+ identifiers_to_security_ids = fetch_security_ids_from_identifiers(
31
+ identifiers=parsed_identifiers, api_client=api_client
32
+ )
33
+
34
+ tasks = [
35
+ Task(
36
+ func=api_client.fetch_cusip,
37
+ kwargs=dict(security_id=security_id),
38
+ result_key=identifier,
39
+ )
40
+ for identifier, security_id in identifiers_to_security_ids.items()
41
+ ]
42
+
43
+ cusip_responses = process_tasks_in_thread_pool_executor(api_client=api_client, tasks=tasks)
44
+
45
+ return {str(identifier): resp["cusip"] for identifier, resp in cusip_responses.items()}
46
+
47
+
48
+ class GetIsinFromIdentifiersArgs(BaseModel):
49
+ security_ids: list[int]
50
+
51
+
52
+ class GetIsinFromIdentifiers(KfinanceTool):
53
+ name: str = "get_isin_from_identifiers"
54
+ description: str = "Get the ISINs for a group of identifiers."
55
+ args_schema: Type[BaseModel] = ToolArgsWithIdentifiers
56
+ accepted_permissions: set[Permission] | None = {Permission.IDPermission}
57
+
58
+ def _run(self, identifiers: list[str]) -> dict:
59
+ """Sample response:
60
+
61
+ {"SPGI": "US78409V1044"}
62
+ """
63
+ api_client = self.kfinance_client.kfinance_api_client
64
+ parsed_identifiers = parse_identifiers(identifiers=identifiers, api_client=api_client)
65
+ identifiers_to_security_ids = fetch_security_ids_from_identifiers(
66
+ identifiers=parsed_identifiers, api_client=api_client
67
+ )
68
+
69
+ tasks = [
70
+ Task(
71
+ func=api_client.fetch_isin,
72
+ kwargs=dict(security_id=security_id),
73
+ result_key=identifier,
74
+ )
75
+ for identifier, security_id in identifiers_to_security_ids.items()
76
+ ]
77
+
78
+ isin_responses = process_tasks_in_thread_pool_executor(api_client=api_client, tasks=tasks)
79
+
80
+ return {str(identifier): resp["isin"] for identifier, resp in isin_responses.items()}
File without changes
@@ -0,0 +1,57 @@
1
+ from requests_mock import Mocker
2
+
3
+ from kfinance.client.kfinance import Client
4
+ from kfinance.domains.companies.company_models import COMPANY_ID_PREFIX
5
+ from kfinance.domains.cusip_and_isin.cusip_and_isin_tools import (
6
+ GetCusipFromIdentifiers,
7
+ GetIsinFromIdentifiers,
8
+ )
9
+ from kfinance.integrations.tool_calling.tool_calling_models import ToolArgsWithIdentifiers
10
+
11
+
12
+ class TestGetCusipFromIdentifiers:
13
+ def test_get_cusip_from_identifiers(self, requests_mock: Mocker, mock_client: Client):
14
+ """
15
+ GIVEN the GetCusipFromIdentifiers tool
16
+ WHEN we request the CUSIPs for multiple companies
17
+ THEN we get back the corresponding CUSIPs
18
+ """
19
+
20
+ company_ids = [1, 2]
21
+ expected_response = {"C_1": "CU1", "C_2": "CU2"}
22
+ for security_id in company_ids:
23
+ requests_mock.get(
24
+ url=f"https://kfinance.kensho.com/api/v1/cusip/{security_id}",
25
+ json={"cusip": f"CU{security_id}"},
26
+ )
27
+ tool = GetCusipFromIdentifiers(kfinance_client=mock_client)
28
+ resp = tool.run(
29
+ ToolArgsWithIdentifiers(
30
+ identifiers=[f"{COMPANY_ID_PREFIX}{company_id}" for company_id in company_ids]
31
+ ).model_dump(mode="json")
32
+ )
33
+ assert resp == expected_response
34
+
35
+
36
+ class TestGetIsinFromSecurityIds:
37
+ def test_get_isin_from_security_ids(self, requests_mock: Mocker, mock_client: Client):
38
+ """
39
+ GIVEN the GetIsinFromSecurityIds tool
40
+ WHEN we request the ISINs for multiple security ids
41
+ THEN we get back the corresponding ISINs
42
+ """
43
+
44
+ company_ids = [1, 2]
45
+ expected_response = {"C_1": "IS1", "C_2": "IS2"}
46
+ for security_id in company_ids:
47
+ requests_mock.get(
48
+ url=f"https://kfinance.kensho.com/api/v1/isin/{security_id}",
49
+ json={"isin": f"IS{security_id}"},
50
+ )
51
+ tool = GetIsinFromIdentifiers(kfinance_client=mock_client)
52
+ resp = tool.run(
53
+ ToolArgsWithIdentifiers(
54
+ identifiers=[f"{COMPANY_ID_PREFIX}{company_id}" for company_id in company_ids]
55
+ ).model_dump(mode="json")
56
+ )
57
+ assert resp == expected_response
File without changes
@@ -0,0 +1,41 @@
1
+ from datetime import datetime, timezone
2
+
3
+ from pydantic import AliasPath, BaseModel, Field
4
+
5
+
6
+ class EarningsCall(BaseModel):
7
+ name: str
8
+ key_dev_id: int
9
+ datetime: datetime
10
+
11
+
12
+ class EarningsCallResp(BaseModel):
13
+ earnings_calls: list[EarningsCall] = Field(validation_alias=AliasPath("earnings"))
14
+
15
+ @property
16
+ def most_recent_earnings(self) -> EarningsCall | None:
17
+ """Returns the most recent earnings call if available."""
18
+
19
+ past_earnings = [e for e in self.earnings_calls if e.datetime < datetime.now(timezone.utc)]
20
+ if past_earnings:
21
+ return max(past_earnings, key=lambda x: x.datetime)
22
+ return None
23
+
24
+ @property
25
+ def next_earnings(self) -> EarningsCall | None:
26
+ """Returns the next earnings call if available."""
27
+
28
+ future_earnings = [
29
+ e for e in self.earnings_calls if e.datetime > datetime.now(timezone.utc)
30
+ ]
31
+ if future_earnings:
32
+ return min(future_earnings, key=lambda x: x.datetime)
33
+ return None
34
+
35
+
36
+ class TranscriptComponent(BaseModel):
37
+ """A transcript component with person name, text, and component type."""
38
+
39
+ person_name: str
40
+ text: str
41
+ component_type: str
@@ -0,0 +1,174 @@
1
+ from textwrap import dedent
2
+ from typing import Type
3
+
4
+ from pydantic import BaseModel, Field
5
+
6
+ from kfinance.client.batch_request_handling import Task, process_tasks_in_thread_pool_executor
7
+ from kfinance.client.fetch import KFinanceApiClient
8
+ from kfinance.client.permission_models import Permission
9
+ from kfinance.domains.companies.company_identifiers import (
10
+ CompanyIdentifier,
11
+ fetch_company_ids_from_identifiers,
12
+ parse_identifiers,
13
+ )
14
+ from kfinance.domains.earnings.earning_models import EarningsCallResp
15
+ from kfinance.integrations.tool_calling.tool_calling_models import (
16
+ KfinanceTool,
17
+ ToolArgsWithIdentifiers,
18
+ )
19
+
20
+
21
+ class GetEarningsFromIdentifiers(KfinanceTool):
22
+ name: str = "get_earnings_from_identifiers"
23
+ description: str = dedent("""
24
+ Get all earnings for a list of identifiers.
25
+
26
+ Returns a list of dictionaries, with 'name' (str), 'key_dev_id' (int), and 'datetime' (str in ISO 8601 format with UTC timezone) attributes for each identifier.
27
+ """).strip()
28
+ args_schema: Type[BaseModel] = ToolArgsWithIdentifiers
29
+ accepted_permissions: set[Permission] | None = {
30
+ Permission.EarningsPermission,
31
+ Permission.TranscriptsPermission,
32
+ }
33
+
34
+ def _run(self, identifiers: list[str]) -> dict:
35
+ """Sample response:
36
+
37
+ {
38
+ 'SPGI': [
39
+ {
40
+ 'datetime': '2025-04-29T12:30:00Z',
41
+ 'key_dev_id': 12346,
42
+ 'name': 'SPGI Q1 2025 Earnings Call'
43
+ }
44
+ ]
45
+ }
46
+
47
+ """
48
+ earnings_responses = get_earnings_from_identifiers(
49
+ identifiers=identifiers, kfinance_api_client=self.kfinance_client.kfinance_api_client
50
+ )
51
+ return {
52
+ str(identifier): earnings.model_dump(mode="json")["earnings_calls"]
53
+ for identifier, earnings in earnings_responses.items()
54
+ }
55
+
56
+
57
+ class GetLatestEarningsFromIdentifiers(KfinanceTool):
58
+ name: str = "get_latest_earnings_from_identifiers"
59
+ description: str = dedent("""
60
+ Get the latest earnings for a list of identifiers.
61
+
62
+ Returns a dictionary with 'name' (str), 'key_dev_id' (int), and 'datetime' (str in ISO 8601 format with UTC timezone) attributes for each identifier.
63
+ """).strip()
64
+ args_schema: Type[BaseModel] = ToolArgsWithIdentifiers
65
+ accepted_permissions: set[Permission] | None = {
66
+ Permission.EarningsPermission,
67
+ Permission.TranscriptsPermission,
68
+ }
69
+
70
+ def _run(self, identifiers: list[str]) -> dict:
71
+ """Sample response:
72
+
73
+ {
74
+ 'JPM': {
75
+ 'datetime': '2025-04-29T12:30:00Z',
76
+ 'key_dev_id': 12346,
77
+ 'name': 'SPGI Q1 2025 Earnings Call'
78
+ },
79
+ 'SPGI': 'No latest earnings available.'
80
+ }
81
+ """
82
+ earnings_responses = get_earnings_from_identifiers(
83
+ identifiers=identifiers, kfinance_api_client=self.kfinance_client.kfinance_api_client
84
+ )
85
+ output = {}
86
+ for identifier, earnings in earnings_responses.items():
87
+ most_recent_earnings = earnings.most_recent_earnings
88
+ if most_recent_earnings:
89
+ identifier_output: str | dict = most_recent_earnings.model_dump(mode="json")
90
+ else:
91
+ identifier_output = f"No latest earnings available."
92
+ output[str(identifier)] = identifier_output
93
+ return output
94
+
95
+
96
+ class GetNextEarningsFromIdentifiers(KfinanceTool):
97
+ name: str = "get_next_earnings_from_identifiers"
98
+ description: str = dedent("""
99
+ Get the next earnings for a given identifier.
100
+
101
+ Returns a dictionary with 'name' (str), 'key_dev_id' (int), and 'datetime' (str in ISO 8601 format with UTC timezone) attributes for each identifier."
102
+ """).strip()
103
+ args_schema: Type[BaseModel] = ToolArgsWithIdentifiers
104
+ accepted_permissions: set[Permission] | None = {
105
+ Permission.EarningsPermission,
106
+ Permission.TranscriptsPermission,
107
+ }
108
+
109
+ def _run(self, identifiers: list[str]) -> dict:
110
+ """Sample response:
111
+
112
+ {
113
+ 'JPM': {
114
+ 'datetime': '2025-04-29T12:30:00Z',
115
+ 'key_dev_id': 12346,
116
+ 'name': 'SPGI Q1 2025 Earnings Call'
117
+ },
118
+ 'SPGI': 'No next earnings available.'
119
+ }
120
+ """
121
+ earnings_responses = get_earnings_from_identifiers(
122
+ identifiers=identifiers, kfinance_api_client=self.kfinance_client.kfinance_api_client
123
+ )
124
+ output = {}
125
+ for identifier, earnings in earnings_responses.items():
126
+ next_earnings = earnings.next_earnings
127
+ if next_earnings:
128
+ identifier_output: str | dict = next_earnings.model_dump(mode="json")
129
+ else:
130
+ identifier_output = f"No next earnings available."
131
+ output[str(identifier)] = identifier_output
132
+ return output
133
+
134
+
135
+ def get_earnings_from_identifiers(
136
+ identifiers: list[str], kfinance_api_client: KFinanceApiClient
137
+ ) -> dict[CompanyIdentifier, EarningsCallResp]:
138
+ """Return the earnings call response for all passed identifiers."""
139
+
140
+ parsed_identifiers = parse_identifiers(identifiers=identifiers, api_client=kfinance_api_client)
141
+ identifiers_to_company_ids = fetch_company_ids_from_identifiers(
142
+ identifiers=parsed_identifiers, api_client=kfinance_api_client
143
+ )
144
+
145
+ tasks = [
146
+ Task(
147
+ func=kfinance_api_client.fetch_earnings,
148
+ kwargs=dict(company_id=company_id),
149
+ result_key=identifier,
150
+ )
151
+ for identifier, company_id in identifiers_to_company_ids.items()
152
+ ]
153
+
154
+ earnings_responses = process_tasks_in_thread_pool_executor(
155
+ api_client=kfinance_api_client, tasks=tasks
156
+ )
157
+ return earnings_responses
158
+
159
+
160
+ class GetTranscriptFromKeyDevIdArgs(BaseModel):
161
+ """Tool argument with a key_dev_id."""
162
+
163
+ key_dev_id: int = Field(description="The key dev ID for the earnings call")
164
+
165
+
166
+ class GetTranscriptFromKeyDevId(KfinanceTool):
167
+ name: str = "get_transcript_from_key_dev_id"
168
+ description: str = "Get the raw transcript text for an earnings call by key dev ID."
169
+ args_schema: Type[BaseModel] = GetTranscriptFromKeyDevIdArgs
170
+ accepted_permissions: set[Permission] | None = {Permission.TranscriptsPermission}
171
+
172
+ def _run(self, key_dev_id: int) -> str:
173
+ transcript = self.kfinance_client.transcript(key_dev_id)
174
+ return transcript.raw
File without changes
@@ -0,0 +1,195 @@
1
+ from datetime import datetime
2
+
3
+ from requests_mock import Mocker
4
+ import time_machine
5
+
6
+ from kfinance.client.kfinance import Client
7
+ from kfinance.conftest import SPGI_COMPANY_ID
8
+ from kfinance.domains.earnings.earning_tools import (
9
+ GetEarningsFromIdentifiers,
10
+ GetLatestEarningsFromIdentifiers,
11
+ GetNextEarningsFromIdentifiers,
12
+ GetTranscriptFromKeyDevId,
13
+ GetTranscriptFromKeyDevIdArgs,
14
+ )
15
+ from kfinance.integrations.tool_calling.tool_calling_models import ToolArgsWithIdentifiers
16
+
17
+
18
+ class TestGetEarnings:
19
+ earnings_response = {
20
+ "earnings": [
21
+ {
22
+ "name": "SPGI Q1 2025 Earnings Call",
23
+ "datetime": "2025-04-29T12:30:00Z",
24
+ "key_dev_id": 12346,
25
+ },
26
+ {
27
+ "name": "SPGI Q4 2024 Earnings Call",
28
+ "datetime": "2025-02-11T13:30:00Z",
29
+ "key_dev_id": 12345,
30
+ },
31
+ ]
32
+ }
33
+
34
+ def test_get_earnings_from_identifiers(self, requests_mock: Mocker, mock_client: Client):
35
+ """
36
+ GIVEN the GetEarnings tool
37
+ WHEN we request all earnings for SPGI
38
+ THEN we get back all SPGI earnings
39
+ """
40
+
41
+ requests_mock.get(
42
+ url=f"https://kfinance.kensho.com/api/v1/earnings/{SPGI_COMPANY_ID}",
43
+ json=self.earnings_response,
44
+ )
45
+
46
+ expected_response = {
47
+ "SPGI": [
48
+ {
49
+ "datetime": "2025-04-29T12:30:00Z",
50
+ "key_dev_id": 12346,
51
+ "name": "SPGI Q1 2025 Earnings Call",
52
+ },
53
+ {
54
+ "datetime": "2025-02-11T13:30:00Z",
55
+ "key_dev_id": 12345,
56
+ "name": "SPGI Q4 2024 Earnings Call",
57
+ },
58
+ ]
59
+ }
60
+
61
+ tool = GetEarningsFromIdentifiers(kfinance_client=mock_client)
62
+ response = tool.run(ToolArgsWithIdentifiers(identifiers=["SPGI"]).model_dump(mode="json"))
63
+ assert response == expected_response
64
+
65
+ @time_machine.travel(
66
+ datetime(
67
+ 2025,
68
+ 5,
69
+ 1,
70
+ )
71
+ )
72
+ def test_get_latest_earnings_from_identifiers(self, requests_mock: Mocker, mock_client: Client):
73
+ """
74
+ GIVEN the GetLatestEarnings tool
75
+ WHEN we request the latest earnings for SPGI
76
+ THEN we get back the latest SPGI earnings
77
+ """
78
+
79
+ requests_mock.get(
80
+ url=f"https://kfinance.kensho.com/api/v1/earnings/{SPGI_COMPANY_ID}",
81
+ json=self.earnings_response,
82
+ )
83
+
84
+ expected_response = {
85
+ "SPGI": {
86
+ "datetime": "2025-04-29T12:30:00Z",
87
+ "key_dev_id": 12346,
88
+ "name": "SPGI Q1 2025 Earnings Call",
89
+ }
90
+ }
91
+
92
+ tool = GetLatestEarningsFromIdentifiers(kfinance_client=mock_client)
93
+ response = tool.run(ToolArgsWithIdentifiers(identifiers=["SPGI"]).model_dump(mode="json"))
94
+ assert response == expected_response
95
+
96
+ def test_get_latest_earnings_no_data(self, requests_mock: Mocker, mock_client: Client):
97
+ """
98
+ GIVEN the GetLatestEarnings tool
99
+ WHEN we request the latest earnings for a company with no data
100
+ THEN we get a `No latest earnings available` message.
101
+ """
102
+ requests_mock.get(
103
+ url=f"https://kfinance.kensho.com/api/v1/earnings/{SPGI_COMPANY_ID}",
104
+ json={"earnings": []},
105
+ )
106
+ expected_response = {"SPGI": "No latest earnings available."}
107
+
108
+ tool = GetLatestEarningsFromIdentifiers(kfinance_client=mock_client)
109
+ response = tool.run(ToolArgsWithIdentifiers(identifiers=["SPGI"]).model_dump(mode="json"))
110
+ assert response == expected_response
111
+
112
+ @time_machine.travel(
113
+ datetime(
114
+ 2025,
115
+ 4,
116
+ 1,
117
+ )
118
+ )
119
+ def test_get_next_earnings(self, requests_mock: Mocker, mock_client: Client):
120
+ """
121
+ GIVEN the GetNextEarningsFromIdentifiers tool
122
+ WHEN we request the next earnings for SPGI
123
+ THEN we get back the next SPGI earnings
124
+ """
125
+
126
+ requests_mock.get(
127
+ url=f"https://kfinance.kensho.com/api/v1/earnings/{SPGI_COMPANY_ID}",
128
+ json=self.earnings_response,
129
+ )
130
+
131
+ expected_response = {
132
+ "SPGI": {
133
+ "datetime": "2025-04-29T12:30:00Z",
134
+ "key_dev_id": 12346,
135
+ "name": "SPGI Q1 2025 Earnings Call",
136
+ }
137
+ }
138
+
139
+ tool = GetNextEarningsFromIdentifiers(kfinance_client=mock_client)
140
+ response = tool.run(ToolArgsWithIdentifiers(identifiers=["SPGI"]).model_dump(mode="json"))
141
+ assert response == expected_response
142
+
143
+ def test_get_next_earnings_no_data(self, requests_mock: Mocker, mock_client: Client):
144
+ """
145
+ GIVEN the GetNextEarnings tool
146
+ WHEN we request the next earnings for a company with no data
147
+ THEN we get a `No next earnings available` message.
148
+ """
149
+ earnings_data = {"earnings": []}
150
+
151
+ requests_mock.get(
152
+ url=f"https://kfinance.kensho.com/api/v1/earnings/{SPGI_COMPANY_ID}",
153
+ json=earnings_data,
154
+ )
155
+ expected_response = {"SPGI": "No next earnings available."}
156
+
157
+ tool = GetNextEarningsFromIdentifiers(kfinance_client=mock_client)
158
+ response = tool.run(ToolArgsWithIdentifiers(identifiers=["SPGI"]).model_dump(mode="json"))
159
+ assert response == expected_response
160
+
161
+
162
+ class TestGetTranscript:
163
+ def test_get_transcript(self, requests_mock: Mocker, mock_client: Client):
164
+ """
165
+ GIVEN the GetTranscript tool
166
+ WHEN we request a transcript by key_dev_id
167
+ THEN we get back the transcript text
168
+ """
169
+ transcript_data = {
170
+ "transcript": [
171
+ {
172
+ "person_name": "Operator",
173
+ "text": "Good morning, everyone.",
174
+ "component_type": "speech",
175
+ },
176
+ {
177
+ "person_name": "CEO",
178
+ "text": "Thank you for joining us today.",
179
+ "component_type": "speech",
180
+ },
181
+ ]
182
+ }
183
+
184
+ requests_mock.get(
185
+ url="https://kfinance.kensho.com/api/v1/transcript/12345",
186
+ json=transcript_data,
187
+ )
188
+
189
+ expected_response = (
190
+ "Operator: Good morning, everyone.\n\nCEO: Thank you for joining us today."
191
+ )
192
+
193
+ tool = GetTranscriptFromKeyDevId(kfinance_client=mock_client)
194
+ response = tool.run(GetTranscriptFromKeyDevIdArgs(key_dev_id=12345).model_dump(mode="json"))
195
+ assert response == expected_response
File without changes
@@ -0,0 +1,114 @@
1
+ from textwrap import dedent
2
+ from typing import Literal, Type
3
+
4
+ import numpy as np
5
+ import pandas as pd
6
+ from pydantic import BaseModel, Field
7
+
8
+ from kfinance.client.batch_request_handling import Task, process_tasks_in_thread_pool_executor
9
+ from kfinance.client.models.date_and_period_models import PeriodType
10
+ from kfinance.client.permission_models import Permission
11
+ from kfinance.domains.companies.company_identifiers import (
12
+ fetch_company_ids_from_identifiers,
13
+ parse_identifiers,
14
+ )
15
+ from kfinance.domains.line_items.line_item_models import LINE_ITEM_NAMES_AND_ALIASES
16
+ from kfinance.integrations.tool_calling.tool_calling_models import (
17
+ KfinanceTool,
18
+ ToolArgsWithIdentifiers,
19
+ )
20
+
21
+
22
+ class GetFinancialLineItemFromIdentifiersArgs(ToolArgsWithIdentifiers):
23
+ # Note: mypy will not enforce this literal because of the type: ignore.
24
+ # But pydantic still uses the literal to check for allowed values and only includes
25
+ # allowed values in generated schemas.
26
+ line_item: Literal[tuple(LINE_ITEM_NAMES_AND_ALIASES)] = Field( # type: ignore[valid-type]
27
+ description="The type of financial line_item requested"
28
+ )
29
+ period_type: PeriodType | None = Field(default=None, description="The period type")
30
+ start_year: int | None = Field(default=None, description="The starting year for the data range")
31
+ end_year: int | None = Field(default=None, description="The ending year for the data range")
32
+ start_quarter: Literal[1, 2, 3, 4] | None = Field(default=None, description="Starting quarter")
33
+ end_quarter: Literal[1, 2, 3, 4] | None = Field(default=None, description="Ending quarter")
34
+
35
+
36
+ class GetFinancialLineItemFromIdentifiers(KfinanceTool):
37
+ name: str = "get_financial_line_item_from_identifiers"
38
+ description: str = dedent("""
39
+ Get the financial line item associated with a list of identifiers.
40
+
41
+ - When possible, pass multiple identifiers in a single call rather than making multiple calls.
42
+ - To fetch the most recent value for the line item, leave start_year, start_quarter, end_year, and end_quarter as None.
43
+
44
+ Example:
45
+ Query: "What are the revenues of Lowe's and Home Depot?"
46
+ Function: get_financial_line_item_from_identifiers(line_item="revenue", company_ids=["LW", "HD"])
47
+ """).strip()
48
+ args_schema: Type[BaseModel] = GetFinancialLineItemFromIdentifiersArgs
49
+ accepted_permissions: set[Permission] | None = {Permission.StatementsPermission}
50
+
51
+ def _run(
52
+ self,
53
+ identifiers: list[str],
54
+ line_item: str,
55
+ period_type: PeriodType | None = None,
56
+ start_year: int | None = None,
57
+ end_year: int | None = None,
58
+ start_quarter: Literal[1, 2, 3, 4] | None = None,
59
+ end_quarter: Literal[1, 2, 3, 4] | None = None,
60
+ ) -> dict:
61
+ """Sample response:
62
+
63
+ {
64
+ 'SPGI': {
65
+ '2022': {'revenue': 11181000000.0},
66
+ '2023': {'revenue': 12497000000.0},
67
+ '2024': {'revenue': 14208000000.0}
68
+ }
69
+ }
70
+ """
71
+ api_client = self.kfinance_client.kfinance_api_client
72
+ parsed_identifiers = parse_identifiers(identifiers=identifiers, api_client=api_client)
73
+ identifiers_to_company_ids = fetch_company_ids_from_identifiers(
74
+ identifiers=parsed_identifiers, api_client=api_client
75
+ )
76
+
77
+ tasks = [
78
+ Task(
79
+ func=api_client.fetch_line_item,
80
+ kwargs=dict(
81
+ company_id=company_id,
82
+ line_item=line_item,
83
+ period_type=period_type,
84
+ start_year=start_year,
85
+ end_year=end_year,
86
+ start_quarter=start_quarter,
87
+ end_quarter=end_quarter,
88
+ ),
89
+ result_key=identifier,
90
+ )
91
+ for identifier, company_id in identifiers_to_company_ids.items()
92
+ ]
93
+
94
+ line_item_responses = process_tasks_in_thread_pool_executor(
95
+ api_client=api_client, tasks=tasks
96
+ )
97
+
98
+ output = dict()
99
+ for identifier, result in line_item_responses.items():
100
+ df = pd.DataFrame(result).apply(pd.to_numeric).replace(np.nan, None)
101
+ # If no date and multiple companies, only return the most recent value.
102
+ # By default, we return 5 years of data, which can be too much when
103
+ # returning data for many companies.
104
+ if (
105
+ start_year is None
106
+ and end_year is None
107
+ and start_quarter is None
108
+ and end_quarter is None
109
+ and len(identifiers) > 1
110
+ ):
111
+ df = df.tail(1)
112
+ output[str(identifier)] = df.transpose().set_index(pd.Index([line_item])).to_dict()
113
+
114
+ return output
File without changes