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

Files changed (45) hide show
  1. {kensho_kfinance-3.0.3.dist-info → kensho_kfinance-3.1.0.dist-info}/METADATA +1 -1
  2. {kensho_kfinance-3.0.3.dist-info → kensho_kfinance-3.1.0.dist-info}/RECORD +45 -44
  3. kfinance/CHANGELOG.md +3 -0
  4. kfinance/client/fetch.py +26 -17
  5. kfinance/client/kfinance.py +17 -18
  6. kfinance/client/meta_classes.py +2 -2
  7. kfinance/client/tests/test_fetch.py +36 -24
  8. kfinance/client/tests/test_objects.py +112 -120
  9. kfinance/conftest.py +49 -5
  10. kfinance/domains/business_relationships/business_relationship_tools.py +30 -19
  11. kfinance/domains/business_relationships/tests/test_business_relationship_tools.py +18 -15
  12. kfinance/domains/capitalizations/capitalization_models.py +1 -1
  13. kfinance/domains/capitalizations/capitalization_tools.py +41 -24
  14. kfinance/domains/capitalizations/tests/test_capitalization_tools.py +38 -13
  15. kfinance/domains/companies/company_identifiers.py +0 -175
  16. kfinance/domains/companies/company_models.py +98 -5
  17. kfinance/domains/companies/company_tools.py +33 -29
  18. kfinance/domains/companies/tests/test_company_tools.py +11 -4
  19. kfinance/domains/competitors/competitor_tools.py +21 -21
  20. kfinance/domains/competitors/tests/test_competitor_tools.py +21 -7
  21. kfinance/domains/cusip_and_isin/cusip_and_isin_tools.py +38 -26
  22. kfinance/domains/cusip_and_isin/tests/test_cusip_and_isin_tools.py +27 -26
  23. kfinance/domains/earnings/earning_tools.py +54 -47
  24. kfinance/domains/earnings/tests/test_earnings_tools.py +58 -63
  25. kfinance/domains/line_items/line_item_tools.py +29 -36
  26. kfinance/domains/line_items/tests/test_line_item_tools.py +23 -5
  27. kfinance/domains/mergers_and_acquisitions/merger_and_acquisition_models.py +15 -0
  28. kfinance/domains/mergers_and_acquisitions/merger_and_acquisition_tools.py +55 -38
  29. kfinance/domains/mergers_and_acquisitions/tests/test_merger_and_acquisition_tools.py +22 -9
  30. kfinance/domains/prices/price_models.py +9 -9
  31. kfinance/domains/prices/price_tools.py +49 -38
  32. kfinance/domains/prices/tests/test_price_tools.py +52 -36
  33. kfinance/domains/segments/segment_models.py +7 -0
  34. kfinance/domains/segments/segment_tools.py +37 -20
  35. kfinance/domains/segments/tests/test_segment_tools.py +13 -6
  36. kfinance/domains/statements/statement_models.py +7 -0
  37. kfinance/domains/statements/statement_tools.py +38 -40
  38. kfinance/domains/statements/tests/test_statement_tools.py +39 -10
  39. kfinance/integrations/tool_calling/tests/test_tool_calling_models.py +2 -2
  40. kfinance/integrations/tool_calling/tool_calling_models.py +24 -5
  41. kfinance/version.py +2 -2
  42. {kensho_kfinance-3.0.3.dist-info → kensho_kfinance-3.1.0.dist-info}/WHEEL +0 -0
  43. {kensho_kfinance-3.0.3.dist-info → kensho_kfinance-3.1.0.dist-info}/licenses/AUTHORS.md +0 -0
  44. {kensho_kfinance-3.0.3.dist-info → kensho_kfinance-3.1.0.dist-info}/licenses/LICENSE +0 -0
  45. {kensho_kfinance-3.0.3.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
- '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
- ]
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
- 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
- )
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(company_id=company_id, start_date=start_date, end_date=end_date),
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, company_id in identifiers_to_company_ids.items()
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
- 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
- }
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 market cap
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
- "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
- ]
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
- "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
- ],
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 NamedTuple
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(NamedTuple):
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
- "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
- }
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
- 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
- )
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, company_id in identifiers_to_company_ids.items()
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(api_client=api_client, tasks=tasks)
65
-
66
- return {str(identifier): result for identifier, result in info_responses.items()}
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 = {"SPGI": info_resp}
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(ToolArgsWithIdentifiers(identifiers=["SPGI"]).model_dump(mode="json"))
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
- 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
- }
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
- 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
- )
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, company_id in identifiers_to_company_ids.items()
53
+ for identifier, id_triple in id_triple_resp.identifiers_to_id_triples.items()
54
54
  ]
55
55
 
56
- competitor_responses: dict[Identifier, CompetitorResponse] = (
57
- process_tasks_in_thread_pool_executor(api_client=api_client, tasks=tasks)
56
+ competitor_responses: dict[str, CompetitorResponse] = process_tasks_in_thread_pool_executor(
57
+ api_client=api_client, tasks=tasks
58
58
  )
59
- return {
60
- str(identifier): competitors.model_dump(mode="json")
61
- for identifier, competitors in competitor_responses.items()
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")