kensho-kfinance 2.8.0__py3-none-any.whl → 3.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.

Potentially problematic release.


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

Files changed (134) hide show
  1. {kensho_kfinance-2.8.0.dist-info → kensho_kfinance-3.0.0.dist-info}/METADATA +2 -2
  2. kensho_kfinance-3.0.0.dist-info/RECORD +110 -0
  3. kfinance/CHANGELOG.md +6 -0
  4. kfinance/__init__.py +1 -0
  5. kfinance/client/README.md +9 -0
  6. kfinance/{batch_request_handling.py → client/batch_request_handling.py} +63 -27
  7. kfinance/{fetch.py → client/fetch.py} +23 -29
  8. kfinance/{kfinance.py → client/kfinance.py} +105 -111
  9. kfinance/{meta_classes.py → client/meta_classes.py} +26 -35
  10. kfinance/{decimal_with_unit.py → client/models/decimal_with_unit.py} +1 -1
  11. kfinance/{tests → client/models/tests}/test_decimal_with_unit.py +1 -1
  12. kfinance/client/tests/__init__.py +0 -0
  13. kfinance/{tests → client/tests}/test_batch_requests.py +8 -6
  14. kfinance/{tests → client/tests}/test_client.py +25 -19
  15. kfinance/{tests → client/tests}/test_fetch.py +11 -29
  16. kfinance/{tests → client/tests}/test_group_objects.py +1 -1
  17. kfinance/{tests → client/tests}/test_objects.py +111 -63
  18. kfinance/{tests/conftest.py → conftest.py} +14 -2
  19. kfinance/domains/README.md +14 -0
  20. kfinance/domains/__init__.py +0 -0
  21. kfinance/domains/business_relationships/__init__.py +0 -0
  22. kfinance/{models → domains/business_relationships}/business_relationship_models.py +10 -0
  23. kfinance/domains/business_relationships/business_relationship_tools.py +74 -0
  24. kfinance/domains/business_relationships/tests/__init__.py +0 -0
  25. kfinance/domains/business_relationships/tests/test_business_relationship_tools.py +55 -0
  26. kfinance/domains/capitalizations/__init__.py +0 -0
  27. kfinance/{models → domains/capitalizations}/capitalization_models.py +24 -17
  28. kfinance/domains/capitalizations/capitalization_tools.py +89 -0
  29. kfinance/domains/capitalizations/tests/__init__.py +0 -0
  30. kfinance/{tests/test_models → domains/capitalizations/tests}/test_capitalization_models.py +8 -10
  31. kfinance/domains/capitalizations/tests/test_capitalization_tools.py +85 -0
  32. kfinance/domains/companies/__init__.py +0 -0
  33. kfinance/domains/companies/company_identifiers.py +175 -0
  34. kfinance/domains/companies/company_models.py +27 -0
  35. kfinance/domains/companies/company_tools.py +66 -0
  36. kfinance/domains/companies/tests/__init__.py +0 -0
  37. kfinance/domains/companies/tests/test_company_tools.py +26 -0
  38. kfinance/domains/competitors/__init__.py +0 -0
  39. kfinance/{models → domains/competitors}/competitor_models.py +7 -0
  40. kfinance/domains/competitors/competitor_tools.py +62 -0
  41. kfinance/domains/competitors/tests/__init__.py +0 -0
  42. kfinance/domains/competitors/tests/test_competitor_tools.py +45 -0
  43. kfinance/domains/cusip_and_isin/__init__.py +0 -0
  44. kfinance/domains/cusip_and_isin/cusip_and_isin_tools.py +80 -0
  45. kfinance/domains/cusip_and_isin/tests/__init__.py +0 -0
  46. kfinance/domains/cusip_and_isin/tests/test_cusip_and_isin_tools.py +57 -0
  47. kfinance/domains/earnings/__init__.py +0 -0
  48. kfinance/domains/earnings/earning_models.py +41 -0
  49. kfinance/domains/earnings/earning_tools.py +174 -0
  50. kfinance/domains/earnings/tests/__init__.py +0 -0
  51. kfinance/domains/earnings/tests/test_earnings_tools.py +195 -0
  52. kfinance/domains/line_items/__init__.py +0 -0
  53. kfinance/domains/line_items/line_item_tools.py +114 -0
  54. kfinance/domains/line_items/tests/__init__.py +0 -0
  55. kfinance/domains/line_items/tests/test_line_item_tools.py +86 -0
  56. kfinance/domains/mergers_and_acquisitions/__init__.py +0 -0
  57. kfinance/domains/mergers_and_acquisitions/merger_and_acquisition_tools.py +176 -0
  58. kfinance/domains/mergers_and_acquisitions/tests/__init__.py +0 -0
  59. kfinance/domains/mergers_and_acquisitions/tests/test_merger_and_acquisition_tools.py +124 -0
  60. kfinance/domains/prices/__init__.py +0 -0
  61. kfinance/{models → domains/prices}/price_models.py +1 -1
  62. kfinance/domains/prices/price_tools.py +165 -0
  63. kfinance/domains/prices/tests/__init__.py +0 -0
  64. kfinance/{tests/test_models → domains/prices/tests}/test_price_models.py +2 -2
  65. kfinance/domains/prices/tests/test_price_tools.py +141 -0
  66. kfinance/domains/segments/__init__.py +0 -0
  67. kfinance/domains/segments/segment_tools.py +91 -0
  68. kfinance/domains/segments/tests/__init__.py +0 -0
  69. kfinance/domains/segments/tests/test_segment_tools.py +80 -0
  70. kfinance/domains/statements/__init__.py +0 -0
  71. kfinance/domains/statements/statement_tools.py +113 -0
  72. kfinance/domains/statements/tests/__init__.py +0 -0
  73. kfinance/domains/statements/tests/test_statement_tools.py +73 -0
  74. kfinance/integrations/README.md +8 -0
  75. kfinance/integrations/__init__.py +0 -0
  76. kfinance/integrations/mcp/__init__.py +0 -0
  77. kfinance/{mcp.py → integrations/mcp/mcp.py} +2 -2
  78. kfinance/integrations/tests/__init__.py +0 -0
  79. kfinance/{tests → integrations/tests}/test_example_notebook.py +4 -4
  80. kfinance/{tool_calling → integrations/tool_calling}/README.md +2 -2
  81. kfinance/integrations/tool_calling/__init__.py +0 -0
  82. kfinance/integrations/tool_calling/all_tools.py +55 -0
  83. kfinance/{tool_calling → integrations/tool_calling}/prompts.py +3 -2
  84. kfinance/integrations/tool_calling/static_tools/README.md +4 -0
  85. kfinance/integrations/tool_calling/static_tools/__init__.py +0 -0
  86. kfinance/{tool_calling → integrations/tool_calling/static_tools}/get_latest.py +3 -3
  87. kfinance/{tool_calling → integrations/tool_calling/static_tools}/get_n_quarters_ago.py +3 -3
  88. kfinance/integrations/tool_calling/static_tools/tests/__init__.py +0 -0
  89. kfinance/integrations/tool_calling/static_tools/tests/test_get_lastest.py +30 -0
  90. kfinance/integrations/tool_calling/static_tools/tests/test_get_n_quarters_ago.py +24 -0
  91. kfinance/integrations/tool_calling/tests/__init__.py +0 -0
  92. kfinance/integrations/tool_calling/tests/test_tool_calling_models.py +69 -0
  93. kfinance/{tool_calling/shared_models.py → integrations/tool_calling/tool_calling_models.py} +37 -7
  94. kfinance/version.py +2 -2
  95. kensho_kfinance-2.8.0.dist-info/RECORD +0 -70
  96. kfinance/models/id_models.py +0 -7
  97. kfinance/prompt.py +0 -526
  98. kfinance/pydantic_models.py +0 -33
  99. kfinance/tests/test_tools.py +0 -804
  100. kfinance/tool_calling/__init__.py +0 -53
  101. kfinance/tool_calling/get_advisors_for_company_in_transaction_from_identifier.py +0 -39
  102. kfinance/tool_calling/get_business_relationship_from_identifier.py +0 -30
  103. kfinance/tool_calling/get_capitalization_from_identifier.py +0 -35
  104. kfinance/tool_calling/get_competitors_from_identifier.py +0 -25
  105. kfinance/tool_calling/get_cusip_from_ticker.py +0 -20
  106. kfinance/tool_calling/get_earnings.py +0 -33
  107. kfinance/tool_calling/get_financial_line_item_from_identifier.py +0 -48
  108. kfinance/tool_calling/get_financial_statement_from_identifier.py +0 -44
  109. kfinance/tool_calling/get_history_metadata_from_identifier.py +0 -17
  110. kfinance/tool_calling/get_info_from_identifier.py +0 -16
  111. kfinance/tool_calling/get_isin_from_ticker.py +0 -20
  112. kfinance/tool_calling/get_latest_earnings.py +0 -30
  113. kfinance/tool_calling/get_merger_info_from_transaction_id.py +0 -68
  114. kfinance/tool_calling/get_mergers_from_identifier.py +0 -41
  115. kfinance/tool_calling/get_next_earnings.py +0 -30
  116. kfinance/tool_calling/get_prices_from_identifier.py +0 -46
  117. kfinance/tool_calling/get_segments_from_identifier.py +0 -44
  118. kfinance/tool_calling/get_transcript.py +0 -23
  119. kfinance/tool_calling/resolve_identifier.py +0 -18
  120. {kensho_kfinance-2.8.0.dist-info → kensho_kfinance-3.0.0.dist-info}/WHEEL +0 -0
  121. {kensho_kfinance-2.8.0.dist-info → kensho_kfinance-3.0.0.dist-info}/licenses/AUTHORS.md +0 -0
  122. {kensho_kfinance-2.8.0.dist-info → kensho_kfinance-3.0.0.dist-info}/licenses/LICENSE +0 -0
  123. {kensho_kfinance-2.8.0.dist-info → kensho_kfinance-3.0.0.dist-info}/top_level.txt +0 -0
  124. /kfinance/{models → client}/__init__.py +0 -0
  125. /kfinance/{models → client}/industry_models.py +0 -0
  126. /kfinance/{tests → client/models}/__init__.py +0 -0
  127. /kfinance/{models → client/models}/currency_models.py +0 -0
  128. /kfinance/{models → client/models}/date_and_period_models.py +0 -0
  129. /kfinance/{tests/test_models → client/models/tests}/__init__.py +0 -0
  130. /kfinance/{models → client}/permission_models.py +0 -0
  131. /kfinance/{server_thread.py → client/server_thread.py} +0 -0
  132. /kfinance/{models → domains/line_items}/line_item_models.py +0 -0
  133. /kfinance/{models → domains/segments}/segment_models.py +0 -0
  134. /kfinance/{models → domains/statements}/statement_models.py +0 -0
@@ -0,0 +1,89 @@
1
+ from datetime import date
2
+ from textwrap import dedent
3
+ from typing import Type
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+ from kfinance.client.batch_request_handling import Task, process_tasks_in_thread_pool_executor
8
+ from kfinance.client.permission_models import Permission
9
+ from kfinance.domains.capitalizations.capitalization_models import Capitalization
10
+ from kfinance.domains.companies.company_identifiers import (
11
+ fetch_company_ids_from_identifiers,
12
+ parse_identifiers,
13
+ )
14
+ from kfinance.integrations.tool_calling.tool_calling_models import (
15
+ KfinanceTool,
16
+ ToolArgsWithIdentifiers,
17
+ )
18
+
19
+
20
+ class GetCapitalizationFromIdentifiersArgs(ToolArgsWithIdentifiers):
21
+ # no description because the description for enum fields comes from the enum docstring.
22
+ capitalization: Capitalization
23
+ start_date: date | None = Field(
24
+ description="The start date for historical capitalization retrieval", default=None
25
+ )
26
+ end_date: date | None = Field(
27
+ description="The end date for historical capitalization retrieval", default=None
28
+ )
29
+
30
+
31
+ class GetCapitalizationFromIdentifiers(KfinanceTool):
32
+ name: str = "get_capitalization_from_identifiers"
33
+ description: str = dedent("""
34
+ Get the historical market cap, tev (Total Enterprise Value), or shares outstanding for a group of identifiers between inclusive start_date and inclusive end date.
35
+
36
+ - When possible, pass multiple identifiers in a single call rather than making multiple calls.
37
+ - When requesting the most recent values, leave start_date and end_date empty.
38
+
39
+ Example:
40
+ Query: "What are the market caps of AAPL and WMT?"
41
+ Function: get_capitalization_from_identifiers(capitalization=Capitalization.market_cap, identifiers=["AAPL", "WMT"])
42
+ """).strip()
43
+ args_schema: Type[BaseModel] = GetCapitalizationFromIdentifiersArgs
44
+ accepted_permissions: set[Permission] | None = {Permission.PricingPermission}
45
+
46
+ def _run(
47
+ self,
48
+ identifiers: list[str],
49
+ capitalization: Capitalization,
50
+ start_date: str | None = None,
51
+ end_date: str | None = None,
52
+ ) -> dict:
53
+ """Sample response:
54
+
55
+ {
56
+ 'SPGI': [
57
+ {'date': '2024-04-10', 'market_cap': {'unit': 'USD', 'value': '132766738270.00'}},
58
+ {'date': '2024-04-11', 'market_cap': {'unit': 'USD', 'value': '132416066761.00'}}
59
+ ]
60
+ }
61
+ """
62
+ api_client = self.kfinance_client.kfinance_api_client
63
+ parsed_identifiers = parse_identifiers(identifiers=identifiers, api_client=api_client)
64
+ identifiers_to_company_ids = fetch_company_ids_from_identifiers(
65
+ identifiers=parsed_identifiers, api_client=api_client
66
+ )
67
+
68
+ tasks = [
69
+ Task(
70
+ func=api_client.fetch_market_caps_tevs_and_shares_outstanding,
71
+ kwargs=dict(company_id=company_id, start_date=start_date, end_date=end_date),
72
+ result_key=identifier,
73
+ )
74
+ for identifier, company_id in identifiers_to_company_ids.items()
75
+ ]
76
+
77
+ capitalization_responses = process_tasks_in_thread_pool_executor(
78
+ api_client=api_client, tasks=tasks
79
+ )
80
+
81
+ return {
82
+ str(identifier): capitalization_response.model_dump_json_single_metric(
83
+ capitalization_metric=capitalization,
84
+ only_include_most_recent_value=True
85
+ if (len(identifiers) > 1 and start_date == end_date is None)
86
+ else False,
87
+ )
88
+ for identifier, capitalization_response in capitalization_responses.items()
89
+ }
File without changes
@@ -1,8 +1,8 @@
1
1
  from datetime import date
2
2
  from decimal import Decimal
3
3
 
4
- from kfinance.decimal_with_unit import Money, Shares
5
- from kfinance.models.capitalization_models import (
4
+ from kfinance.client.models.decimal_with_unit import Money, Shares
5
+ from kfinance.domains.capitalizations.capitalization_models import (
6
6
  Capitalization,
7
7
  Capitalizations,
8
8
  DailyCapitalization,
@@ -69,15 +69,13 @@ class TestCapitalizations:
69
69
  def test_single_attribute_serialization(self):
70
70
  """
71
71
  GIVEN a Capitalizations object
72
- WHEN we only want to jsonify a single attribute like market_cap
72
+ WHEN we only want to dump a single attribute like market_cap
73
73
  THEN that attribute gets returned in an LLM-readable, jsonifyable dict format
74
74
  """
75
- expected_serialization = {
76
- "market_cap": [
77
- {"2024-06-24": {"unit": "USD", "value": "139231113000.00"}},
78
- {"2024-06-25": {"unit": "USD", "value": "140423262000.00"}},
79
- ]
80
- }
75
+ expected_serialization = [
76
+ {"date": "2024-06-24", "market_cap": {"unit": "USD", "value": "139231113000.00"}},
77
+ {"date": "2024-06-25", "market_cap": {"unit": "USD", "value": "140423262000.00"}},
78
+ ]
81
79
  capitalizations = Capitalizations.model_validate(self.api_resp)
82
- serialized = capitalizations.jsonify_single_attribute(Capitalization.market_cap)
80
+ serialized = capitalizations.model_dump_json_single_metric(Capitalization.market_cap)
83
81
  assert serialized == expected_serialization
@@ -0,0 +1,85 @@
1
+ from requests_mock import Mocker
2
+
3
+ from kfinance.client.kfinance import Client
4
+ from kfinance.conftest import SPGI_COMPANY_ID
5
+ from kfinance.domains.capitalizations.capitalization_models import Capitalization
6
+ from kfinance.domains.capitalizations.capitalization_tools import (
7
+ GetCapitalizationFromIdentifiers,
8
+ GetCapitalizationFromIdentifiersArgs,
9
+ )
10
+ from kfinance.domains.companies.company_models import COMPANY_ID_PREFIX
11
+
12
+
13
+ class TestGetCapitalizationFromCompanyIds:
14
+ market_caps_resp = {
15
+ "currency": "USD",
16
+ "market_caps": [
17
+ {
18
+ "date": "2024-04-10",
19
+ "market_cap": "132766738270.000000",
20
+ "tev": "147455738270.000000",
21
+ "shares_outstanding": 313099562,
22
+ },
23
+ {
24
+ "date": "2024-04-11",
25
+ "market_cap": "132416066761.000000",
26
+ "tev": "147105066761.000000",
27
+ "shares_outstanding": 313099562,
28
+ },
29
+ ],
30
+ }
31
+
32
+ def test_get_capitalization_from_identifiers(self, requests_mock: Mocker, mock_client: Client):
33
+ """
34
+ GIVEN the GetCapitalizationFromIdentifiers tool
35
+ WHEN we request the SPGI market cap
36
+ THEN we get back the SPGI market cap
37
+ """
38
+ requests_mock.get(
39
+ url=f"https://kfinance.kensho.com/api/v1/market_cap/{SPGI_COMPANY_ID}/none/none",
40
+ json=self.market_caps_resp,
41
+ )
42
+
43
+ expected_response = {
44
+ "SPGI": [
45
+ {"date": "2024-04-10", "market_cap": {"unit": "USD", "value": "132766738270.00"}},
46
+ {"date": "2024-04-11", "market_cap": {"unit": "USD", "value": "132416066761.00"}},
47
+ ]
48
+ }
49
+
50
+ tool = GetCapitalizationFromIdentifiers(kfinance_client=mock_client)
51
+ args = GetCapitalizationFromIdentifiersArgs(
52
+ identifiers=["SPGI"], capitalization=Capitalization.market_cap
53
+ )
54
+ response = tool.run(args.model_dump(mode="json"))
55
+ assert response == expected_response
56
+
57
+ def test_most_recent_request(self, requests_mock: Mocker, mock_client: Client) -> None:
58
+ """
59
+ GIVEN the GetCapitalizationFromIdentifiers tool
60
+ WHEN we request most recent market caps for multiple companies
61
+ THEN we only get back the most recent market cap for each company
62
+ """
63
+ expected_response = {
64
+ "C_1": [
65
+ {"date": "2024-04-10", "market_cap": {"unit": "USD", "value": "132766738270.00"}}
66
+ ],
67
+ "C_2": [
68
+ {"date": "2024-04-10", "market_cap": {"unit": "USD", "value": "132766738270.00"}}
69
+ ],
70
+ }
71
+
72
+ company_ids = [1, 2]
73
+ for company_id in company_ids:
74
+ requests_mock.get(
75
+ url=f"https://kfinance.kensho.com/api/v1/market_cap/{company_id}/none/none",
76
+ json=self.market_caps_resp,
77
+ )
78
+ tool = GetCapitalizationFromIdentifiers(kfinance_client=mock_client)
79
+ args = GetCapitalizationFromIdentifiersArgs(
80
+ identifiers=[f"{COMPANY_ID_PREFIX}{company_id}" for company_id in company_ids],
81
+ capitalization=Capitalization.market_cap,
82
+ )
83
+
84
+ response = tool.run(args.model_dump(mode="json"))
85
+ assert response == expected_response
File without changes
@@ -0,0 +1,175 @@
1
+ from dataclasses import dataclass
2
+ from typing import Hashable, Protocol
3
+
4
+ from kfinance.client.batch_request_handling import Task, process_tasks_in_thread_pool_executor
5
+ from kfinance.client.fetch import KFinanceApiClient
6
+ from kfinance.domains.companies.company_models import COMPANY_ID_PREFIX, IdentificationTriple
7
+
8
+
9
+ class CompanyIdentifier(Protocol, Hashable):
10
+ """A CompanyIdentifier is an identifier that can be resolved to a company, security, or trading item id.
11
+
12
+ The two current identifiers are:
13
+ - Ticker/CUSIP/ISIN (resolve through ID triple)
14
+ - company id (from tools like business relationships)
15
+
16
+ These identifiers both have ways of fetching company, security, and trading
17
+ item ids but the paths differ, so this protocol defines the functional requirements
18
+ and leaves the implementation to sub classes.
19
+ """
20
+
21
+ api_client: KFinanceApiClient
22
+
23
+ def fetch_company_id(self) -> int:
24
+ """Return the company_id associated with the CompanyIdentifier."""
25
+
26
+ def fetch_security_id(self) -> int:
27
+ """Return the security_id associated with the CompanyIdentifier."""
28
+
29
+ def fetch_trading_item_id(self) -> int:
30
+ """Return the trading_item_id associated with the CompanyIdentifier."""
31
+
32
+
33
+ @dataclass
34
+ class Identifier(CompanyIdentifier):
35
+ """An identifier (ticker, CUSIP, ISIN), which can be resolved to company, security, or trading item id.
36
+
37
+ The resolution happens by fetching the id triple.
38
+ """
39
+
40
+ identifier: str
41
+ api_client: KFinanceApiClient
42
+ _id_triple: IdentificationTriple | None = None
43
+
44
+ def __str__(self) -> str:
45
+ return self.identifier
46
+
47
+ def __hash__(self) -> int:
48
+ return hash(self.identifier)
49
+
50
+ @property
51
+ def id_triple(self) -> IdentificationTriple:
52
+ """Return the id triple of the company."""
53
+ if self._id_triple is None:
54
+ id_triple_resp = self.api_client.fetch_id_triple(identifier=self.identifier)
55
+ self._id_triple = IdentificationTriple(
56
+ trading_item_id=id_triple_resp["trading_item_id"],
57
+ security_id=id_triple_resp["security_id"],
58
+ company_id=id_triple_resp["company_id"],
59
+ )
60
+ return self._id_triple
61
+
62
+ def fetch_company_id(self) -> int:
63
+ """Return the company_id associated with the Identifier."""
64
+ return self.id_triple.company_id
65
+
66
+ def fetch_security_id(self) -> int:
67
+ """Return the security_id associated with the Identifier."""
68
+ return self.id_triple.security_id
69
+
70
+ def fetch_trading_item_id(self) -> int:
71
+ """Return the trading_item_id associated with the Identifier."""
72
+ return self.id_triple.trading_item_id
73
+
74
+
75
+ @dataclass
76
+ class CompanyId(CompanyIdentifier):
77
+ """A company id, which can be resolved to security and trading item id.
78
+
79
+ The resolution happens by fetching the primary security and trading item id
80
+ associated with the company id.
81
+ """
82
+
83
+ company_id: int
84
+ api_client: KFinanceApiClient
85
+ _security_id: int | None = None
86
+ _trading_item_id: int | None = None
87
+
88
+ def __str__(self) -> str:
89
+ return f"{COMPANY_ID_PREFIX}{self.company_id}"
90
+
91
+ def __hash__(self) -> int:
92
+ return hash(self.company_id)
93
+
94
+ def fetch_company_id(self) -> int:
95
+ """Return the company_id."""
96
+ return self.company_id
97
+
98
+ def fetch_security_id(self) -> int:
99
+ """Return the security_id associated with the CompanyId."""
100
+ if self._security_id is None:
101
+ security_resp = self.api_client.fetch_primary_security(company_id=self.company_id)
102
+ self._security_id = security_resp["primary_security"]
103
+ return self._security_id
104
+
105
+ def fetch_trading_item_id(self) -> int:
106
+ """Return the trading_item_id associated with the CompanyId."""
107
+ if self._trading_item_id is None:
108
+ trading_item_resp = self.api_client.fetch_primary_trading_item(
109
+ security_id=self.fetch_security_id()
110
+ )
111
+ self._trading_item_id = trading_item_resp["primary_trading_item"]
112
+ return self._trading_item_id
113
+
114
+
115
+ def parse_identifiers(
116
+ identifiers: list[str], api_client: KFinanceApiClient
117
+ ) -> list[CompanyIdentifier]:
118
+ """Return a list of CompanyIdentifier based on a list of string identifiers."""
119
+
120
+ parsed_identifiers: list[CompanyIdentifier] = []
121
+ for identifier in identifiers:
122
+ if identifier.startswith(COMPANY_ID_PREFIX):
123
+ parsed_identifiers.append(
124
+ CompanyId(
125
+ company_id=int(identifier[len(COMPANY_ID_PREFIX) :]), api_client=api_client
126
+ )
127
+ )
128
+ else:
129
+ parsed_identifiers.append(Identifier(identifier=identifier, api_client=api_client))
130
+
131
+ return parsed_identifiers
132
+
133
+
134
+ def fetch_company_ids_from_identifiers(
135
+ identifiers: list[CompanyIdentifier], api_client: KFinanceApiClient
136
+ ) -> dict[CompanyIdentifier, int]:
137
+ """Resolve a list of CompanyIdentifier to the corresponding company_ids."""
138
+
139
+ tasks = [
140
+ Task(
141
+ func=identifier.fetch_company_id,
142
+ result_key=identifier,
143
+ )
144
+ for identifier in identifiers
145
+ ]
146
+ return process_tasks_in_thread_pool_executor(api_client=api_client, tasks=tasks)
147
+
148
+
149
+ def fetch_security_ids_from_identifiers(
150
+ identifiers: list[CompanyIdentifier], api_client: KFinanceApiClient
151
+ ) -> dict[CompanyIdentifier, int]:
152
+ """Resolve a list of CompanyIdentifier to the corresponding security_ids."""
153
+
154
+ tasks = [
155
+ Task(
156
+ func=identifier.fetch_security_id,
157
+ result_key=identifier,
158
+ )
159
+ for identifier in identifiers
160
+ ]
161
+ return process_tasks_in_thread_pool_executor(api_client=api_client, tasks=tasks)
162
+
163
+
164
+ def fetch_trading_item_ids_from_identifiers(
165
+ identifiers: list[CompanyIdentifier], api_client: KFinanceApiClient
166
+ ) -> dict[CompanyIdentifier, int]:
167
+ """Resolve a list of CompanyIdentifier to the corresponding trading_item_ids."""
168
+ tasks = [
169
+ Task(
170
+ func=identifier.fetch_trading_item_id,
171
+ result_key=identifier,
172
+ )
173
+ for identifier in identifiers
174
+ ]
175
+ return process_tasks_in_thread_pool_executor(api_client=api_client, tasks=tasks)
@@ -0,0 +1,27 @@
1
+ from typing import NamedTuple
2
+
3
+ from pydantic import BaseModel, field_serializer
4
+
5
+
6
+ COMPANY_ID_PREFIX = "C_"
7
+
8
+
9
+ class CompanyIdAndName(BaseModel):
10
+ """A company_id and name"""
11
+
12
+ company_id: int
13
+ company_name: str
14
+
15
+ @field_serializer("company_id")
16
+ def serialize_with_prefix(self, company_id: int) -> str:
17
+ """Serialize the company_id with a prefix ("C_<company_id>").
18
+
19
+ Including the prefix allows us to distinguish tickers and company_ids.
20
+ """
21
+ return f"{COMPANY_ID_PREFIX}{company_id}"
22
+
23
+
24
+ class IdentificationTriple(NamedTuple):
25
+ trading_item_id: int
26
+ security_id: int
27
+ company_id: int
@@ -0,0 +1,66 @@
1
+ from textwrap import dedent
2
+ from typing import Type
3
+
4
+ from pydantic import BaseModel
5
+
6
+ from kfinance.client.batch_request_handling import Task, process_tasks_in_thread_pool_executor
7
+ from kfinance.client.permission_models import Permission
8
+ from kfinance.domains.companies.company_identifiers import (
9
+ fetch_company_ids_from_identifiers,
10
+ parse_identifiers,
11
+ )
12
+ from kfinance.integrations.tool_calling.tool_calling_models import (
13
+ KfinanceTool,
14
+ ToolArgsWithIdentifiers,
15
+ )
16
+
17
+
18
+ class GetInfoFromIdentifiers(KfinanceTool):
19
+ name: str = "get_info_from_identifiers"
20
+ description: str = dedent("""
21
+ Get the information associated with a list of identifiers. Info includes company name, status, type, simple industry, number of employees (if available), founding date, webpage, HQ address, HQ city, HQ zip code, HQ state, HQ country, and HQ country iso code.
22
+
23
+ - When possible, pass multiple identifiers in a single call rather than making multiple calls.
24
+ """).strip()
25
+ args_schema: Type[BaseModel] = ToolArgsWithIdentifiers
26
+ accepted_permissions: set[Permission] | None = None
27
+
28
+ def _run(self, identifiers: list[str]) -> dict:
29
+ """Sample response:
30
+
31
+ {
32
+ "SPGI": {
33
+ "name": "S&P Global Inc.",
34
+ "status": "Operating",
35
+ "type": "Public Company",
36
+ "simple_industry": "Capital Markets",
37
+ "number_of_employees": "42350.0000",
38
+ "founding_date": "1860-01-01",
39
+ "webpage": "www.spglobal.com",
40
+ "address": "55 Water Street",
41
+ "city": "New York",
42
+ "zip_code": "10041-0001",
43
+ "state": "New York",
44
+ "country": "United States",
45
+ "iso_country": "USA"
46
+ }
47
+ }
48
+ """
49
+ api_client = self.kfinance_client.kfinance_api_client
50
+ parsed_identifiers = parse_identifiers(identifiers=identifiers, api_client=api_client)
51
+ identifiers_to_company_ids = fetch_company_ids_from_identifiers(
52
+ identifiers=parsed_identifiers, api_client=api_client
53
+ )
54
+
55
+ tasks = [
56
+ Task(
57
+ func=api_client.fetch_info,
58
+ kwargs=dict(company_id=company_id),
59
+ result_key=identifier,
60
+ )
61
+ for identifier, company_id in identifiers_to_company_ids.items()
62
+ ]
63
+
64
+ info_responses = process_tasks_in_thread_pool_executor(api_client=api_client, tasks=tasks)
65
+
66
+ return {str(identifier): result for identifier, result in info_responses.items()}
File without changes
@@ -0,0 +1,26 @@
1
+ from requests_mock import Mocker
2
+
3
+ from kfinance.client.kfinance import Client
4
+ from kfinance.conftest import SPGI_COMPANY_ID
5
+ from kfinance.domains.companies.company_tools import GetInfoFromIdentifiers
6
+ from kfinance.integrations.tool_calling.tool_calling_models import ToolArgsWithIdentifiers
7
+
8
+
9
+ class TestGetInfoFromIdentifiers:
10
+ def test_get_info_from_identifiers(self, mock_client: Client, requests_mock: Mocker):
11
+ """
12
+ GIVEN the GetInfoFromIdentifiers tool
13
+ WHEN request info for SPGI
14
+ THEN we get back info for SPGI
15
+ """
16
+
17
+ info_resp = {"name": "S&P Global Inc.", "status": "Operating"}
18
+ expected_response = {"SPGI": info_resp}
19
+ requests_mock.get(
20
+ url=f"https://kfinance.kensho.com/api/v1/info/{SPGI_COMPANY_ID}",
21
+ json=info_resp,
22
+ )
23
+
24
+ tool = GetInfoFromIdentifiers(kfinance_client=mock_client)
25
+ resp = tool.run(ToolArgsWithIdentifiers(identifiers=["SPGI"]).model_dump(mode="json"))
26
+ assert resp == expected_response
File without changes
@@ -1,5 +1,8 @@
1
+ from pydantic import BaseModel
1
2
  from strenum import StrEnum
2
3
 
4
+ from kfinance.domains.companies.company_models import CompanyIdAndName
5
+
3
6
 
4
7
  class CompetitorSource(StrEnum):
5
8
  """The source type of the competitor information: 'filing' (from SEC filings), 'key_dev' (from key developments), 'contact' (from contact relationships), 'third_party' (from third-party sources), 'self_identified' (self-identified), 'named_by_competitor' (from competitor's perspective)."""
@@ -11,3 +14,7 @@ class CompetitorSource(StrEnum):
11
14
  third_party = "third_party"
12
15
  self_identified = "self_identified"
13
16
  named_by_competitor = "named_by_competitor"
17
+
18
+
19
+ class CompetitorResponse(BaseModel):
20
+ competitors: list[CompanyIdAndName]
@@ -0,0 +1,62 @@
1
+ from kfinance.client.batch_request_handling import Task, process_tasks_in_thread_pool_executor
2
+ from kfinance.client.permission_models import Permission
3
+ from kfinance.domains.companies.company_identifiers import (
4
+ Identifier,
5
+ fetch_company_ids_from_identifiers,
6
+ parse_identifiers,
7
+ )
8
+ from kfinance.domains.competitors.competitor_models import CompetitorResponse, CompetitorSource
9
+ from kfinance.integrations.tool_calling.tool_calling_models import (
10
+ KfinanceTool,
11
+ ToolArgsWithIdentifiers,
12
+ )
13
+
14
+
15
+ class GetCompetitorsFromIdentifiersArgs(ToolArgsWithIdentifiers):
16
+ # no description because the description for enum fields comes from the enum docstring.
17
+ competitor_source: CompetitorSource
18
+
19
+
20
+ class GetCompetitorsFromIdentifiers(KfinanceTool):
21
+ name: str = "get_competitors_from_identifiers"
22
+ description: str = "Retrieves a list of company_id and company_name that are competitors for a list of companies, optionally filtered by the source of the competitor information."
23
+ args_schema = GetCompetitorsFromIdentifiersArgs
24
+ accepted_permissions: set[Permission] | None = {Permission.CompetitorsPermission}
25
+
26
+ def _run(
27
+ self,
28
+ identifiers: list[str],
29
+ competitor_source: CompetitorSource,
30
+ ) -> dict:
31
+ """Sample response:
32
+
33
+ {
34
+ SPGI: {
35
+ {'company_id': "C_35352", 'company_name': 'The Descartes Systems Group Inc.'},
36
+ {'company_id': "C_4003514", 'company_name': 'London Stock Exchange Group plc'}
37
+ }
38
+ }
39
+ """
40
+
41
+ api_client = self.kfinance_client.kfinance_api_client
42
+ parsed_identifiers = parse_identifiers(identifiers=identifiers, api_client=api_client)
43
+ identifiers_to_company_ids = fetch_company_ids_from_identifiers(
44
+ identifiers=parsed_identifiers, api_client=api_client
45
+ )
46
+
47
+ tasks = [
48
+ Task(
49
+ func=api_client.fetch_competitors,
50
+ kwargs=dict(company_id=company_id, competitor_source=competitor_source),
51
+ result_key=identifier,
52
+ )
53
+ for identifier, company_id in identifiers_to_company_ids.items()
54
+ ]
55
+
56
+ competitor_responses: dict[Identifier, CompetitorResponse] = (
57
+ process_tasks_in_thread_pool_executor(api_client=api_client, tasks=tasks)
58
+ )
59
+ return {
60
+ str(identifier): competitors.model_dump(mode="json")
61
+ for identifier, competitors in competitor_responses.items()
62
+ }
File without changes
@@ -0,0 +1,45 @@
1
+ from requests_mock import Mocker
2
+
3
+ from kfinance.client.kfinance import Client
4
+ from kfinance.conftest import SPGI_COMPANY_ID
5
+ from kfinance.domains.competitors.competitor_models import CompetitorSource
6
+ from kfinance.domains.competitors.competitor_tools import (
7
+ GetCompetitorsFromIdentifiers,
8
+ GetCompetitorsFromIdentifiersArgs,
9
+ )
10
+
11
+
12
+ class TestGetCompetitorsFromIdentifiers:
13
+ def test_get_competitors_from_identifiers(self, mock_client: Client, requests_mock: Mocker):
14
+ """
15
+ GIVEN the GetCompetitorsFromIdentifiers tool
16
+ WHEN we request the SPGI competitors that are named by competitors
17
+ THEN we get back the SPGI competitors that are named by competitors
18
+ """
19
+ competitors_response = {
20
+ "competitors": [
21
+ {"company_id": 35352, "company_name": "The Descartes Systems Group Inc."},
22
+ {"company_id": 4003514, "company_name": "London Stock Exchange Group plc"},
23
+ ]
24
+ }
25
+ expected_response = {
26
+ "SPGI": {
27
+ "competitors": [
28
+ {"company_id": "C_35352", "company_name": "The Descartes Systems Group Inc."},
29
+ {"company_id": "C_4003514", "company_name": "London Stock Exchange Group plc"},
30
+ ]
31
+ }
32
+ }
33
+
34
+ requests_mock.get(
35
+ url=f"https://kfinance.kensho.com/api/v1/competitors/{SPGI_COMPANY_ID}/named_by_competitor",
36
+ # truncated from the original API response
37
+ json=competitors_response,
38
+ )
39
+
40
+ tool = GetCompetitorsFromIdentifiers(kfinance_client=mock_client)
41
+ args = GetCompetitorsFromIdentifiersArgs(
42
+ identifiers=["SPGI"], competitor_source=CompetitorSource.named_by_competitor
43
+ )
44
+ response = tool.run(args.model_dump(mode="json"))
45
+ assert response == expected_response
File without changes