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.
- {kensho_kfinance-2.8.0.dist-info → kensho_kfinance-3.0.0.dist-info}/METADATA +2 -2
- kensho_kfinance-3.0.0.dist-info/RECORD +110 -0
- kfinance/CHANGELOG.md +6 -0
- kfinance/__init__.py +1 -0
- kfinance/client/README.md +9 -0
- kfinance/{batch_request_handling.py → client/batch_request_handling.py} +63 -27
- kfinance/{fetch.py → client/fetch.py} +23 -29
- kfinance/{kfinance.py → client/kfinance.py} +105 -111
- kfinance/{meta_classes.py → client/meta_classes.py} +26 -35
- kfinance/{decimal_with_unit.py → client/models/decimal_with_unit.py} +1 -1
- kfinance/{tests → client/models/tests}/test_decimal_with_unit.py +1 -1
- kfinance/client/tests/__init__.py +0 -0
- kfinance/{tests → client/tests}/test_batch_requests.py +8 -6
- kfinance/{tests → client/tests}/test_client.py +25 -19
- kfinance/{tests → client/tests}/test_fetch.py +11 -29
- kfinance/{tests → client/tests}/test_group_objects.py +1 -1
- kfinance/{tests → client/tests}/test_objects.py +111 -63
- kfinance/{tests/conftest.py → conftest.py} +14 -2
- kfinance/domains/README.md +14 -0
- kfinance/domains/__init__.py +0 -0
- kfinance/domains/business_relationships/__init__.py +0 -0
- kfinance/{models → domains/business_relationships}/business_relationship_models.py +10 -0
- kfinance/domains/business_relationships/business_relationship_tools.py +74 -0
- kfinance/domains/business_relationships/tests/__init__.py +0 -0
- kfinance/domains/business_relationships/tests/test_business_relationship_tools.py +55 -0
- kfinance/domains/capitalizations/__init__.py +0 -0
- kfinance/{models → domains/capitalizations}/capitalization_models.py +24 -17
- kfinance/domains/capitalizations/capitalization_tools.py +89 -0
- kfinance/domains/capitalizations/tests/__init__.py +0 -0
- kfinance/{tests/test_models → domains/capitalizations/tests}/test_capitalization_models.py +8 -10
- kfinance/domains/capitalizations/tests/test_capitalization_tools.py +85 -0
- kfinance/domains/companies/__init__.py +0 -0
- kfinance/domains/companies/company_identifiers.py +175 -0
- kfinance/domains/companies/company_models.py +27 -0
- kfinance/domains/companies/company_tools.py +66 -0
- kfinance/domains/companies/tests/__init__.py +0 -0
- kfinance/domains/companies/tests/test_company_tools.py +26 -0
- kfinance/domains/competitors/__init__.py +0 -0
- kfinance/{models → domains/competitors}/competitor_models.py +7 -0
- kfinance/domains/competitors/competitor_tools.py +62 -0
- kfinance/domains/competitors/tests/__init__.py +0 -0
- kfinance/domains/competitors/tests/test_competitor_tools.py +45 -0
- kfinance/domains/cusip_and_isin/__init__.py +0 -0
- kfinance/domains/cusip_and_isin/cusip_and_isin_tools.py +80 -0
- kfinance/domains/cusip_and_isin/tests/__init__.py +0 -0
- kfinance/domains/cusip_and_isin/tests/test_cusip_and_isin_tools.py +57 -0
- kfinance/domains/earnings/__init__.py +0 -0
- kfinance/domains/earnings/earning_models.py +41 -0
- kfinance/domains/earnings/earning_tools.py +174 -0
- kfinance/domains/earnings/tests/__init__.py +0 -0
- kfinance/domains/earnings/tests/test_earnings_tools.py +195 -0
- kfinance/domains/line_items/__init__.py +0 -0
- kfinance/domains/line_items/line_item_tools.py +114 -0
- kfinance/domains/line_items/tests/__init__.py +0 -0
- kfinance/domains/line_items/tests/test_line_item_tools.py +86 -0
- kfinance/domains/mergers_and_acquisitions/__init__.py +0 -0
- kfinance/domains/mergers_and_acquisitions/merger_and_acquisition_tools.py +176 -0
- kfinance/domains/mergers_and_acquisitions/tests/__init__.py +0 -0
- kfinance/domains/mergers_and_acquisitions/tests/test_merger_and_acquisition_tools.py +124 -0
- kfinance/domains/prices/__init__.py +0 -0
- kfinance/{models → domains/prices}/price_models.py +1 -1
- kfinance/domains/prices/price_tools.py +165 -0
- kfinance/domains/prices/tests/__init__.py +0 -0
- kfinance/{tests/test_models → domains/prices/tests}/test_price_models.py +2 -2
- kfinance/domains/prices/tests/test_price_tools.py +141 -0
- kfinance/domains/segments/__init__.py +0 -0
- kfinance/domains/segments/segment_tools.py +91 -0
- kfinance/domains/segments/tests/__init__.py +0 -0
- kfinance/domains/segments/tests/test_segment_tools.py +80 -0
- kfinance/domains/statements/__init__.py +0 -0
- kfinance/domains/statements/statement_tools.py +113 -0
- kfinance/domains/statements/tests/__init__.py +0 -0
- kfinance/domains/statements/tests/test_statement_tools.py +73 -0
- kfinance/integrations/README.md +8 -0
- kfinance/integrations/__init__.py +0 -0
- kfinance/integrations/mcp/__init__.py +0 -0
- kfinance/{mcp.py → integrations/mcp/mcp.py} +2 -2
- kfinance/integrations/tests/__init__.py +0 -0
- kfinance/{tests → integrations/tests}/test_example_notebook.py +4 -4
- kfinance/{tool_calling → integrations/tool_calling}/README.md +2 -2
- kfinance/integrations/tool_calling/__init__.py +0 -0
- kfinance/integrations/tool_calling/all_tools.py +55 -0
- kfinance/{tool_calling → integrations/tool_calling}/prompts.py +3 -2
- kfinance/integrations/tool_calling/static_tools/README.md +4 -0
- kfinance/integrations/tool_calling/static_tools/__init__.py +0 -0
- kfinance/{tool_calling → integrations/tool_calling/static_tools}/get_latest.py +3 -3
- kfinance/{tool_calling → integrations/tool_calling/static_tools}/get_n_quarters_ago.py +3 -3
- kfinance/integrations/tool_calling/static_tools/tests/__init__.py +0 -0
- kfinance/integrations/tool_calling/static_tools/tests/test_get_lastest.py +30 -0
- kfinance/integrations/tool_calling/static_tools/tests/test_get_n_quarters_ago.py +24 -0
- kfinance/integrations/tool_calling/tests/__init__.py +0 -0
- kfinance/integrations/tool_calling/tests/test_tool_calling_models.py +69 -0
- kfinance/{tool_calling/shared_models.py → integrations/tool_calling/tool_calling_models.py} +37 -7
- kfinance/version.py +2 -2
- kensho_kfinance-2.8.0.dist-info/RECORD +0 -70
- kfinance/models/id_models.py +0 -7
- kfinance/prompt.py +0 -526
- kfinance/pydantic_models.py +0 -33
- kfinance/tests/test_tools.py +0 -804
- kfinance/tool_calling/__init__.py +0 -53
- kfinance/tool_calling/get_advisors_for_company_in_transaction_from_identifier.py +0 -39
- kfinance/tool_calling/get_business_relationship_from_identifier.py +0 -30
- kfinance/tool_calling/get_capitalization_from_identifier.py +0 -35
- kfinance/tool_calling/get_competitors_from_identifier.py +0 -25
- kfinance/tool_calling/get_cusip_from_ticker.py +0 -20
- kfinance/tool_calling/get_earnings.py +0 -33
- kfinance/tool_calling/get_financial_line_item_from_identifier.py +0 -48
- kfinance/tool_calling/get_financial_statement_from_identifier.py +0 -44
- kfinance/tool_calling/get_history_metadata_from_identifier.py +0 -17
- kfinance/tool_calling/get_info_from_identifier.py +0 -16
- kfinance/tool_calling/get_isin_from_ticker.py +0 -20
- kfinance/tool_calling/get_latest_earnings.py +0 -30
- kfinance/tool_calling/get_merger_info_from_transaction_id.py +0 -68
- kfinance/tool_calling/get_mergers_from_identifier.py +0 -41
- kfinance/tool_calling/get_next_earnings.py +0 -30
- kfinance/tool_calling/get_prices_from_identifier.py +0 -46
- kfinance/tool_calling/get_segments_from_identifier.py +0 -44
- kfinance/tool_calling/get_transcript.py +0 -23
- kfinance/tool_calling/resolve_identifier.py +0 -18
- {kensho_kfinance-2.8.0.dist-info → kensho_kfinance-3.0.0.dist-info}/WHEEL +0 -0
- {kensho_kfinance-2.8.0.dist-info → kensho_kfinance-3.0.0.dist-info}/licenses/AUTHORS.md +0 -0
- {kensho_kfinance-2.8.0.dist-info → kensho_kfinance-3.0.0.dist-info}/licenses/LICENSE +0 -0
- {kensho_kfinance-2.8.0.dist-info → kensho_kfinance-3.0.0.dist-info}/top_level.txt +0 -0
- /kfinance/{models → client}/__init__.py +0 -0
- /kfinance/{models → client}/industry_models.py +0 -0
- /kfinance/{tests → client/models}/__init__.py +0 -0
- /kfinance/{models → client/models}/currency_models.py +0 -0
- /kfinance/{models → client/models}/date_and_period_models.py +0 -0
- /kfinance/{tests/test_models → client/models/tests}/__init__.py +0 -0
- /kfinance/{models → client}/permission_models.py +0 -0
- /kfinance/{server_thread.py → client/server_thread.py} +0 -0
- /kfinance/{models → domains/line_items}/line_item_models.py +0 -0
- /kfinance/{models → domains/segments}/segment_models.py +0 -0
- /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.
|
|
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
|
|
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
|
-
|
|
78
|
-
|
|
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.
|
|
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
|