kensho-kfinance 3.0.2__py3-none-any.whl → 3.1.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.
- {kensho_kfinance-3.0.2.dist-info → kensho_kfinance-3.1.0.dist-info}/METADATA +1 -1
- {kensho_kfinance-3.0.2.dist-info → kensho_kfinance-3.1.0.dist-info}/RECORD +46 -45
- kfinance/CHANGELOG.md +7 -1
- kfinance/client/fetch.py +29 -19
- kfinance/client/kfinance.py +17 -18
- kfinance/client/meta_classes.py +12 -13
- kfinance/client/tests/test_fetch.py +51 -34
- kfinance/client/tests/test_objects.py +130 -129
- kfinance/conftest.py +49 -5
- kfinance/domains/business_relationships/business_relationship_tools.py +30 -19
- kfinance/domains/business_relationships/tests/test_business_relationship_tools.py +18 -15
- kfinance/domains/capitalizations/capitalization_models.py +1 -1
- kfinance/domains/capitalizations/capitalization_tools.py +41 -24
- kfinance/domains/capitalizations/tests/test_capitalization_tools.py +38 -13
- kfinance/domains/companies/company_identifiers.py +0 -175
- kfinance/domains/companies/company_models.py +98 -5
- kfinance/domains/companies/company_tools.py +33 -29
- kfinance/domains/companies/tests/test_company_tools.py +11 -4
- kfinance/domains/competitors/competitor_tools.py +21 -21
- kfinance/domains/competitors/tests/test_competitor_tools.py +21 -7
- kfinance/domains/cusip_and_isin/cusip_and_isin_tools.py +38 -26
- kfinance/domains/cusip_and_isin/tests/test_cusip_and_isin_tools.py +27 -26
- kfinance/domains/earnings/earning_tools.py +54 -47
- kfinance/domains/earnings/tests/test_earnings_tools.py +58 -63
- kfinance/domains/line_items/line_item_models.py +7 -0
- kfinance/domains/line_items/line_item_tools.py +31 -30
- kfinance/domains/line_items/tests/test_line_item_tools.py +23 -5
- kfinance/domains/mergers_and_acquisitions/merger_and_acquisition_models.py +15 -0
- kfinance/domains/mergers_and_acquisitions/merger_and_acquisition_tools.py +55 -38
- kfinance/domains/mergers_and_acquisitions/tests/test_merger_and_acquisition_tools.py +22 -9
- kfinance/domains/prices/price_models.py +9 -9
- kfinance/domains/prices/price_tools.py +49 -38
- kfinance/domains/prices/tests/test_price_tools.py +52 -36
- kfinance/domains/segments/segment_models.py +7 -0
- kfinance/domains/segments/segment_tools.py +37 -20
- kfinance/domains/segments/tests/test_segment_tools.py +13 -6
- kfinance/domains/statements/statement_models.py +7 -0
- kfinance/domains/statements/statement_tools.py +38 -40
- kfinance/domains/statements/tests/test_statement_tools.py +39 -10
- kfinance/integrations/tool_calling/tests/test_tool_calling_models.py +2 -2
- kfinance/integrations/tool_calling/tool_calling_models.py +24 -5
- kfinance/version.py +2 -2
- {kensho_kfinance-3.0.2.dist-info → kensho_kfinance-3.1.0.dist-info}/WHEEL +0 -0
- {kensho_kfinance-3.0.2.dist-info → kensho_kfinance-3.1.0.dist-info}/licenses/AUTHORS.md +0 -0
- {kensho_kfinance-3.0.2.dist-info → kensho_kfinance-3.1.0.dist-info}/licenses/LICENSE +0 -0
- {kensho_kfinance-3.0.2.dist-info → kensho_kfinance-3.1.0.dist-info}/top_level.txt +0 -0
|
@@ -6,14 +6,11 @@ from pydantic import BaseModel, Field
|
|
|
6
6
|
|
|
7
7
|
from kfinance.client.batch_request_handling import Task, process_tasks_in_thread_pool_executor
|
|
8
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
|
-
)
|
|
9
|
+
from kfinance.domains.capitalizations.capitalization_models import Capitalization, Capitalizations
|
|
14
10
|
from kfinance.integrations.tool_calling.tool_calling_models import (
|
|
15
11
|
KfinanceTool,
|
|
16
12
|
ToolArgsWithIdentifiers,
|
|
13
|
+
ToolRespWithErrors,
|
|
17
14
|
)
|
|
18
15
|
|
|
19
16
|
|
|
@@ -28,6 +25,10 @@ class GetCapitalizationFromIdentifiersArgs(ToolArgsWithIdentifiers):
|
|
|
28
25
|
)
|
|
29
26
|
|
|
30
27
|
|
|
28
|
+
class GetCapitalizationFromIdentifiersResp(ToolRespWithErrors):
|
|
29
|
+
results: dict[str, Capitalizations]
|
|
30
|
+
|
|
31
|
+
|
|
31
32
|
class GetCapitalizationFromIdentifiers(KfinanceTool):
|
|
32
33
|
name: str = "get_capitalization_from_identifiers"
|
|
33
34
|
description: str = dedent("""
|
|
@@ -53,37 +54,53 @@ class GetCapitalizationFromIdentifiers(KfinanceTool):
|
|
|
53
54
|
"""Sample response:
|
|
54
55
|
|
|
55
56
|
{
|
|
56
|
-
'
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
'results': {
|
|
58
|
+
'SPGI': {
|
|
59
|
+
'capitalizations': [
|
|
60
|
+
{'date': '2024-04-10', 'market_cap': {'value': '132766738270.00', 'unit': 'USD'}},
|
|
61
|
+
{'date': '2024-04-11', 'market_cap': {'value': '132416066761.00', 'unit': 'USD'}}
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
'errors': ['No identification triple found for the provided identifier: NON-EXISTENT of type: ticker']
|
|
60
66
|
}
|
|
61
67
|
"""
|
|
62
68
|
api_client = self.kfinance_client.kfinance_api_client
|
|
63
|
-
|
|
64
|
-
identifiers_to_company_ids = fetch_company_ids_from_identifiers(
|
|
65
|
-
identifiers=parsed_identifiers, api_client=api_client
|
|
66
|
-
)
|
|
69
|
+
id_triple_resp = api_client.unified_fetch_id_triples(identifiers=identifiers)
|
|
67
70
|
|
|
68
71
|
tasks = [
|
|
69
72
|
Task(
|
|
70
73
|
func=api_client.fetch_market_caps_tevs_and_shares_outstanding,
|
|
71
|
-
kwargs=dict(
|
|
74
|
+
kwargs=dict(
|
|
75
|
+
company_id=id_triple.company_id, start_date=start_date, end_date=end_date
|
|
76
|
+
),
|
|
72
77
|
result_key=identifier,
|
|
73
78
|
)
|
|
74
|
-
for identifier,
|
|
79
|
+
for identifier, id_triple in id_triple_resp.identifiers_to_id_triples.items()
|
|
75
80
|
]
|
|
76
81
|
|
|
77
82
|
capitalization_responses = process_tasks_in_thread_pool_executor(
|
|
78
83
|
api_client=api_client, tasks=tasks
|
|
79
84
|
)
|
|
80
85
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
86
|
+
for capitalization_response in capitalization_responses.values():
|
|
87
|
+
# If we return results for more than one company and the start and end dates are unset,
|
|
88
|
+
# truncate data to only return the most recent datapoint.
|
|
89
|
+
if len(capitalization_responses) > 1 and start_date is None and end_date is None:
|
|
90
|
+
capitalization_response.capitalizations = capitalization_response.capitalizations[
|
|
91
|
+
-1:
|
|
92
|
+
]
|
|
93
|
+
# Set capitalizations that were not requested to None.
|
|
94
|
+
# That way, they can be skipped for serialization via `exclude_none=True`
|
|
95
|
+
for daily_capitalization in capitalization_response.capitalizations:
|
|
96
|
+
if capitalization is not Capitalization.market_cap:
|
|
97
|
+
daily_capitalization.market_cap = None
|
|
98
|
+
if capitalization is not Capitalization.tev:
|
|
99
|
+
daily_capitalization.tev = None
|
|
100
|
+
if capitalization is not Capitalization.shares_outstanding:
|
|
101
|
+
daily_capitalization.shares_outstanding = None
|
|
102
|
+
|
|
103
|
+
resp_model = GetCapitalizationFromIdentifiersResp(
|
|
104
|
+
results=capitalization_responses, errors=list(id_triple_resp.errors.values())
|
|
105
|
+
)
|
|
106
|
+
return resp_model.model_dump(mode="json", exclude_none=True)
|
|
@@ -32,8 +32,8 @@ class TestGetCapitalizationFromCompanyIds:
|
|
|
32
32
|
def test_get_capitalization_from_identifiers(self, requests_mock: Mocker, mock_client: Client):
|
|
33
33
|
"""
|
|
34
34
|
GIVEN the GetCapitalizationFromIdentifiers tool
|
|
35
|
-
WHEN we request the SPGI
|
|
36
|
-
THEN we get back the SPGI market cap
|
|
35
|
+
WHEN we request the market cap for SPGI and a non-existent company
|
|
36
|
+
THEN we get back the SPGI market cap and error for the non-existent company
|
|
37
37
|
"""
|
|
38
38
|
requests_mock.get(
|
|
39
39
|
url=f"https://kfinance.kensho.com/api/v1/market_cap/{SPGI_COMPANY_ID}/none/none",
|
|
@@ -41,15 +41,28 @@ class TestGetCapitalizationFromCompanyIds:
|
|
|
41
41
|
)
|
|
42
42
|
|
|
43
43
|
expected_response = {
|
|
44
|
-
"
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
"results": {
|
|
45
|
+
"SPGI": {
|
|
46
|
+
"capitalizations": [
|
|
47
|
+
{
|
|
48
|
+
"date": "2024-04-10",
|
|
49
|
+
"market_cap": {"value": "132766738270.00", "unit": "USD"},
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"date": "2024-04-11",
|
|
53
|
+
"market_cap": {"value": "132416066761.00", "unit": "USD"},
|
|
54
|
+
},
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"errors": [
|
|
59
|
+
"No identification triple found for the provided identifier: NON-EXISTENT of type: ticker"
|
|
60
|
+
],
|
|
48
61
|
}
|
|
49
62
|
|
|
50
63
|
tool = GetCapitalizationFromIdentifiers(kfinance_client=mock_client)
|
|
51
64
|
args = GetCapitalizationFromIdentifiersArgs(
|
|
52
|
-
identifiers=["SPGI"], capitalization=Capitalization.market_cap
|
|
65
|
+
identifiers=["SPGI", "non-existent"], capitalization=Capitalization.market_cap
|
|
53
66
|
)
|
|
54
67
|
response = tool.run(args.model_dump(mode="json"))
|
|
55
68
|
assert response == expected_response
|
|
@@ -61,12 +74,24 @@ class TestGetCapitalizationFromCompanyIds:
|
|
|
61
74
|
THEN we only get back the most recent market cap for each company
|
|
62
75
|
"""
|
|
63
76
|
expected_response = {
|
|
64
|
-
"
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
77
|
+
"results": {
|
|
78
|
+
"C_1": {
|
|
79
|
+
"capitalizations": [
|
|
80
|
+
{
|
|
81
|
+
"date": "2024-04-11",
|
|
82
|
+
"market_cap": {"unit": "USD", "value": "132416066761.00"},
|
|
83
|
+
}
|
|
84
|
+
]
|
|
85
|
+
},
|
|
86
|
+
"C_2": {
|
|
87
|
+
"capitalizations": [
|
|
88
|
+
{
|
|
89
|
+
"date": "2024-04-11",
|
|
90
|
+
"market_cap": {"unit": "USD", "value": "132416066761.00"},
|
|
91
|
+
}
|
|
92
|
+
]
|
|
93
|
+
},
|
|
94
|
+
}
|
|
70
95
|
}
|
|
71
96
|
|
|
72
97
|
company_ids = [1, 2]
|
|
@@ -1,175 +0,0 @@
|
|
|
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)
|
|
@@ -1,11 +1,16 @@
|
|
|
1
|
-
from typing import
|
|
1
|
+
from typing import Any
|
|
2
2
|
|
|
3
|
-
from pydantic import BaseModel, field_serializer
|
|
3
|
+
from pydantic import BaseModel, ConfigDict, Field, field_serializer, model_validator
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
COMPANY_ID_PREFIX = "C_"
|
|
7
7
|
|
|
8
8
|
|
|
9
|
+
def prefix_company_id(company_id: int) -> str:
|
|
10
|
+
"""Return the company_id with the COMPANY_ID_PREFIX"""
|
|
11
|
+
return f"{COMPANY_ID_PREFIX}{company_id}"
|
|
12
|
+
|
|
13
|
+
|
|
9
14
|
class CompanyIdAndName(BaseModel):
|
|
10
15
|
"""A company_id and name"""
|
|
11
16
|
|
|
@@ -21,7 +26,95 @@ class CompanyIdAndName(BaseModel):
|
|
|
21
26
|
return f"{COMPANY_ID_PREFIX}{company_id}"
|
|
22
27
|
|
|
23
28
|
|
|
24
|
-
class IdentificationTriple(
|
|
25
|
-
trading_item_id: int
|
|
26
|
-
security_id: int
|
|
29
|
+
class IdentificationTriple(BaseModel):
|
|
27
30
|
company_id: int
|
|
31
|
+
security_id: int | None = Field(description="Private companies do not have a security_id.")
|
|
32
|
+
trading_item_id: int | None = Field(
|
|
33
|
+
description="Private companies do not have a trading_item_id."
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# frozen to allow hashing
|
|
37
|
+
model_config = ConfigDict(frozen=True)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class IdTripleResolutionError(BaseModel):
|
|
41
|
+
"""Error returned when an identifier cannot be resolved."""
|
|
42
|
+
|
|
43
|
+
error: str
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class UnifiedIdTripleResponse(BaseModel):
|
|
47
|
+
"""A response from the unified id triple endpoint (POST /ids).
|
|
48
|
+
|
|
49
|
+
For easier handling within tools, we split the api response into
|
|
50
|
+
identifiers_to_id_triples (successful resolution) and errors (resolution failed).
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
identifiers_to_id_triples: dict[str, IdentificationTriple] = Field(
|
|
54
|
+
description="A mapping of all identifiers that could successfully be resolved"
|
|
55
|
+
"to the corresponding identification triples."
|
|
56
|
+
)
|
|
57
|
+
errors: dict[str, str] = Field(
|
|
58
|
+
description="A mapping of all identifiers that could not be resolved or don't have "
|
|
59
|
+
"a required field like security_id with the corresponding error messages."
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
@model_validator(mode="before")
|
|
63
|
+
@classmethod
|
|
64
|
+
def separate_successful_and_failed_resolutions(cls, data: Any) -> Any:
|
|
65
|
+
"""Split response into identifiers_to_id_triples (success) and errors
|
|
66
|
+
|
|
67
|
+
Pre-processed API response:
|
|
68
|
+
{
|
|
69
|
+
'data': {
|
|
70
|
+
'SPGI': {'trading_item_id': 2629108, 'security_id': 2629107, 'company_id': 21719},
|
|
71
|
+
'non-existent': {'error': 'No identification triple found for the provided identifier: NON-EXISTENT of type: ticker'}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
Post-processed API response:
|
|
76
|
+
{
|
|
77
|
+
'identifiers_to_id_triples': {
|
|
78
|
+
'SPGI': {'trading_item_id': 2629108, 'security_id': 2629107, 'company_id': 21719},
|
|
79
|
+
},
|
|
80
|
+
'errors': {
|
|
81
|
+
'non-existent': 'No identification triple found for the provided identifier: NON-EXISTENT of type: ticker'
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
"""
|
|
87
|
+
output: dict[str, dict] = dict(identifiers_to_id_triples=dict(), errors=dict())
|
|
88
|
+
if isinstance(data, dict) and "data" in data:
|
|
89
|
+
for key, val in data["data"].items():
|
|
90
|
+
if "error" in val:
|
|
91
|
+
output["errors"][key] = val["error"]
|
|
92
|
+
else:
|
|
93
|
+
output["identifiers_to_id_triples"][key] = val
|
|
94
|
+
return output
|
|
95
|
+
|
|
96
|
+
def filter_out_companies_without_security_ids(self) -> None:
|
|
97
|
+
"""Filter out companies that don't have a security_id and add an error for them."""
|
|
98
|
+
|
|
99
|
+
identifiers_to_remove = [
|
|
100
|
+
identifier
|
|
101
|
+
for identifier, id_triple in self.identifiers_to_id_triples.items()
|
|
102
|
+
if id_triple.security_id is None
|
|
103
|
+
]
|
|
104
|
+
for identifier in identifiers_to_remove:
|
|
105
|
+
self.errors[identifier] = f"{identifier} is a private company without a security_id."
|
|
106
|
+
self.identifiers_to_id_triples.pop(identifier)
|
|
107
|
+
|
|
108
|
+
def filter_out_companies_without_trading_item_ids(self) -> None:
|
|
109
|
+
"""Filter out companies that don't have a trading_item_id and add an error for them."""
|
|
110
|
+
|
|
111
|
+
identifiers_to_remove = [
|
|
112
|
+
identifier
|
|
113
|
+
for identifier, id_triple in self.identifiers_to_id_triples.items()
|
|
114
|
+
if id_triple.trading_item_id is None
|
|
115
|
+
]
|
|
116
|
+
for identifier in identifiers_to_remove:
|
|
117
|
+
self.errors[identifier] = (
|
|
118
|
+
f"{identifier} is a private company without a trading_item_id."
|
|
119
|
+
)
|
|
120
|
+
self.identifiers_to_id_triples.pop(identifier)
|
|
@@ -5,16 +5,17 @@ from pydantic import BaseModel
|
|
|
5
5
|
|
|
6
6
|
from kfinance.client.batch_request_handling import Task, process_tasks_in_thread_pool_executor
|
|
7
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
8
|
from kfinance.integrations.tool_calling.tool_calling_models import (
|
|
13
9
|
KfinanceTool,
|
|
14
10
|
ToolArgsWithIdentifiers,
|
|
11
|
+
ToolRespWithErrors,
|
|
15
12
|
)
|
|
16
13
|
|
|
17
14
|
|
|
15
|
+
class GetInfoFromIdentifiersResp(ToolRespWithErrors):
|
|
16
|
+
results: dict[str, dict]
|
|
17
|
+
|
|
18
|
+
|
|
18
19
|
class GetInfoFromIdentifiers(KfinanceTool):
|
|
19
20
|
name: str = "get_info_from_identifiers"
|
|
20
21
|
description: str = dedent("""
|
|
@@ -28,39 +29,42 @@ class GetInfoFromIdentifiers(KfinanceTool):
|
|
|
28
29
|
def _run(self, identifiers: list[str]) -> dict:
|
|
29
30
|
"""Sample response:
|
|
30
31
|
|
|
31
|
-
{
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
32
|
+
{ "results": {
|
|
33
|
+
"SPGI": {
|
|
34
|
+
"name": "S&P Global Inc.",
|
|
35
|
+
"status": "Operating",
|
|
36
|
+
"type": "Public Company",
|
|
37
|
+
"simple_industry": "Capital Markets",
|
|
38
|
+
"number_of_employees": "42350.0000",
|
|
39
|
+
"founding_date": "1860-01-01",
|
|
40
|
+
"webpage": "www.spglobal.com",
|
|
41
|
+
"address": "55 Water Street",
|
|
42
|
+
"city": "New York",
|
|
43
|
+
"zip_code": "10041-0001",
|
|
44
|
+
"state": "New York",
|
|
45
|
+
"country": "United States",
|
|
46
|
+
"iso_country": "USA"
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"errors": [['No identification triple found for the provided identifier: NON-EXISTENT of type: ticker']
|
|
47
50
|
}
|
|
48
51
|
"""
|
|
49
52
|
api_client = self.kfinance_client.kfinance_api_client
|
|
50
|
-
|
|
51
|
-
identifiers_to_company_ids = fetch_company_ids_from_identifiers(
|
|
52
|
-
identifiers=parsed_identifiers, api_client=api_client
|
|
53
|
-
)
|
|
53
|
+
id_triple_resp = api_client.unified_fetch_id_triples(identifiers=identifiers)
|
|
54
54
|
|
|
55
55
|
tasks = [
|
|
56
56
|
Task(
|
|
57
57
|
func=api_client.fetch_info,
|
|
58
|
-
kwargs=dict(company_id=company_id),
|
|
58
|
+
kwargs=dict(company_id=id_triple.company_id),
|
|
59
59
|
result_key=identifier,
|
|
60
60
|
)
|
|
61
|
-
for identifier,
|
|
61
|
+
for identifier, id_triple in id_triple_resp.identifiers_to_id_triples.items()
|
|
62
62
|
]
|
|
63
63
|
|
|
64
|
-
info_responses = process_tasks_in_thread_pool_executor(
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
info_responses: dict[str, dict] = process_tasks_in_thread_pool_executor(
|
|
65
|
+
api_client=api_client, tasks=tasks
|
|
66
|
+
)
|
|
67
|
+
resp_model = GetInfoFromIdentifiersResp(
|
|
68
|
+
results=info_responses, errors=list(id_triple_resp.errors.values())
|
|
69
|
+
)
|
|
70
|
+
return resp_model.model_dump(mode="json")
|
|
@@ -10,17 +10,24 @@ class TestGetInfoFromIdentifiers:
|
|
|
10
10
|
def test_get_info_from_identifiers(self, mock_client: Client, requests_mock: Mocker):
|
|
11
11
|
"""
|
|
12
12
|
GIVEN the GetInfoFromIdentifiers tool
|
|
13
|
-
WHEN request info for SPGI
|
|
14
|
-
THEN we get back info for SPGI
|
|
13
|
+
WHEN request info for SPGI and a non-existent company
|
|
14
|
+
THEN we get back info for SPGI and an error for the non-existent company
|
|
15
15
|
"""
|
|
16
16
|
|
|
17
17
|
info_resp = {"name": "S&P Global Inc.", "status": "Operating"}
|
|
18
|
-
expected_response = {
|
|
18
|
+
expected_response = {
|
|
19
|
+
"results": {"SPGI": info_resp},
|
|
20
|
+
"errors": [
|
|
21
|
+
"No identification triple found for the provided identifier: NON-EXISTENT of type: ticker"
|
|
22
|
+
],
|
|
23
|
+
}
|
|
19
24
|
requests_mock.get(
|
|
20
25
|
url=f"https://kfinance.kensho.com/api/v1/info/{SPGI_COMPANY_ID}",
|
|
21
26
|
json=info_resp,
|
|
22
27
|
)
|
|
23
28
|
|
|
24
29
|
tool = GetInfoFromIdentifiers(kfinance_client=mock_client)
|
|
25
|
-
resp = tool.run(
|
|
30
|
+
resp = tool.run(
|
|
31
|
+
ToolArgsWithIdentifiers(identifiers=["SPGI", "non-existent"]).model_dump(mode="json")
|
|
32
|
+
)
|
|
26
33
|
assert resp == expected_response
|
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
from kfinance.client.batch_request_handling import Task, process_tasks_in_thread_pool_executor
|
|
2
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
3
|
from kfinance.domains.competitors.competitor_models import CompetitorResponse, CompetitorSource
|
|
9
4
|
from kfinance.integrations.tool_calling.tool_calling_models import (
|
|
10
5
|
KfinanceTool,
|
|
11
6
|
ToolArgsWithIdentifiers,
|
|
7
|
+
ToolRespWithErrors,
|
|
12
8
|
)
|
|
13
9
|
|
|
14
10
|
|
|
@@ -17,6 +13,10 @@ class GetCompetitorsFromIdentifiersArgs(ToolArgsWithIdentifiers):
|
|
|
17
13
|
competitor_source: CompetitorSource
|
|
18
14
|
|
|
19
15
|
|
|
16
|
+
class GetCompetitorsFromIdentifiersResp(ToolRespWithErrors):
|
|
17
|
+
results: dict[str, CompetitorResponse]
|
|
18
|
+
|
|
19
|
+
|
|
20
20
|
class GetCompetitorsFromIdentifiers(KfinanceTool):
|
|
21
21
|
name: str = "get_competitors_from_identifiers"
|
|
22
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."
|
|
@@ -31,32 +31,32 @@ class GetCompetitorsFromIdentifiers(KfinanceTool):
|
|
|
31
31
|
"""Sample response:
|
|
32
32
|
|
|
33
33
|
{
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
"results": {
|
|
35
|
+
"SPGI": {
|
|
36
|
+
{'company_id': "C_35352", 'company_name': 'The Descartes Systems Group Inc.'},
|
|
37
|
+
{'company_id': "C_4003514", 'company_name': 'London Stock Exchange Group plc'}
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
'errors': ['No identification triple found for the provided identifier: NON-EXISTENT of type: ticker']
|
|
38
41
|
}
|
|
39
42
|
"""
|
|
40
43
|
|
|
41
44
|
api_client = self.kfinance_client.kfinance_api_client
|
|
42
|
-
|
|
43
|
-
identifiers_to_company_ids = fetch_company_ids_from_identifiers(
|
|
44
|
-
identifiers=parsed_identifiers, api_client=api_client
|
|
45
|
-
)
|
|
45
|
+
id_triple_resp = api_client.unified_fetch_id_triples(identifiers=identifiers)
|
|
46
46
|
|
|
47
47
|
tasks = [
|
|
48
48
|
Task(
|
|
49
49
|
func=api_client.fetch_competitors,
|
|
50
|
-
kwargs=dict(company_id=company_id, competitor_source=competitor_source),
|
|
50
|
+
kwargs=dict(company_id=id_triple.company_id, competitor_source=competitor_source),
|
|
51
51
|
result_key=identifier,
|
|
52
52
|
)
|
|
53
|
-
for identifier,
|
|
53
|
+
for identifier, id_triple in id_triple_resp.identifiers_to_id_triples.items()
|
|
54
54
|
]
|
|
55
55
|
|
|
56
|
-
competitor_responses: dict[
|
|
57
|
-
|
|
56
|
+
competitor_responses: dict[str, CompetitorResponse] = process_tasks_in_thread_pool_executor(
|
|
57
|
+
api_client=api_client, tasks=tasks
|
|
58
58
|
)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
59
|
+
resp_model = GetCompetitorsFromIdentifiersResp(
|
|
60
|
+
results=competitor_responses, errors=list(id_triple_resp.errors.values())
|
|
61
|
+
)
|
|
62
|
+
return resp_model.model_dump(mode="json")
|