kensho-kfinance 2.4.2__py3-none-any.whl → 2.6.1__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 (31) hide show
  1. {kensho_kfinance-2.4.2.dist-info → kensho_kfinance-2.6.1.dist-info}/METADATA +1 -1
  2. {kensho_kfinance-2.4.2.dist-info → kensho_kfinance-2.6.1.dist-info}/RECORD +29 -26
  3. kfinance/CHANGELOG.md +15 -0
  4. kfinance/batch_request_handling.py +2 -16
  5. kfinance/constants.py +14 -2
  6. kfinance/fetch.py +56 -0
  7. kfinance/kfinance.py +403 -90
  8. kfinance/mcp.py +23 -35
  9. kfinance/meta_classes.py +37 -12
  10. kfinance/tests/conftest.py +4 -0
  11. kfinance/tests/scratch.py +0 -0
  12. kfinance/tests/test_batch_requests.py +0 -32
  13. kfinance/tests/test_fetch.py +21 -0
  14. kfinance/tests/test_objects.py +239 -3
  15. kfinance/tests/test_tools.py +136 -24
  16. kfinance/tool_calling/__init__.py +2 -4
  17. kfinance/tool_calling/get_advisors_for_company_in_transaction_from_identifier.py +39 -0
  18. kfinance/tool_calling/get_competitors_from_identifier.py +24 -0
  19. kfinance/tool_calling/get_financial_line_item_from_identifier.py +3 -3
  20. kfinance/tool_calling/get_financial_statement_from_identifier.py +3 -3
  21. kfinance/tool_calling/get_merger_info_from_transaction_id.py +62 -0
  22. kfinance/tool_calling/get_mergers_from_identifier.py +41 -0
  23. kfinance/tool_calling/get_segments_from_identifier.py +3 -3
  24. kfinance/tool_calling/shared_models.py +17 -2
  25. kfinance/version.py +2 -2
  26. kfinance/tests/test_mcp.py +0 -16
  27. kfinance/tool_calling/get_earnings_call_datetimes_from_identifier.py +0 -19
  28. {kensho_kfinance-2.4.2.dist-info → kensho_kfinance-2.6.1.dist-info}/WHEEL +0 -0
  29. {kensho_kfinance-2.4.2.dist-info → kensho_kfinance-2.6.1.dist-info}/licenses/AUTHORS.md +0 -0
  30. {kensho_kfinance-2.4.2.dist-info → kensho_kfinance-2.6.1.dist-info}/licenses/LICENSE +0 -0
  31. {kensho_kfinance-2.4.2.dist-info → kensho_kfinance-2.6.1.dist-info}/top_level.txt +0 -0
@@ -1,16 +1,27 @@
1
+ import contextlib
2
+ from contextlib import nullcontext as does_not_raise
1
3
  from datetime import date, datetime
2
4
 
3
5
  from langchain_core.utils.function_calling import convert_to_openai_tool
6
+ from pydantic import BaseModel, ValidationError
7
+ import pytest
4
8
  from pytest import raises
5
9
  from requests_mock import Mocker
6
10
  import time_machine
7
11
 
8
- from kfinance.constants import BusinessRelationshipType, Capitalization, SegmentType, StatementType
12
+ from kfinance.constants import (
13
+ BusinessRelationshipType,
14
+ Capitalization,
15
+ CompetitorSource,
16
+ SegmentType,
17
+ StatementType,
18
+ )
9
19
  from kfinance.kfinance import Client, NoEarningsDataError
10
20
  from kfinance.tests.conftest import SPGI_COMPANY_ID, SPGI_SECURITY_ID, SPGI_TRADING_ITEM_ID
21
+ from kfinance.tests.test_objects import MOCK_COMPANY_DB, MOCK_MERGERS_DB, ordered
11
22
  from kfinance.tool_calling import (
23
+ GetCompetitorsFromIdentifier,
12
24
  GetEarnings,
13
- GetEarningsCallDatetimesFromIdentifier,
14
25
  GetFinancialLineItemFromIdentifier,
15
26
  GetFinancialStatementFromIdentifier,
16
27
  GetHistoryMetadataFromIdentifier,
@@ -24,6 +35,10 @@ from kfinance.tool_calling import (
24
35
  GetTranscript,
25
36
  ResolveIdentifier,
26
37
  )
38
+ from kfinance.tool_calling.get_advisors_for_company_in_transaction_from_identifier import (
39
+ GetAdvisorsForCompanyInTransactionFromIdentifier,
40
+ GetAdvisorsForCompanyInTransactionFromIdentifierArgs,
41
+ )
27
42
  from kfinance.tool_calling.get_business_relationship_from_identifier import (
28
43
  GetBusinessRelationshipFromIdentifier,
29
44
  GetBusinessRelationshipFromIdentifierArgs,
@@ -32,6 +47,9 @@ from kfinance.tool_calling.get_capitalization_from_identifier import (
32
47
  GetCapitalizationFromIdentifier,
33
48
  GetCapitalizationFromIdentifierArgs,
34
49
  )
50
+ from kfinance.tool_calling.get_competitors_from_identifier import (
51
+ GetCompetitorsFromIdentifierArgs,
52
+ )
35
53
  from kfinance.tool_calling.get_cusip_from_ticker import GetCusipFromTicker, GetCusipFromTickerArgs
36
54
  from kfinance.tool_calling.get_financial_line_item_from_identifier import (
37
55
  GetFinancialLineItemFromIdentifierArgs,
@@ -41,6 +59,11 @@ from kfinance.tool_calling.get_financial_statement_from_identifier import (
41
59
  )
42
60
  from kfinance.tool_calling.get_isin_from_ticker import GetIsinFromTickerArgs
43
61
  from kfinance.tool_calling.get_latest import GetLatestArgs
62
+ from kfinance.tool_calling.get_merger_info_from_transaction_id import (
63
+ GetMergerInfoFromTransactionId,
64
+ GetMergerInfoFromTransactionIdArgs,
65
+ )
66
+ from kfinance.tool_calling.get_mergers_from_identifier import GetMergersFromIdentifier
44
67
  from kfinance.tool_calling.get_n_quarters_ago import GetNQuartersAgoArgs
45
68
  from kfinance.tool_calling.get_prices_from_identifier import GetPricesFromIdentifierArgs
46
69
  from kfinance.tool_calling.get_segments_from_identifier import (
@@ -48,7 +71,60 @@ from kfinance.tool_calling.get_segments_from_identifier import (
48
71
  GetSegmentsFromIdentifierArgs,
49
72
  )
50
73
  from kfinance.tool_calling.get_transcript import GetTranscriptArgs
51
- from kfinance.tool_calling.shared_models import ToolArgsWithIdentifier
74
+ from kfinance.tool_calling.shared_models import ToolArgsWithIdentifier, ValidQuarter
75
+
76
+
77
+ class TestGetCompaniesAdvisingCompanyInTransactionFromIdentifier:
78
+ def test_get_companies_advising_company_in_transaction_from_identifier(
79
+ self, requests_mock: Mocker, mock_client: Client
80
+ ):
81
+ expected_response = {
82
+ "advisors": [
83
+ {
84
+ "advisor_company_id": 251994106,
85
+ "advisor_company_name": "Kensho Technologies, Inc.",
86
+ "advisor_type_name": "Professional Mongo Enjoyer",
87
+ }
88
+ ]
89
+ }
90
+ transaction_id = 517414
91
+ requests_mock.get(
92
+ url=f"https://kfinance.kensho.com/api/v1/merger/info/{transaction_id}/advisors/21835",
93
+ json=expected_response,
94
+ )
95
+ tool = GetAdvisorsForCompanyInTransactionFromIdentifier(kfinance_client=mock_client)
96
+ args = GetAdvisorsForCompanyInTransactionFromIdentifierArgs(
97
+ identifier="MSFT", transaction_id=transaction_id
98
+ )
99
+ response = tool.run(args.model_dump(mode="json"))
100
+ assert response == expected_response["advisors"]
101
+
102
+
103
+ class TestGetMergerInfoFromTransactionId:
104
+ def test_get_merger_info_from_transaction_id(self, requests_mock: Mocker, mock_client: Client):
105
+ expected_response = MOCK_MERGERS_DB["517414"]
106
+ transaction_id = 517414
107
+ requests_mock.get(
108
+ url=f"https://kfinance.kensho.com/api/v1/merger/info/{transaction_id}",
109
+ json=expected_response,
110
+ )
111
+ tool = GetMergerInfoFromTransactionId(kfinance_client=mock_client)
112
+ args = GetMergerInfoFromTransactionIdArgs(transaction_id=transaction_id)
113
+ response = tool.run(args.model_dump(mode="json"))
114
+ assert ordered(response) == ordered(expected_response)
115
+
116
+
117
+ class TestGetMergersFromIdentifier:
118
+ def test_get_mergers_from_identifier(self, requests_mock: Mocker, mock_client: Client):
119
+ expected_response = MOCK_COMPANY_DB["21835"]["mergers"]
120
+ company_id = 21835
121
+ requests_mock.get(
122
+ url=f"https://kfinance.kensho.com/api/v1/mergers/{company_id}", json=expected_response
123
+ )
124
+ tool = GetMergersFromIdentifier(kfinance_client=mock_client)
125
+ args = ToolArgsWithIdentifier(identifier="MSFT")
126
+ response = tool.run(args.model_dump(mode="json"))
127
+ assert ordered(response) == ordered(expected_response)
52
128
 
53
129
 
54
130
  class TestGetBusinessRelationshipFromIdentifier:
@@ -133,27 +209,6 @@ class TestGetCusipFromTicker:
133
209
  assert resp == spgi_cusip
134
210
 
135
211
 
136
- class TestGetEarningsCallDatetimesFromTicker:
137
- def test_get_earnings_call_datetimes_from_ticker(
138
- self, requests_mock: Mocker, mock_client: Client
139
- ):
140
- """
141
- GIVEN the GetEarningsCallDatetimesFromIdentifier tool
142
- WHEN we request earnings call datetimes for SPGI
143
- THEN we get back the expected SPGI earnings call datetimes
144
- """
145
-
146
- requests_mock.get(
147
- url=f"https://kfinance.kensho.com/api/v1/earnings/{SPGI_COMPANY_ID}/dates",
148
- json={"earnings": ["2025-04-29T12:30:00", "2025-02-11T13:30:00"]},
149
- )
150
- expected_response = '["2025-04-29T12:30:00+00:00", "2025-02-11T13:30:00+00:00"]'
151
-
152
- tool = GetEarningsCallDatetimesFromIdentifier(kfinance_client=mock_client)
153
- response = tool.run(ToolArgsWithIdentifier(identifier="SPGI").model_dump(mode="json"))
154
- assert response == expected_response
155
-
156
-
157
212
  class TestGetFinancialLineItemFromIdentifier:
158
213
  def test_get_financial_line_item_from_identifier(
159
214
  self, mock_client: Client, requests_mock: Mocker
@@ -638,3 +693,60 @@ class TestGetTranscript:
638
693
  tool = GetTranscript(kfinance_client=mock_client)
639
694
  response = tool.run(GetTranscriptArgs(key_dev_id=12345).model_dump(mode="json"))
640
695
  assert response == expected_response
696
+
697
+
698
+ class TestGetCompetitorsFromIdentifier:
699
+ def test_get_competitors_from_identifier(self, mock_client: Client, requests_mock: Mocker):
700
+ """
701
+ GIVEN the GetCompetitorsFromIdentifier tool
702
+ WHEN we request the SPGI competitors that are named by competitors
703
+ THEN we get back the SPGI competitors that are named by competitors
704
+ """
705
+ expected_competitors_response = {
706
+ "companies": [
707
+ {"company_id": 35352, "company_name": "The Descartes Systems Group Inc."},
708
+ {"company_id": 4003514, "company_name": "London Stock Exchange Group plc"},
709
+ ]
710
+ }
711
+ requests_mock.get(
712
+ url=f"https://kfinance.kensho.com/api/v1/competitors/{SPGI_COMPANY_ID}/named_by_competitor",
713
+ # truncated from the original API response
714
+ json=expected_competitors_response,
715
+ )
716
+
717
+ tool = GetCompetitorsFromIdentifier(kfinance_client=mock_client)
718
+ args = GetCompetitorsFromIdentifierArgs(
719
+ identifier="SPGI", competitor_source=CompetitorSource.named_by_competitor
720
+ )
721
+ response = tool.run(args.model_dump(mode="json"))
722
+ assert response == expected_competitors_response
723
+
724
+
725
+ class TestValidQuarter:
726
+ class QuarterModel(BaseModel):
727
+ quarter: ValidQuarter | None
728
+
729
+ @pytest.mark.parametrize(
730
+ "input_quarter, expectation, expected_quarter",
731
+ [
732
+ pytest.param(1, does_not_raise(), 1, id="int input works"),
733
+ pytest.param("1", does_not_raise(), 1, id="str input works"),
734
+ pytest.param(None, does_not_raise(), None, id="None input works"),
735
+ pytest.param(5, pytest.raises(ValidationError), None, id="invalid int raises"),
736
+ pytest.param("5", pytest.raises(ValidationError), None, id="invalid str raises"),
737
+ ],
738
+ )
739
+ def test_valid_quarter(
740
+ self,
741
+ input_quarter: int | str | None,
742
+ expectation: contextlib.AbstractContextManager,
743
+ expected_quarter: int | None,
744
+ ) -> None:
745
+ """
746
+ GIVEN a model that uses `ValidQuarter`
747
+ WHEN we deserialize with int, str, or None
748
+ THEN valid str get coerced to int. Invalid values raise.
749
+ """
750
+ with expectation:
751
+ res = self.QuarterModel.model_validate(dict(quarter=input_quarter))
752
+ assert res.quarter == expected_quarter
@@ -4,11 +4,9 @@ from kfinance.tool_calling.get_business_relationship_from_identifier import (
4
4
  GetBusinessRelationshipFromIdentifier,
5
5
  )
6
6
  from kfinance.tool_calling.get_capitalization_from_identifier import GetCapitalizationFromIdentifier
7
+ from kfinance.tool_calling.get_competitors_from_identifier import GetCompetitorsFromIdentifier
7
8
  from kfinance.tool_calling.get_cusip_from_ticker import GetCusipFromTicker
8
9
  from kfinance.tool_calling.get_earnings import GetEarnings
9
- from kfinance.tool_calling.get_earnings_call_datetimes_from_identifier import (
10
- GetEarningsCallDatetimesFromIdentifier,
11
- )
12
10
  from kfinance.tool_calling.get_financial_line_item_from_identifier import (
13
11
  GetFinancialLineItemFromIdentifier,
14
12
  )
@@ -39,7 +37,6 @@ ALL_TOOLS: list[Type[KfinanceTool]] = [
39
37
  GetIsinFromTicker,
40
38
  GetCusipFromTicker,
41
39
  GetInfoFromIdentifier,
42
- GetEarningsCallDatetimesFromIdentifier,
43
40
  GetEarnings,
44
41
  GetLatestEarnings,
45
42
  GetNextEarnings,
@@ -52,4 +49,5 @@ ALL_TOOLS: list[Type[KfinanceTool]] = [
52
49
  GetBusinessRelationshipFromIdentifier,
53
50
  ResolveIdentifier,
54
51
  GetSegmentsFromIdentifier,
52
+ GetCompetitorsFromIdentifier,
55
53
  ]
@@ -0,0 +1,39 @@
1
+ from typing import Type
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+ from kfinance.constants import Permission
6
+ from kfinance.kfinance import AdvisedCompany
7
+ from kfinance.tool_calling.shared_models import KfinanceTool, ToolArgsWithIdentifier
8
+
9
+
10
+ class GetAdvisorsForCompanyInTransactionFromIdentifierArgs(ToolArgsWithIdentifier):
11
+ transaction_id: int | None = Field(description="The ID of the merger.", default=None)
12
+
13
+
14
+ class GetAdvisorsForCompanyInTransactionFromIdentifier(KfinanceTool):
15
+ name: str = "get_advisors_for_company_in_transaction_from_identifier"
16
+ description: str = 'Get the companies advising a company in a given transaction. For example, "Who advised S&P Global during their purchase of Kensho?"'
17
+ args_schema: Type[BaseModel] = GetAdvisorsForCompanyInTransactionFromIdentifierArgs
18
+ required_permission: Permission | None = Permission.MergersPermission
19
+
20
+ def _run(self, identifier: str, transaction_id: int) -> list:
21
+ ticker = self.kfinance_client.ticker(identifier)
22
+ advised_company = AdvisedCompany(
23
+ kfinance_api_client=ticker.kfinance_api_client,
24
+ company_id=ticker.company.company_id,
25
+ transaction_id=transaction_id,
26
+ )
27
+ advisors = advised_company.advisors
28
+
29
+ if advisors:
30
+ return [
31
+ {
32
+ "advisor_company_id": advisor.company_id,
33
+ "advisor_company_name": advisor.name,
34
+ "advisor_type_name": advisor.advisor_type_name,
35
+ }
36
+ for advisor in advisors
37
+ ]
38
+ else:
39
+ return []
@@ -0,0 +1,24 @@
1
+ from kfinance.constants import CompetitorSource, Permission
2
+ from kfinance.tool_calling.shared_models import KfinanceTool, ToolArgsWithIdentifier
3
+
4
+
5
+ class GetCompetitorsFromIdentifierArgs(ToolArgsWithIdentifier):
6
+ # no description because the description for enum fields comes from the enum docstring.
7
+ competitor_source: CompetitorSource
8
+
9
+
10
+ class GetCompetitorsFromIdentifier(KfinanceTool):
11
+ name: str = "get_competitors_from_identifier"
12
+ description: str = "Retrieves a list of competitors for a given company, optionally filtered by the source of the competitor information."
13
+ args_schema = GetCompetitorsFromIdentifierArgs
14
+ required_permission: Permission | None = Permission.CompetitorsPermission
15
+
16
+ def _run(
17
+ self,
18
+ identifier: str,
19
+ competitor_source: CompetitorSource,
20
+ ) -> dict:
21
+ ticker = self.kfinance_client.ticker(identifier)
22
+ return self.kfinance_client.kfinance_api_client.fetch_competitors(
23
+ company_id=ticker.company_id, competitor_source=competitor_source
24
+ )
@@ -3,7 +3,7 @@ from typing import Literal, Type
3
3
  from pydantic import BaseModel, Field
4
4
 
5
5
  from kfinance.constants import LINE_ITEM_NAMES_AND_ALIASES, PeriodType, Permission
6
- from kfinance.tool_calling.shared_models import KfinanceTool, ToolArgsWithIdentifier
6
+ from kfinance.tool_calling.shared_models import KfinanceTool, ToolArgsWithIdentifier, ValidQuarter
7
7
 
8
8
 
9
9
  class GetFinancialLineItemFromIdentifierArgs(ToolArgsWithIdentifier):
@@ -16,8 +16,8 @@ class GetFinancialLineItemFromIdentifierArgs(ToolArgsWithIdentifier):
16
16
  period_type: PeriodType | None = Field(default=None, description="The period type")
17
17
  start_year: int | None = Field(default=None, description="The starting year for the data range")
18
18
  end_year: int | None = Field(default=None, description="The ending year for the data range")
19
- start_quarter: Literal[1, 2, 3, 4] | None = Field(default=None, description="Starting quarter")
20
- end_quarter: Literal[1, 2, 3, 4] | None = Field(default=None, description="Ending quarter")
19
+ start_quarter: ValidQuarter | None = Field(default=None, description="Starting quarter")
20
+ end_quarter: ValidQuarter | None = Field(default=None, description="Ending quarter")
21
21
 
22
22
 
23
23
  class GetFinancialLineItemFromIdentifier(KfinanceTool):
@@ -3,7 +3,7 @@ from typing import Literal, Type
3
3
  from pydantic import BaseModel, Field
4
4
 
5
5
  from kfinance.constants import PeriodType, Permission, StatementType
6
- from kfinance.tool_calling.shared_models import KfinanceTool, ToolArgsWithIdentifier
6
+ from kfinance.tool_calling.shared_models import KfinanceTool, ToolArgsWithIdentifier, ValidQuarter
7
7
 
8
8
 
9
9
  class GetFinancialStatementFromIdentifierArgs(ToolArgsWithIdentifier):
@@ -12,8 +12,8 @@ class GetFinancialStatementFromIdentifierArgs(ToolArgsWithIdentifier):
12
12
  period_type: PeriodType | None = Field(default=None, description="The period type")
13
13
  start_year: int | None = Field(default=None, description="The starting year for the data range")
14
14
  end_year: int | None = Field(default=None, description="The ending year for the data range")
15
- start_quarter: Literal[1, 2, 3, 4] | None = Field(default=None, description="Starting quarter")
16
- end_quarter: Literal[1, 2, 3, 4] | None = Field(default=None, description="Ending quarter")
15
+ start_quarter: ValidQuarter | None = Field(default=None, description="Starting quarter")
16
+ end_quarter: ValidQuarter | None = Field(default=None, description="Ending quarter")
17
17
 
18
18
 
19
19
  class GetFinancialStatementFromIdentifier(KfinanceTool):
@@ -0,0 +1,62 @@
1
+ from typing import Type
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+ from kfinance.constants import Permission
6
+ from kfinance.kfinance import MergerOrAcquisition
7
+ from kfinance.tool_calling.shared_models import KfinanceTool
8
+
9
+
10
+ class GetMergerInfoFromTransactionIdArgs(BaseModel):
11
+ transaction_id: int | None = Field(description="The ID of the merger.", default=None)
12
+
13
+
14
+ class GetMergerInfoFromTransactionId(KfinanceTool):
15
+ name: str = "get_merger_info_from_transaction_id"
16
+ description: str = 'Get the timeline, the participants, and the consideration of the merger or acquisition from the given transaction ID. For example, "How much was Ben & Jerrys purchased for?" or "What was the price per share for LinkedIn?" or "When did S&P purchase Kensho?"'
17
+ args_schema: Type[BaseModel] = GetMergerInfoFromTransactionIdArgs
18
+ required_permission: Permission | None = Permission.MergersPermission
19
+
20
+ def _run(self, transaction_id: int) -> dict:
21
+ merger_or_acquisition = MergerOrAcquisition(
22
+ kfinance_api_client=self.kfinance_client.kfinance_api_client,
23
+ transaction_id=transaction_id,
24
+ merger_title=None,
25
+ )
26
+ merger_timeline = merger_or_acquisition.get_timeline
27
+ merger_participants = merger_or_acquisition.get_participants
28
+ merger_consideration = merger_or_acquisition.get_consideration
29
+
30
+ return {
31
+ "timeline": [
32
+ {"status": timeline["status"], "date": timeline["date"].strftime("%Y-%m-%d")}
33
+ for timeline in merger_timeline.to_dict(orient="records")
34
+ ],
35
+ "participants": {
36
+ "target": {
37
+ "company_id": merger_participants["target"].company_id,
38
+ "company_name": merger_participants["target"].name,
39
+ },
40
+ "buyers": [
41
+ {"company_id": buyer.company_id, "company_name": buyer.name}
42
+ for buyer in merger_participants["buyers"]
43
+ ],
44
+ "sellers": [
45
+ {"company_id": seller.company_id, "company_name": seller.name}
46
+ for seller in merger_participants["sellers"]
47
+ ],
48
+ },
49
+ "consideration": {
50
+ "currency_name": merger_consideration["currency_name"],
51
+ "current_calculated_gross_total_transaction_value": merger_consideration[
52
+ "current_calculated_gross_total_transaction_value"
53
+ ],
54
+ "current_calculated_implied_equity_value": merger_consideration[
55
+ "current_calculated_implied_equity_value"
56
+ ],
57
+ "current_calculated_implied_enterprise_value": merger_consideration[
58
+ "current_calculated_implied_enterprise_value"
59
+ ],
60
+ "details": merger_consideration["details"].to_dict(orient="records"),
61
+ },
62
+ }
@@ -0,0 +1,41 @@
1
+ from typing import Type
2
+
3
+ from pydantic import BaseModel
4
+
5
+ from kfinance.constants import Permission
6
+ from kfinance.tool_calling.shared_models import KfinanceTool, ToolArgsWithIdentifier
7
+
8
+
9
+ class GetMergersFromIdentifier(KfinanceTool):
10
+ name: str = "get_mergers_from_identifier"
11
+ description: str = 'Get the transaction IDs that involve the given identifier. For example, "Which companies did Microsoft purchase?" or "Which company bought Ben & Jerrys?"'
12
+ args_schema: Type[BaseModel] = ToolArgsWithIdentifier
13
+ required_permission: Permission | None = Permission.MergersPermission
14
+
15
+ def _run(self, identifier: str) -> dict:
16
+ ticker = self.kfinance_client.ticker(identifier)
17
+ mergers_and_acquisitions = ticker.company.mergers_and_acquisitions
18
+
19
+ return {
20
+ "target": [
21
+ {
22
+ "transaction_id": merger_or_acquisition.transaction_id,
23
+ "merger_title": merger_or_acquisition.merger_title,
24
+ }
25
+ for merger_or_acquisition in mergers_and_acquisitions["target"]
26
+ ],
27
+ "buyer": [
28
+ {
29
+ "transaction_id": merger_or_acquisition.transaction_id,
30
+ "merger_title": merger_or_acquisition.merger_title,
31
+ }
32
+ for merger_or_acquisition in mergers_and_acquisitions["buyer"]
33
+ ],
34
+ "seller": [
35
+ {
36
+ "transaction_id": merger_or_acquisition.transaction_id,
37
+ "merger_title": merger_or_acquisition.merger_title,
38
+ }
39
+ for merger_or_acquisition in mergers_and_acquisitions["seller"]
40
+ ],
41
+ }
@@ -3,7 +3,7 @@ from typing import Literal, Type
3
3
  from pydantic import BaseModel, Field
4
4
 
5
5
  from kfinance.constants import PeriodType, Permission, SegmentType
6
- from kfinance.tool_calling.shared_models import KfinanceTool, ToolArgsWithIdentifier
6
+ from kfinance.tool_calling.shared_models import KfinanceTool, ToolArgsWithIdentifier, ValidQuarter
7
7
 
8
8
 
9
9
  class GetSegmentsFromIdentifierArgs(ToolArgsWithIdentifier):
@@ -12,8 +12,8 @@ class GetSegmentsFromIdentifierArgs(ToolArgsWithIdentifier):
12
12
  period_type: PeriodType | None = Field(default=None, description="The period type")
13
13
  start_year: int | None = Field(default=None, description="The starting year for the data range")
14
14
  end_year: int | None = Field(default=None, description="The ending year for the data range")
15
- start_quarter: Literal[1, 2, 3, 4] | None = Field(default=None, description="Starting quarter")
16
- end_quarter: Literal[1, 2, 3, 4] | None = Field(default=None, description="Ending quarter")
15
+ start_quarter: ValidQuarter | None = Field(default=None, description="Starting quarter")
16
+ end_quarter: ValidQuarter | None = Field(default=None, description="Ending quarter")
17
17
 
18
18
 
19
19
  class GetSegmentsFromIdentifier(KfinanceTool):
@@ -1,7 +1,7 @@
1
- from typing import Any, Type
1
+ from typing import Annotated, Any, Literal, Type
2
2
 
3
3
  from langchain_core.tools import BaseTool
4
- from pydantic import BaseModel, ConfigDict, Field
4
+ from pydantic import BaseModel, BeforeValidator, ConfigDict, Field
5
5
 
6
6
  from kfinance.constants import Permission
7
7
  from kfinance.kfinance import Client
@@ -53,3 +53,18 @@ class ToolArgsWithIdentifier(BaseModel):
53
53
  identifier: str = Field(
54
54
  description="The identifier, which can be a ticker symbol, ISIN, or CUSIP"
55
55
  )
56
+
57
+
58
+ def convert_str_to_int(v: Any) -> Any:
59
+ """Convert strings to integers if possible."""
60
+ if isinstance(v, str) and v.isdigit():
61
+ return int(v)
62
+ return v
63
+
64
+
65
+ # Valid Quarter is a literal type, which converts strings to int before
66
+ # validating them.
67
+ # Claude seems to often pass strings to int literals, which raise a
68
+ # ValidationError during deserialization unless they have been converted
69
+ # to int.
70
+ ValidQuarter = Annotated[Literal[1, 2, 3, 4], BeforeValidator(convert_str_to_int)]
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.4.2'
21
- __version_tuple__ = version_tuple = (2, 4, 2)
20
+ __version__ = version = '2.6.1'
21
+ __version_tuple__ = version_tuple = (2, 6, 1)
@@ -1,16 +0,0 @@
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,19 +0,0 @@
1
- import json
2
- from typing import Type
3
-
4
- from pydantic import BaseModel
5
-
6
- from kfinance.constants import Permission
7
- from kfinance.tool_calling.shared_models import KfinanceTool, ToolArgsWithIdentifier
8
-
9
-
10
- class GetEarningsCallDatetimesFromIdentifier(KfinanceTool):
11
- name: str = "get_earnings_call_datetimes_from_identifier"
12
- description: str = "Get earnings call datetimes associated with an identifier."
13
- args_schema: Type[BaseModel] = ToolArgsWithIdentifier
14
- required_permission: Permission | None = Permission.EarningsPermission
15
-
16
- def _run(self, identifier: str) -> str:
17
- ticker = self.kfinance_client.ticker(identifier)
18
- earnings_call_datetimes = ticker.earnings_call_datetimes
19
- return json.dumps([dt.isoformat() for dt in earnings_call_datetimes])