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
@@ -14,13 +14,13 @@ from kfinance.client.kfinance import (
14
14
  BusinessRelationships,
15
15
  Company,
16
16
  Earnings,
17
- MergerOrAcquisition,
18
17
  ParticipantInMerger,
19
18
  Security,
20
19
  Ticker,
21
20
  TradingItem,
22
21
  Transcript,
23
22
  )
23
+ from kfinance.client.models.response_models import PostResponse
24
24
  from kfinance.domains.business_relationships.business_relationship_models import (
25
25
  BusinessRelationshipType,
26
26
  RelationshipResponse,
@@ -28,8 +28,11 @@ from kfinance.domains.business_relationships.business_relationship_models import
28
28
  from kfinance.domains.capitalizations.capitalization_models import Capitalizations
29
29
  from kfinance.domains.companies.company_models import CompanyIdAndName, IdentificationTriple
30
30
  from kfinance.domains.earnings.earning_models import EarningsCallResp
31
- from kfinance.domains.line_items.line_item_models import LineItemResponse
32
- from kfinance.domains.mergers_and_acquisitions.merger_and_acquisition_models import MergersResp
31
+ from kfinance.domains.line_items.line_item_models import LineItemResp
32
+ from kfinance.domains.mergers_and_acquisitions.merger_and_acquisition_models import (
33
+ MergerInfo,
34
+ MergersResp,
35
+ )
33
36
  from kfinance.domains.prices.price_models import HistoryMetadataResp
34
37
  from kfinance.domains.segments.segment_models import SegmentsResp
35
38
  from kfinance.domains.statements.statement_models import StatementsResp
@@ -103,36 +106,103 @@ MOCK_COMPANY_DB = {
103
106
  }
104
107
  ),
105
108
  "line_items": {
106
- "revenue": LineItemResponse.model_validate(
109
+ "revenue": LineItemResp.model_validate(
107
110
  {
108
- "line_item": {
109
- "2019": "125843000000.000000",
110
- "2020": "143015000000.000000",
111
- "2021": "168088000000.000000",
112
- "2022": "198270000000.000000",
113
- "2023": "211915000000.000000",
114
- }
111
+ "currency": "USD",
112
+ "periods": {
113
+ "CY2019": {
114
+ "period_end_date": "2019-12-31",
115
+ "num_months": 12,
116
+ "line_item": {
117
+ "name": "Revenue",
118
+ "value": "125843000000.000000",
119
+ "sources": [],
120
+ },
121
+ },
122
+ "CY2020": {
123
+ "period_end_date": "2020-12-31",
124
+ "num_months": 12,
125
+ "line_item": {
126
+ "name": "Revenue",
127
+ "value": "143015000000.000000",
128
+ "sources": [],
129
+ },
130
+ },
131
+ "CY2021": {
132
+ "period_end_date": "2021-12-31",
133
+ "num_months": 12,
134
+ "line_item": {
135
+ "name": "Revenue",
136
+ "value": "168088000000.000000",
137
+ "sources": [],
138
+ },
139
+ },
140
+ "CY2022": {
141
+ "period_end_date": "2022-12-31",
142
+ "num_months": 12,
143
+ "line_item": {
144
+ "name": "Revenue",
145
+ "value": "198270000000.000000",
146
+ "sources": [],
147
+ },
148
+ },
149
+ "CY2023": {
150
+ "period_end_date": "2023-12-31",
151
+ "num_months": 12,
152
+ "line_item": {
153
+ "name": "Revenue",
154
+ "value": "211915000000.000000",
155
+ "sources": [],
156
+ },
157
+ },
158
+ },
115
159
  }
116
160
  )
117
161
  },
118
162
  "segments": SegmentsResp.model_validate(
119
163
  {
120
- "segments": {
121
- "2024": {
122
- "Intelligent Cloud": {
123
- "Operating Income": 49584000000.0,
124
- "Revenue": 105362000000.0,
125
- },
126
- "More Personal Computing": {
127
- "Operating Income": 19309000000.0,
128
- "Revenue": 62032000000.0,
129
- },
130
- "Productivity and Business Processes": {
131
- "Operating Income": 40540000000.0,
132
- "Revenue": 77728000000.0,
133
- },
164
+ "currency": "USD",
165
+ "periods": {
166
+ "CY2024": {
167
+ "period_end_date": "2024-12-31",
168
+ "num_months": 12,
169
+ "segments": [
170
+ {
171
+ "name": "Intelligent Cloud",
172
+ "line_items": [
173
+ {
174
+ "name": "Operating Income",
175
+ "value": 49584000000.0,
176
+ "sources": [],
177
+ },
178
+ {"name": "Revenue", "value": 105362000000.0, "sources": []},
179
+ ],
180
+ },
181
+ {
182
+ "name": "More Personal Computing",
183
+ "line_items": [
184
+ {
185
+ "name": "Operating Income",
186
+ "value": 19309000000.0,
187
+ "sources": [],
188
+ },
189
+ {"name": "Revenue", "value": 62032000000.0, "sources": []},
190
+ ],
191
+ },
192
+ {
193
+ "name": "Productivity and Business Processes",
194
+ "line_items": [
195
+ {
196
+ "name": "Operating Income",
197
+ "value": 40540000000.0,
198
+ "sources": [],
199
+ },
200
+ {"name": "Revenue", "value": 77728000000.0, "sources": []},
201
+ ],
202
+ },
203
+ ],
134
204
  }
135
- }
205
+ },
136
206
  }
137
207
  ),
138
208
  "advisors": {
@@ -191,12 +261,26 @@ MOCK_TRANSCRIPT_DB = {
191
261
 
192
262
  INCOME_STATEMENT = StatementsResp.model_validate(
193
263
  {
194
- "statements": {
195
- "2019": {
196
- "Revenues": "125843000000.000000",
197
- "Total Revenues": "125843000000.000000",
264
+ "currency": "USD",
265
+ "periods": {
266
+ "CY2019": {
267
+ "period_end_date": "2019-12-31",
268
+ "num_months": 12,
269
+ "statements": [
270
+ {
271
+ "name": "Income Statement",
272
+ "line_items": [
273
+ {"name": "Revenues", "value": "125843000000.000000", "sources": []},
274
+ {
275
+ "name": "Total Revenues",
276
+ "value": "125843000000.000000",
277
+ "sources": [],
278
+ },
279
+ ],
280
+ }
281
+ ],
198
282
  }
199
- }
283
+ },
200
284
  }
201
285
  )
202
286
 
@@ -254,39 +338,41 @@ MOCK_ISIN_DB = {msft_isin: msft_id_triple.model_dump(mode="json")}
254
338
  MOCK_CUSIP_DB = {msft_cusip: msft_id_triple.model_dump(mode="json")}
255
339
 
256
340
  MOCK_MERGERS_DB = {
257
- msft_buys_mongo: {
258
- "timeline": [
259
- {"status": "Announced", "date": "2000-09-12"},
260
- {"status": "Closed", "date": "2000-09-12"},
261
- ],
262
- "participants": {
263
- "target": {"company_id": 31696, "company_name": "MongoMusic, Inc."},
264
- "buyers": [{"company_id": 21835, "company_name": "Microsoft Corporation"}],
265
- "sellers": [
266
- {"company_id": 18805, "company_name": "Angel Investors L.P."},
267
- {"company_id": 20087, "company_name": "Draper Richards, L.P."},
268
- {"company_id": 22103, "company_name": "BRV Partners, LLC"},
269
- {"company_id": 23745, "company_name": "Venture Frogs, LLC"},
270
- {"company_id": 105902, "company_name": "ARGUS Capital International Limited"},
271
- {"company_id": 880300, "company_name": "Sony Music Entertainment, Inc."},
272
- ],
273
- },
274
- "consideration": {
275
- "currency_name": "US Dollar",
276
- "current_calculated_gross_total_transaction_value": "51609375.000000",
277
- "current_calculated_implied_equity_value": "51609375.000000",
278
- "current_calculated_implied_enterprise_value": "51609375.000000",
279
- "details": [
280
- {
281
- "scenario": "Stock Lump Sum",
282
- "subtype": "Common Equity",
283
- "cash_or_cash_equivalent_per_target_share_unit": None,
284
- "number_of_target_shares_sought": "1000000.000000",
285
- "current_calculated_gross_value_of_consideration": "51609375.000000",
286
- }
341
+ msft_buys_mongo: MergerInfo.model_validate(
342
+ {
343
+ "timeline": [
344
+ {"status": "Announced", "date": "2000-09-12"},
345
+ {"status": "Closed", "date": "2000-09-12"},
287
346
  ],
288
- },
289
- }
347
+ "participants": {
348
+ "target": {"company_id": 31696, "company_name": "MongoMusic, Inc."},
349
+ "buyers": [{"company_id": 21835, "company_name": "Microsoft Corporation"}],
350
+ "sellers": [
351
+ {"company_id": 18805, "company_name": "Angel Investors L.P."},
352
+ {"company_id": 20087, "company_name": "Draper Richards, L.P."},
353
+ {"company_id": 22103, "company_name": "BRV Partners, LLC"},
354
+ {"company_id": 23745, "company_name": "Venture Frogs, LLC"},
355
+ {"company_id": 105902, "company_name": "ARGUS Capital International Limited"},
356
+ {"company_id": 880300, "company_name": "Sony Music Entertainment, Inc."},
357
+ ],
358
+ },
359
+ "consideration": {
360
+ "currency_name": "US Dollar",
361
+ "current_calculated_gross_total_transaction_value": "51609375.000000",
362
+ "current_calculated_implied_equity_value": "51609375.000000",
363
+ "current_calculated_implied_enterprise_value": "51609375.000000",
364
+ "details": [
365
+ {
366
+ "scenario": "Stock Lump Sum",
367
+ "subtype": "Common Equity",
368
+ "cash_or_cash_equivalent_per_target_share_unit": None,
369
+ "number_of_target_shares_sought": "1000000.000000",
370
+ "current_calculated_gross_value_of_consideration": "51609375.000000",
371
+ }
372
+ ],
373
+ },
374
+ }
375
+ )
290
376
  }
291
377
 
292
378
 
@@ -341,7 +427,7 @@ class MockKFinanceApiClient:
341
427
 
342
428
  def fetch_statement(
343
429
  self,
344
- company_id,
430
+ company_ids,
345
431
  statement_type,
346
432
  period_type,
347
433
  start_year,
@@ -350,13 +436,26 @@ class MockKFinanceApiClient:
350
436
  end_quarter,
351
437
  ):
352
438
  """Get a statement"""
353
- return INCOME_STATEMENT
439
+ return PostResponse[StatementsResp](
440
+ results={str(company_ids[0]): INCOME_STATEMENT}, errors={}
441
+ )
354
442
 
355
443
  def fetch_line_item(
356
- self, company_id, line_item, period_type, start_year, end_year, start_quarter, end_quarter
444
+ self,
445
+ company_ids,
446
+ line_item,
447
+ period_type,
448
+ start_year,
449
+ end_year,
450
+ start_quarter,
451
+ end_quarter,
452
+ calendar_type=None,
453
+ num_periods=None,
454
+ num_periods_back=None,
357
455
  ):
358
456
  """Get a statement"""
359
- return MOCK_COMPANY_DB[company_id]["line_items"][line_item]
457
+ line_item_resp = MOCK_COMPANY_DB[company_ids[0]]["line_items"][line_item]
458
+ return PostResponse[LineItemResp](results={str(company_ids[0]): line_item_resp}, errors={})
360
459
 
361
460
  def fetch_market_caps_tevs_and_shares_outstanding(
362
461
  self,
@@ -386,7 +485,7 @@ class MockKFinanceApiClient:
386
485
 
387
486
  def fetch_segments(
388
487
  self,
389
- company_id,
488
+ company_ids,
390
489
  segment_type,
391
490
  period_type,
392
491
  start_year,
@@ -395,7 +494,9 @@ class MockKFinanceApiClient:
395
494
  end_quarter,
396
495
  ):
397
496
  """Get a segment"""
398
- return MOCK_COMPANY_DB[company_id]["segments"]
497
+ return PostResponse[SegmentsResp](
498
+ results={str(company_ids[0]): MOCK_COMPANY_DB[company_ids[0]]["segments"]}, errors={}
499
+ )
399
500
 
400
501
  def fetch_companies_from_business_relationship(
401
502
  self, company_id: int, relationship_type: BusinessRelationshipType
@@ -519,10 +620,18 @@ class TestCompany(TestCase):
519
620
 
520
621
  def test_income_statement(self) -> None:
521
622
  """test income statement"""
623
+ # Extract statements data from the periods structure
624
+ periods_data = INCOME_STATEMENT.model_dump(mode="json")["periods"]
625
+ statements_data = {}
626
+ for period_key, period_data in periods_data.items():
627
+ period_statements = {}
628
+ for statement in period_data["statements"]:
629
+ for line_item in statement["line_items"]:
630
+ period_statements[line_item["name"]] = line_item["value"]
631
+ statements_data[period_key] = period_statements
632
+
522
633
  expected_income_statement = (
523
- pd.DataFrame(INCOME_STATEMENT.model_dump(mode="json")["statements"])
524
- .apply(pd.to_numeric)
525
- .replace(np.nan, None)
634
+ pd.DataFrame(statements_data).apply(pd.to_numeric).replace(np.nan, None)
526
635
  )
527
636
 
528
637
  income_statement = self.msft_company.company.income_statement()
@@ -530,11 +639,14 @@ class TestCompany(TestCase):
530
639
 
531
640
  def test_revenue(self) -> None:
532
641
  """test revenue"""
533
- line_item_response: LineItemResponse = MOCK_COMPANY_DB[msft_company_id]["line_items"][
534
- "revenue"
535
- ]
642
+ line_item_response: LineItemResp = MOCK_COMPANY_DB[msft_company_id]["line_items"]["revenue"]
643
+
644
+ line_item_data = {}
645
+ for period_key, period_data in line_item_response.periods.items():
646
+ line_item_data[period_key] = period_data.line_item.value
647
+
536
648
  expected_revenue = (
537
- pd.DataFrame({"line_item": line_item_response.line_item})
649
+ pd.DataFrame({"line_item": line_item_data})
538
650
  .transpose()
539
651
  .apply(pd.to_numeric)
540
652
  .replace(np.nan, None)
@@ -546,7 +658,7 @@ class TestCompany(TestCase):
546
658
  def test_business_segments(self) -> None:
547
659
  """test business statement"""
548
660
  expected_segments = MOCK_COMPANY_DB[msft_company_id]["segments"].model_dump(mode="json")[
549
- "segments"
661
+ "periods"
550
662
  ]
551
663
 
552
664
  business_segment = self.msft_company.company.business_segments()
@@ -840,10 +952,18 @@ class TestTicker(TestCase):
840
952
 
841
953
  def test_income_statement(self) -> None:
842
954
  """test income statement"""
955
+ # Extract statements data from the periods structure
956
+ periods_data = INCOME_STATEMENT.model_dump(mode="json")["periods"]
957
+ statements_data = {}
958
+ for period_key, period_data in periods_data.items():
959
+ period_statements = {}
960
+ for statement in period_data["statements"]:
961
+ for line_item in statement["line_items"]:
962
+ period_statements[line_item["name"]] = line_item["value"]
963
+ statements_data[period_key] = period_statements
964
+
843
965
  expected_income_statement = (
844
- pd.DataFrame(INCOME_STATEMENT.model_dump(mode="json")["statements"])
845
- .apply(pd.to_numeric)
846
- .replace(np.nan, None)
966
+ pd.DataFrame(statements_data).apply(pd.to_numeric).replace(np.nan, None)
847
967
  )
848
968
 
849
969
  income_statement = self.msft_ticker_from_ticker.income_statement()
@@ -860,11 +980,14 @@ class TestTicker(TestCase):
860
980
 
861
981
  def test_revenue(self) -> None:
862
982
  """test revenue"""
863
- line_item_response: LineItemResponse = MOCK_COMPANY_DB[msft_company_id]["line_items"][
864
- "revenue"
865
- ]
983
+ line_item_response: LineItemResp = MOCK_COMPANY_DB[msft_company_id]["line_items"]["revenue"]
984
+
985
+ line_item_data = {}
986
+ for period_key, period_data in line_item_response.periods.items():
987
+ line_item_data[period_key] = period_data.line_item.value
988
+
866
989
  expected_revenue = (
867
- pd.DataFrame({"line_item": line_item_response.line_item})
990
+ pd.DataFrame({"line_item": line_item_data})
868
991
  .transpose()
869
992
  .apply(pd.to_numeric)
870
993
  .replace(np.nan, None)
@@ -1012,64 +1135,3 @@ class TestCompanyEarnings(TestCase):
1012
1135
  """test company next_earnings property"""
1013
1136
  next_earnings = self.msft_company.next_earnings
1014
1137
  self.assertEqual(next_earnings.key_dev_id, 1916266380)
1015
-
1016
-
1017
- class TestMerger(TestCase):
1018
- def setUp(self):
1019
- self.kfinance_api_client = MockKFinanceApiClient()
1020
- self.merger = MergerOrAcquisition(
1021
- kfinance_api_client=self.kfinance_api_client,
1022
- transaction_id=int(msft_buys_mongo),
1023
- merger_title="Closed M/A of MongoMusic, Inc.",
1024
- closed_date=date(2021, 1, 1),
1025
- )
1026
-
1027
- def test_merger_info(self) -> None:
1028
- expected_merger_info = MOCK_MERGERS_DB[msft_buys_mongo]
1029
- merger_info = {
1030
- "timeline": [
1031
- {"status": timeline["status"], "date": timeline["date"].strftime("%Y-%m-%d")}
1032
- for timeline in self.merger.get_timeline.to_dict(orient="records")
1033
- ],
1034
- "participants": {
1035
- "target": {
1036
- "company_id": self.merger.get_participants["target"].company.company_id,
1037
- "company_name": self.merger.get_participants["target"].company.name,
1038
- },
1039
- "buyers": [
1040
- {"company_id": buyer.company.company_id, "company_name": buyer.company.name}
1041
- for buyer in self.merger.get_participants["buyers"]
1042
- ],
1043
- "sellers": [
1044
- {"company_id": seller.company.company_id, "company_name": seller.company.name}
1045
- for seller in self.merger.get_participants["sellers"]
1046
- ],
1047
- },
1048
- "consideration": {
1049
- "currency_name": self.merger.get_consideration["currency_name"],
1050
- "current_calculated_gross_total_transaction_value": self.merger.get_consideration[
1051
- "current_calculated_gross_total_transaction_value"
1052
- ],
1053
- "current_calculated_implied_equity_value": self.merger.get_consideration[
1054
- "current_calculated_implied_equity_value"
1055
- ],
1056
- "current_calculated_implied_enterprise_value": self.merger.get_consideration[
1057
- "current_calculated_implied_enterprise_value"
1058
- ],
1059
- "details": [
1060
- {
1061
- "scenario": detail["scenario"],
1062
- "subtype": detail["subtype"],
1063
- "cash_or_cash_equivalent_per_target_share_unit": detail[
1064
- "cash_or_cash_equivalent_per_target_share_unit"
1065
- ],
1066
- "number_of_target_shares_sought": detail["number_of_target_shares_sought"],
1067
- "current_calculated_gross_value_of_consideration": detail[
1068
- "current_calculated_gross_value_of_consideration"
1069
- ],
1070
- }
1071
- for detail in self.merger.get_consideration["details"].to_dict(orient="records")
1072
- ],
1073
- },
1074
- }
1075
- self.assertEqual(ordered(expected_merger_info), ordered(merger_info))
kfinance/conftest.py CHANGED
@@ -64,6 +64,16 @@ def mock_client(requests_mock: Mocker) -> Client:
64
64
  }
65
65
  },
66
66
  )
67
+ # Fetch a fake company
68
+ requests_mock.post(
69
+ url="https://kfinance.kensho.com/api/v1/ids",
70
+ additional_matcher=lambda req: req.json().get("identifiers") == ["C_1"],
71
+ json={
72
+ "data": {
73
+ "C_1": {"company_id": 1, "security_id": 1, "trading_item_id": 1},
74
+ },
75
+ },
76
+ )
67
77
  # Fetch SPGI and a non-existent company (which will include an error)
68
78
  requests_mock.post(
69
79
  url="https://kfinance.kensho.com/api/v1/ids",
@@ -29,16 +29,27 @@ class GetBusinessRelationshipFromIdentifiersResp(BaseModel):
29
29
  class GetBusinessRelationshipFromIdentifiers(KfinanceTool):
30
30
  name: str = "get_business_relationship_from_identifiers"
31
31
  description: str = dedent("""
32
- Get the current and previous company IDs that are relationship_type for a list of identifiers.
32
+ Get the current and previous companies that have a specified business relationship with each of the provided identifiers.
33
33
 
34
- Example:
35
- Query: "What are the previous borrowers of SPGI and JPM?"
36
- Function: get_business_relationship_from_identifiers(identifiers=["SPGI", "JPM"], business_relationship=BusinessRelationshipType.borrower)
34
+ - When possible, pass multiple identifiers in a single call rather than making multiple calls.
35
+ - Results include both "current" (active) and "previous" (historical) relationships.
36
+
37
+ Examples:
38
+ Query: "Who are the current and previous suppliers of Intel?"
39
+ Function: get_business_relationship_from_identifiers(identifiers=["Intel"], business_relationship="supplier")
40
+
41
+ Query: "What are the borrowers of SPGI and JPM?"
42
+ Function: get_business_relationship_from_identifiers(identifiers=["SPGI", "JPM"], business_relationship="borrower")
43
+
44
+ Query: "Who are Dell's customers?"
45
+ Function: get_business_relationship_from_identifiers(identifiers=["Dell"], business_relationship="customer")
37
46
  """).strip()
38
47
  args_schema: Type[BaseModel] = GetBusinessRelationshipFromIdentifiersArgs
39
48
  accepted_permissions: set[Permission] | None = {Permission.RelationshipPermission}
40
49
 
41
- def _run(self, identifiers: list[str], business_relationship: BusinessRelationshipType) -> dict:
50
+ def _run(
51
+ self, identifiers: list[str], business_relationship: BusinessRelationshipType
52
+ ) -> GetBusinessRelationshipFromIdentifiersResp:
42
53
  """Sample response:
43
54
 
44
55
  {
@@ -76,10 +87,8 @@ class GetBusinessRelationshipFromIdentifiers(KfinanceTool):
76
87
  api_client=api_client, tasks=tasks
77
88
  )
78
89
 
79
- output_model = GetBusinessRelationshipFromIdentifiersResp(
90
+ return GetBusinessRelationshipFromIdentifiersResp(
80
91
  business_relationship=business_relationship,
81
92
  results=relationship_responses,
82
93
  errors=list(id_triple_resp.errors.values()),
83
94
  )
84
-
85
- return output_model.model_dump(mode="json")
@@ -8,6 +8,7 @@ from kfinance.domains.business_relationships.business_relationship_models import
8
8
  from kfinance.domains.business_relationships.business_relationship_tools import (
9
9
  GetBusinessRelationshipFromIdentifiers,
10
10
  GetBusinessRelationshipFromIdentifiersArgs,
11
+ GetBusinessRelationshipFromIdentifiersResp,
11
12
  )
12
13
 
13
14
 
@@ -27,21 +28,23 @@ class TestGetBusinessRelationshipFromIdentifiers:
27
28
  {"company_id": 8182358, "company_name": "Eloqua, Inc."},
28
29
  ],
29
30
  }
30
- expected_result = {
31
- "business_relationship": "supplier",
32
- "results": {
33
- "SPGI": {
34
- "current": [{"company_id": "C_883103", "company_name": "CRISIL Limited"}],
35
- "previous": [
36
- {"company_id": "C_472898", "company_name": "Morgan Stanley"},
37
- {"company_id": "C_8182358", "company_name": "Eloqua, Inc."},
38
- ],
39
- }
40
- },
41
- "errors": [
42
- "No identification triple found for the provided identifier: NON-EXISTENT of type: ticker"
43
- ],
44
- }
31
+ expected_result = GetBusinessRelationshipFromIdentifiersResp.model_validate(
32
+ {
33
+ "business_relationship": "supplier",
34
+ "results": {
35
+ "SPGI": {
36
+ "current": [{"company_id": 883103, "company_name": "CRISIL Limited"}],
37
+ "previous": [
38
+ {"company_id": 472898, "company_name": "Morgan Stanley"},
39
+ {"company_id": 8182358, "company_name": "Eloqua, Inc."},
40
+ ],
41
+ }
42
+ },
43
+ "errors": [
44
+ "No identification triple found for the provided identifier: NON-EXISTENT of type: ticker"
45
+ ],
46
+ }
47
+ )
45
48
 
46
49
  requests_mock.get(
47
50
  url=f"https://kfinance.kensho.com/api/v1/relationship/{SPGI_COMPANY_ID}/supplier",
@@ -54,5 +57,4 @@ class TestGetBusinessRelationshipFromIdentifiers:
54
57
  business_relationship=BusinessRelationshipType.supplier,
55
58
  )
56
59
  resp = tool.run(args.model_dump(mode="json"))
57
- resp["results"]["SPGI"]["previous"].sort(key=lambda x: x["company_id"])
58
60
  assert resp == expected_result
@@ -2,7 +2,7 @@ from copy import deepcopy
2
2
  from datetime import date
3
3
  from typing import Any
4
4
 
5
- from pydantic import BaseModel, Field, model_validator
5
+ from pydantic import BaseModel, ConfigDict, Field, model_validator
6
6
  from strenum import StrEnum
7
7
 
8
8
  from kfinance.client.models.decimal_with_unit import Money, Shares
@@ -20,14 +20,15 @@ class DailyCapitalization(BaseModel):
20
20
  """DailyCapitalization represents market cap, TEV, and shares outstanding for a day"""
21
21
 
22
22
  date: date
23
- market_cap: Money
24
- tev: Money
25
- shares_outstanding: Shares
23
+ market_cap: Money | None
24
+ tev: Money | None
25
+ shares_outstanding: Shares | None
26
26
 
27
27
 
28
28
  class Capitalizations(BaseModel):
29
29
  """Capitalizations represents market cap, TEV, and shares outstanding for a date range"""
30
30
 
31
+ model_config = ConfigDict(validate_by_name=True)
31
32
  capitalizations: list[DailyCapitalization] = Field(validation_alias="market_caps")
32
33
 
33
34
  @model_validator(mode="before")
@@ -65,7 +66,8 @@ class Capitalizations(BaseModel):
65
66
  currency = data["currency"]
66
67
  for capitalization in data["market_caps"]:
67
68
  for key in ["market_cap", "tev"]:
68
- capitalization[key] = dict(unit=currency, value=capitalization[key])
69
+ if capitalization[key] is not None:
70
+ capitalization[key] = dict(unit=currency, value=capitalization[key])
69
71
  return data
70
72
 
71
73
  def model_dump_json_single_metric(