kensho-kfinance 2.4.3__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.

kfinance/meta_classes.py CHANGED
@@ -1,5 +1,5 @@
1
+ from abc import abstractmethod
1
2
  from datetime import datetime
2
- from functools import cached_property
3
3
  import logging
4
4
  from typing import TYPE_CHECKING, Any, Callable, Literal, Optional
5
5
 
@@ -7,13 +7,19 @@ from cachetools import LRUCache, cached
7
7
  import numpy as np
8
8
  import pandas as pd
9
9
 
10
- from .constants import LINE_ITEMS, BusinessRelationshipType, PeriodType, SegmentType
10
+ from .constants import (
11
+ LINE_ITEMS,
12
+ BusinessRelationshipType,
13
+ CompetitorSource,
14
+ PeriodType,
15
+ SegmentType,
16
+ )
11
17
  from .fetch import KFinanceApiClient
12
18
  from .pydantic_models import RelationshipResponse
13
19
 
14
20
 
15
21
  if TYPE_CHECKING:
16
- from .kfinance import BusinessRelationships
22
+ from .kfinance import BusinessRelationships, Companies
17
23
 
18
24
  logger = logging.getLogger(__name__)
19
25
 
@@ -21,7 +27,8 @@ logger = logging.getLogger(__name__)
21
27
  class CompanyFunctionsMetaClass:
22
28
  kfinance_api_client: KFinanceApiClient
23
29
 
24
- @cached_property
30
+ @property
31
+ @abstractmethod
25
32
  def company_id(self) -> Any:
26
33
  """Set and return the company id for the object"""
27
34
  raise NotImplementedError("child classes must implement company id property")
@@ -213,6 +220,7 @@ class CompanyFunctionsMetaClass:
213
220
  .set_index(pd.Index([line_item]))
214
221
  )
215
222
 
223
+ @cached(cache=LRUCache(maxsize=100))
216
224
  def relationships(self, relationship_type: BusinessRelationshipType) -> "BusinessRelationships":
217
225
  """Returns a BusinessRelationships object that includes the current and previous Companies associated with company_id and filtered by relationship_type. The function calls fetch_companies_from_business_relationship.
218
226
 
@@ -416,6 +424,24 @@ class CompanyFunctionsMetaClass:
416
424
  end_quarter=end_quarter,
417
425
  )
418
426
 
427
+ def competitors(
428
+ self, competitor_source: CompetitorSource = CompetitorSource.all
429
+ ) -> "Companies":
430
+ """Get the list of companies that are competitors of company_id, optionally filtered by the competitor_source type.
431
+
432
+ :return: The list of companies that are competitors of company_id, optionally filtered by the competitor_source type
433
+ :rtype: Companies
434
+ """
435
+ from .kfinance import Companies
436
+
437
+ competitors_data = self.kfinance_api_client.fetch_competitors(
438
+ company_id=self.company_id, competitor_source=competitor_source
439
+ )["companies"]
440
+ return Companies(
441
+ kfinance_api_client=self.kfinance_api_client,
442
+ company_ids=[company["company_id"] for company in competitors_data],
443
+ )
444
+
419
445
 
420
446
  for line_item in LINE_ITEMS:
421
447
  line_item_name = line_item["name"]
@@ -494,7 +520,7 @@ class DelegatedCompanyFunctionsMetaClass(CompanyFunctionsMetaClass):
494
520
  delegated_function(company_function_name),
495
521
  )
496
522
 
497
- @cached_property
523
+ @property
498
524
  def company(self) -> Any:
499
525
  """Set and return the company for the object"""
500
526
  raise NotImplementedError("child classes must implement company property")
@@ -502,8 +528,8 @@ class DelegatedCompanyFunctionsMetaClass(CompanyFunctionsMetaClass):
502
528
 
503
529
  for relationship in BusinessRelationshipType:
504
530
 
505
- def _relationship_outer_wrapper(relationship_type: BusinessRelationshipType) -> cached_property:
506
- """Creates a cached property for a relationship type.
531
+ def _relationship_outer_wrapper(relationship_type: BusinessRelationshipType) -> property:
532
+ """Creates a property for a relationship type.
507
533
 
508
534
  This function returns a property that retrieves the associated company's current and previous
509
535
  relationships of the specified type.
@@ -512,7 +538,7 @@ for relationship in BusinessRelationshipType:
512
538
  relationship_type (BusinessRelationshipType): The type of relationship to be wrapped.
513
539
 
514
540
  Returns:
515
- property: A cached property that calls the inner wrapper to retrieve the relationship data.
541
+ property: A property that calls the inner wrapper to retrieve the relationship data.
516
542
  """
517
543
 
518
544
  def relationship_inner_wrapper(
@@ -533,12 +559,11 @@ for relationship in BusinessRelationshipType:
533
559
  relationship_inner_wrapper.__doc__ = doc
534
560
  relationship_inner_wrapper.__name__ = relationship
535
561
 
536
- return cached_property(relationship_inner_wrapper)
562
+ return property(relationship_inner_wrapper)
537
563
 
538
- relationship_cached_property = _relationship_outer_wrapper(relationship)
539
- relationship_cached_property.__set_name__(CompanyFunctionsMetaClass, relationship)
564
+ relationship_property = _relationship_outer_wrapper(relationship)
540
565
  setattr(
541
566
  CompanyFunctionsMetaClass,
542
567
  relationship,
543
- relationship_cached_property,
568
+ relationship_property,
544
569
  )
@@ -29,4 +29,8 @@ def mock_client(requests_mock: Mocker) -> Client:
29
29
  "company_id": SPGI_COMPANY_ID,
30
30
  },
31
31
  )
32
+ requests_mock.get(
33
+ url="https://kfinance.kensho.com/api/v1/id/MSFT",
34
+ json={"trading_item_id": 2630413, "security_id": 2630412, "company_id": 21835},
35
+ )
32
36
  return client
@@ -67,38 +67,6 @@ class TestTradingItem(TestCase):
67
67
  expected_id_based_result = {1001: "Mock City A", 1002: "Mock City B"}
68
68
  self.assertDictEqual(id_based_result, expected_id_based_result)
69
69
 
70
- @requests_mock.Mocker()
71
- def test_batch_request_cached_properties(self, m):
72
- """GIVEN a kfinance group object like Companies
73
- WHEN we batch request a cached property for each object in the group
74
- THEN the batch request completes successfully and we get back a mapping of
75
- company objects to the corresponding values."""
76
-
77
- m.get(
78
- "https://kfinance.kensho.com/api/v1/securities/1001",
79
- json={"securities": [101, 102, 103]},
80
- )
81
- m.get(
82
- "https://kfinance.kensho.com/api/v1/securities/1002",
83
- json={"securities": [104, 105, 106, 107]},
84
- )
85
- m.get("https://kfinance.kensho.com/api/v1/securities/1005", json={"securities": [108, 109]})
86
-
87
- companies = Companies(self.kfinance_api_client, [1001, 1002, 1005])
88
- result = companies.securities
89
-
90
- id_based_result = self.company_object_keys_as_company_id(result)
91
- for k, v in id_based_result.items():
92
- id_based_result[k] = set(map(lambda s: s.security_id, v))
93
-
94
- expected_id_based_result = {
95
- 1001: set([101, 102, 103]),
96
- 1002: set([104, 105, 106, 107]),
97
- 1005: set([108, 109]),
98
- }
99
-
100
- self.assertDictEqual(id_based_result, expected_id_based_result)
101
-
102
70
  @requests_mock.Mocker()
103
71
  def test_batch_request_function(self, m):
104
72
  """GIVEN a kfinance group object like TradingItems
@@ -272,6 +272,27 @@ class TestFetchItem(TestCase):
272
272
  )
273
273
  self.kfinance_api_client.fetch.assert_called_with(expected_fetch_url)
274
274
 
275
+ def test_fetch_mergers_for_company(self) -> None:
276
+ company_id = 21719
277
+ expected_fetch_url = f"{self.kfinance_api_client.url_base}mergers/{company_id}"
278
+ self.kfinance_api_client.fetch_mergers_for_company(company_id=company_id)
279
+ self.kfinance_api_client.fetch.assert_called_with(expected_fetch_url)
280
+
281
+ def test_fetch_merger_info(self) -> None:
282
+ transaction_id = 554979212
283
+ expected_fetch_url = f"{self.kfinance_api_client.url_base}merger/info/{transaction_id}"
284
+ self.kfinance_api_client.fetch_merger_info(transaction_id=transaction_id)
285
+ self.kfinance_api_client.fetch.assert_called_with(expected_fetch_url)
286
+
287
+ def test_fetch_advisors_for_company_in_merger(self) -> None:
288
+ transaction_id = 554979212
289
+ advised_company_id = 251994106
290
+ expected_fetch_url = f"{self.kfinance_api_client.url_base}merger/info/{transaction_id}/advisors/{advised_company_id}"
291
+ self.kfinance_api_client.fetch_advisors_for_company_in_merger(
292
+ transaction_id=transaction_id, advised_company_id=advised_company_id
293
+ )
294
+ self.kfinance_api_client.fetch.assert_called_with(expected_fetch_url)
295
+
275
296
 
276
297
  class TestMarketCap:
277
298
  @pytest.mark.parametrize(
@@ -1,3 +1,4 @@
1
+ import copy
1
2
  from datetime import date, datetime, timezone
2
3
  from io import BytesIO
3
4
  import re
@@ -9,7 +10,19 @@ import pandas as pd
9
10
  from PIL.Image import open as image_open
10
11
  import time_machine
11
12
 
12
- from kfinance.kfinance import Company, Earnings, Security, Ticker, TradingItem, Transcript
13
+ from kfinance.constants import BusinessRelationshipType
14
+ from kfinance.kfinance import (
15
+ AdvisedCompany,
16
+ BusinessRelationships,
17
+ Company,
18
+ Earnings,
19
+ MergerOrAcquisition,
20
+ Security,
21
+ Ticker,
22
+ TradingItem,
23
+ Transcript,
24
+ )
25
+ from kfinance.pydantic_models import CompanyIdAndName, RelationshipResponse
13
26
 
14
27
 
15
28
  msft_company_id = "21835"
@@ -17,6 +30,7 @@ msft_security_id = "2630412"
17
30
  msft_isin = "US5949181045"
18
31
  msft_cusip = "594918104"
19
32
  msft_trading_item_id = "2630413"
33
+ msft_buys_mongo = "517414"
20
34
 
21
35
 
22
36
  MOCK_TRADING_ITEM_DB = {
@@ -108,7 +122,47 @@ MOCK_COMPANY_DB = {
108
122
  },
109
123
  }
110
124
  },
111
- }
125
+ "mergers": {
126
+ "target": [
127
+ {"transaction_id": 10998717, "merger_title": "Closed M/A of Microsoft Corporation"},
128
+ {"transaction_id": 28237969, "merger_title": "Closed M/A of Microsoft Corporation"},
129
+ ],
130
+ "buyer": [
131
+ {"transaction_id": 517414, "merger_title": "Closed M/A of MongoMusic, Inc."},
132
+ {"transaction_id": 596722, "merger_title": "Closed M/A of Digital Anvil, Inc."},
133
+ ],
134
+ "seller": [
135
+ {"transaction_id": 455551, "merger_title": "Closed M/A of VacationSpot.com, Inc."},
136
+ {"transaction_id": 456045, "merger_title": "Closed M/A of TransPoint, LLC"},
137
+ ],
138
+ },
139
+ "advisors": {
140
+ msft_buys_mongo: {
141
+ "advisors": [
142
+ {
143
+ "advisor_company_id": 251994106,
144
+ "advisor_company_name": "Kensho Technologies, Inc.",
145
+ "advisor_type_name": "Professional Mongo Enjoyer",
146
+ }
147
+ ]
148
+ }
149
+ },
150
+ BusinessRelationshipType.supplier: RelationshipResponse(
151
+ current=[CompanyIdAndName(company_name="foo", company_id=883103)],
152
+ previous=[
153
+ CompanyIdAndName(company_name="bar", company_id=472898),
154
+ CompanyIdAndName(company_name="baz", company_id=8182358),
155
+ ],
156
+ ),
157
+ },
158
+ 31696: {"info": {"name": "MongoMusic, Inc."}},
159
+ 21835: {"info": {"name": "Microsoft Corporation"}},
160
+ 18805: {"info": {"name": "Angel Investors L.P."}},
161
+ 20087: {"info": {"name": "Draper Richards, L.P."}},
162
+ 22103: {"info": {"name": "BRV Partners, LLC"}},
163
+ 23745: {"info": {"name": "Venture Frogs, LLC"}},
164
+ 105902: {"info": {"name": "ARGUS Capital International Limited"}},
165
+ 880300: {"info": {"name": "Sony Music Entertainment, Inc."}},
112
166
  }
113
167
 
114
168
  MOCK_TRANSCRIPT_DB = {
@@ -165,6 +219,51 @@ MOCK_CUSIP_DB = {
165
219
  }
166
220
  }
167
221
 
222
+ MOCK_MERGERS_DB = {
223
+ msft_buys_mongo: {
224
+ "timeline": [
225
+ {"status": "Announced", "date": "2000-09-12"},
226
+ {"status": "Closed", "date": "2000-09-12"},
227
+ ],
228
+ "participants": {
229
+ "target": {"company_id": 31696, "company_name": "MongoMusic, Inc."},
230
+ "buyers": [{"company_id": 21835, "company_name": "Microsoft Corporation"}],
231
+ "sellers": [
232
+ {"company_id": 18805, "company_name": "Angel Investors L.P."},
233
+ {"company_id": 20087, "company_name": "Draper Richards, L.P."},
234
+ {"company_id": 22103, "company_name": "BRV Partners, LLC"},
235
+ {"company_id": 23745, "company_name": "Venture Frogs, LLC"},
236
+ {"company_id": 105902, "company_name": "ARGUS Capital International Limited"},
237
+ {"company_id": 880300, "company_name": "Sony Music Entertainment, Inc."},
238
+ ],
239
+ },
240
+ "consideration": {
241
+ "currency_name": "US Dollar",
242
+ "current_calculated_gross_total_transaction_value": "51609375.000000",
243
+ "current_calculated_implied_equity_value": "51609375.000000",
244
+ "current_calculated_implied_enterprise_value": "51609375.000000",
245
+ "details": [
246
+ {
247
+ "scenario": "Stock Lump Sum",
248
+ "subtype": "Common Equity",
249
+ "cash_or_cash_equivalent_per_target_share_unit": None,
250
+ "number_of_target_shares_sought": "1000000.000000",
251
+ "current_calculated_gross_value_of_consideration": "51609375.000000",
252
+ }
253
+ ],
254
+ },
255
+ }
256
+ }
257
+
258
+
259
+ def ordered(obj):
260
+ if isinstance(obj, dict):
261
+ return sorted((k, ordered(v)) for k, v in obj.items())
262
+ if isinstance(obj, list):
263
+ return sorted(ordered(x) for x in obj)
264
+ else:
265
+ return obj
266
+
168
267
 
169
268
  class MockKFinanceApiClient:
170
269
  def __init__(self):
@@ -261,6 +360,11 @@ class MockKFinanceApiClient:
261
360
  """Get a segment"""
262
361
  return MOCK_COMPANY_DB[company_id]
263
362
 
363
+ def fetch_companies_from_business_relationship(
364
+ self, company_id: int, relationship_type: BusinessRelationshipType
365
+ ) -> RelationshipResponse:
366
+ return MOCK_COMPANY_DB[company_id][relationship_type]
367
+
264
368
  def fetch_earnings(self, company_id: int) -> dict:
265
369
  """Get the earnings for a company."""
266
370
  return MOCK_COMPANY_DB[company_id]["earnings"]
@@ -269,6 +373,15 @@ class MockKFinanceApiClient:
269
373
  """Get the transcript for an earnings item."""
270
374
  return MOCK_TRANSCRIPT_DB[key_dev_id]
271
375
 
376
+ def fetch_mergers_for_company(self, company_id):
377
+ return copy.deepcopy(MOCK_COMPANY_DB[company_id]["mergers"])
378
+
379
+ def fetch_merger_info(self, transaction_id):
380
+ return copy.deepcopy(MOCK_MERGERS_DB[transaction_id])
381
+
382
+ def fetch_advisors_for_company_in_merger(self, transaction_id, advised_company_id):
383
+ return copy.deepcopy(MOCK_COMPANY_DB[advised_company_id]["advisors"][transaction_id])
384
+
272
385
 
273
386
  class TestTradingItem(TestCase):
274
387
  def setUp(self):
@@ -327,7 +440,9 @@ class TestCompany(TestCase):
327
440
  def setUp(self):
328
441
  """setup tests"""
329
442
  self.kfinance_api_client = MockKFinanceApiClient()
330
- self.msft_company = Company(self.kfinance_api_client, msft_company_id)
443
+ self.msft_company = AdvisedCompany(
444
+ self.kfinance_api_client, company_id=msft_company_id, transaction_id=msft_buys_mongo
445
+ )
331
446
 
332
447
  def test_company_id(self) -> None:
333
448
  """test company id"""
@@ -396,6 +511,67 @@ class TestCompany(TestCase):
396
511
  business_segment = self.msft_company.business_segments()
397
512
  self.assertEqual(expected_segments, business_segment)
398
513
 
514
+ def test_relationships(self) -> None:
515
+ """
516
+ WHEN we fetch the relationships of a company
517
+ THEN we get back a BusinessRelationships object.
518
+ """
519
+
520
+ expected_suppliers = MOCK_COMPANY_DB[msft_company_id][BusinessRelationshipType.supplier]
521
+
522
+ suppliers_via_method = self.msft_company.relationships(BusinessRelationshipType.supplier)
523
+ self.assertIsInstance(suppliers_via_method, BusinessRelationships)
524
+ # Company ids should match
525
+ self.assertEqual(
526
+ sorted([c.company_id for c in suppliers_via_method.current]),
527
+ sorted([c.company_id for c in expected_suppliers.current]),
528
+ )
529
+ self.assertEqual(
530
+ sorted([c.company_id for c in suppliers_via_method.previous]),
531
+ sorted([c.company_id for c in expected_suppliers.previous]),
532
+ )
533
+
534
+ # Fetching via property should return the same result
535
+ suppliers_via_property = self.msft_company.supplier
536
+ self.assertEqual(suppliers_via_property, suppliers_via_method)
537
+
538
+ def test_mergers(self) -> None:
539
+ expected_mergers = MOCK_COMPANY_DB[msft_company_id]["mergers"]
540
+ mergers = self.msft_company.mergers_and_acquisitions
541
+ mergers_json = {
542
+ "target": [
543
+ {"transaction_id": merger.transaction_id, "merger_title": merger.merger_title}
544
+ for merger in mergers["target"]
545
+ ],
546
+ "buyer": [
547
+ {"transaction_id": merger.transaction_id, "merger_title": merger.merger_title}
548
+ for merger in mergers["buyer"]
549
+ ],
550
+ "seller": [
551
+ {"transaction_id": merger.transaction_id, "merger_title": merger.merger_title}
552
+ for merger in mergers["seller"]
553
+ ],
554
+ }
555
+ self.assertEqual(ordered(expected_mergers), ordered(mergers_json))
556
+
557
+ def test_advisors(self) -> None:
558
+ expected_advisors_json = MOCK_COMPANY_DB[msft_company_id]["advisors"][msft_buys_mongo][
559
+ "advisors"
560
+ ]
561
+ expected_company_ids: list[int] = []
562
+ expected_advisor_type_names: list[str] = []
563
+ for advisor in expected_advisors_json:
564
+ expected_company_ids.append(int(advisor["advisor_company_id"]))
565
+ expected_advisor_type_names.append(str(advisor["advisor_type_name"]))
566
+ advisors = self.msft_company.advisors
567
+ company_ids: list[int] = []
568
+ advisor_type_names: list[str] = []
569
+ for advisor in advisors:
570
+ company_ids.append(advisor.company_id)
571
+ advisor_type_names.append(advisor.advisor_type_name)
572
+ self.assertListEqual(expected_company_ids, company_ids)
573
+ self.assertListEqual(expected_advisor_type_names, advisor_type_names)
574
+
399
575
 
400
576
  class TestSecurity(TestCase):
401
577
  def setUp(self):
@@ -782,3 +958,63 @@ class TestCompanyEarnings(TestCase):
782
958
  """test company next_earnings property"""
783
959
  next_earnings = self.msft_company.next_earnings
784
960
  self.assertEqual(next_earnings.key_dev_id, 1916266380)
961
+
962
+
963
+ class TestMerger(TestCase):
964
+ def setUp(self):
965
+ self.kfinance_api_client = MockKFinanceApiClient()
966
+ self.merger = MergerOrAcquisition(
967
+ self.kfinance_api_client,
968
+ transaction_id=msft_buys_mongo,
969
+ merger_title="Closed M/A of MongoMusic, Inc.",
970
+ )
971
+
972
+ def test_merger_info(self) -> None:
973
+ expected_merger_info = MOCK_MERGERS_DB[msft_buys_mongo]
974
+ merger_info = {
975
+ "timeline": [
976
+ {"status": timeline["status"], "date": timeline["date"].strftime("%Y-%m-%d")}
977
+ for timeline in self.merger.get_timeline.to_dict(orient="records")
978
+ ],
979
+ "participants": {
980
+ "target": {
981
+ "company_id": self.merger.get_participants["target"].company_id,
982
+ "company_name": self.merger.get_participants["target"].name,
983
+ },
984
+ "buyers": [
985
+ {"company_id": company.company_id, "company_name": company.name}
986
+ for company in self.merger.get_participants["buyers"]
987
+ ],
988
+ "sellers": [
989
+ {"company_id": company.company_id, "company_name": company.name}
990
+ for company in self.merger.get_participants["sellers"]
991
+ ],
992
+ },
993
+ "consideration": {
994
+ "currency_name": self.merger.get_consideration["currency_name"],
995
+ "current_calculated_gross_total_transaction_value": self.merger.get_consideration[
996
+ "current_calculated_gross_total_transaction_value"
997
+ ],
998
+ "current_calculated_implied_equity_value": self.merger.get_consideration[
999
+ "current_calculated_implied_equity_value"
1000
+ ],
1001
+ "current_calculated_implied_enterprise_value": self.merger.get_consideration[
1002
+ "current_calculated_implied_enterprise_value"
1003
+ ],
1004
+ "details": [
1005
+ {
1006
+ "scenario": detail["scenario"],
1007
+ "subtype": detail["subtype"],
1008
+ "cash_or_cash_equivalent_per_target_share_unit": detail[
1009
+ "cash_or_cash_equivalent_per_target_share_unit"
1010
+ ],
1011
+ "number_of_target_shares_sought": detail["number_of_target_shares_sought"],
1012
+ "current_calculated_gross_value_of_consideration": detail[
1013
+ "current_calculated_gross_value_of_consideration"
1014
+ ],
1015
+ }
1016
+ for detail in self.merger.get_consideration["details"].to_dict(orient="records")
1017
+ ],
1018
+ },
1019
+ }
1020
+ self.assertEqual(ordered(expected_merger_info), ordered(merger_info))
@@ -9,12 +9,19 @@ from pytest import raises
9
9
  from requests_mock import Mocker
10
10
  import time_machine
11
11
 
12
- from kfinance.constants import BusinessRelationshipType, Capitalization, SegmentType, StatementType
12
+ from kfinance.constants import (
13
+ BusinessRelationshipType,
14
+ Capitalization,
15
+ CompetitorSource,
16
+ SegmentType,
17
+ StatementType,
18
+ )
13
19
  from kfinance.kfinance import Client, NoEarningsDataError
14
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
15
22
  from kfinance.tool_calling import (
23
+ GetCompetitorsFromIdentifier,
16
24
  GetEarnings,
17
- GetEarningsCallDatetimesFromIdentifier,
18
25
  GetFinancialLineItemFromIdentifier,
19
26
  GetFinancialStatementFromIdentifier,
20
27
  GetHistoryMetadataFromIdentifier,
@@ -28,6 +35,10 @@ from kfinance.tool_calling import (
28
35
  GetTranscript,
29
36
  ResolveIdentifier,
30
37
  )
38
+ from kfinance.tool_calling.get_advisors_for_company_in_transaction_from_identifier import (
39
+ GetAdvisorsForCompanyInTransactionFromIdentifier,
40
+ GetAdvisorsForCompanyInTransactionFromIdentifierArgs,
41
+ )
31
42
  from kfinance.tool_calling.get_business_relationship_from_identifier import (
32
43
  GetBusinessRelationshipFromIdentifier,
33
44
  GetBusinessRelationshipFromIdentifierArgs,
@@ -36,6 +47,9 @@ from kfinance.tool_calling.get_capitalization_from_identifier import (
36
47
  GetCapitalizationFromIdentifier,
37
48
  GetCapitalizationFromIdentifierArgs,
38
49
  )
50
+ from kfinance.tool_calling.get_competitors_from_identifier import (
51
+ GetCompetitorsFromIdentifierArgs,
52
+ )
39
53
  from kfinance.tool_calling.get_cusip_from_ticker import GetCusipFromTicker, GetCusipFromTickerArgs
40
54
  from kfinance.tool_calling.get_financial_line_item_from_identifier import (
41
55
  GetFinancialLineItemFromIdentifierArgs,
@@ -45,6 +59,11 @@ from kfinance.tool_calling.get_financial_statement_from_identifier import (
45
59
  )
46
60
  from kfinance.tool_calling.get_isin_from_ticker import GetIsinFromTickerArgs
47
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
48
67
  from kfinance.tool_calling.get_n_quarters_ago import GetNQuartersAgoArgs
49
68
  from kfinance.tool_calling.get_prices_from_identifier import GetPricesFromIdentifierArgs
50
69
  from kfinance.tool_calling.get_segments_from_identifier import (
@@ -55,6 +74,59 @@ from kfinance.tool_calling.get_transcript import GetTranscriptArgs
55
74
  from kfinance.tool_calling.shared_models import ToolArgsWithIdentifier, ValidQuarter
56
75
 
57
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)
128
+
129
+
58
130
  class TestGetBusinessRelationshipFromIdentifier:
59
131
  def test_get_business_relationship_from_identifier(
60
132
  self, requests_mock: Mocker, mock_client: Client
@@ -137,27 +209,6 @@ class TestGetCusipFromTicker:
137
209
  assert resp == spgi_cusip
138
210
 
139
211
 
140
- class TestGetEarningsCallDatetimesFromTicker:
141
- def test_get_earnings_call_datetimes_from_ticker(
142
- self, requests_mock: Mocker, mock_client: Client
143
- ):
144
- """
145
- GIVEN the GetEarningsCallDatetimesFromIdentifier tool
146
- WHEN we request earnings call datetimes for SPGI
147
- THEN we get back the expected SPGI earnings call datetimes
148
- """
149
-
150
- requests_mock.get(
151
- url=f"https://kfinance.kensho.com/api/v1/earnings/{SPGI_COMPANY_ID}/dates",
152
- json={"earnings": ["2025-04-29T12:30:00", "2025-02-11T13:30:00"]},
153
- )
154
- expected_response = '["2025-04-29T12:30:00+00:00", "2025-02-11T13:30:00+00:00"]'
155
-
156
- tool = GetEarningsCallDatetimesFromIdentifier(kfinance_client=mock_client)
157
- response = tool.run(ToolArgsWithIdentifier(identifier="SPGI").model_dump(mode="json"))
158
- assert response == expected_response
159
-
160
-
161
212
  class TestGetFinancialLineItemFromIdentifier:
162
213
  def test_get_financial_line_item_from_identifier(
163
214
  self, mock_client: Client, requests_mock: Mocker
@@ -644,6 +695,33 @@ class TestGetTranscript:
644
695
  assert response == expected_response
645
696
 
646
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
+
647
725
  class TestValidQuarter:
648
726
  class QuarterModel(BaseModel):
649
727
  quarter: ValidQuarter | None