kensho-kfinance 2.9.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.9.0.dist-info → kensho_kfinance-3.0.0.dist-info}/METADATA +1 -1
  2. kensho_kfinance-3.0.0.dist-info/RECORD +110 -0
  3. kfinance/CHANGELOG.md +3 -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} +37 -33
  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 +33 -29
  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.9.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 -42
  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 -69
  114. kfinance/tool_calling/get_mergers_from_identifier.py +0 -44
  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.9.0.dist-info → kensho_kfinance-3.0.0.dist-info}/WHEEL +0 -0
  121. {kensho_kfinance-2.9.0.dist-info → kensho_kfinance-3.0.0.dist-info}/licenses/AUTHORS.md +0 -0
  122. {kensho_kfinance-2.9.0.dist-info → kensho_kfinance-3.0.0.dist-info}/licenses/LICENSE +0 -0
  123. {kensho_kfinance-2.9.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,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
@@ -0,0 +1,80 @@
1
+ from typing import Type
2
+
3
+ from pydantic import BaseModel
4
+
5
+ from kfinance.client.batch_request_handling import Task, process_tasks_in_thread_pool_executor
6
+ from kfinance.client.permission_models import Permission
7
+ from kfinance.domains.companies.company_identifiers import (
8
+ fetch_security_ids_from_identifiers,
9
+ parse_identifiers,
10
+ )
11
+ from kfinance.integrations.tool_calling.tool_calling_models import (
12
+ KfinanceTool,
13
+ ToolArgsWithIdentifiers,
14
+ )
15
+
16
+
17
+ class GetCusipFromIdentifiers(KfinanceTool):
18
+ name: str = "get_cusip_from_identifiers"
19
+ description: str = "Get the CUSIPs for a group of identifiers."
20
+ args_schema: Type[BaseModel] = ToolArgsWithIdentifiers
21
+ accepted_permissions: set[Permission] | None = {Permission.IDPermission}
22
+
23
+ def _run(self, identifiers: list[str]) -> dict[str, str]:
24
+ """Sample response:
25
+
26
+ {"SPGI": "78409V104"}
27
+ """
28
+ api_client = self.kfinance_client.kfinance_api_client
29
+ parsed_identifiers = parse_identifiers(identifiers=identifiers, api_client=api_client)
30
+ identifiers_to_security_ids = fetch_security_ids_from_identifiers(
31
+ identifiers=parsed_identifiers, api_client=api_client
32
+ )
33
+
34
+ tasks = [
35
+ Task(
36
+ func=api_client.fetch_cusip,
37
+ kwargs=dict(security_id=security_id),
38
+ result_key=identifier,
39
+ )
40
+ for identifier, security_id in identifiers_to_security_ids.items()
41
+ ]
42
+
43
+ cusip_responses = process_tasks_in_thread_pool_executor(api_client=api_client, tasks=tasks)
44
+
45
+ return {str(identifier): resp["cusip"] for identifier, resp in cusip_responses.items()}
46
+
47
+
48
+ class GetIsinFromIdentifiersArgs(BaseModel):
49
+ security_ids: list[int]
50
+
51
+
52
+ class GetIsinFromIdentifiers(KfinanceTool):
53
+ name: str = "get_isin_from_identifiers"
54
+ description: str = "Get the ISINs for a group of identifiers."
55
+ args_schema: Type[BaseModel] = ToolArgsWithIdentifiers
56
+ accepted_permissions: set[Permission] | None = {Permission.IDPermission}
57
+
58
+ def _run(self, identifiers: list[str]) -> dict:
59
+ """Sample response:
60
+
61
+ {"SPGI": "US78409V1044"}
62
+ """
63
+ api_client = self.kfinance_client.kfinance_api_client
64
+ parsed_identifiers = parse_identifiers(identifiers=identifiers, api_client=api_client)
65
+ identifiers_to_security_ids = fetch_security_ids_from_identifiers(
66
+ identifiers=parsed_identifiers, api_client=api_client
67
+ )
68
+
69
+ tasks = [
70
+ Task(
71
+ func=api_client.fetch_isin,
72
+ kwargs=dict(security_id=security_id),
73
+ result_key=identifier,
74
+ )
75
+ for identifier, security_id in identifiers_to_security_ids.items()
76
+ ]
77
+
78
+ isin_responses = process_tasks_in_thread_pool_executor(api_client=api_client, tasks=tasks)
79
+
80
+ return {str(identifier): resp["isin"] for identifier, resp in isin_responses.items()}
File without changes
@@ -0,0 +1,57 @@
1
+ from requests_mock import Mocker
2
+
3
+ from kfinance.client.kfinance import Client
4
+ from kfinance.domains.companies.company_models import COMPANY_ID_PREFIX
5
+ from kfinance.domains.cusip_and_isin.cusip_and_isin_tools import (
6
+ GetCusipFromIdentifiers,
7
+ GetIsinFromIdentifiers,
8
+ )
9
+ from kfinance.integrations.tool_calling.tool_calling_models import ToolArgsWithIdentifiers
10
+
11
+
12
+ class TestGetCusipFromIdentifiers:
13
+ def test_get_cusip_from_identifiers(self, requests_mock: Mocker, mock_client: Client):
14
+ """
15
+ GIVEN the GetCusipFromIdentifiers tool
16
+ WHEN we request the CUSIPs for multiple companies
17
+ THEN we get back the corresponding CUSIPs
18
+ """
19
+
20
+ company_ids = [1, 2]
21
+ expected_response = {"C_1": "CU1", "C_2": "CU2"}
22
+ for security_id in company_ids:
23
+ requests_mock.get(
24
+ url=f"https://kfinance.kensho.com/api/v1/cusip/{security_id}",
25
+ json={"cusip": f"CU{security_id}"},
26
+ )
27
+ tool = GetCusipFromIdentifiers(kfinance_client=mock_client)
28
+ resp = tool.run(
29
+ ToolArgsWithIdentifiers(
30
+ identifiers=[f"{COMPANY_ID_PREFIX}{company_id}" for company_id in company_ids]
31
+ ).model_dump(mode="json")
32
+ )
33
+ assert resp == expected_response
34
+
35
+
36
+ class TestGetIsinFromSecurityIds:
37
+ def test_get_isin_from_security_ids(self, requests_mock: Mocker, mock_client: Client):
38
+ """
39
+ GIVEN the GetIsinFromSecurityIds tool
40
+ WHEN we request the ISINs for multiple security ids
41
+ THEN we get back the corresponding ISINs
42
+ """
43
+
44
+ company_ids = [1, 2]
45
+ expected_response = {"C_1": "IS1", "C_2": "IS2"}
46
+ for security_id in company_ids:
47
+ requests_mock.get(
48
+ url=f"https://kfinance.kensho.com/api/v1/isin/{security_id}",
49
+ json={"isin": f"IS{security_id}"},
50
+ )
51
+ tool = GetIsinFromIdentifiers(kfinance_client=mock_client)
52
+ resp = tool.run(
53
+ ToolArgsWithIdentifiers(
54
+ identifiers=[f"{COMPANY_ID_PREFIX}{company_id}" for company_id in company_ids]
55
+ ).model_dump(mode="json")
56
+ )
57
+ assert resp == expected_response
File without changes