kensho-kfinance 3.0.3__py3-none-any.whl → 3.1.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 (45) hide show
  1. {kensho_kfinance-3.0.3.dist-info → kensho_kfinance-3.1.0.dist-info}/METADATA +1 -1
  2. {kensho_kfinance-3.0.3.dist-info → kensho_kfinance-3.1.0.dist-info}/RECORD +45 -44
  3. kfinance/CHANGELOG.md +3 -0
  4. kfinance/client/fetch.py +26 -17
  5. kfinance/client/kfinance.py +17 -18
  6. kfinance/client/meta_classes.py +2 -2
  7. kfinance/client/tests/test_fetch.py +36 -24
  8. kfinance/client/tests/test_objects.py +112 -120
  9. kfinance/conftest.py +49 -5
  10. kfinance/domains/business_relationships/business_relationship_tools.py +30 -19
  11. kfinance/domains/business_relationships/tests/test_business_relationship_tools.py +18 -15
  12. kfinance/domains/capitalizations/capitalization_models.py +1 -1
  13. kfinance/domains/capitalizations/capitalization_tools.py +41 -24
  14. kfinance/domains/capitalizations/tests/test_capitalization_tools.py +38 -13
  15. kfinance/domains/companies/company_identifiers.py +0 -175
  16. kfinance/domains/companies/company_models.py +98 -5
  17. kfinance/domains/companies/company_tools.py +33 -29
  18. kfinance/domains/companies/tests/test_company_tools.py +11 -4
  19. kfinance/domains/competitors/competitor_tools.py +21 -21
  20. kfinance/domains/competitors/tests/test_competitor_tools.py +21 -7
  21. kfinance/domains/cusip_and_isin/cusip_and_isin_tools.py +38 -26
  22. kfinance/domains/cusip_and_isin/tests/test_cusip_and_isin_tools.py +27 -26
  23. kfinance/domains/earnings/earning_tools.py +54 -47
  24. kfinance/domains/earnings/tests/test_earnings_tools.py +58 -63
  25. kfinance/domains/line_items/line_item_tools.py +29 -36
  26. kfinance/domains/line_items/tests/test_line_item_tools.py +23 -5
  27. kfinance/domains/mergers_and_acquisitions/merger_and_acquisition_models.py +15 -0
  28. kfinance/domains/mergers_and_acquisitions/merger_and_acquisition_tools.py +55 -38
  29. kfinance/domains/mergers_and_acquisitions/tests/test_merger_and_acquisition_tools.py +22 -9
  30. kfinance/domains/prices/price_models.py +9 -9
  31. kfinance/domains/prices/price_tools.py +49 -38
  32. kfinance/domains/prices/tests/test_price_tools.py +52 -36
  33. kfinance/domains/segments/segment_models.py +7 -0
  34. kfinance/domains/segments/segment_tools.py +37 -20
  35. kfinance/domains/segments/tests/test_segment_tools.py +13 -6
  36. kfinance/domains/statements/statement_models.py +7 -0
  37. kfinance/domains/statements/statement_tools.py +38 -40
  38. kfinance/domains/statements/tests/test_statement_tools.py +39 -10
  39. kfinance/integrations/tool_calling/tests/test_tool_calling_models.py +2 -2
  40. kfinance/integrations/tool_calling/tool_calling_models.py +24 -5
  41. kfinance/version.py +2 -2
  42. {kensho_kfinance-3.0.3.dist-info → kensho_kfinance-3.1.0.dist-info}/WHEEL +0 -0
  43. {kensho_kfinance-3.0.3.dist-info → kensho_kfinance-3.1.0.dist-info}/licenses/AUTHORS.md +0 -0
  44. {kensho_kfinance-3.0.3.dist-info → kensho_kfinance-3.1.0.dist-info}/licenses/LICENSE +0 -0
  45. {kensho_kfinance-3.0.3.dist-info → kensho_kfinance-3.1.0.dist-info}/top_level.txt +0 -0
@@ -17,8 +17,8 @@ class TestGetHistoryMetadataFromIdentifiers:
17
17
  ):
18
18
  """
19
19
  GIVEN the GetHistoryMetadataFromIdentifiers tool
20
- WHEN we request the history metadata for SPGI
21
- THEN we get back SPGI's history metadata
20
+ WHEN we request the history metadata for SPGI and a non-existent company
21
+ THEN we get back SPGI's history metadata and an error for the non-existent company.
22
22
  """
23
23
  metadata_resp = {
24
24
  "currency": "USD",
@@ -27,7 +27,12 @@ class TestGetHistoryMetadataFromIdentifiers:
27
27
  "instrument_type": "Equity",
28
28
  "symbol": "SPGI",
29
29
  }
30
- expected_resp = {"SPGI": metadata_resp}
30
+ expected_resp = {
31
+ "results": {"SPGI": metadata_resp},
32
+ "errors": [
33
+ "No identification triple found for the provided identifier: NON-EXISTENT of type: ticker"
34
+ ],
35
+ }
31
36
 
32
37
  requests_mock.get(
33
38
  url=f"https://kfinance.kensho.com/api/v1/pricing/{SPGI_TRADING_ITEM_ID}/metadata",
@@ -35,11 +40,13 @@ class TestGetHistoryMetadataFromIdentifiers:
35
40
  )
36
41
 
37
42
  tool = GetHistoryMetadataFromIdentifiers(kfinance_client=mock_client)
38
- resp = tool.run(ToolArgsWithIdentifiers(identifiers=["SPGI"]).model_dump(mode="json"))
43
+ resp = tool.run(
44
+ ToolArgsWithIdentifiers(identifiers=["SPGI", "non-existent"]).model_dump(mode="json")
45
+ )
39
46
  assert resp == expected_resp
40
47
 
41
48
 
42
- class TestPricesFromIdentifiers:
49
+ class TestGetPricesFromIdentifiers:
43
50
  prices_resp = {
44
51
  "currency": "USD",
45
52
  "prices": [
@@ -65,8 +72,8 @@ class TestPricesFromIdentifiers:
65
72
  def test_get_prices_from_identifiers(self, mock_client: Client, requests_mock: Mocker):
66
73
  """
67
74
  GIVEN the GetPricesFromIdentifiers tool
68
- WHEN we request prices for SPGI
69
- THEN we get back prices for SPGI
75
+ WHEN we request prices for SPGI and a non-existent company
76
+ THEN we get back prices for SPGI and an erro for the non-existent company
70
77
  """
71
78
 
72
79
  requests_mock.get(
@@ -74,31 +81,38 @@ class TestPricesFromIdentifiers:
74
81
  json=self.prices_resp,
75
82
  )
76
83
  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
- }
84
+ "results": {
85
+ "SPGI": {
86
+ "prices": [
87
+ {
88
+ "date": "2024-04-11",
89
+ "open": {"value": "424.26", "unit": "USD"},
90
+ "high": {"value": "425.99", "unit": "USD"},
91
+ "low": {"value": "422.04", "unit": "USD"},
92
+ "close": {"value": "422.92", "unit": "USD"},
93
+ "volume": {"value": "1129158", "unit": "Shares"},
94
+ },
95
+ {
96
+ "date": "2024-04-12",
97
+ "open": {"value": "419.23", "unit": "USD"},
98
+ "high": {"value": "421.94", "unit": "USD"},
99
+ "low": {"value": "416.45", "unit": "USD"},
100
+ "close": {"value": "417.81", "unit": "USD"},
101
+ "volume": {"value": "1182229", "unit": "Shares"},
102
+ },
103
+ ]
104
+ }
105
+ },
106
+ "errors": [
107
+ "No identification triple found for the provided identifier: NON-EXISTENT of type: ticker"
108
+ ],
97
109
  }
98
110
 
99
111
  tool = GetPricesFromIdentifiers(kfinance_client=mock_client)
100
112
  response = tool.run(
101
- GetPricesFromIdentifiersArgs(identifiers=["SPGI"]).model_dump(mode="json")
113
+ GetPricesFromIdentifiersArgs(identifiers=["SPGI", "non-existent"]).model_dump(
114
+ mode="json"
115
+ )
102
116
  )
103
117
  assert response == expected_response
104
118
 
@@ -119,18 +133,20 @@ class TestPricesFromIdentifiers:
119
133
  expected_single_company_response = {
120
134
  "prices": [
121
135
  {
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"},
136
+ "date": "2024-04-12",
137
+ "open": {"value": "419.23", "unit": "USD"},
138
+ "high": {"value": "421.94", "unit": "USD"},
139
+ "low": {"value": "416.45", "unit": "USD"},
140
+ "close": {"value": "417.81", "unit": "USD"},
141
+ "volume": {"value": "1182229", "unit": "Shares"},
128
142
  }
129
143
  ]
130
144
  }
131
145
  expected_response = {
132
- "C_1": expected_single_company_response,
133
- "C_2": expected_single_company_response,
146
+ "results": {
147
+ "C_1": expected_single_company_response,
148
+ "C_2": expected_single_company_response,
149
+ },
134
150
  }
135
151
  tool = GetPricesFromIdentifiers(kfinance_client=mock_client)
136
152
  response = tool.run(
@@ -1,3 +1,6 @@
1
+ from typing import Any
2
+
3
+ from pydantic import BaseModel
1
4
  from strenum import StrEnum
2
5
 
3
6
 
@@ -6,3 +9,7 @@ class SegmentType(StrEnum):
6
9
 
7
10
  business = "business"
8
11
  geographic = "geographic"
12
+
13
+
14
+ class SegmentsResp(BaseModel):
15
+ segments: dict[str, Any]
@@ -5,14 +5,11 @@ from pydantic import BaseModel, Field
5
5
  from kfinance.client.batch_request_handling import Task, process_tasks_in_thread_pool_executor
6
6
  from kfinance.client.models.date_and_period_models import PeriodType
7
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
8
+ from kfinance.domains.segments.segment_models import SegmentsResp, SegmentType
13
9
  from kfinance.integrations.tool_calling.tool_calling_models import (
14
10
  KfinanceTool,
15
11
  ToolArgsWithIdentifiers,
12
+ ToolRespWithErrors,
16
13
  ValidQuarter,
17
14
  )
18
15
 
@@ -27,6 +24,10 @@ class GetSegmentsFromIdentifiersArgs(ToolArgsWithIdentifiers):
27
24
  end_quarter: ValidQuarter | None = Field(default=None, description="Ending quarter")
28
25
 
29
26
 
27
+ class GetSegmentsFromIdentifiersResp(ToolRespWithErrors):
28
+ results: dict[str, SegmentsResp]
29
+
30
+
30
31
  class GetSegmentsFromIdentifiers(KfinanceTool):
31
32
  name: str = "get_segments_from_identifiers"
32
33
  description: str = "Get the templated segments associated with a list of identifiers."
@@ -43,17 +44,33 @@ class GetSegmentsFromIdentifiers(KfinanceTool):
43
44
  start_quarter: Literal[1, 2, 3, 4] | None = None,
44
45
  end_quarter: Literal[1, 2, 3, 4] | None = None,
45
46
  ) -> dict:
47
+ """Sample Response:
48
+
49
+ {
50
+ 'results': {
51
+ 'SPGI': {
52
+ 'segments': {
53
+ '2021': {
54
+ 'Commodity Insights': {'CAPEX': -2000000.0, 'D&A': 12000000.0},
55
+ 'Unallocated Assets Held for Sale': {'Total Assets': 321000000.0}
56
+ }
57
+ }
58
+ }
59
+ },
60
+ 'errors': ['No identification triple found for the provided identifier: NON-EXISTENT of type: ticker']
61
+ }
62
+
63
+
64
+ """
65
+
46
66
  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
- )
67
+ id_triple_resp = api_client.unified_fetch_id_triples(identifiers=identifiers)
51
68
 
52
69
  tasks = [
53
70
  Task(
54
71
  func=api_client.fetch_segments,
55
72
  kwargs=dict(
56
- company_id=company_id,
73
+ company_id=id_triple.company_id,
57
74
  segment_type=segment_type,
58
75
  period_type=period_type,
59
76
  start_year=start_year,
@@ -63,10 +80,10 @@ class GetSegmentsFromIdentifiers(KfinanceTool):
63
80
  ),
64
81
  result_key=identifier,
65
82
  )
66
- for identifier, company_id in identifiers_to_company_ids.items()
83
+ for identifier, id_triple in id_triple_resp.identifiers_to_id_triples.items()
67
84
  ]
68
85
 
69
- segments_responses = process_tasks_in_thread_pool_executor(
86
+ segments_responses: dict[str, SegmentsResp] = process_tasks_in_thread_pool_executor(
70
87
  api_client=api_client, tasks=tasks
71
88
  )
72
89
 
@@ -78,14 +95,14 @@ class GetSegmentsFromIdentifiers(KfinanceTool):
78
95
  and end_year is None
79
96
  and start_quarter is None
80
97
  and end_quarter is None
81
- and len(identifiers) > 1
98
+ and len(segments_responses) > 1
82
99
  ):
83
100
  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}
101
+ most_recent_year = max(segments_response.segments.keys())
102
+ most_recent_year_data = segments_response.segments[most_recent_year]
103
+ segments_response.segments = {most_recent_year: most_recent_year_data}
87
104
 
88
- return {
89
- str(identifier): segments["segments"]
90
- for identifier, segments in segments_responses.items()
91
- }
105
+ output_model = GetSegmentsFromIdentifiersResp(
106
+ results=segments_responses, errors=list(id_triple_resp.errors.values())
107
+ )
108
+ return output_model.model_dump(mode="json")
@@ -33,8 +33,8 @@ class TestGetSegmentsFromIdentifier:
33
33
  def test_get_segments_from_identifier(self, mock_client: Client, requests_mock: Mocker):
34
34
  """
35
35
  GIVEN the GetSegmentsFromIdentifier tool
36
- WHEN we request the SPGI business segment
37
- THEN we get back the SPGI business segment
36
+ WHEN we request the business segment for SPGI and an non-existent company
37
+ THEN we get back the SPGI business segment and an error for the non-existent company.
38
38
  """
39
39
 
40
40
  requests_mock.get(
@@ -43,11 +43,16 @@ class TestGetSegmentsFromIdentifier:
43
43
  json=self.segments_response,
44
44
  )
45
45
 
46
- expected_response = {"SPGI": self.segments_response["segments"]}
46
+ expected_response = {
47
+ "results": {"SPGI": self.segments_response},
48
+ "errors": [
49
+ "No identification triple found for the provided identifier: NON-EXISTENT of type: ticker"
50
+ ],
51
+ }
47
52
 
48
53
  tool = GetSegmentsFromIdentifiers(kfinance_client=mock_client)
49
54
  args = GetSegmentsFromIdentifiersArgs(
50
- identifiers=["SPGI"], segment_type=SegmentType.business
55
+ identifiers=["SPGI", "non-existent"], segment_type=SegmentType.business
51
56
  )
52
57
  response = tool.run(args.model_dump(mode="json"))
53
58
  assert response == expected_response
@@ -61,8 +66,10 @@ class TestGetSegmentsFromIdentifier:
61
66
 
62
67
  company_ids = [1, 2]
63
68
  expected_response = {
64
- "C_1": {"2021": self.segments_response["segments"]["2021"]},
65
- "C_2": {"2021": self.segments_response["segments"]["2021"]},
69
+ "results": {
70
+ "C_1": {"segments": {"2021": self.segments_response["segments"]["2021"]}},
71
+ "C_2": {"segments": {"2021": self.segments_response["segments"]["2021"]}},
72
+ }
66
73
  }
67
74
 
68
75
  for company_id in company_ids:
@@ -1,3 +1,6 @@
1
+ from typing import Any
2
+
3
+ from pydantic import BaseModel
1
4
  from strenum import StrEnum
2
5
 
3
6
 
@@ -7,3 +10,7 @@ class StatementType(StrEnum):
7
10
  balance_sheet = "balance_sheet"
8
11
  income_statement = "income_statement"
9
12
  cashflow = "cashflow"
13
+
14
+
15
+ class StatementsResp(BaseModel):
16
+ statements: dict[str, Any]
@@ -1,21 +1,16 @@
1
1
  from textwrap import dedent
2
2
  from typing import Literal, Type
3
3
 
4
- import numpy as np
5
- import pandas as pd
6
4
  from pydantic import BaseModel, Field
7
5
 
8
6
  from kfinance.client.batch_request_handling import Task, process_tasks_in_thread_pool_executor
9
7
  from kfinance.client.models.date_and_period_models import PeriodType
10
8
  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
9
+ from kfinance.domains.statements.statement_models import StatementsResp, StatementType
16
10
  from kfinance.integrations.tool_calling.tool_calling_models import (
17
11
  KfinanceTool,
18
12
  ToolArgsWithIdentifiers,
13
+ ToolRespWithErrors,
19
14
  )
20
15
 
21
16
 
@@ -29,6 +24,10 @@ class GetFinancialStatementFromIdentifiersArgs(ToolArgsWithIdentifiers):
29
24
  end_quarter: Literal[1, 2, 3, 4] | None = Field(default=None, description="Ending quarter")
30
25
 
31
26
 
27
+ class GetFinancialStatementFromIdentifiersResp(ToolRespWithErrors):
28
+ results: dict[str, StatementsResp]
29
+
30
+
32
31
  class GetFinancialStatementFromIdentifiers(KfinanceTool):
33
32
  name: str = "get_financial_statement_from_identifiers"
34
33
  description: str = dedent("""
@@ -59,23 +58,25 @@ class GetFinancialStatementFromIdentifiers(KfinanceTool):
59
58
  """Sample response:
60
59
 
61
60
  {
62
- 'SPGI': {
63
- 'Revenues': {'2020': 7442000000.0, '2021': 8243000000.0},
64
- 'Total Revenues': {'2020': 7442000000.0, '2021': 8243000000.0}
65
- }
61
+ 'results': {
62
+ 'SPGI': {
63
+ 'statements': {
64
+ '2020': {'Revenues': '7442000000.000000', 'Total Revenues': '7442000000.000000'},
65
+ '2021': {'Revenues': '8243000000.000000', 'Total Revenues': '8243000000.000000'}
66
+ }
67
+ }
68
+ },
69
+ 'errors': ['No identification triple found for the provided identifier: NON-EXISTENT of type: ticker']
66
70
  }
67
71
  """
68
72
  api_client = self.kfinance_client.kfinance_api_client
69
- parsed_identifiers = parse_identifiers(identifiers, api_client=api_client)
70
- identifiers_to_company_ids = fetch_company_ids_from_identifiers(
71
- identifiers=parsed_identifiers, api_client=api_client
72
- )
73
+ id_triple_resp = api_client.unified_fetch_id_triples(identifiers=identifiers)
73
74
 
74
75
  tasks = [
75
76
  Task(
76
77
  func=api_client.fetch_statement,
77
78
  kwargs=dict(
78
- company_id=company_id,
79
+ company_id=id_triple.company_id,
79
80
  statement_type=statement.value,
80
81
  period_type=period_type,
81
82
  start_year=start_year,
@@ -85,32 +86,29 @@ class GetFinancialStatementFromIdentifiers(KfinanceTool):
85
86
  ),
86
87
  result_key=identifier,
87
88
  )
88
- for identifier, company_id in identifiers_to_company_ids.items()
89
+ for identifier, id_triple in id_triple_resp.identifiers_to_id_triples.items()
89
90
  ]
90
91
 
91
- statement_responses = process_tasks_in_thread_pool_executor(
92
+ statement_responses: dict[str, StatementsResp] = process_tasks_in_thread_pool_executor(
92
93
  api_client=api_client, tasks=tasks
93
94
  )
94
95
 
95
- output = dict()
96
- for identifier, result in statement_responses.items():
97
- df = (
98
- pd.DataFrame(result["statements"])
99
- .apply(pd.to_numeric)
100
- .replace(np.nan, None)
101
- .transpose()
102
- )
103
- # If no date and multiple companies, only return the most recent value.
104
- # By default, we return 5 years of data, which can be too much when
105
- # returning data for many companies.
106
- if (
107
- start_year is None
108
- and end_year is None
109
- and start_quarter is None
110
- and end_quarter is None
111
- and len(identifiers) > 1
112
- ):
113
- df = df.tail(1)
114
- output[str(identifier)] = df.to_dict()
115
-
116
- return output
96
+ # If no date and multiple companies, only return the most recent value.
97
+ # By default, we return 5 years of data, which can be too much when
98
+ # returning data for many companies.
99
+ if (
100
+ start_year is None
101
+ and end_year is None
102
+ and start_quarter is None
103
+ and end_quarter is None
104
+ and len(statement_responses) > 1
105
+ ):
106
+ for statement_response in statement_responses.values():
107
+ most_recent_year = max(statement_response.statements.keys())
108
+ most_recent_year_data = statement_response.statements[most_recent_year]
109
+ statement_response.statements = {most_recent_year: most_recent_year_data}
110
+
111
+ output_model = GetFinancialStatementFromIdentifiersResp(
112
+ results=statement_responses, errors=list(id_triple_resp.errors.values())
113
+ )
114
+ return output_model.model_dump(mode="json")
@@ -23,8 +23,8 @@ class TestGetFinancialStatementFromIdentifiers:
23
23
  ):
24
24
  """
25
25
  GIVEN the GetFinancialLineItemFromIdentifiers tool
26
- WHEN we request the SPGI income statement
27
- THEN we get back the SPGI income statement
26
+ WHEN we request the income statement for SPGI and a non-existent company
27
+ THEN we get back the SPGI income statement and an error for the non-existent company.
28
28
  """
29
29
 
30
30
  requests_mock.get(
@@ -32,30 +32,59 @@ class TestGetFinancialStatementFromIdentifiers:
32
32
  json=self.statement_resp,
33
33
  )
34
34
  expected_response = {
35
- "SPGI": {
36
- "Revenues": {"2020": 7442000000.0, "2021": 8243000000.0},
37
- "Total Revenues": {"2020": 7442000000.0, "2021": 8243000000.0},
38
- }
35
+ "results": {
36
+ "SPGI": {
37
+ "statements": {
38
+ "2020": {
39
+ "Revenues": "7442000000.000000",
40
+ "Total Revenues": "7442000000.000000",
41
+ },
42
+ "2021": {
43
+ "Revenues": "8243000000.000000",
44
+ "Total Revenues": "8243000000.000000",
45
+ },
46
+ }
47
+ }
48
+ },
49
+ "errors": [
50
+ "No identification triple found for the provided identifier: NON-EXISTENT of type: ticker"
51
+ ],
39
52
  }
40
53
 
41
54
  tool = GetFinancialStatementFromIdentifiers(kfinance_client=mock_client)
42
55
  args = GetFinancialStatementFromIdentifiersArgs(
43
- identifiers=["SPGI"], statement=StatementType.income_statement
56
+ identifiers=["SPGI", "non-existent"], statement=StatementType.income_statement
44
57
  )
45
58
  response = tool.run(args.model_dump(mode="json"))
46
59
  assert response == expected_response
47
60
 
48
61
  def test_most_recent_request(self, requests_mock: Mocker, mock_client: Client) -> None:
49
62
  """
50
- GIVEN the GetFinancialLineItemFromIdentifiers tool
63
+ GIVEN the GetFinancialStatementFromIdentifiers tool
51
64
  WHEN we request most recent statement for multiple companies
52
65
  THEN we only get back the most recent statement for each company
53
66
  """
54
67
 
55
68
  company_ids = [1, 2]
56
69
  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}},
70
+ "results": {
71
+ "C_1": {
72
+ "statements": {
73
+ "2021": {
74
+ "Revenues": "8243000000.000000",
75
+ "Total Revenues": "8243000000.000000",
76
+ }
77
+ }
78
+ },
79
+ "C_2": {
80
+ "statements": {
81
+ "2021": {
82
+ "Revenues": "8243000000.000000",
83
+ "Total Revenues": "8243000000.000000",
84
+ }
85
+ }
86
+ },
87
+ }
59
88
  }
60
89
 
61
90
  for company_id in company_ids:
@@ -24,10 +24,10 @@ class TestGetEndpointsFromToolCallsWithGrounding:
24
24
  # truncated from the original
25
25
  resp_data = {"name": "S&P Global Inc.", "status": "Operating"}
26
26
  resp_endpoint = [
27
- "https://kfinance.kensho.com/api/v1/id/SPGI",
27
+ "https://kfinance.kensho.com/api/v1/ids",
28
28
  "https://kfinance.kensho.com/api/v1/info/21719",
29
29
  ]
30
- expected_resp = {"data": {"SPGI": resp_data}, "endpoint_urls": resp_endpoint}
30
+ expected_resp = {"data": {"results": {"SPGI": resp_data}}, "endpoint_urls": resp_endpoint}
31
31
 
32
32
  requests_mock.get(
33
33
  url=f"https://kfinance.kensho.com/api/v1/info/{SPGI_COMPANY_ID}",
@@ -1,7 +1,7 @@
1
- from typing import Annotated, Any, Literal, Type
1
+ from typing import Annotated, Any, Callable, Dict, Literal, Type
2
2
 
3
3
  from langchain_core.tools import BaseTool
4
- from pydantic import BaseModel, BeforeValidator, ConfigDict, Field
4
+ from pydantic import BaseModel, BeforeValidator, ConfigDict, Field, model_serializer
5
5
 
6
6
  from kfinance.client.kfinance import Client
7
7
  from kfinance.client.permission_models import Permission
@@ -65,9 +65,7 @@ class KfinanceTool(BaseTool):
65
65
  Where feasible and useful, tools should use batch processing to parallelize
66
66
  requests, usually by allowing callers to pass in multiple identifiers.
67
67
  The usual processing order for functions with multiple identifiers is:
68
- - Parse string identifiers into `CompanyIdentifier`
69
- - batch fetch the required id for the api call (company/security/trading item id) via
70
- fetch_<relevant>_ids_from_identifiers
68
+ - batch fetch id triples via unified_fetch_id_triples
71
69
  - batch fetch the required info based on the ids via process_tasks_in_thread_pool_executor
72
70
  - format results for output
73
71
 
@@ -119,3 +117,24 @@ def convert_str_to_int(v: Any) -> Any:
119
117
  # ValidationError during deserialization unless they have been converted
120
118
  # to int.
121
119
  ValidQuarter = Annotated[Literal[1, 2, 3, 4], BeforeValidator(convert_str_to_int)]
120
+
121
+
122
+ class ToolRespWithErrors(BaseModel):
123
+ """A tool response with an `errors` field.
124
+
125
+ - `errors` is always the last field in the response.
126
+ - `errors` is only included if there is at least one error.
127
+ """
128
+
129
+ errors: list[str] = Field(default_factory=list)
130
+
131
+ @model_serializer(mode="wrap")
132
+ def serialize_model(self, handler: Callable) -> Dict[str, Any]:
133
+ """Make `errors` the last response field and only include if there is at least one error."""
134
+ data = handler(self)
135
+ errors = data.pop("errors")
136
+ # data = copy(data)
137
+ # data.keys()
138
+ if errors:
139
+ data["errors"] = errors
140
+ return data
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 = '3.0.3'
21
- __version_tuple__ = version_tuple = (3, 0, 3)
20
+ __version__ = version = '3.1.0'
21
+ __version_tuple__ = version_tuple = (3, 1, 0)