kensho-kfinance 2.4.2__py3-none-any.whl → 2.6.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of kensho-kfinance might be problematic. Click here for more details.

Files changed (31) hide show
  1. {kensho_kfinance-2.4.2.dist-info → kensho_kfinance-2.6.1.dist-info}/METADATA +1 -1
  2. {kensho_kfinance-2.4.2.dist-info → kensho_kfinance-2.6.1.dist-info}/RECORD +29 -26
  3. kfinance/CHANGELOG.md +15 -0
  4. kfinance/batch_request_handling.py +2 -16
  5. kfinance/constants.py +14 -2
  6. kfinance/fetch.py +56 -0
  7. kfinance/kfinance.py +403 -90
  8. kfinance/mcp.py +23 -35
  9. kfinance/meta_classes.py +37 -12
  10. kfinance/tests/conftest.py +4 -0
  11. kfinance/tests/scratch.py +0 -0
  12. kfinance/tests/test_batch_requests.py +0 -32
  13. kfinance/tests/test_fetch.py +21 -0
  14. kfinance/tests/test_objects.py +239 -3
  15. kfinance/tests/test_tools.py +136 -24
  16. kfinance/tool_calling/__init__.py +2 -4
  17. kfinance/tool_calling/get_advisors_for_company_in_transaction_from_identifier.py +39 -0
  18. kfinance/tool_calling/get_competitors_from_identifier.py +24 -0
  19. kfinance/tool_calling/get_financial_line_item_from_identifier.py +3 -3
  20. kfinance/tool_calling/get_financial_statement_from_identifier.py +3 -3
  21. kfinance/tool_calling/get_merger_info_from_transaction_id.py +62 -0
  22. kfinance/tool_calling/get_mergers_from_identifier.py +41 -0
  23. kfinance/tool_calling/get_segments_from_identifier.py +3 -3
  24. kfinance/tool_calling/shared_models.py +17 -2
  25. kfinance/version.py +2 -2
  26. kfinance/tests/test_mcp.py +0 -16
  27. kfinance/tool_calling/get_earnings_call_datetimes_from_identifier.py +0 -19
  28. {kensho_kfinance-2.4.2.dist-info → kensho_kfinance-2.6.1.dist-info}/WHEEL +0 -0
  29. {kensho_kfinance-2.4.2.dist-info → kensho_kfinance-2.6.1.dist-info}/licenses/AUTHORS.md +0 -0
  30. {kensho_kfinance-2.4.2.dist-info → kensho_kfinance-2.6.1.dist-info}/licenses/LICENSE +0 -0
  31. {kensho_kfinance-2.4.2.dist-info → kensho_kfinance-2.6.1.dist-info}/top_level.txt +0 -0
kfinance/mcp.py CHANGED
@@ -1,44 +1,36 @@
1
- from textwrap import dedent
2
1
  from typing import Literal, Optional
3
2
 
4
3
  import click
5
4
  from fastmcp import FastMCP
5
+ from fastmcp.tools import FunctionTool
6
6
  from fastmcp.utilities.logging import get_logger
7
+ from langchain_core.utils.function_calling import convert_to_openai_tool
7
8
 
8
9
  from kfinance.kfinance import Client
9
- from kfinance.tool_calling.shared_models import KfinanceTool
10
+ from kfinance.tool_calling import KfinanceTool
10
11
 
11
12
 
12
13
  logger = get_logger(__name__)
13
14
 
14
15
 
15
- def build_doc_string(tool: KfinanceTool) -> str:
16
- """Build a formatted documentation string for a Kfinance tool.
16
+ def build_mcp_tool_from_kfinance_tool(kfinance_tool: KfinanceTool) -> FunctionTool:
17
+ """Build an MCP FunctionTool from a langchain KfinanceTool."""
17
18
 
18
- This function takes a KfinanceTool object and constructs a comprehensive
19
- documentation string that includes the tool's description and detailed
20
- information about its arguments, including default values and descriptions.
21
-
22
- :param tool: The Kfinance tool object containing metadata about the tool's functionality, description, and argument schema.
23
- :type tool: KfinanceTool
24
- :return: A formatted documentation string containing for the tool description with detailed argument information.
25
- :rtype: str
26
- """
27
-
28
- description = dedent(f"""
29
- {tool.description}
30
-
31
- Args:
32
- """).strip()
33
-
34
- for arg_name, arg_field in tool.args_schema.model_fields.items():
35
- default_value_description = (
36
- f"Default: {arg_field.default}. " if not arg_field.is_required() else ""
37
- )
38
- param_description = f"\n {arg_name}: {default_value_description}{arg_field.description}"
39
- description += param_description
40
-
41
- return description
19
+ return FunctionTool(
20
+ name=kfinance_tool.name,
21
+ description=kfinance_tool.description,
22
+ # MCP expects a JSON schema for tool params, which we
23
+ # can generate similar to how langchain generates openai json schemas.
24
+ parameters=convert_to_openai_tool(kfinance_tool)["function"]["parameters"],
25
+ # The langchain runner internally validates input arguments via the args_schema.
26
+ # When running with mcp, we need to reproduce that validation ourselves in
27
+ # run_without_langchain (which then calls _run).
28
+ # If we pass in the underlying _run method directly, mcp generates a schema from
29
+ # the _run type hints but bypasses our internal validation. This causes errors,
30
+ # for example with integer literals, which our args models allow but the
31
+ # mcp-internal validation disallows.
32
+ fn=kfinance_tool.run_without_langchain,
33
+ )
42
34
 
43
35
 
44
36
  @click.command()
@@ -85,13 +77,9 @@ def run_mcp(
85
77
  kfinance_client = Client()
86
78
 
87
79
  kfinance_mcp: FastMCP = FastMCP("Kfinance")
88
- for tool in kfinance_client.langchain_tools:
89
- logger.info("Adding %s to server", tool.name)
90
- kfinance_mcp.tool(
91
- name_or_fn=getattr(tool, "_run"),
92
- name=tool.name,
93
- description=build_doc_string(tool),
94
- )
80
+ for langchain_tool in kfinance_client.langchain_tools:
81
+ logger.info("Adding %s to server", langchain_tool.name)
82
+ kfinance_mcp.add_tool(build_mcp_tool_from_kfinance_tool(langchain_tool))
95
83
 
96
84
  logger.info("Server starting")
97
85
  kfinance_mcp.run(transport=transport)
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
File without changes
@@ -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))