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,141 @@
1
+ from requests_mock import Mocker
2
+
3
+ from kfinance.client.kfinance import Client
4
+ from kfinance.conftest import SPGI_TRADING_ITEM_ID
5
+ from kfinance.domains.companies.company_models import COMPANY_ID_PREFIX
6
+ from kfinance.domains.prices.price_tools import (
7
+ GetHistoryMetadataFromIdentifiers,
8
+ GetPricesFromIdentifiers,
9
+ GetPricesFromIdentifiersArgs,
10
+ )
11
+ from kfinance.integrations.tool_calling.tool_calling_models import ToolArgsWithIdentifiers
12
+
13
+
14
+ class TestGetHistoryMetadataFromIdentifiers:
15
+ def test_get_history_metadata_from_identifiers(
16
+ self, mock_client: Client, requests_mock: Mocker
17
+ ):
18
+ """
19
+ GIVEN the GetHistoryMetadataFromIdentifiers tool
20
+ WHEN we request the history metadata for SPGI
21
+ THEN we get back SPGI's history metadata
22
+ """
23
+ metadata_resp = {
24
+ "currency": "USD",
25
+ "exchange_name": "NYSE",
26
+ "first_trade_date": "1968-01-02",
27
+ "instrument_type": "Equity",
28
+ "symbol": "SPGI",
29
+ }
30
+ expected_resp = {"SPGI": metadata_resp}
31
+
32
+ requests_mock.get(
33
+ url=f"https://kfinance.kensho.com/api/v1/pricing/{SPGI_TRADING_ITEM_ID}/metadata",
34
+ json=metadata_resp,
35
+ )
36
+
37
+ tool = GetHistoryMetadataFromIdentifiers(kfinance_client=mock_client)
38
+ resp = tool.run(ToolArgsWithIdentifiers(identifiers=["SPGI"]).model_dump(mode="json"))
39
+ assert resp == expected_resp
40
+
41
+
42
+ class TestPricesFromIdentifiers:
43
+ prices_resp = {
44
+ "currency": "USD",
45
+ "prices": [
46
+ {
47
+ "date": "2024-04-11",
48
+ "open": "424.260000",
49
+ "high": "425.990000",
50
+ "low": "422.040000",
51
+ "close": "422.920000",
52
+ "volume": "1129158",
53
+ },
54
+ {
55
+ "date": "2024-04-12",
56
+ "open": "419.230000",
57
+ "high": "421.940000",
58
+ "low": "416.450000",
59
+ "close": "417.810000",
60
+ "volume": "1182229",
61
+ },
62
+ ],
63
+ }
64
+
65
+ def test_get_prices_from_identifiers(self, mock_client: Client, requests_mock: Mocker):
66
+ """
67
+ GIVEN the GetPricesFromIdentifiers tool
68
+ WHEN we request prices for SPGI
69
+ THEN we get back prices for SPGI
70
+ """
71
+
72
+ requests_mock.get(
73
+ url=f"https://kfinance.kensho.com/api/v1/pricing/{SPGI_TRADING_ITEM_ID}/none/none/day/adjusted",
74
+ json=self.prices_resp,
75
+ )
76
+ expected_response = {
77
+ "SPGI": {
78
+ "prices": [
79
+ {
80
+ "date": "2024-04-11",
81
+ "open": {"value": "424.26", "unit": "USD"},
82
+ "high": {"value": "425.99", "unit": "USD"},
83
+ "low": {"value": "422.04", "unit": "USD"},
84
+ "close": {"value": "422.92", "unit": "USD"},
85
+ "volume": {"value": "1129158", "unit": "Shares"},
86
+ },
87
+ {
88
+ "date": "2024-04-12",
89
+ "open": {"value": "419.23", "unit": "USD"},
90
+ "high": {"value": "421.94", "unit": "USD"},
91
+ "low": {"value": "416.45", "unit": "USD"},
92
+ "close": {"value": "417.81", "unit": "USD"},
93
+ "volume": {"value": "1182229", "unit": "Shares"},
94
+ },
95
+ ]
96
+ }
97
+ }
98
+
99
+ tool = GetPricesFromIdentifiers(kfinance_client=mock_client)
100
+ response = tool.run(
101
+ GetPricesFromIdentifiersArgs(identifiers=["SPGI"]).model_dump(mode="json")
102
+ )
103
+ assert response == expected_response
104
+
105
+ def test_most_recent_request(self, requests_mock: Mocker, mock_client: Client) -> None:
106
+ """
107
+ GIVEN the GetPricesFromIdentifiers tool
108
+ WHEN we request most recent prices for multiple companies
109
+ THEN we only get back the most recent prices for each company
110
+ """
111
+
112
+ company_ids = [1, 2]
113
+ for trading_item_id in company_ids:
114
+ requests_mock.get(
115
+ url=f"https://kfinance.kensho.com/api/v1/pricing/{trading_item_id}/none/none/day/adjusted",
116
+ json=self.prices_resp,
117
+ )
118
+
119
+ expected_single_company_response = {
120
+ "prices": [
121
+ {
122
+ "date": "2024-04-11",
123
+ "open": {"value": "424.26", "unit": "USD"},
124
+ "high": {"value": "425.99", "unit": "USD"},
125
+ "low": {"value": "422.04", "unit": "USD"},
126
+ "close": {"value": "422.92", "unit": "USD"},
127
+ "volume": {"value": "1129158", "unit": "Shares"},
128
+ }
129
+ ]
130
+ }
131
+ expected_response = {
132
+ "C_1": expected_single_company_response,
133
+ "C_2": expected_single_company_response,
134
+ }
135
+ tool = GetPricesFromIdentifiers(kfinance_client=mock_client)
136
+ response = tool.run(
137
+ GetPricesFromIdentifiersArgs(
138
+ identifiers=[f"{COMPANY_ID_PREFIX}{company_id}" for company_id in company_ids]
139
+ ).model_dump(mode="json")
140
+ )
141
+ assert response == expected_response
File without changes
@@ -0,0 +1,91 @@
1
+ from typing import Literal, Type
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+ from kfinance.client.batch_request_handling import Task, process_tasks_in_thread_pool_executor
6
+ from kfinance.client.models.date_and_period_models import PeriodType
7
+ from kfinance.client.permission_models import Permission
8
+ from kfinance.domains.companies.company_identifiers import (
9
+ fetch_company_ids_from_identifiers,
10
+ parse_identifiers,
11
+ )
12
+ from kfinance.domains.segments.segment_models import SegmentType
13
+ from kfinance.integrations.tool_calling.tool_calling_models import (
14
+ KfinanceTool,
15
+ ToolArgsWithIdentifiers,
16
+ ValidQuarter,
17
+ )
18
+
19
+
20
+ class GetSegmentsFromIdentifiersArgs(ToolArgsWithIdentifiers):
21
+ # no description because the description for enum fields comes from the enum docstring.
22
+ segment_type: SegmentType
23
+ period_type: PeriodType | None = Field(default=None, description="The period type")
24
+ start_year: int | None = Field(default=None, description="The starting year for the data range")
25
+ end_year: int | None = Field(default=None, description="The ending year for the data range")
26
+ start_quarter: ValidQuarter | None = Field(default=None, description="Starting quarter")
27
+ end_quarter: ValidQuarter | None = Field(default=None, description="Ending quarter")
28
+
29
+
30
+ class GetSegmentsFromIdentifiers(KfinanceTool):
31
+ name: str = "get_segments_from_identifiers"
32
+ description: str = "Get the templated segments associated with a list of identifiers."
33
+ args_schema: Type[BaseModel] = GetSegmentsFromIdentifiersArgs
34
+ accepted_permissions: set[Permission] | None = {Permission.SegmentsPermission}
35
+
36
+ def _run(
37
+ self,
38
+ identifiers: list[str],
39
+ segment_type: SegmentType,
40
+ period_type: PeriodType | None = None,
41
+ start_year: int | None = None,
42
+ end_year: int | None = None,
43
+ start_quarter: Literal[1, 2, 3, 4] | None = None,
44
+ end_quarter: Literal[1, 2, 3, 4] | None = None,
45
+ ) -> dict:
46
+ api_client = self.kfinance_client.kfinance_api_client
47
+ parsed_identifiers = parse_identifiers(identifiers=identifiers, api_client=api_client)
48
+ identifiers_to_company_ids = fetch_company_ids_from_identifiers(
49
+ identifiers=parsed_identifiers, api_client=api_client
50
+ )
51
+
52
+ tasks = [
53
+ Task(
54
+ func=api_client.fetch_segments,
55
+ kwargs=dict(
56
+ company_id=company_id,
57
+ segment_type=segment_type,
58
+ period_type=period_type,
59
+ start_year=start_year,
60
+ end_year=end_year,
61
+ start_quarter=start_quarter,
62
+ end_quarter=end_quarter,
63
+ ),
64
+ result_key=identifier,
65
+ )
66
+ for identifier, company_id in identifiers_to_company_ids.items()
67
+ ]
68
+
69
+ segments_responses = process_tasks_in_thread_pool_executor(
70
+ api_client=api_client, tasks=tasks
71
+ )
72
+
73
+ # If no date and multiple companies, only return the most recent value.
74
+ # By default, we return 5 years of data, which can be too much when
75
+ # returning data for many companies.
76
+ if (
77
+ start_year is None
78
+ and end_year is None
79
+ and start_quarter is None
80
+ and end_quarter is None
81
+ and len(identifiers) > 1
82
+ ):
83
+ for segments_response in segments_responses.values():
84
+ most_recent_year = max(segments_response["segments"].keys())
85
+ most_recent_year_data = segments_response["segments"][most_recent_year]
86
+ segments_response["segments"] = {most_recent_year: most_recent_year_data}
87
+
88
+ return {
89
+ str(identifier): segments["segments"]
90
+ for identifier, segments in segments_responses.items()
91
+ }
File without changes
@@ -0,0 +1,80 @@
1
+ from requests_mock import Mocker
2
+
3
+ from kfinance.client.kfinance import Client
4
+ from kfinance.conftest import SPGI_COMPANY_ID
5
+ from kfinance.domains.companies.company_models import COMPANY_ID_PREFIX
6
+ from kfinance.domains.segments.segment_models import SegmentType
7
+ from kfinance.domains.segments.segment_tools import (
8
+ GetSegmentsFromIdentifiers,
9
+ GetSegmentsFromIdentifiersArgs,
10
+ )
11
+
12
+
13
+ class TestGetSegmentsFromIdentifier:
14
+ segments_response = {
15
+ "segments": {
16
+ "2020": {
17
+ "Commodity Insights": {
18
+ "CAPEX": -7000000.0,
19
+ "D&A": 17000000.0,
20
+ },
21
+ "Unallocated Assets Held for Sale": None,
22
+ },
23
+ "2021": {
24
+ "Commodity Insights": {
25
+ "CAPEX": -2000000.0,
26
+ "D&A": 12000000.0,
27
+ },
28
+ "Unallocated Assets Held for Sale": {"Total Assets": 321000000.0},
29
+ },
30
+ },
31
+ }
32
+
33
+ def test_get_segments_from_identifier(self, mock_client: Client, requests_mock: Mocker):
34
+ """
35
+ GIVEN the GetSegmentsFromIdentifier tool
36
+ WHEN we request the SPGI business segment
37
+ THEN we get back the SPGI business segment
38
+ """
39
+
40
+ requests_mock.get(
41
+ url=f"https://kfinance.kensho.com/api/v1/segments/{SPGI_COMPANY_ID}/business/none/none/none/none/none",
42
+ # truncated from the original API response
43
+ json=self.segments_response,
44
+ )
45
+
46
+ expected_response = {"SPGI": self.segments_response["segments"]}
47
+
48
+ tool = GetSegmentsFromIdentifiers(kfinance_client=mock_client)
49
+ args = GetSegmentsFromIdentifiersArgs(
50
+ identifiers=["SPGI"], segment_type=SegmentType.business
51
+ )
52
+ response = tool.run(args.model_dump(mode="json"))
53
+ assert response == expected_response
54
+
55
+ def test_most_recent_request(self, requests_mock: Mocker, mock_client: Client) -> None:
56
+ """
57
+ GIVEN the GetFinancialLineItemFromIdentifiers tool
58
+ WHEN we request most recent statement for multiple companies
59
+ THEN we only get back the most recent statement for each company
60
+ """
61
+
62
+ company_ids = [1, 2]
63
+ expected_response = {
64
+ "C_1": {"2021": self.segments_response["segments"]["2021"]},
65
+ "C_2": {"2021": self.segments_response["segments"]["2021"]},
66
+ }
67
+
68
+ for company_id in company_ids:
69
+ requests_mock.get(
70
+ url=f"https://kfinance.kensho.com/api/v1/segments/{company_id}/business/none/none/none/none/none",
71
+ json=self.segments_response,
72
+ )
73
+
74
+ tool = GetSegmentsFromIdentifiers(kfinance_client=mock_client)
75
+ args = GetSegmentsFromIdentifiersArgs(
76
+ identifiers=[f"{COMPANY_ID_PREFIX}{company_id}" for company_id in company_ids],
77
+ segment_type=SegmentType.business,
78
+ )
79
+ response = tool.run(args.model_dump(mode="json"))
80
+ assert response == expected_response
File without changes
@@ -0,0 +1,113 @@
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.statements.statement_models import StatementType
16
+ from kfinance.integrations.tool_calling.tool_calling_models import (
17
+ KfinanceTool,
18
+ ToolArgsWithIdentifiers,
19
+ )
20
+
21
+
22
+ class GetFinancialStatementFromIdentifiersArgs(ToolArgsWithIdentifiers):
23
+ # no description because the description for enum fields comes from the enum docstring.
24
+ statement: StatementType
25
+ period_type: PeriodType | None = Field(default=None, description="The period type")
26
+ start_year: int | None = Field(default=None, description="The starting year for the data range")
27
+ end_year: int | None = Field(default=None, description="The ending year for the data range")
28
+ start_quarter: Literal[1, 2, 3, 4] | None = Field(default=None, description="Starting quarter")
29
+ end_quarter: Literal[1, 2, 3, 4] | None = Field(default=None, description="Ending quarter")
30
+
31
+
32
+ class GetFinancialStatementFromIdentifiers(KfinanceTool):
33
+ name: str = "get_financial_statement_from_identifiers"
34
+ description: str = dedent("""
35
+ Get a financial statement associated with a group of identifiers.
36
+
37
+ - To fetch the most recent value for the statement, leave start_year, start_quarter, end_year, and end_quarter as None.
38
+
39
+ Example:
40
+ Query: "Fetch the balance sheets of BAC and GS for 2024"
41
+ Function: get_financial_statement_from_company_ids(identifiers=["BAC", "GS"], statement=StatementType.balance_sheet, start_year=2024, end_year=2024)
42
+ """).strip()
43
+ args_schema: Type[BaseModel] = GetFinancialStatementFromIdentifiersArgs
44
+ accepted_permissions: set[Permission] | None = {Permission.StatementsPermission}
45
+
46
+ def _run(
47
+ self,
48
+ identifiers: list[str],
49
+ statement: StatementType,
50
+ period_type: PeriodType | None = None,
51
+ start_year: int | None = None,
52
+ end_year: int | None = None,
53
+ start_quarter: Literal[1, 2, 3, 4] | None = None,
54
+ end_quarter: Literal[1, 2, 3, 4] | None = None,
55
+ ) -> dict:
56
+ """Sample response:
57
+
58
+ {
59
+ 'SPGI': {
60
+ 'Revenues': {'2020': 7442000000.0, '2021': 8243000000.0},
61
+ 'Total Revenues': {'2020': 7442000000.0, '2021': 8243000000.0}
62
+ }
63
+ }
64
+ """
65
+ api_client = self.kfinance_client.kfinance_api_client
66
+ parsed_identifiers = parse_identifiers(identifiers, api_client=api_client)
67
+ identifiers_to_company_ids = fetch_company_ids_from_identifiers(
68
+ identifiers=parsed_identifiers, api_client=api_client
69
+ )
70
+
71
+ tasks = [
72
+ Task(
73
+ func=api_client.fetch_statement,
74
+ kwargs=dict(
75
+ company_id=company_id,
76
+ statement_type=statement.value,
77
+ period_type=period_type,
78
+ start_year=start_year,
79
+ end_year=end_year,
80
+ start_quarter=start_quarter,
81
+ end_quarter=end_quarter,
82
+ ),
83
+ result_key=identifier,
84
+ )
85
+ for identifier, company_id in identifiers_to_company_ids.items()
86
+ ]
87
+
88
+ statement_responses = process_tasks_in_thread_pool_executor(
89
+ api_client=api_client, tasks=tasks
90
+ )
91
+
92
+ output = dict()
93
+ for identifier, result in statement_responses.items():
94
+ df = (
95
+ pd.DataFrame(result["statements"])
96
+ .apply(pd.to_numeric)
97
+ .replace(np.nan, None)
98
+ .transpose()
99
+ )
100
+ # If no date and multiple companies, only return the most recent value.
101
+ # By default, we return 5 years of data, which can be too much when
102
+ # returning data for many companies.
103
+ if (
104
+ start_year is None
105
+ and end_year is None
106
+ and start_quarter is None
107
+ and end_quarter is None
108
+ and len(identifiers) > 1
109
+ ):
110
+ df = df.tail(1)
111
+ output[str(identifier)] = df.to_dict()
112
+
113
+ return output
File without changes
@@ -0,0 +1,73 @@
1
+ from requests_mock import Mocker
2
+
3
+ from kfinance.client.kfinance import Client
4
+ from kfinance.conftest import SPGI_COMPANY_ID
5
+ from kfinance.domains.companies.company_models import COMPANY_ID_PREFIX
6
+ from kfinance.domains.statements.statement_models import StatementType
7
+ from kfinance.domains.statements.statement_tools import (
8
+ GetFinancialStatementFromIdentifiers,
9
+ GetFinancialStatementFromIdentifiersArgs,
10
+ )
11
+
12
+
13
+ class TestGetFinancialStatementFromIdentifiers:
14
+ statement_resp = {
15
+ "statements": {
16
+ "2020": {"Revenues": "7442000000.000000", "Total Revenues": "7442000000.000000"},
17
+ "2021": {"Revenues": "8243000000.000000", "Total Revenues": "8243000000.000000"},
18
+ }
19
+ }
20
+
21
+ def test_get_financial_statement_from_identifiers(
22
+ self, mock_client: Client, requests_mock: Mocker
23
+ ):
24
+ """
25
+ GIVEN the GetFinancialLineItemFromIdentifiers tool
26
+ WHEN we request the SPGI income statement
27
+ THEN we get back the SPGI income statement
28
+ """
29
+
30
+ requests_mock.get(
31
+ url=f"https://kfinance.kensho.com/api/v1/statements/{SPGI_COMPANY_ID}/income_statement/none/none/none/none/none",
32
+ json=self.statement_resp,
33
+ )
34
+ expected_response = {
35
+ "SPGI": {
36
+ "Revenues": {"2020": 7442000000.0, "2021": 8243000000.0},
37
+ "Total Revenues": {"2020": 7442000000.0, "2021": 8243000000.0},
38
+ }
39
+ }
40
+
41
+ tool = GetFinancialStatementFromIdentifiers(kfinance_client=mock_client)
42
+ args = GetFinancialStatementFromIdentifiersArgs(
43
+ identifiers=["SPGI"], statement=StatementType.income_statement
44
+ )
45
+ response = tool.run(args.model_dump(mode="json"))
46
+ assert response == expected_response
47
+
48
+ def test_most_recent_request(self, requests_mock: Mocker, mock_client: Client) -> None:
49
+ """
50
+ GIVEN the GetFinancialLineItemFromIdentifiers tool
51
+ WHEN we request most recent statement for multiple companies
52
+ THEN we only get back the most recent statement for each company
53
+ """
54
+
55
+ company_ids = [1, 2]
56
+ expected_response = {
57
+ "C_1": {"Revenues": {"2021": 8243000000.0}, "Total Revenues": {"2021": 8243000000.0}},
58
+ "C_2": {"Revenues": {"2021": 8243000000.0}, "Total Revenues": {"2021": 8243000000.0}},
59
+ }
60
+
61
+ for company_id in company_ids:
62
+ requests_mock.get(
63
+ url=f"https://kfinance.kensho.com/api/v1/statements/{company_id}/income_statement/none/none/none/none/none",
64
+ json=self.statement_resp,
65
+ )
66
+
67
+ tool = GetFinancialStatementFromIdentifiers(kfinance_client=mock_client)
68
+ args = GetFinancialStatementFromIdentifiersArgs(
69
+ identifiers=[f"{COMPANY_ID_PREFIX}{company_id}" for company_id in company_ids],
70
+ statement=StatementType.income_statement,
71
+ )
72
+ response = tool.run(args.model_dump(mode="json"))
73
+ assert response == expected_response
@@ -0,0 +1,8 @@
1
+ # Integrations
2
+
3
+ Integrations holds integrations downstream from the main
4
+ Kfinance `Client`. This currently includes tool calling
5
+ and MCP. Note that only shared functionality for tool
6
+ calling should be put into the `tool_calling` directory.
7
+ The tools themselves should be defined in the relevant
8
+ `domains` sub directory.
File without changes
File without changes
@@ -6,8 +6,8 @@ from fastmcp.tools import FunctionTool
6
6
  from fastmcp.utilities.logging import get_logger
7
7
  from langchain_core.utils.function_calling import convert_to_openai_tool
8
8
 
9
- from kfinance.kfinance import Client
10
- from kfinance.tool_calling import KfinanceTool
9
+ from kfinance.client.kfinance import Client
10
+ from kfinance.integrations.tool_calling.tool_calling_models import KfinanceTool
11
11
 
12
12
 
13
13
  logger = get_logger(__name__)
File without changes
@@ -55,7 +55,7 @@ def test_run_notebook(jupyter_kernel_name: str):
55
55
  # - mocks for all calls made by the client while executing the notebook
56
56
  startup_cell_code = dedent("""
57
57
  from datetime import datetime
58
- from kfinance.kfinance import Client
58
+ from kfinance.client.kfinance import Client
59
59
  kfinance_client = Client(refresh_token="foo")
60
60
  api_client = kfinance_client.kfinance_api_client
61
61
  # Set access token so that the client doesn't try to fetch it.
@@ -161,13 +161,13 @@ def test_run_notebook(jupyter_kernel_name: str):
161
161
  )
162
162
  # Mock out image_open so that we don't have to return an actual png.
163
163
  from unittest.mock import MagicMock
164
- import kfinance.kfinance
165
- kfinance.kfinance.image_open = MagicMock()
164
+ import kfinance.client.kfinance
165
+ kfinance.client.kfinance.image_open = MagicMock()
166
166
  """)
167
167
 
168
168
  # Load the notebook
169
169
  notebook_path = Path(
170
- Path(__file__).parent.parent.parent, "example_notebooks", "basic_usage.ipynb"
170
+ Path(__file__).parent.parent.parent.parent, "example_notebooks", "basic_usage.ipynb"
171
171
  )
172
172
  with notebook_path.open() as f:
173
173
  nb = nbformat.read(f, as_version=4)
@@ -2,7 +2,7 @@
2
2
 
3
3
  The tools defined in this directory are intended for tool calling.
4
4
 
5
- Each tool is a subclass of [KfinanceTool](shared_models.py), which is
5
+ Each tool is a subclass of [KfinanceTool](tool_calling_models.py), which is
6
6
  turn a subclass of `BaseTool` from langchain. We use langchain to convert these tools
7
7
  into LLM-specific tool descriptions.
8
8
 
@@ -14,7 +14,7 @@ of the arguments.
14
14
  - args_schema: A pydantic model defining the input schema for the function (more on that below)
15
15
  - _run: the source code for the tool.
16
16
 
17
- All new tools have to be added to the `ALL_TOOLS` [list](__init__.py).
17
+ All new tools have to be added to the `ALL_TOOLS` [list](all_tools.py).
18
18
 
19
19
 
20
20
  When initializing a `KfinanceTool`, it's always required to pass in an initialized Kfinance
File without changes
@@ -0,0 +1,55 @@
1
+ from kfinance.domains.business_relationships.business_relationship_tools import (
2
+ GetBusinessRelationshipFromIdentifiers,
3
+ )
4
+ from kfinance.domains.capitalizations.capitalization_tools import GetCapitalizationFromIdentifiers
5
+ from kfinance.domains.companies.company_tools import GetInfoFromIdentifiers
6
+ from kfinance.domains.competitors.competitor_tools import GetCompetitorsFromIdentifiers
7
+ from kfinance.domains.cusip_and_isin.cusip_and_isin_tools import (
8
+ GetCusipFromIdentifiers,
9
+ GetIsinFromIdentifiers,
10
+ )
11
+ from kfinance.domains.earnings.earning_tools import (
12
+ GetEarningsFromIdentifiers,
13
+ GetLatestEarningsFromIdentifiers,
14
+ GetNextEarningsFromIdentifiers,
15
+ GetTranscriptFromKeyDevId,
16
+ )
17
+ from kfinance.domains.line_items.line_item_tools import GetFinancialLineItemFromIdentifiers
18
+ from kfinance.domains.prices.price_tools import GetPricesFromIdentifiers
19
+ from kfinance.domains.segments.segment_tools import GetSegmentsFromIdentifiers
20
+ from kfinance.domains.statements.statement_tools import GetFinancialStatementFromIdentifiers
21
+ from kfinance.integrations.tool_calling.static_tools.get_latest import GetLatest
22
+ from kfinance.integrations.tool_calling.static_tools.get_n_quarters_ago import GetNQuartersAgo
23
+ from kfinance.integrations.tool_calling.tool_calling_models import KfinanceTool
24
+
25
+
26
+ # A list of all available tools
27
+ ALL_TOOLS: list[type[KfinanceTool]] = [
28
+ # Static / no API call tools
29
+ GetLatest,
30
+ GetNQuartersAgo,
31
+ # Business Relationships
32
+ GetBusinessRelationshipFromIdentifiers,
33
+ # Capitalizations
34
+ GetCapitalizationFromIdentifiers,
35
+ # Companies
36
+ GetInfoFromIdentifiers,
37
+ # Competitors
38
+ GetCompetitorsFromIdentifiers,
39
+ # Cusip and Isin
40
+ GetCusipFromIdentifiers,
41
+ GetIsinFromIdentifiers,
42
+ # Earnings
43
+ GetEarningsFromIdentifiers,
44
+ GetLatestEarningsFromIdentifiers,
45
+ GetNextEarningsFromIdentifiers,
46
+ GetTranscriptFromKeyDevId,
47
+ # Line Items
48
+ GetFinancialLineItemFromIdentifiers,
49
+ # Prices
50
+ GetPricesFromIdentifiers,
51
+ # Segments
52
+ GetSegmentsFromIdentifiers,
53
+ # Statements
54
+ GetFinancialStatementFromIdentifiers,
55
+ ]
@@ -1,4 +1,5 @@
1
- from kfinance.tool_calling import GetFinancialLineItemFromIdentifier, GetLatest
1
+ from kfinance.domains.line_items.line_item_tools import GetFinancialLineItemFromIdentifiers
2
+ from kfinance.integrations.tool_calling.static_tools.get_latest import GetLatest
2
3
 
3
4
 
4
5
  BASE_PROMPT = f"""
@@ -7,7 +8,7 @@ BASE_PROMPT = f"""
7
8
 
8
9
  - Always use the `{GetLatest.model_fields["name"].default}` function when asked about the last or most recent quarter or
9
10
  when the time is unspecified in the question.
10
- - Try to use `{GetFinancialLineItemFromIdentifier.model_fields["name"].default}` for questions about a company's
11
+ - Try to use `{GetFinancialLineItemFromIdentifiers.model_fields["name"].default}` for questions about a company's
11
12
  finances.
12
13
  - If the tools do not respond with data that answers the question, then respond by saying that
13
14
  you don't have the data available.
@@ -0,0 +1,4 @@
1
+ # Static Tools
2
+
3
+ The static_tools directory contains tools that don't have to make
4
+ any API calls.