kensho-kfinance 2.0.1__py3-none-any.whl → 2.2.2__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.0.1.dist-info → kensho_kfinance-2.2.2.dist-info}/METADATA +9 -1
- kensho_kfinance-2.2.2.dist-info/RECORD +42 -0
- {kensho_kfinance-2.0.1.dist-info → kensho_kfinance-2.2.2.dist-info}/WHEEL +1 -1
- kfinance/CHANGELOG.md +21 -0
- kfinance/batch_request_handling.py +32 -27
- kfinance/constants.py +23 -7
- kfinance/fetch.py +106 -40
- kfinance/kfinance.py +164 -89
- kfinance/meta_classes.py +118 -9
- kfinance/tests/conftest.py +32 -0
- kfinance/tests/test_batch_requests.py +46 -8
- kfinance/tests/test_client.py +54 -0
- kfinance/tests/test_example_notebook.py +194 -0
- kfinance/tests/test_fetch.py +31 -2
- kfinance/tests/test_group_objects.py +32 -0
- kfinance/tests/test_objects.py +40 -0
- kfinance/tests/test_tools.py +13 -61
- kfinance/tool_calling/__init__.py +2 -6
- kfinance/tool_calling/get_business_relationship_from_identifier.py +2 -1
- kfinance/tool_calling/get_capitalization_from_identifier.py +2 -1
- kfinance/tool_calling/get_cusip_from_ticker.py +2 -0
- kfinance/tool_calling/get_earnings_call_datetimes_from_identifier.py +2 -0
- kfinance/tool_calling/get_financial_line_item_from_identifier.py +2 -1
- kfinance/tool_calling/get_financial_statement_from_identifier.py +2 -1
- kfinance/tool_calling/get_history_metadata_from_identifier.py +2 -1
- kfinance/tool_calling/get_info_from_identifier.py +3 -1
- kfinance/tool_calling/get_isin_from_ticker.py +2 -0
- kfinance/tool_calling/get_latest.py +2 -1
- kfinance/tool_calling/get_n_quarters_ago.py +2 -1
- kfinance/tool_calling/get_prices_from_identifier.py +2 -1
- kfinance/tool_calling/resolve_identifier.py +18 -0
- kfinance/tool_calling/shared_models.py +2 -0
- kfinance/version.py +2 -2
- kensho_kfinance-2.0.1.dist-info/RECORD +0 -40
- kfinance/tool_calling/get_company_id_from_identifier.py +0 -14
- kfinance/tool_calling/get_security_id_from_identifier.py +0 -14
- kfinance/tool_calling/get_trading_item_id_from_identifier.py +0 -14
- {kensho_kfinance-2.0.1.dist-info → kensho_kfinance-2.2.2.dist-info}/licenses/AUTHORS.md +0 -0
- {kensho_kfinance-2.0.1.dist-info → kensho_kfinance-2.2.2.dist-info}/licenses/LICENSE +0 -0
- {kensho_kfinance-2.0.1.dist-info → kensho_kfinance-2.2.2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from unittest.mock import Mock
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from kfinance.constants import Permission
|
|
6
|
+
from kfinance.kfinance import Client
|
|
7
|
+
from kfinance.tool_calling import (
|
|
8
|
+
GetBusinessRelationshipFromIdentifier,
|
|
9
|
+
GetFinancialStatementFromIdentifier,
|
|
10
|
+
GetLatest,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TestLangchainTools:
|
|
15
|
+
@pytest.mark.parametrize(
|
|
16
|
+
"fetch_value, parsed_permissions",
|
|
17
|
+
[
|
|
18
|
+
pytest.param(["RelationshipPermission"], {Permission.RelationshipPermission}),
|
|
19
|
+
pytest.param([], set(), id="empty permissions don't raise."),
|
|
20
|
+
pytest.param(
|
|
21
|
+
["InvalidPermission"], set(), id="invalid permissions get logged but don't raise."
|
|
22
|
+
),
|
|
23
|
+
],
|
|
24
|
+
)
|
|
25
|
+
def test_user_permissions(
|
|
26
|
+
self, fetch_value: list[str], parsed_permissions: set[Permission], mock_client: Client
|
|
27
|
+
) -> None:
|
|
28
|
+
"""
|
|
29
|
+
WHEN we fetch user permissions from the fetch_permissions endpoint
|
|
30
|
+
THEN we correctly parse those permission strings into Permission enums.
|
|
31
|
+
"""
|
|
32
|
+
mock_client.kfinance_api_client.fetch_permissions = Mock()
|
|
33
|
+
mock_client.kfinance_api_client.fetch_permissions.return_value = {
|
|
34
|
+
"permissions": fetch_value
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
assert mock_client.kfinance_api_client.user_permissions == parsed_permissions
|
|
38
|
+
|
|
39
|
+
def test_permission_filtering(self, mock_client: Client):
|
|
40
|
+
"""
|
|
41
|
+
GIVEN a user with limited permissions
|
|
42
|
+
WHEN we filter tools by permissions
|
|
43
|
+
THEN we only return tools that either don't require permissions or tools that the user
|
|
44
|
+
specifically has access to.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
mock_client.kfinance_api_client._user_permissions = {Permission.RelationshipPermission} # noqa: SLF001
|
|
48
|
+
tool_classes = [type(t) for t in mock_client.langchain_tools]
|
|
49
|
+
# User should have access to GetBusinessRelationshipFromIdentifier
|
|
50
|
+
assert GetBusinessRelationshipFromIdentifier in tool_classes
|
|
51
|
+
# User should have access to functions that don't require permissions
|
|
52
|
+
assert GetLatest in tool_classes
|
|
53
|
+
# User should not have access to functions that require statement permissions
|
|
54
|
+
assert GetFinancialStatementFromIdentifier not in tool_classes
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
import subprocess
|
|
3
|
+
import sys
|
|
4
|
+
from textwrap import dedent
|
|
5
|
+
import uuid
|
|
6
|
+
|
|
7
|
+
from nbconvert.preprocessors import ExecutePreprocessor
|
|
8
|
+
import nbformat
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.fixture(scope="session")
|
|
13
|
+
def jupyter_kernel_name() -> str:
|
|
14
|
+
"""Create a jupyter kernel for a test run and yield its name.
|
|
15
|
+
|
|
16
|
+
The kernel gets removed at the end of the test run.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
kernel_name = f"test-kfinance-kernel-{uuid.uuid4().hex[:8]}"
|
|
20
|
+
|
|
21
|
+
# Install a new kernel pointing to the current interpreter.
|
|
22
|
+
subprocess.run(
|
|
23
|
+
[
|
|
24
|
+
sys.executable,
|
|
25
|
+
"-m",
|
|
26
|
+
"ipykernel",
|
|
27
|
+
"install",
|
|
28
|
+
"--user",
|
|
29
|
+
"--name",
|
|
30
|
+
kernel_name,
|
|
31
|
+
"--display-name",
|
|
32
|
+
f"Python ({kernel_name})",
|
|
33
|
+
],
|
|
34
|
+
check=True,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
yield kernel_name
|
|
38
|
+
|
|
39
|
+
# Remove the kernel after the test.
|
|
40
|
+
subprocess.run(
|
|
41
|
+
[sys.executable, "-m", "jupyter", "kernelspec", "uninstall", "-f", kernel_name], check=True
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_run_notebook(jupyter_kernel_name: str):
|
|
46
|
+
"""
|
|
47
|
+
GIVEN the usage_examples.ipynb notebook
|
|
48
|
+
WHEN the notebook gets run with a mock client and mock responses
|
|
49
|
+
THEN all cells of the notebook complete without errors.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
# Create a replacement startup cell for the normal notebook client init.
|
|
53
|
+
# This cell contains:
|
|
54
|
+
# - setup of a mock client
|
|
55
|
+
# - mocks for all calls made by the client while executing the notebook
|
|
56
|
+
startup_cell_code = dedent("""
|
|
57
|
+
from datetime import datetime
|
|
58
|
+
from kfinance.kfinance import Client
|
|
59
|
+
kfinance_client = Client(refresh_token="foo")
|
|
60
|
+
api_client = kfinance_client.kfinance_api_client
|
|
61
|
+
# Set access token so that the client doesn't try to fetch it.
|
|
62
|
+
api_client._access_token = "foo"
|
|
63
|
+
api_client._access_token_expiry = datetime(2100, 1, 1).timestamp()
|
|
64
|
+
|
|
65
|
+
# Mock out all necessary requests with requests_mock
|
|
66
|
+
import requests_mock
|
|
67
|
+
mocker = requests_mock.Mocker()
|
|
68
|
+
mocker.start()
|
|
69
|
+
|
|
70
|
+
id_triple_resp = {
|
|
71
|
+
"trading_item_id": 2629108,
|
|
72
|
+
"security_id": 2629107,
|
|
73
|
+
"company_id": 21719
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
# spgi = kfinance_client.ticker("SPGI")
|
|
77
|
+
mocker.get(
|
|
78
|
+
url="https://kfinance.kensho.com/api/v1/id/SPGI",
|
|
79
|
+
json=id_triple_resp
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
mocker.get(
|
|
83
|
+
url="https://kfinance.kensho.com/api/v1/info/21719",
|
|
84
|
+
json={"name": "S&P Global Inc."}
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
balance_sheet_resp = {
|
|
88
|
+
"statements": {
|
|
89
|
+
"2022Q3": {"Cash And Equivalents": "1387000000.000000"},
|
|
90
|
+
"2022Q4": {"Cash And Equivalents": "1286000000.000000"}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
# spgi.balance_sheet()
|
|
95
|
+
mocker.get(
|
|
96
|
+
url="https://kfinance.kensho.com/api/v1/statements/21719/balance_sheet/none/none/none/none/none",
|
|
97
|
+
json=balance_sheet_resp
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# spgi.balance_sheet(period_type=PeriodType.annual, start_year=2010, end_year=2019)
|
|
101
|
+
mocker.get(
|
|
102
|
+
url="https://kfinance.kensho.com/api/v1/statements/21719/balance_sheet/annual/2010/2019/none/none",
|
|
103
|
+
json=balance_sheet_resp
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# kfinance_client.ticker("JPM").balance_sheet()
|
|
107
|
+
# (leads to fetching SPGI balance sheet when requesting JPM because they return the same
|
|
108
|
+
# company id)
|
|
109
|
+
mocker.get(
|
|
110
|
+
url="https://kfinance.kensho.com/api/v1/id/JPM",
|
|
111
|
+
json=id_triple_resp
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# spgi.net_income(period_type=PeriodType.annual, start_year=2010, end_year=2019)
|
|
115
|
+
mocker.get(
|
|
116
|
+
url="https://kfinance.kensho.com/api/v1/line_item/21719/net_income/annual/2010/2019/none/none",
|
|
117
|
+
json={"line_item": {"2010": "828000000.000000"}}
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
prices_resp = {
|
|
121
|
+
"prices": [
|
|
122
|
+
{
|
|
123
|
+
"date": "2024-05-20",
|
|
124
|
+
"open": "439.540000",
|
|
125
|
+
"high": "441.570000",
|
|
126
|
+
"low": "437.000000",
|
|
127
|
+
"close": "437.740000",
|
|
128
|
+
"volume": "1080006"
|
|
129
|
+
}
|
|
130
|
+
]
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
# spgi.history()
|
|
134
|
+
mocker.get(
|
|
135
|
+
url="https://kfinance.kensho.com/api/v1/pricing/2629108/none/none/day/adjusted",
|
|
136
|
+
json=prices_resp
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# spgi.history(
|
|
140
|
+
# periodicity=Periodicity.month,
|
|
141
|
+
# adjusted=False,
|
|
142
|
+
# start_date="2010-01-01",
|
|
143
|
+
# end_date="2019-12-31"
|
|
144
|
+
# )
|
|
145
|
+
mocker.get(
|
|
146
|
+
url="https://kfinance.kensho.com/api/v1/pricing/2629108/2010-01-01/2019-12-31/month/unadjusted",
|
|
147
|
+
json=prices_resp
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# spgi.price_chart(
|
|
151
|
+
# periodicity=Periodicity.month,
|
|
152
|
+
# adjusted=False,
|
|
153
|
+
# start_date="2010-01-01",
|
|
154
|
+
# end_date="2019-12-31"
|
|
155
|
+
# )
|
|
156
|
+
mocker.get(
|
|
157
|
+
url='https://kfinance.kensho.com/api/v1/price_chart/2629108/2010-01-01/2019-12-31/month/unadjusted',
|
|
158
|
+
content=b"",
|
|
159
|
+
headers={'Content-Type': 'image/png'}
|
|
160
|
+
)
|
|
161
|
+
# Mock out image_open so that we don't have to return an actual png.
|
|
162
|
+
from unittest.mock import MagicMock
|
|
163
|
+
import kfinance.kfinance
|
|
164
|
+
kfinance.kfinance.image_open = MagicMock()
|
|
165
|
+
""")
|
|
166
|
+
|
|
167
|
+
# Load the notebook
|
|
168
|
+
notebook_path = Path(Path(__file__).parent.parent.parent, "usage_examples.ipynb")
|
|
169
|
+
with notebook_path.open() as f:
|
|
170
|
+
nb = nbformat.read(f, as_version=4)
|
|
171
|
+
|
|
172
|
+
# Set up the notebook executor
|
|
173
|
+
ep = ExecutePreprocessor(timeout=600, kernel_name=jupyter_kernel_name)
|
|
174
|
+
|
|
175
|
+
# Identify the start of the example section
|
|
176
|
+
example_sections_heading = "## Example functions"
|
|
177
|
+
examples_start_cell_id = None
|
|
178
|
+
for idx, cell in enumerate(nb.cells):
|
|
179
|
+
if cell["source"] == example_sections_heading:
|
|
180
|
+
examples_start_cell_id = idx
|
|
181
|
+
|
|
182
|
+
if not examples_start_cell_id:
|
|
183
|
+
raise ValueError(
|
|
184
|
+
f"Did not find a cell with content {example_sections_heading} that "
|
|
185
|
+
f"indicates the start of the examples."
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# Combine the startup cell with the start of the examples
|
|
189
|
+
# i.e. toss everything before the examples start
|
|
190
|
+
nb.cells = [nbformat.v4.new_code_cell(startup_cell_code)] + nb.cells[examples_start_cell_id:]
|
|
191
|
+
|
|
192
|
+
# Run the notebook.
|
|
193
|
+
# The test passes if the notebook runs without errors.
|
|
194
|
+
ep.preprocess(nb, {"metadata": {"path": notebook_path.parent}})
|
kfinance/tests/test_fetch.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from unittest import TestCase
|
|
2
|
-
from unittest.mock import
|
|
2
|
+
from unittest.mock import MagicMock
|
|
3
3
|
|
|
4
4
|
import pytest
|
|
5
5
|
|
|
@@ -10,7 +10,7 @@ from kfinance.fetch import KFinanceApiClient
|
|
|
10
10
|
def build_mock_api_client() -> KFinanceApiClient:
|
|
11
11
|
"""Create a KFinanceApiClient with mocked-out fetch function."""
|
|
12
12
|
kfinance_api_client = KFinanceApiClient(refresh_token="fake_refresh_token")
|
|
13
|
-
kfinance_api_client.fetch =
|
|
13
|
+
kfinance_api_client.fetch = MagicMock()
|
|
14
14
|
return kfinance_api_client
|
|
15
15
|
|
|
16
16
|
|
|
@@ -229,6 +229,29 @@ class TestFetchItem(TestCase):
|
|
|
229
229
|
)
|
|
230
230
|
self.kfinance_api_client.fetch.assert_called_once_with(expected_fetch_url)
|
|
231
231
|
|
|
232
|
+
def test_fetch_segments(self) -> None:
|
|
233
|
+
company_id = 21719
|
|
234
|
+
segment_type = "business"
|
|
235
|
+
expected_fetch_url = f"{self.kfinance_api_client.url_base}segments/{company_id}/{segment_type}/none/none/none/none/none"
|
|
236
|
+
self.kfinance_api_client.fetch_segments(company_id=company_id, segment_type=segment_type)
|
|
237
|
+
self.kfinance_api_client.fetch.assert_called_with(expected_fetch_url)
|
|
238
|
+
period_type = PeriodType.quarterly
|
|
239
|
+
start_year = 2023
|
|
240
|
+
end_year = 2023
|
|
241
|
+
start_quarter = 1
|
|
242
|
+
end_quarter = 4
|
|
243
|
+
expected_fetch_url = f"{self.kfinance_api_client.url_base}segments/{company_id}/{segment_type}/{period_type.value}/{start_year}/{end_year}/{start_quarter}/{end_quarter}"
|
|
244
|
+
self.kfinance_api_client.fetch_segments(
|
|
245
|
+
company_id=company_id,
|
|
246
|
+
segment_type=segment_type,
|
|
247
|
+
period_type=period_type,
|
|
248
|
+
start_year=start_year,
|
|
249
|
+
end_year=end_year,
|
|
250
|
+
start_quarter=start_quarter,
|
|
251
|
+
end_quarter=end_quarter,
|
|
252
|
+
)
|
|
253
|
+
self.kfinance_api_client.fetch.assert_called_with(expected_fetch_url)
|
|
254
|
+
|
|
232
255
|
|
|
233
256
|
class TestMarketCap:
|
|
234
257
|
@pytest.mark.parametrize(
|
|
@@ -250,3 +273,9 @@ class TestMarketCap:
|
|
|
250
273
|
company_id=company_id, start_date=start_date, end_date=end_date
|
|
251
274
|
)
|
|
252
275
|
client.fetch.assert_called_with(expected_fetch_url)
|
|
276
|
+
|
|
277
|
+
def test_fetch_permissions(self):
|
|
278
|
+
client = build_mock_api_client()
|
|
279
|
+
expected_fetch_url = f"{client.url_base}users/permissions"
|
|
280
|
+
client.fetch_permissions()
|
|
281
|
+
client.fetch.assert_called_with(expected_fetch_url)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from unittest.mock import Mock
|
|
2
|
+
|
|
3
|
+
from kfinance.kfinance import Client, IdentificationTriple, Tickers
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TestTickers:
|
|
7
|
+
def test_tickers(self, mock_client: Client):
|
|
8
|
+
"""
|
|
9
|
+
WHEN the client requests tickers using multiple filters
|
|
10
|
+
THEN only tickers matching all filter criteria are returned
|
|
11
|
+
"""
|
|
12
|
+
ticker_1 = IdentificationTriple(company_id=1, security_id=1, trading_item_id=1)
|
|
13
|
+
ticker_2 = IdentificationTriple(company_id=2, security_id=2, trading_item_id=2)
|
|
14
|
+
ticker_3 = IdentificationTriple(company_id=3, security_id=3, trading_item_id=3)
|
|
15
|
+
expected_intersection = Tickers(
|
|
16
|
+
kfinance_api_client=mock_client.kfinance_api_client, id_triples=[ticker_2]
|
|
17
|
+
)
|
|
18
|
+
# tickers() calls both fetch_ticker_combined and fetch_ticker_from_industry_code so we set two different return values to test intersection()
|
|
19
|
+
mock_client.kfinance_api_client.fetch_ticker_combined = Mock()
|
|
20
|
+
mock_client.kfinance_api_client.fetch_ticker_combined.return_value = [ticker_1, ticker_2]
|
|
21
|
+
mock_client.kfinance_api_client.fetch_ticker_from_industry_code = Mock()
|
|
22
|
+
mock_client.kfinance_api_client.fetch_ticker_from_industry_code.return_value = [
|
|
23
|
+
ticker_3,
|
|
24
|
+
ticker_2,
|
|
25
|
+
]
|
|
26
|
+
tickers_object = mock_client.tickers(
|
|
27
|
+
country_iso_code="USA", state_iso_code="FL", sic="6141", gics="2419512"
|
|
28
|
+
)
|
|
29
|
+
# fetch_ticker_from_industry_code should be called once for SIC and GICS
|
|
30
|
+
assert mock_client.kfinance_api_client.fetch_ticker_from_industry_code.call_count == 2
|
|
31
|
+
# Only the common tickers are returned
|
|
32
|
+
assert tickers_object == expected_intersection
|
kfinance/tests/test_objects.py
CHANGED
|
@@ -75,6 +75,19 @@ MOCK_COMPANY_DB = {
|
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
},
|
|
78
|
+
"segments": {
|
|
79
|
+
"2024": {
|
|
80
|
+
"Intelligent Cloud": {"Operating Income": 49584000000.0, "Revenue": 105362000000.0},
|
|
81
|
+
"More Personal Computing": {
|
|
82
|
+
"Operating Income": 19309000000.0,
|
|
83
|
+
"Revenue": 62032000000.0,
|
|
84
|
+
},
|
|
85
|
+
"Productivity and Business Processes": {
|
|
86
|
+
"Operating Income": 40540000000.0,
|
|
87
|
+
"Revenue": 77728000000.0,
|
|
88
|
+
},
|
|
89
|
+
}
|
|
90
|
+
},
|
|
78
91
|
}
|
|
79
92
|
}
|
|
80
93
|
|
|
@@ -189,6 +202,19 @@ class MockKFinanceApiClient:
|
|
|
189
202
|
]
|
|
190
203
|
}
|
|
191
204
|
|
|
205
|
+
def fetch_segments(
|
|
206
|
+
self,
|
|
207
|
+
company_id,
|
|
208
|
+
segment_type,
|
|
209
|
+
period_type,
|
|
210
|
+
start_year,
|
|
211
|
+
end_year,
|
|
212
|
+
start_quarter,
|
|
213
|
+
end_quarter,
|
|
214
|
+
):
|
|
215
|
+
"""Get a segment"""
|
|
216
|
+
return MOCK_COMPANY_DB[company_id]
|
|
217
|
+
|
|
192
218
|
|
|
193
219
|
class TestTradingItem(TestCase):
|
|
194
220
|
def setUp(self):
|
|
@@ -309,6 +335,20 @@ class TestCompany(TestCase):
|
|
|
309
335
|
revenue = self.msft_company.revenue()
|
|
310
336
|
pd.testing.assert_frame_equal(expected_revenue, revenue)
|
|
311
337
|
|
|
338
|
+
def test_business_segments(self) -> None:
|
|
339
|
+
"""test business statement"""
|
|
340
|
+
rows = []
|
|
341
|
+
for period, segments in MOCK_COMPANY_DB[msft_company_id]["segments"].items():
|
|
342
|
+
for segment_name, line_items in segments.items():
|
|
343
|
+
for line_item, value in line_items.items():
|
|
344
|
+
rows.append([period, segment_name, line_item, value])
|
|
345
|
+
expected_segments = pd.DataFrame(
|
|
346
|
+
rows, columns=["Year", "Segment Name", "Line Item", "Value"]
|
|
347
|
+
).replace(np.nan, None)
|
|
348
|
+
|
|
349
|
+
business_segment = self.msft_company.business_segments()
|
|
350
|
+
pd.testing.assert_frame_equal(expected_segments, business_segment)
|
|
351
|
+
|
|
312
352
|
|
|
313
353
|
class TestSecurity(TestCase):
|
|
314
354
|
def setUp(self):
|
kfinance/tests/test_tools.py
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
from datetime import date, datetime
|
|
2
2
|
|
|
3
3
|
from langchain_core.utils.function_calling import convert_to_openai_tool
|
|
4
|
-
import pytest
|
|
5
4
|
from requests_mock import Mocker
|
|
6
5
|
import time_machine
|
|
7
6
|
|
|
8
7
|
from kfinance.constants import BusinessRelationshipType, Capitalization, StatementType
|
|
9
8
|
from kfinance.kfinance import Client
|
|
9
|
+
from kfinance.tests.conftest import SPGI_COMPANY_ID, SPGI_SECURITY_ID, SPGI_TRADING_ITEM_ID
|
|
10
10
|
from kfinance.tool_calling import (
|
|
11
|
-
GetCompanyIdFromIdentifier,
|
|
12
11
|
GetEarningsCallDatetimesFromIdentifier,
|
|
13
12
|
GetFinancialLineItemFromIdentifier,
|
|
14
13
|
GetFinancialStatementFromIdentifier,
|
|
@@ -18,8 +17,7 @@ from kfinance.tool_calling import (
|
|
|
18
17
|
GetLatest,
|
|
19
18
|
GetNQuartersAgo,
|
|
20
19
|
GetPricesFromIdentifier,
|
|
21
|
-
|
|
22
|
-
GetTradingItemIdFromIdentifier,
|
|
20
|
+
ResolveIdentifier,
|
|
23
21
|
)
|
|
24
22
|
from kfinance.tool_calling.get_business_relationship_from_identifier import (
|
|
25
23
|
GetBusinessRelationshipFromIdentifier,
|
|
@@ -43,32 +41,6 @@ from kfinance.tool_calling.get_prices_from_identifier import GetPricesFromIdenti
|
|
|
43
41
|
from kfinance.tool_calling.shared_models import ToolArgsWithIdentifier
|
|
44
42
|
|
|
45
43
|
|
|
46
|
-
SPGI_COMPANY_ID = 21719
|
|
47
|
-
SPGI_SECURITY_ID = 2629107
|
|
48
|
-
SPGI_TRADING_ITEM_ID = 2629108
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
@pytest.fixture
|
|
52
|
-
def mock_client(requests_mock: Mocker) -> Client:
|
|
53
|
-
"""Create a KFinanceApiClient with a mock response for the SPGI id triple."""
|
|
54
|
-
|
|
55
|
-
client = Client(refresh_token="foo")
|
|
56
|
-
# Set access token so that the client doesn't try to fetch it.
|
|
57
|
-
client.kfinance_api_client._access_token = "foo" # noqa: SLF001
|
|
58
|
-
client.kfinance_api_client._access_token_expiry = datetime(2100, 1, 1).timestamp() # noqa: SLF001
|
|
59
|
-
|
|
60
|
-
# Create a mock for the SPGI id triple.
|
|
61
|
-
requests_mock.get(
|
|
62
|
-
url="https://kfinance.kensho.com/api/v1/id/SPGI",
|
|
63
|
-
json={
|
|
64
|
-
"trading_item_id": SPGI_TRADING_ITEM_ID,
|
|
65
|
-
"security_id": SPGI_SECURITY_ID,
|
|
66
|
-
"company_id": SPGI_COMPANY_ID,
|
|
67
|
-
},
|
|
68
|
-
)
|
|
69
|
-
return client
|
|
70
|
-
|
|
71
|
-
|
|
72
44
|
class TestGetBusinessRelationshipFromIdentifier:
|
|
73
45
|
def test_get_business_relationship_from_identifier(
|
|
74
46
|
self, requests_mock: Mocker, mock_client: Client
|
|
@@ -133,18 +105,6 @@ class TestGetCapitalizationFromIdentifier:
|
|
|
133
105
|
assert response == expected_response
|
|
134
106
|
|
|
135
107
|
|
|
136
|
-
class TestGetCompanyIdFromIdentifier:
|
|
137
|
-
def test_get_company_id_from_identifier(self, mock_client: Client):
|
|
138
|
-
"""
|
|
139
|
-
GIVEN the GetCompanyIdFromIdentifier tool
|
|
140
|
-
WHEN request the company id for SPGI
|
|
141
|
-
THEN we get back the SPGI company id
|
|
142
|
-
"""
|
|
143
|
-
tool = GetCompanyIdFromIdentifier(kfinance_client=mock_client)
|
|
144
|
-
resp = tool.run(ToolArgsWithIdentifier(identifier="SPGI").model_dump(mode="json"))
|
|
145
|
-
assert resp == SPGI_COMPANY_ID
|
|
146
|
-
|
|
147
|
-
|
|
148
108
|
class TestGetCusipFromTicker:
|
|
149
109
|
def test_get_cusip_from_ticker(self, requests_mock: Mocker, mock_client: Client):
|
|
150
110
|
"""
|
|
@@ -406,25 +366,17 @@ class TestPricesFromIdentifier:
|
|
|
406
366
|
assert response == expected_response
|
|
407
367
|
|
|
408
368
|
|
|
409
|
-
class
|
|
410
|
-
def
|
|
369
|
+
class TestResolveIdentifier:
|
|
370
|
+
def test_resolve_identifier(self, mock_client: Client):
|
|
411
371
|
"""
|
|
412
|
-
GIVEN the
|
|
413
|
-
WHEN
|
|
414
|
-
THEN we get back the SPGI
|
|
372
|
+
GIVEN the ResolveIdentifier tool
|
|
373
|
+
WHEN request to resolve SPGI
|
|
374
|
+
THEN we get back a dict with the SPGI company id, security id, and trading item id
|
|
415
375
|
"""
|
|
416
|
-
tool =
|
|
376
|
+
tool = ResolveIdentifier(kfinance_client=mock_client)
|
|
417
377
|
resp = tool.run(ToolArgsWithIdentifier(identifier="SPGI").model_dump(mode="json"))
|
|
418
|
-
assert resp ==
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
"""
|
|
424
|
-
GIVEN the GetTradingItemIdFromIdentifier tool
|
|
425
|
-
WHEN we request the trading item id for SPGI
|
|
426
|
-
THEN we get back the SPGI primary trading item id
|
|
427
|
-
"""
|
|
428
|
-
tool = GetTradingItemIdFromIdentifier(kfinance_client=mock_client)
|
|
429
|
-
resp = tool.run(ToolArgsWithIdentifier(identifier="SPGI").model_dump(mode="json"))
|
|
430
|
-
assert resp == SPGI_TRADING_ITEM_ID
|
|
378
|
+
assert resp == {
|
|
379
|
+
"company_id": SPGI_COMPANY_ID,
|
|
380
|
+
"security_id": SPGI_SECURITY_ID,
|
|
381
|
+
"trading_item_id": SPGI_TRADING_ITEM_ID,
|
|
382
|
+
}
|
|
@@ -4,7 +4,6 @@ from kfinance.tool_calling.get_business_relationship_from_identifier import (
|
|
|
4
4
|
GetBusinessRelationshipFromIdentifier,
|
|
5
5
|
)
|
|
6
6
|
from kfinance.tool_calling.get_capitalization_from_identifier import GetCapitalizationFromIdentifier
|
|
7
|
-
from kfinance.tool_calling.get_company_id_from_identifier import GetCompanyIdFromIdentifier
|
|
8
7
|
from kfinance.tool_calling.get_cusip_from_ticker import GetCusipFromTicker
|
|
9
8
|
from kfinance.tool_calling.get_earnings_call_datetimes_from_identifier import (
|
|
10
9
|
GetEarningsCallDatetimesFromIdentifier,
|
|
@@ -23,17 +22,13 @@ from kfinance.tool_calling.get_isin_from_ticker import GetIsinFromTicker
|
|
|
23
22
|
from kfinance.tool_calling.get_latest import GetLatest
|
|
24
23
|
from kfinance.tool_calling.get_n_quarters_ago import GetNQuartersAgo
|
|
25
24
|
from kfinance.tool_calling.get_prices_from_identifier import GetPricesFromIdentifier
|
|
26
|
-
from kfinance.tool_calling.
|
|
27
|
-
from kfinance.tool_calling.get_trading_item_id_from_identifier import GetTradingItemIdFromIdentifier
|
|
25
|
+
from kfinance.tool_calling.resolve_identifier import ResolveIdentifier
|
|
28
26
|
from kfinance.tool_calling.shared_models import KfinanceTool
|
|
29
27
|
|
|
30
28
|
|
|
31
29
|
ALL_TOOLS: list[Type[KfinanceTool]] = [
|
|
32
30
|
GetLatest,
|
|
33
31
|
GetNQuartersAgo,
|
|
34
|
-
GetCompanyIdFromIdentifier,
|
|
35
|
-
GetSecurityIdFromIdentifier,
|
|
36
|
-
GetTradingItemIdFromIdentifier,
|
|
37
32
|
GetIsinFromTicker,
|
|
38
33
|
GetCusipFromTicker,
|
|
39
34
|
GetInfoFromIdentifier,
|
|
@@ -44,4 +39,5 @@ ALL_TOOLS: list[Type[KfinanceTool]] = [
|
|
|
44
39
|
GetFinancialStatementFromIdentifier,
|
|
45
40
|
GetFinancialLineItemFromIdentifier,
|
|
46
41
|
GetBusinessRelationshipFromIdentifier,
|
|
42
|
+
ResolveIdentifier,
|
|
47
43
|
]
|
|
@@ -2,7 +2,7 @@ from typing import Type
|
|
|
2
2
|
|
|
3
3
|
from pydantic import BaseModel
|
|
4
4
|
|
|
5
|
-
from kfinance.constants import BusinessRelationshipType
|
|
5
|
+
from kfinance.constants import BusinessRelationshipType, Permission
|
|
6
6
|
from kfinance.kfinance import BusinessRelationships
|
|
7
7
|
from kfinance.tool_calling.shared_models import KfinanceTool, ToolArgsWithIdentifier
|
|
8
8
|
|
|
@@ -16,6 +16,7 @@ class GetBusinessRelationshipFromIdentifier(KfinanceTool):
|
|
|
16
16
|
name: str = "get_business_relationship_from_identifier"
|
|
17
17
|
description: str = 'Get the current and previous company IDs that are relationship_type of a given identifier. For example, "What are the current distributors of SPGI?" or "What are the previous borrowers of JPM?"'
|
|
18
18
|
args_schema: Type[BaseModel] = GetBusinessRelationshipFromIdentifierArgs
|
|
19
|
+
required_permission: Permission | None = Permission.RelationshipPermission
|
|
19
20
|
|
|
20
21
|
def _run(self, identifier: str, business_relationship: BusinessRelationshipType) -> dict:
|
|
21
22
|
ticker = self.kfinance_client.ticker(identifier)
|
|
@@ -2,7 +2,7 @@ from datetime import date
|
|
|
2
2
|
|
|
3
3
|
from pydantic import Field
|
|
4
4
|
|
|
5
|
-
from kfinance.constants import Capitalization
|
|
5
|
+
from kfinance.constants import Capitalization, Permission
|
|
6
6
|
from kfinance.tool_calling.shared_models import KfinanceTool, ToolArgsWithIdentifier
|
|
7
7
|
|
|
8
8
|
|
|
@@ -21,6 +21,7 @@ class GetCapitalizationFromIdentifier(KfinanceTool):
|
|
|
21
21
|
name: str = "get_capitalization_from_identifier"
|
|
22
22
|
description: str = "Get the historical market cap, tev (Total Enterprise Value), or shares outstanding of an identifier between inclusive start_date and inclusive end date. When requesting the most recent values, leave start_date and end_date empty."
|
|
23
23
|
args_schema = GetCapitalizationFromIdentifierArgs
|
|
24
|
+
required_permission: Permission | None = Permission.PricingPermission
|
|
24
25
|
|
|
25
26
|
def _run(
|
|
26
27
|
self,
|
|
@@ -2,6 +2,7 @@ from typing import Type
|
|
|
2
2
|
|
|
3
3
|
from pydantic import BaseModel, Field
|
|
4
4
|
|
|
5
|
+
from kfinance.constants import Permission
|
|
5
6
|
from kfinance.tool_calling.shared_models import KfinanceTool
|
|
6
7
|
|
|
7
8
|
|
|
@@ -13,6 +14,7 @@ class GetCusipFromTicker(KfinanceTool):
|
|
|
13
14
|
name: str = "get_cusip_from_ticker"
|
|
14
15
|
description: str = "Get the CUSIP associated with a ticker."
|
|
15
16
|
args_schema: Type[BaseModel] = GetCusipFromTickerArgs
|
|
17
|
+
required_permission: Permission | None = Permission.IDPermission
|
|
16
18
|
|
|
17
19
|
def _run(self, ticker_str: str) -> str:
|
|
18
20
|
return self.kfinance_client.ticker(ticker_str).cusip
|
|
@@ -3,6 +3,7 @@ from typing import Type
|
|
|
3
3
|
|
|
4
4
|
from pydantic import BaseModel
|
|
5
5
|
|
|
6
|
+
from kfinance.constants import Permission
|
|
6
7
|
from kfinance.tool_calling.shared_models import KfinanceTool, ToolArgsWithIdentifier
|
|
7
8
|
|
|
8
9
|
|
|
@@ -10,6 +11,7 @@ class GetEarningsCallDatetimesFromIdentifier(KfinanceTool):
|
|
|
10
11
|
name: str = "get_earnings_call_datetimes_from_identifier"
|
|
11
12
|
description: str = "Get earnings call datetimes associated with an identifier."
|
|
12
13
|
args_schema: Type[BaseModel] = ToolArgsWithIdentifier
|
|
14
|
+
required_permission: Permission | None = Permission.EarningsPermission
|
|
13
15
|
|
|
14
16
|
def _run(self, identifier: str) -> str:
|
|
15
17
|
ticker = self.kfinance_client.ticker(identifier)
|
|
@@ -2,7 +2,7 @@ from typing import Literal, Type
|
|
|
2
2
|
|
|
3
3
|
from pydantic import BaseModel, Field
|
|
4
4
|
|
|
5
|
-
from kfinance.constants import LINE_ITEM_NAMES_AND_ALIASES, PeriodType
|
|
5
|
+
from kfinance.constants import LINE_ITEM_NAMES_AND_ALIASES, PeriodType, Permission
|
|
6
6
|
from kfinance.tool_calling.shared_models import KfinanceTool, ToolArgsWithIdentifier
|
|
7
7
|
|
|
8
8
|
|
|
@@ -24,6 +24,7 @@ class GetFinancialLineItemFromIdentifier(KfinanceTool):
|
|
|
24
24
|
name: str = "get_financial_line_item_from_identifier"
|
|
25
25
|
description: str = "Get the financial line item associated with an identifier."
|
|
26
26
|
args_schema: Type[BaseModel] = GetFinancialLineItemFromIdentifierArgs
|
|
27
|
+
required_permission: Permission | None = Permission.StatementsPermission
|
|
27
28
|
|
|
28
29
|
def _run(
|
|
29
30
|
self,
|
|
@@ -2,7 +2,7 @@ from typing import Literal, Type
|
|
|
2
2
|
|
|
3
3
|
from pydantic import BaseModel, Field
|
|
4
4
|
|
|
5
|
-
from kfinance.constants import PeriodType, StatementType
|
|
5
|
+
from kfinance.constants import PeriodType, Permission, StatementType
|
|
6
6
|
from kfinance.tool_calling.shared_models import KfinanceTool, ToolArgsWithIdentifier
|
|
7
7
|
|
|
8
8
|
|
|
@@ -20,6 +20,7 @@ class GetFinancialStatementFromIdentifier(KfinanceTool):
|
|
|
20
20
|
name: str = "get_financial_statement_from_identifier"
|
|
21
21
|
description: str = "Get the financial statement associated with an identifier."
|
|
22
22
|
args_schema: Type[BaseModel] = GetFinancialStatementFromIdentifierArgs
|
|
23
|
+
required_permission: Permission | None = Permission.StatementsPermission
|
|
23
24
|
|
|
24
25
|
def _run(
|
|
25
26
|
self,
|
|
@@ -2,7 +2,7 @@ from typing import Type
|
|
|
2
2
|
|
|
3
3
|
from pydantic import BaseModel
|
|
4
4
|
|
|
5
|
-
from kfinance.constants import HistoryMetadata
|
|
5
|
+
from kfinance.constants import HistoryMetadata, Permission
|
|
6
6
|
from kfinance.tool_calling.shared_models import KfinanceTool, ToolArgsWithIdentifier
|
|
7
7
|
|
|
8
8
|
|
|
@@ -10,6 +10,7 @@ class GetHistoryMetadataFromIdentifier(KfinanceTool):
|
|
|
10
10
|
name: str = "get_history_metadata_from_identifier"
|
|
11
11
|
description: str = "Get the history metadata associated with an identifier. History metadata includes currency, symbol, exchange name, instrument type, and first trade date."
|
|
12
12
|
args_schema: Type[BaseModel] = ToolArgsWithIdentifier
|
|
13
|
+
required_permission: Permission | None = None
|
|
13
14
|
|
|
14
15
|
def _run(self, identifier: str) -> HistoryMetadata:
|
|
15
16
|
return self.kfinance_client.ticker(identifier).history_metadata
|