kensho-kfinance 3.2.4__py3-none-any.whl → 4.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.
Files changed (57) hide show
  1. {kensho_kfinance-3.2.4.dist-info → kensho_kfinance-4.0.0.dist-info}/METADATA +3 -3
  2. {kensho_kfinance-3.2.4.dist-info → kensho_kfinance-4.0.0.dist-info}/RECORD +57 -56
  3. kfinance/CHANGELOG.md +51 -0
  4. kfinance/client/batch_request_handling.py +3 -1
  5. kfinance/client/fetch.py +127 -54
  6. kfinance/client/kfinance.py +38 -39
  7. kfinance/client/meta_classes.py +50 -20
  8. kfinance/client/models/date_and_period_models.py +32 -7
  9. kfinance/client/models/decimal_with_unit.py +14 -2
  10. kfinance/client/models/response_models.py +33 -0
  11. kfinance/client/models/tests/test_decimal_with_unit.py +9 -0
  12. kfinance/client/tests/test_batch_requests.py +5 -4
  13. kfinance/client/tests/test_fetch.py +134 -58
  14. kfinance/client/tests/test_objects.py +207 -145
  15. kfinance/conftest.py +10 -0
  16. kfinance/domains/business_relationships/business_relationship_tools.py +17 -8
  17. kfinance/domains/business_relationships/tests/test_business_relationship_tools.py +18 -16
  18. kfinance/domains/capitalizations/capitalization_models.py +7 -5
  19. kfinance/domains/capitalizations/capitalization_tools.py +38 -20
  20. kfinance/domains/capitalizations/tests/test_capitalization_tools.py +66 -36
  21. kfinance/domains/companies/company_models.py +22 -2
  22. kfinance/domains/companies/company_tools.py +49 -16
  23. kfinance/domains/companies/tests/test_company_tools.py +27 -9
  24. kfinance/domains/competitors/competitor_tools.py +19 -5
  25. kfinance/domains/competitors/tests/test_competitor_tools.py +22 -19
  26. kfinance/domains/cusip_and_isin/cusip_and_isin_tools.py +29 -8
  27. kfinance/domains/cusip_and_isin/tests/test_cusip_and_isin_tools.py +13 -8
  28. kfinance/domains/earnings/earning_tools.py +73 -29
  29. kfinance/domains/earnings/tests/test_earnings_tools.py +52 -43
  30. kfinance/domains/line_items/line_item_models.py +372 -16
  31. kfinance/domains/line_items/line_item_tools.py +198 -46
  32. kfinance/domains/line_items/tests/test_line_item_tools.py +305 -39
  33. kfinance/domains/mergers_and_acquisitions/merger_and_acquisition_models.py +46 -2
  34. kfinance/domains/mergers_and_acquisitions/merger_and_acquisition_tools.py +55 -74
  35. kfinance/domains/mergers_and_acquisitions/tests/test_merger_and_acquisition_tools.py +61 -59
  36. kfinance/domains/prices/price_models.py +7 -6
  37. kfinance/domains/prices/price_tools.py +24 -16
  38. kfinance/domains/prices/tests/test_price_tools.py +47 -39
  39. kfinance/domains/segments/segment_models.py +17 -3
  40. kfinance/domains/segments/segment_tools.py +102 -42
  41. kfinance/domains/segments/tests/test_segment_tools.py +166 -37
  42. kfinance/domains/statements/statement_models.py +17 -3
  43. kfinance/domains/statements/statement_tools.py +130 -46
  44. kfinance/domains/statements/tests/test_statement_tools.py +251 -49
  45. kfinance/integrations/local_mcp/kfinance_mcp.py +1 -1
  46. kfinance/integrations/tests/test_example_notebook.py +57 -16
  47. kfinance/integrations/tool_calling/all_tools.py +5 -1
  48. kfinance/integrations/tool_calling/static_tools/get_n_quarters_ago.py +5 -0
  49. kfinance/integrations/tool_calling/static_tools/tests/test_get_lastest.py +13 -10
  50. kfinance/integrations/tool_calling/static_tools/tests/test_get_n_quarters_ago.py +2 -1
  51. kfinance/integrations/tool_calling/tests/test_tool_calling_models.py +15 -4
  52. kfinance/integrations/tool_calling/tool_calling_models.py +18 -6
  53. kfinance/version.py +2 -2
  54. {kensho_kfinance-3.2.4.dist-info → kensho_kfinance-4.0.0.dist-info}/WHEEL +0 -0
  55. {kensho_kfinance-3.2.4.dist-info → kensho_kfinance-4.0.0.dist-info}/licenses/AUTHORS.md +0 -0
  56. {kensho_kfinance-3.2.4.dist-info → kensho_kfinance-4.0.0.dist-info}/licenses/LICENSE +0 -0
  57. {kensho_kfinance-3.2.4.dist-info → kensho_kfinance-4.0.0.dist-info}/top_level.txt +0 -0
@@ -1,14 +1,17 @@
1
+ from difflib import SequenceMatcher
1
2
  from textwrap import dedent
2
3
  from typing import Literal, Type
3
4
 
4
- from pydantic import BaseModel, Field
5
+ from pydantic import BaseModel, Field, model_validator
5
6
 
6
- from kfinance.client.batch_request_handling import Task, process_tasks_in_thread_pool_executor
7
- from kfinance.client.models.date_and_period_models import PeriodType
7
+ from kfinance.client.models.date_and_period_models import NumPeriods, NumPeriodsBack, PeriodType
8
8
  from kfinance.client.permission_models import Permission
9
9
  from kfinance.domains.line_items.line_item_models import (
10
10
  LINE_ITEM_NAMES_AND_ALIASES,
11
- LineItemResponse,
11
+ LINE_ITEM_TO_DESCRIPTIONS_MAP,
12
+ CalendarType,
13
+ LineItemResp,
14
+ LineItemScore,
12
15
  )
13
16
  from kfinance.integrations.tool_calling.tool_calling_models import (
14
17
  KfinanceTool,
@@ -18,6 +21,75 @@ from kfinance.integrations.tool_calling.tool_calling_models import (
18
21
  )
19
22
 
20
23
 
24
+ def _find_similar_line_items(
25
+ invalid_item: str, descriptors: dict[str, str], max_suggestions: int = 8
26
+ ) -> list[LineItemScore]:
27
+ """Find similar line items using keyword matching and string similarity.
28
+
29
+ Args:
30
+ invalid_item: The invalid line item provided by the user
31
+ descriptors: Dictionary mapping line item names to descriptions
32
+ max_suggestions: Maximum number of suggestions to return
33
+
34
+ Returns:
35
+ List of LineItemScore objects for the best matches
36
+ """
37
+ if not descriptors:
38
+ return []
39
+
40
+ invalid_lower = invalid_item.lower()
41
+ scores: list[LineItemScore] = []
42
+
43
+ for line_item, description in descriptors.items():
44
+ # Calculate similarity scores
45
+ name_similarity = SequenceMatcher(None, invalid_lower, line_item.lower()).ratio()
46
+
47
+ # Check for keyword matches in the line item name
48
+ invalid_words = set(invalid_lower.replace("_", " ").split())
49
+ item_words = set(line_item.lower().replace("_", " ").split())
50
+ keyword_match_score = len(invalid_words.intersection(item_words)) / max(
51
+ len(invalid_words), 1
52
+ )
53
+
54
+ # Check for keyword matches in description
55
+ description_words = set(description.lower().split())
56
+ description_match_score = len(invalid_words.intersection(description_words)) / max(
57
+ len(invalid_words), 1
58
+ )
59
+
60
+ # Combined score (weighted)
61
+ total_score = (
62
+ name_similarity * 0.5 # Direct name similarity
63
+ + keyword_match_score * 0.3 # Keyword matches in name
64
+ + description_match_score * 0.2 # Keyword matches in description
65
+ )
66
+
67
+ scores.append(LineItemScore(name=line_item, description=description, score=total_score))
68
+
69
+ # Sort by score (descending) and return top matches
70
+ scores.sort(reverse=True, key=lambda x: x.score)
71
+ return [item for item in scores[:max_suggestions] if item.score > 0.1]
72
+
73
+
74
+ def _smart_line_item_validator(v: str) -> str:
75
+ """Custom validator that provides intelligent suggestions for invalid line items."""
76
+ if v not in LINE_ITEM_NAMES_AND_ALIASES:
77
+ # Find similar items using pre-computed descriptors
78
+ suggestions = _find_similar_line_items(v, LINE_ITEM_TO_DESCRIPTIONS_MAP)
79
+
80
+ if suggestions:
81
+ suggestion_text = "\n\nDid you mean one of these?\n"
82
+ for item in suggestions:
83
+ suggestion_text += f" • '{item.name}': {item.description}\n"
84
+
85
+ error_msg = f"Invalid line_item '{v}'.{suggestion_text}"
86
+ else:
87
+ error_msg = f"Invalid line_item '{v}'. Please refer to the tool documentation for valid options."
88
+
89
+ raise ValueError(error_msg)
90
+ return v
91
+
92
+
21
93
  class GetFinancialLineItemFromIdentifiersArgs(ToolArgsWithIdentifiers):
22
94
  # Note: mypy will not enforce this literal because of the type: ignore.
23
95
  # But pydantic still uses the literal to check for allowed values and only includes
@@ -25,15 +97,47 @@ class GetFinancialLineItemFromIdentifiersArgs(ToolArgsWithIdentifiers):
25
97
  line_item: Literal[tuple(LINE_ITEM_NAMES_AND_ALIASES)] = Field( # type: ignore[valid-type]
26
98
  description="The type of financial line_item requested"
27
99
  )
28
- period_type: PeriodType | None = Field(default=None, description="The period type")
29
- start_year: int | None = Field(default=None, description="The starting year for the data range")
30
- end_year: int | None = Field(default=None, description="The ending year for the data range")
31
- start_quarter: ValidQuarter | None = Field(default=None, description="Starting quarter")
32
- end_quarter: ValidQuarter | None = Field(default=None, description="Ending quarter")
100
+ period_type: PeriodType | None = Field(
101
+ default=None, description="The period type (annual or quarterly)"
102
+ )
103
+ start_year: int | None = Field(
104
+ default=None,
105
+ description="The starting year for the data range. Use null for the most recent data.",
106
+ )
107
+ end_year: int | None = Field(
108
+ default=None,
109
+ description="The ending year for the data range. Use null for the most recent data.",
110
+ )
111
+ start_quarter: ValidQuarter | None = Field(
112
+ default=None, description="Starting quarter (1-4). Only used when period_type is quarterly."
113
+ )
114
+ end_quarter: ValidQuarter | None = Field(
115
+ default=None, description="Ending quarter (1-4). Only used when period_type is quarterly."
116
+ )
117
+ calendar_type: CalendarType | None = Field(
118
+ default=None, description="Fiscal year or calendar year"
119
+ )
120
+ num_periods: NumPeriods | None = Field(
121
+ default=None, description="The number of periods to retrieve data for (1-99)"
122
+ )
123
+ num_periods_back: NumPeriodsBack | None = Field(
124
+ default=None,
125
+ description="The end period of the data range expressed as number of periods back relative to the present period (0-99)",
126
+ )
127
+
128
+ @model_validator(mode="before")
129
+ @classmethod
130
+ def validate_line_item_with_suggestions(cls, values: dict) -> dict:
131
+ """Custom validator that provides intelligent suggestions for invalid line items."""
132
+ if isinstance(values, dict) and "line_item" in values:
133
+ line_item = values["line_item"]
134
+ # Use the helper function to validate and provide suggestions
135
+ _smart_line_item_validator(line_item)
136
+ return values
33
137
 
34
138
 
35
139
  class GetFinancialLineItemFromIdentifiersResp(ToolRespWithErrors):
36
- results: dict[str, LineItemResponse]
140
+ results: dict[str, LineItemResp] # identifier -> response
37
141
 
38
142
 
39
143
  class GetFinancialLineItemFromIdentifiers(KfinanceTool):
@@ -42,11 +146,27 @@ class GetFinancialLineItemFromIdentifiers(KfinanceTool):
42
146
  Get the financial line item associated with a list of identifiers.
43
147
 
44
148
  - When possible, pass multiple identifiers in a single call rather than making multiple calls.
45
- - To fetch the most recent value for the line item, leave start_year, start_quarter, end_year, and end_quarter as None.
149
+ - To fetch the most recent value, leave start_year, start_quarter, end_year, end_quarter, num_periods, and num_periods_back as null.
150
+ - The tool accepts an optional calendar_type argument, which can either be 'calendar' or 'fiscal'. If 'calendar' is chosen, then start_year and end_year will filter on calendar year, and the output returned will be in calendar years. If 'fiscal' is chosen (which is the default), then start_year and end_year will filter on fiscal year, and the output returned will be in fiscal years.
151
+ - All aliases for a line item return identical data (e.g., 'revenue', 'normal_revenue', and 'regular_revenue' return the same data).
152
+ - Line item names are case-insensitive and use underscores (e.g., 'total_revenue' not 'Total Revenue').
153
+ - To filter by time, use either absolute (start_year, end_year, start_quarter, end_quarter) for specific dates like "in 2023" or "Q2 2021", OR relative (num_periods, num_periods_back) for phrases like "last 3 quarters" or "past five years"—but not both.
46
154
 
47
- Example:
155
+ Examples:
48
156
  Query: "What are the revenues of Lowe's and Home Depot?"
49
- Function: get_financial_line_item_from_identifiers(line_item="revenue", company_ids=["LW", "HD"])
157
+ Function: get_financial_line_item_from_identifiers(line_item="revenue", identifiers=["Lowe's", "Home Depot"])
158
+
159
+ Query: "Get MSFT and AAPL revenue"
160
+ Function: get_financial_line_item_from_identifiers(line_item="revenue", identifiers=["MSFT", "AAPL"])
161
+
162
+ Query: "General Eletrics's ebt excluding unusual items for 2023"
163
+ Function: get_financial_line_item_from_identifiers(line_item="ebt_excluding_unusual_items", identifiers=["General Eletric"], period_type="annual", start_year=2023, end_year=2023)
164
+
165
+ Query: "What is the most recent three quarters' but one ppe for Exxon and Hasbro?"
166
+ Function: get_financial_line_item_from_identifiers(line_item="ppe", period_type="quarterly", num_periods=3, num_periods_back=1, identifiers=["Exxon", "Hasbro"])
167
+
168
+ Query: "What are the ytd operating income values for Hilton for the calendar year 2022?"
169
+ Function: get_financial_line_item_from_identifiers(line_item="operating_income", period_type="ytd", calendar_type="calendar", start_year=2022, end_year=2022, identifiers=["Hilton"])
50
170
  """).strip()
51
171
  args_schema: Type[BaseModel] = GetFinancialLineItemFromIdentifiersArgs
52
172
  accepted_permissions: set[Permission] | None = {
@@ -63,41 +183,72 @@ class GetFinancialLineItemFromIdentifiers(KfinanceTool):
63
183
  end_year: int | None = None,
64
184
  start_quarter: Literal[1, 2, 3, 4] | None = None,
65
185
  end_quarter: Literal[1, 2, 3, 4] | None = None,
66
- ) -> dict:
186
+ calendar_type: CalendarType | None = None,
187
+ num_periods: int | None = None,
188
+ num_periods_back: int | None = None,
189
+ ) -> GetFinancialLineItemFromIdentifiersResp:
67
190
  """Sample response:
68
191
 
69
192
  {
70
193
  'SPGI': {
71
- '2022': {'revenue': 11181000000.0},
72
- '2023': {'revenue': 12497000000.0},
73
- '2024': {'revenue': 14208000000.0}
194
+ 'currency': 'USD',
195
+ 'periods': {
196
+ 'FY2022': {
197
+ 'period_end_date': '2022-12-31',
198
+ 'num_months': 12,
199
+ 'line_item': {
200
+ 'name': 'Revenue',
201
+ 'value': 11181000000.0,
202
+ 'sources': [
203
+ {
204
+ 'type': 'doc-viewer line item',
205
+ 'url': 'https://www.capitaliq.spglobal.com/...'
206
+ }
207
+ ]
208
+ }
209
+ },
210
+ 'FY2023': {
211
+ 'period_end_date': '2023-12-31',
212
+ 'num_months': 12,
213
+ 'line_item': {
214
+ 'name': 'Revenue',
215
+ 'value': 12497000000.0,
216
+ 'sources': [
217
+ {
218
+ 'type': 'doc-viewer line item',
219
+ 'url': 'https://www.capitaliq.spglobal.com/...'
220
+ }
221
+ ]
222
+ }
223
+ }
224
+ }
74
225
  }
75
226
  }
227
+
76
228
  """
77
229
  api_client = self.kfinance_client.kfinance_api_client
78
- id_triple_resp = api_client.unified_fetch_id_triples(identifiers=identifiers)
79
-
80
- tasks = [
81
- Task(
82
- func=api_client.fetch_line_item,
83
- kwargs=dict(
84
- company_id=id_triple.company_id,
85
- line_item=line_item,
86
- period_type=period_type,
87
- start_year=start_year,
88
- end_year=end_year,
89
- start_quarter=start_quarter,
90
- end_quarter=end_quarter,
91
- ),
92
- result_key=identifier,
93
- )
94
- for identifier, id_triple in id_triple_resp.identifiers_to_id_triples.items()
95
- ]
96
-
97
- line_item_responses: dict[str, LineItemResponse] = process_tasks_in_thread_pool_executor(
98
- api_client=api_client, tasks=tasks
230
+
231
+ # First resolve identifiers to company IDs
232
+ ids_response = api_client.unified_fetch_id_triples(identifiers)
233
+
234
+ response = api_client.fetch_line_item(
235
+ company_ids=ids_response.company_ids,
236
+ line_item=line_item,
237
+ period_type=period_type,
238
+ start_year=start_year,
239
+ end_year=end_year,
240
+ start_quarter=start_quarter,
241
+ end_quarter=end_quarter,
242
+ calendar_type=calendar_type,
243
+ num_periods=num_periods,
244
+ num_periods_back=num_periods_back,
99
245
  )
100
246
 
247
+ identifier_to_results = {}
248
+ for company_id_str, line_item_resp in response.results.items():
249
+ original_identifier = ids_response.get_identifier_from_company_id(int(company_id_str))
250
+ identifier_to_results[original_identifier] = line_item_resp
251
+
101
252
  # If no date and multiple companies, only return the most recent value.
102
253
  # By default, we return 5 years of data, which can be too much when
103
254
  # returning data for many companies.
@@ -106,14 +257,15 @@ class GetFinancialLineItemFromIdentifiers(KfinanceTool):
106
257
  and end_year is None
107
258
  and start_quarter is None
108
259
  and end_quarter is None
109
- and len(line_item_responses) > 1
260
+ and num_periods is None
261
+ and num_periods_back is None
262
+ and len(identifier_to_results) > 1
110
263
  ):
111
- for line_item_response in line_item_responses.values():
112
- most_recent_year = max(line_item_response.line_item.keys())
113
- most_recent_year_data = line_item_response.line_item[most_recent_year]
114
- line_item_response.line_item = {most_recent_year: most_recent_year_data}
264
+ for line_item_response in identifier_to_results.values():
265
+ line_item_response.remove_all_periods_other_than_the_most_recent_one()
266
+
267
+ all_errors = list(ids_response.errors.values()) + list(response.errors.values())
115
268
 
116
- output_model = GetFinancialLineItemFromIdentifiersResp(
117
- results=line_item_responses, errors=list(id_triple_resp.errors.values())
269
+ return GetFinancialLineItemFromIdentifiersResp(
270
+ results=identifier_to_results, errors=all_errors
118
271
  )
119
- return output_model.model_dump(mode="json")