kensho-kfinance 2.1.2__py3-none-any.whl → 2.2.4__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.1.2.dist-info → kensho_kfinance-2.2.4.dist-info}/METADATA +7 -1
- {kensho_kfinance-2.1.2.dist-info → kensho_kfinance-2.2.4.dist-info}/RECORD +22 -22
- {kensho_kfinance-2.1.2.dist-info → kensho_kfinance-2.2.4.dist-info}/WHEEL +1 -1
- kfinance/CHANGELOG.md +15 -0
- kfinance/batch_request_handling.py +32 -27
- kfinance/constants.py +8 -0
- kfinance/fetch.py +22 -0
- kfinance/kfinance.py +39 -65
- kfinance/meta_classes.py +95 -1
- kfinance/tests/test_batch_requests.py +33 -1
- kfinance/tests/test_example_notebook.py +194 -0
- kfinance/tests/test_fetch.py +23 -0
- kfinance/tests/test_objects.py +33 -0
- kfinance/tests/test_tools.py +55 -35
- kfinance/tool_calling/__init__.py +6 -6
- kfinance/tool_calling/get_info_from_identifier.py +1 -1
- kfinance/tool_calling/get_segments_from_identifier.py +42 -0
- kfinance/tool_calling/resolve_identifier.py +18 -0
- kfinance/version.py +2 -2
- kfinance/tool_calling/get_company_id_from_identifier.py +0 -16
- kfinance/tool_calling/get_security_id_from_identifier.py +0 -16
- kfinance/tool_calling/get_trading_item_id_from_identifier.py +0 -16
- {kensho_kfinance-2.1.2.dist-info → kensho_kfinance-2.2.4.dist-info}/licenses/AUTHORS.md +0 -0
- {kensho_kfinance-2.1.2.dist-info → kensho_kfinance-2.2.4.dist-info}/licenses/LICENSE +0 -0
- {kensho_kfinance-2.1.2.dist-info → kensho_kfinance-2.2.4.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kensho-kfinance
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.4
|
|
4
4
|
Summary: Python CLI for kFinance
|
|
5
5
|
Author-email: Luke Brown <luke.brown@kensho.com>, Michelle Keoy <michelle.keoy@kensho.com>, Keith Page <keith.page@kensho.com>, Matthew Rosen <matthew.rosen@kensho.com>, Nick Roshdieh <nick.roshdieh@kensho.com>
|
|
6
6
|
Project-URL: source, https://github.com/kensho-technologies/kfinance
|
|
@@ -28,7 +28,11 @@ Requires-Dist: requests<3,>=2.22.0
|
|
|
28
28
|
Requires-Dist: urllib3>=1.21.1
|
|
29
29
|
Provides-Extra: dev
|
|
30
30
|
Requires-Dist: coverage<8,>=7.6.10; extra == "dev"
|
|
31
|
+
Requires-Dist: ipykernel<7,>=6.29; extra == "dev"
|
|
31
32
|
Requires-Dist: mypy<2,>=1.15.0; extra == "dev"
|
|
33
|
+
Requires-Dist: nbconvert<8,>=7.16; extra == "dev"
|
|
34
|
+
Requires-Dist: nbformat<6,>5.10; extra == "dev"
|
|
35
|
+
Requires-Dist: nbqa<2,>1.9; extra == "dev"
|
|
32
36
|
Requires-Dist: pytest<7,>=6.1.2; extra == "dev"
|
|
33
37
|
Requires-Dist: pytest-cov<7,>=6.0.0; extra == "dev"
|
|
34
38
|
Requires-Dist: requests_mock<2,>=1.12; extra == "dev"
|
|
@@ -57,6 +61,8 @@ To receive access, please email [S&P Global Market Intelligence](market.intellig
|
|
|
57
61
|
|
|
58
62
|
Once access is obtained, get started using the [Authentication Guide](https://docs.kensho.com/llmreadyapi/kf-authentication) and [Usage Guide](https://docs.kensho.com/llmreadyapi/usage).
|
|
59
63
|
|
|
64
|
+
We also provide an [interactive notebook](usage_examples.ipynb) that demonstrates some usage examples.
|
|
65
|
+
|
|
60
66
|
# Versioning
|
|
61
67
|
The kFinance uses semantic versioning (major, minor, patch).
|
|
62
68
|
To bump the version, add a new entry in [CHANGELOG.md](kfinance%2FCHANGELOG.md).
|
|
@@ -1,43 +1,43 @@
|
|
|
1
|
-
kensho_kfinance-2.
|
|
2
|
-
kensho_kfinance-2.
|
|
3
|
-
kfinance/CHANGELOG.md,sha256=
|
|
1
|
+
kensho_kfinance-2.2.4.dist-info/licenses/AUTHORS.md,sha256=0h9ClbI0pu1oKj1M28ROUsaxrbZg-6ukQGl6X4y9noI,68
|
|
2
|
+
kensho_kfinance-2.2.4.dist-info/licenses/LICENSE,sha256=bsY4blvSgq6o0FMQ3RXa2NCgco--nHCCchLXzxr6kms,83
|
|
3
|
+
kfinance/CHANGELOG.md,sha256=rBjrg8H9eODymBNuLyOWIRrA5FAVR92WgxswFMkGI9E,1241
|
|
4
4
|
kfinance/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
-
kfinance/batch_request_handling.py,sha256=
|
|
6
|
-
kfinance/constants.py,sha256=
|
|
7
|
-
kfinance/fetch.py,sha256=
|
|
8
|
-
kfinance/kfinance.py,sha256=
|
|
9
|
-
kfinance/meta_classes.py,sha256=
|
|
5
|
+
kfinance/batch_request_handling.py,sha256=p6p_G4_BL06GgeKlh7P1k9CUqOMahWCLEw1NoBwbLvU,5698
|
|
6
|
+
kfinance/constants.py,sha256=UuFzqL253-2tRQfma785K9tfaZGv-o821tO2tVLwc5Q,48813
|
|
7
|
+
kfinance/fetch.py,sha256=fguxYy5rjLQKNkmP7gsvZUJtmt9uDNxg6LN9FRJTAVQ,22936
|
|
8
|
+
kfinance/kfinance.py,sha256=_U69k0Dcwkw1B_lzaUZy8N2-c-v93ZKoUAVVZb6wBUM,52758
|
|
9
|
+
kfinance/meta_classes.py,sha256=1qYkj2L7jcBfdkye6TG2nuRhwzhAbmtxay6rSVY3DsA,19883
|
|
10
10
|
kfinance/prompt.py,sha256=PtVB8c_FcSlVdyGgByAnIFGzuUuBaEjciCqnBJl1hSQ,25133
|
|
11
11
|
kfinance/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
12
|
kfinance/server_thread.py,sha256=jUnt1YGoYDkqqz1MbCwd44zJs1T_Z2BCgvj75bdtLgA,2574
|
|
13
|
-
kfinance/version.py,sha256=
|
|
13
|
+
kfinance/version.py,sha256=Ath1yNFbHtPzluPba0J7EfQTfLH_byRzLfGjqAUl6cY,511
|
|
14
14
|
kfinance/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
15
|
kfinance/tests/conftest.py,sha256=voB-w8P_6L3Nel3rdgylXKe5WWaS1q7nCFt1O04uqoY,948
|
|
16
|
-
kfinance/tests/test_batch_requests.py,sha256=
|
|
16
|
+
kfinance/tests/test_batch_requests.py,sha256=uXJF2IcRdyBm5SthwIUHMKtkGZ21MY84pg_k1JeSNOY,11430
|
|
17
17
|
kfinance/tests/test_client.py,sha256=O7icZCSDhlQ9WGhzoXlpiSvbuA-mQNJHBYVsilyP_dE,2209
|
|
18
|
-
kfinance/tests/
|
|
18
|
+
kfinance/tests/test_example_notebook.py,sha256=XhDAJp3H5Y6usLAt3k4Ug4W3H6gLyh68nzmoWgOOLn4,6441
|
|
19
|
+
kfinance/tests/test_fetch.py,sha256=O9qOsffQYJPSAR1X_P6CNoA0Buyq2kx4vfS0FOc2Goc,13564
|
|
19
20
|
kfinance/tests/test_group_objects.py,sha256=SoMEZmkG4RYdgWOAwxLHHtzIQho92KM01YbQXPUg578,1689
|
|
20
|
-
kfinance/tests/test_objects.py,sha256=
|
|
21
|
-
kfinance/tests/test_tools.py,sha256=
|
|
21
|
+
kfinance/tests/test_objects.py,sha256=CSk3iN-uDt-E6gaOX4jExuYPT8Up-YNC3hafpenadcA,23614
|
|
22
|
+
kfinance/tests/test_tools.py,sha256=ne4XhebhoxDHFl4kRLl10j4K-SXLrxS6jv6Ypt0Gv_E,16846
|
|
22
23
|
kfinance/tool_calling/README.md,sha256=omJq7Us6r4U45QB7hRpLjRJ5BMalCkZkh4uXBjTbJXc,2022
|
|
23
|
-
kfinance/tool_calling/__init__.py,sha256=
|
|
24
|
+
kfinance/tool_calling/__init__.py,sha256=Fb30Snu09B63NscSUJGGnFAdlXSDrTAOJlzjYvMyZXk,1873
|
|
24
25
|
kfinance/tool_calling/get_business_relationship_from_identifier.py,sha256=CipXvyqEjPm6BXYP0CA9Kp1BIyiIEm7abp85x1zXRV4,1472
|
|
25
26
|
kfinance/tool_calling/get_capitalization_from_identifier.py,sha256=TdWdJDeI-jSL-1YfhnnwIA9D1SXobidvoHrjK42QmqQ,1521
|
|
26
|
-
kfinance/tool_calling/get_company_id_from_identifier.py,sha256=GPakqIA27-RoqKFjM7W2nd3xp4v3utIPpH932BNNAlk,577
|
|
27
27
|
kfinance/tool_calling/get_cusip_from_ticker.py,sha256=houhGCYXoSzaaTtCvOBf3pPsYiSbcV1Ej5nAyGuMWcU,644
|
|
28
28
|
kfinance/tool_calling/get_earnings_call_datetimes_from_identifier.py,sha256=Zp7gHP-z2ePu484wWx4xw89hl0z7NLcG3qliJT-6rUo,782
|
|
29
29
|
kfinance/tool_calling/get_financial_line_item_from_identifier.py,sha256=bMK-GUMz_wmJ-7iFS72sM8AvvUvR83JYRWzYtr9Fofw,2153
|
|
30
30
|
kfinance/tool_calling/get_financial_statement_from_identifier.py,sha256=9ouzC43skA78uI3dh3bvtM86db5RGxuWQAAJ24y-kBM,1906
|
|
31
31
|
kfinance/tool_calling/get_history_metadata_from_identifier.py,sha256=kKIInfRC1Plf-Wnx9cmaJ46RRABm6zn3F5_8qAdqrBg,728
|
|
32
|
-
kfinance/tool_calling/get_info_from_identifier.py,sha256=
|
|
32
|
+
kfinance/tool_calling/get_info_from_identifier.py,sha256=PRhSYpYs_iUcIVFGYCMN_7QJyhewua7c7pqngPIp-qg,766
|
|
33
33
|
kfinance/tool_calling/get_isin_from_ticker.py,sha256=2fJBcA-rNGbVOQmQ7qJEYxqejQwJ6nyWOBSFlzxG7dY,638
|
|
34
34
|
kfinance/tool_calling/get_latest.py,sha256=btGeVBmvX5QJutzrKfE6spatGGejDuHxtq53NoAaGNk,786
|
|
35
35
|
kfinance/tool_calling/get_n_quarters_ago.py,sha256=A0ilwPKUqU0YYQSz3gNsVF0Jy4YttXrSaDhYj7y8GHA,713
|
|
36
36
|
kfinance/tool_calling/get_prices_from_identifier.py,sha256=ViJkwLDvStB7grc8RuoKSDXQM399Wru4-OY3E8k1l_U,1882
|
|
37
|
-
kfinance/tool_calling/
|
|
38
|
-
kfinance/tool_calling/
|
|
37
|
+
kfinance/tool_calling/get_segments_from_identifier.py,sha256=WIqJ1wWE6Z87VBREGu42nRc6_eJqUbGKcE9elzqBQJE,1867
|
|
38
|
+
kfinance/tool_calling/resolve_identifier.py,sha256=npslr6bBCu0qEDV1-8d24F5OC3nQ1KBMphuMbHVC1AU,626
|
|
39
39
|
kfinance/tool_calling/shared_models.py,sha256=K-NPQyE_7Ew6Cs0zxG1xO2O47gp5uDHdHtWD7wUDZX4,2132
|
|
40
|
-
kensho_kfinance-2.
|
|
41
|
-
kensho_kfinance-2.
|
|
42
|
-
kensho_kfinance-2.
|
|
43
|
-
kensho_kfinance-2.
|
|
40
|
+
kensho_kfinance-2.2.4.dist-info/METADATA,sha256=sx_SF7gbkLwRfyTRUw2WHaqkoSyqQfUoBUS5majgF7w,3436
|
|
41
|
+
kensho_kfinance-2.2.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
42
|
+
kensho_kfinance-2.2.4.dist-info/top_level.txt,sha256=kT_kNwVhfQoOAecY8W7uYah5xaHMoHoAdBIvXh6DaKM,9
|
|
43
|
+
kensho_kfinance-2.2.4.dist-info/RECORD,,
|
kfinance/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v2.2.4
|
|
4
|
+
- Add segments as llm tools
|
|
5
|
+
|
|
6
|
+
## v2.2.3
|
|
7
|
+
- Change segments return type to nested dictionary
|
|
8
|
+
|
|
9
|
+
## v2.2.2
|
|
10
|
+
- Add segments
|
|
11
|
+
|
|
12
|
+
## v2.2.1
|
|
13
|
+
- Make number of employees optional to reflect backend changes
|
|
14
|
+
|
|
15
|
+
## v2.2.0
|
|
16
|
+
- Replace get company id, get security id, get trading item id tools with resolve identifier tool
|
|
17
|
+
|
|
3
18
|
## v2.1.2
|
|
4
19
|
- Allow batch executor to handle multiple requests
|
|
5
20
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from concurrent.futures import
|
|
1
|
+
from concurrent.futures import Future
|
|
2
2
|
import functools
|
|
3
3
|
from functools import cached_property
|
|
4
4
|
import threading
|
|
@@ -55,36 +55,26 @@ def add_methods_of_singular_class_to_iterable_class(singular_cls: Type[T]) -> Ca
|
|
|
55
55
|
instances of [singular_cls].
|
|
56
56
|
"""
|
|
57
57
|
|
|
58
|
-
def process_in_thread_pool(
|
|
59
|
-
executor: ThreadPoolExecutor, method: Callable, obj: T, *args: Any, **kwargs: Any
|
|
60
|
-
) -> Any:
|
|
61
|
-
with throttle:
|
|
62
|
-
future = executor.submit(method, obj, *args, **kwargs)
|
|
63
|
-
try:
|
|
64
|
-
return future.result()
|
|
65
|
-
except HTTPError as http_err:
|
|
66
|
-
error_code = http_err.response.status_code
|
|
67
|
-
if error_code == 404:
|
|
68
|
-
return None
|
|
69
|
-
else:
|
|
70
|
-
raise http_err
|
|
71
|
-
|
|
72
58
|
def process_in_batch(
|
|
73
59
|
method: Callable, self: IterableKfinanceClass, *args: Any, **kwargs: Any
|
|
74
60
|
) -> dict:
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
kfinance_api_client.thread_pool, method, obj, *args, **kwargs
|
|
83
|
-
)
|
|
84
|
-
for obj in self
|
|
85
|
-
],
|
|
61
|
+
with self.kfinance_api_client.batch_request_header(batch_size=len(self)):
|
|
62
|
+
futures = []
|
|
63
|
+
for obj in self:
|
|
64
|
+
# Acquire throttle before submitting the task
|
|
65
|
+
throttle.acquire()
|
|
66
|
+
future = self.kfinance_api_client.thread_pool.submit(
|
|
67
|
+
method, obj, *args, **kwargs
|
|
86
68
|
)
|
|
87
|
-
|
|
69
|
+
# On success or failure, release the throttle.
|
|
70
|
+
# This releases the throttle before the
|
|
71
|
+
# `resolve_future_with_error_handling` call.
|
|
72
|
+
future.add_done_callback(lambda f: throttle.release())
|
|
73
|
+
futures.append(future)
|
|
74
|
+
|
|
75
|
+
results = {}
|
|
76
|
+
for obj, future in zip(self, futures):
|
|
77
|
+
results[obj] = resolve_future_with_error_handling(future)
|
|
88
78
|
|
|
89
79
|
return results
|
|
90
80
|
|
|
@@ -135,3 +125,18 @@ def add_methods_of_singular_class_to_iterable_class(singular_cls: Type[T]) -> Ca
|
|
|
135
125
|
return iterable_cls
|
|
136
126
|
|
|
137
127
|
return decorator
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def resolve_future_with_error_handling(future: Future) -> Any:
|
|
131
|
+
"""Return the result of a future with error handling for non-200 status codes.
|
|
132
|
+
|
|
133
|
+
If request returned a 404, return None. Otherwise, raise the error.
|
|
134
|
+
"""
|
|
135
|
+
try:
|
|
136
|
+
return future.result()
|
|
137
|
+
except HTTPError as http_err:
|
|
138
|
+
error_code = http_err.response.status_code
|
|
139
|
+
if error_code == 404:
|
|
140
|
+
return None
|
|
141
|
+
else:
|
|
142
|
+
raise http_err
|
kfinance/constants.py
CHANGED
|
@@ -62,6 +62,13 @@ class StatementType(StrEnum):
|
|
|
62
62
|
cf = "cashflow"
|
|
63
63
|
|
|
64
64
|
|
|
65
|
+
class SegmentType(StrEnum):
|
|
66
|
+
"""The type of segment"""
|
|
67
|
+
|
|
68
|
+
business = "business"
|
|
69
|
+
geographic = "geographic"
|
|
70
|
+
|
|
71
|
+
|
|
65
72
|
class BusinessRelationshipType(StrEnum):
|
|
66
73
|
"""The type of business relationship"""
|
|
67
74
|
|
|
@@ -95,6 +102,7 @@ class Permission(StrEnum):
|
|
|
95
102
|
PricingPermission = "PricingPermission"
|
|
96
103
|
RelationshipPermission = "RelationshipPermission"
|
|
97
104
|
StatementsPermission = "StatementsPermission"
|
|
105
|
+
SegmentsPermission = "SegmentsPermission"
|
|
98
106
|
|
|
99
107
|
|
|
100
108
|
class YearAndQuarter(TypedDict):
|
kfinance/fetch.py
CHANGED
|
@@ -15,6 +15,7 @@ from .constants import (
|
|
|
15
15
|
Periodicity,
|
|
16
16
|
PeriodType,
|
|
17
17
|
Permission,
|
|
18
|
+
SegmentType,
|
|
18
19
|
)
|
|
19
20
|
|
|
20
21
|
|
|
@@ -305,6 +306,27 @@ class KFinanceApiClient:
|
|
|
305
306
|
)
|
|
306
307
|
return self.fetch(url)
|
|
307
308
|
|
|
309
|
+
def fetch_segments(
|
|
310
|
+
self,
|
|
311
|
+
company_id: int,
|
|
312
|
+
segment_type: SegmentType,
|
|
313
|
+
period_type: Optional[PeriodType] = None,
|
|
314
|
+
start_year: Optional[int] = None,
|
|
315
|
+
end_year: Optional[int] = None,
|
|
316
|
+
start_quarter: Optional[int] = None,
|
|
317
|
+
end_quarter: Optional[int] = None,
|
|
318
|
+
) -> dict:
|
|
319
|
+
"""Get a specified segment type for a specified duration."""
|
|
320
|
+
url = (
|
|
321
|
+
f"{self.url_base}segments/{company_id}/{segment_type}/"
|
|
322
|
+
f"{period_type if period_type else 'none'}/"
|
|
323
|
+
f"{start_year if start_year is not None else 'none'}/"
|
|
324
|
+
f"{end_year if end_year is not None else 'none'}/"
|
|
325
|
+
f"{start_quarter if start_quarter is not None else 'none'}/"
|
|
326
|
+
f"{end_quarter if end_quarter is not None else 'none'}"
|
|
327
|
+
)
|
|
328
|
+
return self.fetch(url)
|
|
329
|
+
|
|
308
330
|
def fetch_price_chart(
|
|
309
331
|
self,
|
|
310
332
|
trading_item_id: int,
|
kfinance/kfinance.py
CHANGED
|
@@ -262,7 +262,7 @@ class Company(CompanyFunctionsMetaClass):
|
|
|
262
262
|
def info(self) -> dict:
|
|
263
263
|
"""Get the company info
|
|
264
264
|
|
|
265
|
-
:return: a dict with containing: name, status, type, simple industry, number of employees, founding date, webpage, address, city, zip code, state, country, & iso_country
|
|
265
|
+
:return: a dict with containing: name, status, type, simple industry, number of employees (if available), founding date, webpage, address, city, zip code, state, country, & iso_country
|
|
266
266
|
:rtype: dict
|
|
267
267
|
"""
|
|
268
268
|
return self.kfinance_api_client.fetch_info(self.company_id)
|
|
@@ -304,11 +304,11 @@ class Company(CompanyFunctionsMetaClass):
|
|
|
304
304
|
return self.info["simple_industry"]
|
|
305
305
|
|
|
306
306
|
@property
|
|
307
|
-
def number_of_employees(self) -> str:
|
|
308
|
-
"""Get the number of employees the company has
|
|
307
|
+
def number_of_employees(self) -> str | None:
|
|
308
|
+
"""Get the number of employees the company has (if available)
|
|
309
309
|
|
|
310
310
|
:return: how many employees the company has
|
|
311
|
-
:rtype: str
|
|
311
|
+
:rtype: str | None
|
|
312
312
|
"""
|
|
313
313
|
return self.info["number_of_employees"]
|
|
314
314
|
|
|
@@ -537,11 +537,32 @@ class Ticker(DelegatedCompanyFunctionsMetaClass):
|
|
|
537
537
|
|
|
538
538
|
@property
|
|
539
539
|
def id_triple(self) -> IdentificationTriple:
|
|
540
|
-
"""Returns a unique
|
|
540
|
+
"""Returns a unique identification triple for the Ticker object.
|
|
541
|
+
|
|
542
|
+
:return: an identification triple consisting of company_id, security_id, and trading_item_id
|
|
543
|
+
:rtype: IdentificationTriple
|
|
544
|
+
"""
|
|
545
|
+
|
|
546
|
+
if self._company_id is None or self._security_id is None or self._trading_item_id is None:
|
|
547
|
+
if self._identifier is None:
|
|
548
|
+
raise RuntimeError(
|
|
549
|
+
"Fetching the id triple of a Ticker requires an identifier "
|
|
550
|
+
"(ticker, CUSIP, or ISIN)."
|
|
551
|
+
)
|
|
552
|
+
id_triple = self.kfinance_api_client.fetch_id_triple(
|
|
553
|
+
identifier=self._identifier, exchange_code=self.exchange_code
|
|
554
|
+
)
|
|
555
|
+
self._company_id = id_triple["company_id"]
|
|
556
|
+
self._security_id = id_triple["security_id"]
|
|
557
|
+
self._trading_item_id = id_triple["trading_item_id"]
|
|
558
|
+
assert self._company_id
|
|
559
|
+
assert self._security_id
|
|
560
|
+
assert self._trading_item_id
|
|
561
|
+
|
|
541
562
|
return IdentificationTriple(
|
|
542
|
-
company_id=self.
|
|
543
|
-
security_id=self.
|
|
544
|
-
trading_item_id=self.
|
|
563
|
+
company_id=self._company_id,
|
|
564
|
+
security_id=self._security_id,
|
|
565
|
+
trading_item_id=self._trading_item_id,
|
|
545
566
|
)
|
|
546
567
|
|
|
547
568
|
def __hash__(self) -> int:
|
|
@@ -570,79 +591,32 @@ class Ticker(DelegatedCompanyFunctionsMetaClass):
|
|
|
570
591
|
|
|
571
592
|
return f"{type(self).__module__}.{type(self).__qualname__} of {', '.join(str_attributes)}"
|
|
572
593
|
|
|
573
|
-
|
|
574
|
-
"""Get & set company_id, security_id, & trading_item_id for ticker with an exchange"""
|
|
575
|
-
if self._identifier is None:
|
|
576
|
-
raise RuntimeError(
|
|
577
|
-
"Ticker.set_identification_triple was called with a identifier set to None"
|
|
578
|
-
)
|
|
579
|
-
else:
|
|
580
|
-
id_triple = self.kfinance_api_client.fetch_id_triple(
|
|
581
|
-
self._identifier, self.exchange_code
|
|
582
|
-
)
|
|
583
|
-
self.company_id = id_triple["company_id"]
|
|
584
|
-
self.security_id = id_triple["security_id"]
|
|
585
|
-
self.trading_item_id = id_triple["trading_item_id"]
|
|
586
|
-
|
|
587
|
-
def set_company_id(self) -> int:
|
|
588
|
-
"""Set the company id for the object
|
|
589
|
-
|
|
590
|
-
:return: the CIQ company id
|
|
591
|
-
:rtype: int
|
|
592
|
-
"""
|
|
593
|
-
self.set_identification_triple()
|
|
594
|
-
return self.company_id
|
|
595
|
-
|
|
596
|
-
def set_security_id(self) -> int:
|
|
597
|
-
"""Set the security id for the object
|
|
598
|
-
|
|
599
|
-
:return: the CIQ security id
|
|
600
|
-
:rtype: int
|
|
601
|
-
"""
|
|
602
|
-
self.set_identification_triple()
|
|
603
|
-
return self.security_id
|
|
604
|
-
|
|
605
|
-
def set_trading_item_id(self) -> int:
|
|
606
|
-
"""Set the trading item id for the object
|
|
607
|
-
|
|
608
|
-
:return: the CIQ trading item id
|
|
609
|
-
:rtype: int
|
|
610
|
-
"""
|
|
611
|
-
self.set_identification_triple()
|
|
612
|
-
return self.trading_item_id
|
|
613
|
-
|
|
614
|
-
@cached_property
|
|
594
|
+
@property
|
|
615
595
|
def company_id(self) -> int:
|
|
616
596
|
"""Get the company id for the object
|
|
617
597
|
|
|
618
598
|
:return: the CIQ company id
|
|
619
599
|
:rtype: int
|
|
620
600
|
"""
|
|
621
|
-
|
|
622
|
-
return self._company_id
|
|
623
|
-
return self.set_company_id()
|
|
601
|
+
return self.id_triple.company_id
|
|
624
602
|
|
|
625
|
-
@
|
|
603
|
+
@property
|
|
626
604
|
def security_id(self) -> int:
|
|
627
605
|
"""Get the CIQ security id for the object
|
|
628
606
|
|
|
629
607
|
:return: the CIQ security id
|
|
630
608
|
:rtype: int
|
|
631
609
|
"""
|
|
632
|
-
|
|
633
|
-
return self._security_id
|
|
634
|
-
return self.set_security_id()
|
|
610
|
+
return self.id_triple.security_id
|
|
635
611
|
|
|
636
|
-
@
|
|
612
|
+
@property
|
|
637
613
|
def trading_item_id(self) -> int:
|
|
638
614
|
"""Get the CIQ trading item id for the object
|
|
639
615
|
|
|
640
616
|
:return: the CIQ trading item id
|
|
641
617
|
:rtype: int
|
|
642
618
|
"""
|
|
643
|
-
|
|
644
|
-
return self._trading_item_id
|
|
645
|
-
return self.set_trading_item_id()
|
|
619
|
+
return self.id_triple.trading_item_id
|
|
646
620
|
|
|
647
621
|
@cached_property
|
|
648
622
|
def primary_security(self) -> Security:
|
|
@@ -711,7 +685,7 @@ class Ticker(DelegatedCompanyFunctionsMetaClass):
|
|
|
711
685
|
def info(self) -> dict:
|
|
712
686
|
"""Get the company info for the ticker
|
|
713
687
|
|
|
714
|
-
:return: a dict with containing: name, status, type, simple industry, number of employees, founding date, webpage, address, city, zip code, state, country, & iso_country
|
|
688
|
+
:return: a dict with containing: name, status, type, simple industry, number of employees (if available), founding date, webpage, address, city, zip code, state, country, & iso_country
|
|
715
689
|
:rtype: dict
|
|
716
690
|
"""
|
|
717
691
|
return self.company.info
|
|
@@ -753,11 +727,11 @@ class Ticker(DelegatedCompanyFunctionsMetaClass):
|
|
|
753
727
|
return self.company.simple_industry
|
|
754
728
|
|
|
755
729
|
@property
|
|
756
|
-
def number_of_employees(self) -> str:
|
|
757
|
-
"""Get the number of employees the company has
|
|
730
|
+
def number_of_employees(self) -> str | None:
|
|
731
|
+
"""Get the number of employees the company has (if available)
|
|
758
732
|
|
|
759
733
|
:return: how many employees the company has
|
|
760
|
-
:rtype: str
|
|
734
|
+
:rtype: str | None
|
|
761
735
|
"""
|
|
762
736
|
return self.company.number_of_employees
|
|
763
737
|
|
kfinance/meta_classes.py
CHANGED
|
@@ -7,7 +7,7 @@ from cachetools import LRUCache, cached
|
|
|
7
7
|
import numpy as np
|
|
8
8
|
import pandas as pd
|
|
9
9
|
|
|
10
|
-
from .constants import LINE_ITEMS, BusinessRelationshipType, PeriodType
|
|
10
|
+
from .constants import LINE_ITEMS, BusinessRelationshipType, PeriodType, SegmentType
|
|
11
11
|
from .fetch import KFinanceApiClient
|
|
12
12
|
|
|
13
13
|
|
|
@@ -304,6 +304,100 @@ class CompanyFunctionsMetaClass:
|
|
|
304
304
|
)
|
|
305
305
|
return df.set_index("date")[[column_to_extract]].apply(pd.to_numeric).replace(np.nan, None)
|
|
306
306
|
|
|
307
|
+
def _segments(
|
|
308
|
+
self,
|
|
309
|
+
segment_type: SegmentType,
|
|
310
|
+
period_type: Optional[PeriodType] = None,
|
|
311
|
+
start_year: Optional[int] = None,
|
|
312
|
+
end_year: Optional[int] = None,
|
|
313
|
+
start_quarter: Optional[int] = None,
|
|
314
|
+
end_quarter: Optional[int] = None,
|
|
315
|
+
) -> dict:
|
|
316
|
+
"""Get the company's segments"""
|
|
317
|
+
try:
|
|
318
|
+
self.validate_inputs(
|
|
319
|
+
start_year=start_year,
|
|
320
|
+
end_year=end_year,
|
|
321
|
+
start_quarter=start_quarter,
|
|
322
|
+
end_quarter=end_quarter,
|
|
323
|
+
)
|
|
324
|
+
except ValueError:
|
|
325
|
+
return {}
|
|
326
|
+
|
|
327
|
+
return self.kfinance_api_client.fetch_segments(
|
|
328
|
+
company_id=self.company_id,
|
|
329
|
+
segment_type=segment_type,
|
|
330
|
+
period_type=period_type,
|
|
331
|
+
start_year=start_year,
|
|
332
|
+
end_year=end_year,
|
|
333
|
+
start_quarter=start_quarter,
|
|
334
|
+
end_quarter=end_quarter,
|
|
335
|
+
)["segments"]
|
|
336
|
+
|
|
337
|
+
def business_segments(
|
|
338
|
+
self,
|
|
339
|
+
period_type: Optional[PeriodType] = None,
|
|
340
|
+
start_year: Optional[int] = None,
|
|
341
|
+
end_year: Optional[int] = None,
|
|
342
|
+
start_quarter: Optional[int] = None,
|
|
343
|
+
end_quarter: Optional[int] = None,
|
|
344
|
+
) -> dict:
|
|
345
|
+
"""Retrieves the templated line of business segments for a given period_type, start_year, start_quarter, end_year and end_quarter.
|
|
346
|
+
|
|
347
|
+
:param period_type: The period_type requested for. Can be “annual”, “quarterly”, "ytd". Defaults to “annual” when start_quarter and end_quarter are None.
|
|
348
|
+
:type start_year: PeriodType, optional
|
|
349
|
+
:param start_year: The starting calendar year, defaults to None
|
|
350
|
+
:type start_year: int, optional
|
|
351
|
+
:param end_year: The ending calendar year, defaults to None
|
|
352
|
+
:type end_year: int, optional
|
|
353
|
+
:param start_quarter: The starting calendar quarter, defaults to None
|
|
354
|
+
:type start_quarter: int, optional
|
|
355
|
+
:param end_quarter: The ending calendar quarter, defaults to None
|
|
356
|
+
:type end_quarter: int, optional
|
|
357
|
+
:return: A dictionary containing the templated line of business segments for each time period, segment name, line item, and value.
|
|
358
|
+
:rtype: dict
|
|
359
|
+
"""
|
|
360
|
+
return self._segments(
|
|
361
|
+
segment_type=SegmentType.business,
|
|
362
|
+
period_type=period_type,
|
|
363
|
+
start_year=start_year,
|
|
364
|
+
end_year=end_year,
|
|
365
|
+
start_quarter=start_quarter,
|
|
366
|
+
end_quarter=end_quarter,
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
def geographic_segments(
|
|
370
|
+
self,
|
|
371
|
+
period_type: Optional[PeriodType] = None,
|
|
372
|
+
start_year: Optional[int] = None,
|
|
373
|
+
end_year: Optional[int] = None,
|
|
374
|
+
start_quarter: Optional[int] = None,
|
|
375
|
+
end_quarter: Optional[int] = None,
|
|
376
|
+
) -> dict:
|
|
377
|
+
"""Retrieves the templated geographic segments for a given period_type, start_year, start_quarter, end_year and end_quarter.
|
|
378
|
+
|
|
379
|
+
:param period_type: The period_type requested for. Can be “annual”, “quarterly”, "ytd". Defaults to “annual” when start_quarter and end_quarter are None.
|
|
380
|
+
:type start_year: PeriodType, optional
|
|
381
|
+
:param start_year: The starting calendar year, defaults to None
|
|
382
|
+
:type start_year: int, optional
|
|
383
|
+
:param end_year: The ending calendar year, defaults to None
|
|
384
|
+
:type end_year: int, optional
|
|
385
|
+
:param start_quarter: The starting calendar quarter, defaults to None
|
|
386
|
+
:type start_quarter: int, optional
|
|
387
|
+
:param end_quarter: The ending calendar quarter, defaults to None
|
|
388
|
+
:type end_quarter: int, optional
|
|
389
|
+
:return: A dictionary containing the templated geographic segments for each time period, segment name, line item, and value.
|
|
390
|
+
:rtype: dict
|
|
391
|
+
"""
|
|
392
|
+
return self._segments(
|
|
393
|
+
segment_type=SegmentType.geographic,
|
|
394
|
+
period_type=period_type,
|
|
395
|
+
start_year=start_year,
|
|
396
|
+
end_year=end_year,
|
|
397
|
+
start_quarter=start_quarter,
|
|
398
|
+
end_quarter=end_quarter,
|
|
399
|
+
)
|
|
400
|
+
|
|
307
401
|
|
|
308
402
|
for line_item in LINE_ITEMS:
|
|
309
403
|
line_item_name = line_item["name"]
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
from concurrent.futures import ThreadPoolExecutor
|
|
2
|
+
import time
|
|
2
3
|
from typing import Any, Dict
|
|
3
4
|
from unittest import TestCase
|
|
4
|
-
from unittest.mock import patch
|
|
5
|
+
from unittest.mock import PropertyMock, patch
|
|
5
6
|
|
|
6
7
|
import numpy as np
|
|
7
8
|
import pandas as pd
|
|
@@ -9,6 +10,7 @@ import pytest
|
|
|
9
10
|
import requests
|
|
10
11
|
import requests_mock
|
|
11
12
|
|
|
13
|
+
from kfinance.batch_request_handling import MAX_WORKERS_CAP
|
|
12
14
|
from kfinance.fetch import KFinanceApiClient
|
|
13
15
|
from kfinance.kfinance import Companies, Company, Ticker, TradingItems
|
|
14
16
|
|
|
@@ -260,3 +262,33 @@ class TestTradingItem(TestCase):
|
|
|
260
262
|
|
|
261
263
|
expected_id_based_result = {1001: "Mock City A"}
|
|
262
264
|
self.assertDictEqual(id_based_result, expected_id_based_result)
|
|
265
|
+
|
|
266
|
+
@patch.object(Company, "info", new_callable=PropertyMock)
|
|
267
|
+
def test_batch_requests_processed_in_parallel(self, mock_value: PropertyMock):
|
|
268
|
+
"""
|
|
269
|
+
WHEN a batch request gets processed
|
|
270
|
+
THEN the requests are handled in parallel not sequentially.
|
|
271
|
+
"""
|
|
272
|
+
|
|
273
|
+
sleep_duration = 0.05
|
|
274
|
+
|
|
275
|
+
def mock_info_with_sleep() -> dict[str, str]:
|
|
276
|
+
"""Mock an info call with a short sleep"""
|
|
277
|
+
time.sleep(sleep_duration)
|
|
278
|
+
return {"city": "Cambridge"}
|
|
279
|
+
|
|
280
|
+
mock_value.side_effect = mock_info_with_sleep
|
|
281
|
+
|
|
282
|
+
# Create tasks up to the MAX_WORKERS_CAP (max number of parallel tasks)
|
|
283
|
+
companies = Companies(
|
|
284
|
+
self.kfinance_api_client_with_thread_pool, [i for i in range(MAX_WORKERS_CAP)]
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
start = time.perf_counter()
|
|
288
|
+
result = companies.city
|
|
289
|
+
end = time.perf_counter()
|
|
290
|
+
assert len(result) == MAX_WORKERS_CAP
|
|
291
|
+
# Check that the requests run faster than sequential.
|
|
292
|
+
# In practice, the requests should take barely more than the `sleep_duration` but timing
|
|
293
|
+
# based tests can be flaky, especially in CI.
|
|
294
|
+
assert end - start < MAX_WORKERS_CAP * sleep_duration
|
|
@@ -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
|
@@ -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(
|
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,13 @@ 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
|
+
expected_segments = MOCK_COMPANY_DB[msft_company_id]["segments"]
|
|
341
|
+
|
|
342
|
+
business_segment = self.msft_company.business_segments()
|
|
343
|
+
self.assertEqual(expected_segments, business_segment)
|
|
344
|
+
|
|
312
345
|
|
|
313
346
|
class TestSecurity(TestCase):
|
|
314
347
|
def setUp(self):
|
kfinance/tests/test_tools.py
CHANGED
|
@@ -4,11 +4,10 @@ from langchain_core.utils.function_calling import convert_to_openai_tool
|
|
|
4
4
|
from requests_mock import Mocker
|
|
5
5
|
import time_machine
|
|
6
6
|
|
|
7
|
-
from kfinance.constants import BusinessRelationshipType, Capitalization, StatementType
|
|
7
|
+
from kfinance.constants import BusinessRelationshipType, Capitalization, SegmentType, StatementType
|
|
8
8
|
from kfinance.kfinance import Client
|
|
9
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,
|
|
@@ -40,6 +38,10 @@ from kfinance.tool_calling.get_isin_from_ticker import GetIsinFromTickerArgs
|
|
|
40
38
|
from kfinance.tool_calling.get_latest import GetLatestArgs
|
|
41
39
|
from kfinance.tool_calling.get_n_quarters_ago import GetNQuartersAgoArgs
|
|
42
40
|
from kfinance.tool_calling.get_prices_from_identifier import GetPricesFromIdentifierArgs
|
|
41
|
+
from kfinance.tool_calling.get_segments_from_identifier import (
|
|
42
|
+
GetSegmentsFromIdentifier,
|
|
43
|
+
GetSegmentsFromIdentifierArgs,
|
|
44
|
+
)
|
|
43
45
|
from kfinance.tool_calling.shared_models import ToolArgsWithIdentifier
|
|
44
46
|
|
|
45
47
|
|
|
@@ -107,18 +109,6 @@ class TestGetCapitalizationFromIdentifier:
|
|
|
107
109
|
assert response == expected_response
|
|
108
110
|
|
|
109
111
|
|
|
110
|
-
class TestGetCompanyIdFromIdentifier:
|
|
111
|
-
def test_get_company_id_from_identifier(self, mock_client: Client):
|
|
112
|
-
"""
|
|
113
|
-
GIVEN the GetCompanyIdFromIdentifier tool
|
|
114
|
-
WHEN request the company id for SPGI
|
|
115
|
-
THEN we get back the SPGI company id
|
|
116
|
-
"""
|
|
117
|
-
tool = GetCompanyIdFromIdentifier(kfinance_client=mock_client)
|
|
118
|
-
resp = tool.run(ToolArgsWithIdentifier(identifier="SPGI").model_dump(mode="json"))
|
|
119
|
-
assert resp == SPGI_COMPANY_ID
|
|
120
|
-
|
|
121
|
-
|
|
122
112
|
class TestGetCusipFromTicker:
|
|
123
113
|
def test_get_cusip_from_ticker(self, requests_mock: Mocker, mock_client: Client):
|
|
124
114
|
"""
|
|
@@ -231,6 +221,44 @@ class TestGetFinancialStatementFromIdentifier:
|
|
|
231
221
|
assert response == expected_response
|
|
232
222
|
|
|
233
223
|
|
|
224
|
+
class TestGetSegmentsFromIdentifier:
|
|
225
|
+
def test_get_segments_from_identifier(self, mock_client: Client, requests_mock: Mocker):
|
|
226
|
+
"""
|
|
227
|
+
GIVEN the GetSegmentsFromIdentifier tool
|
|
228
|
+
WHEN we request the SPGI business segment
|
|
229
|
+
THEN we get back the SPGI business segment
|
|
230
|
+
"""
|
|
231
|
+
|
|
232
|
+
segments_response = {
|
|
233
|
+
"segments": {
|
|
234
|
+
"2020": {
|
|
235
|
+
"Commodity Insights": {
|
|
236
|
+
"CAPEX": -7000000.0,
|
|
237
|
+
"D&A": 17000000.0,
|
|
238
|
+
},
|
|
239
|
+
"Unallocated Assets Held for Sale": None,
|
|
240
|
+
},
|
|
241
|
+
"2021": {
|
|
242
|
+
"Commodity Insights": {
|
|
243
|
+
"CAPEX": -2000000.0,
|
|
244
|
+
"D&A": 12000000.0,
|
|
245
|
+
},
|
|
246
|
+
"Unallocated Assets Held for Sale": {"Total Assets": 321000000.0},
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
}
|
|
250
|
+
requests_mock.get(
|
|
251
|
+
url=f"https://kfinance.kensho.com/api/v1/segments/{SPGI_COMPANY_ID}/business/none/none/none/none/none",
|
|
252
|
+
# truncated from the original API response
|
|
253
|
+
json=segments_response,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
tool = GetSegmentsFromIdentifier(kfinance_client=mock_client)
|
|
257
|
+
args = GetSegmentsFromIdentifierArgs(identifier="SPGI", segment_type=SegmentType.business)
|
|
258
|
+
response = tool.run(args.model_dump(mode="json"))
|
|
259
|
+
assert response == segments_response["segments"]
|
|
260
|
+
|
|
261
|
+
|
|
234
262
|
class TestGetHistoryMetadataFromIdentifier:
|
|
235
263
|
def test_get_history_metadata_from_identifier(self, mock_client: Client, requests_mock: Mocker):
|
|
236
264
|
"""
|
|
@@ -380,25 +408,17 @@ class TestPricesFromIdentifier:
|
|
|
380
408
|
assert response == expected_response
|
|
381
409
|
|
|
382
410
|
|
|
383
|
-
class
|
|
384
|
-
def
|
|
411
|
+
class TestResolveIdentifier:
|
|
412
|
+
def test_resolve_identifier(self, mock_client: Client):
|
|
385
413
|
"""
|
|
386
|
-
GIVEN the
|
|
387
|
-
WHEN
|
|
388
|
-
THEN we get back the SPGI
|
|
414
|
+
GIVEN the ResolveIdentifier tool
|
|
415
|
+
WHEN request to resolve SPGI
|
|
416
|
+
THEN we get back a dict with the SPGI company id, security id, and trading item id
|
|
389
417
|
"""
|
|
390
|
-
tool =
|
|
418
|
+
tool = ResolveIdentifier(kfinance_client=mock_client)
|
|
391
419
|
resp = tool.run(ToolArgsWithIdentifier(identifier="SPGI").model_dump(mode="json"))
|
|
392
|
-
assert resp ==
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
"""
|
|
398
|
-
GIVEN the GetTradingItemIdFromIdentifier tool
|
|
399
|
-
WHEN we request the trading item id for SPGI
|
|
400
|
-
THEN we get back the SPGI primary trading item id
|
|
401
|
-
"""
|
|
402
|
-
tool = GetTradingItemIdFromIdentifier(kfinance_client=mock_client)
|
|
403
|
-
resp = tool.run(ToolArgsWithIdentifier(identifier="SPGI").model_dump(mode="json"))
|
|
404
|
-
assert resp == SPGI_TRADING_ITEM_ID
|
|
420
|
+
assert resp == {
|
|
421
|
+
"company_id": SPGI_COMPANY_ID,
|
|
422
|
+
"security_id": SPGI_SECURITY_ID,
|
|
423
|
+
"trading_item_id": SPGI_TRADING_ITEM_ID,
|
|
424
|
+
}
|
|
@@ -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,16 @@ 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
|
-
|
|
25
|
+
from kfinance.tool_calling.get_segments_from_identifier import (
|
|
26
|
+
GetSegmentsFromIdentifier,
|
|
27
|
+
)
|
|
28
|
+
from kfinance.tool_calling.resolve_identifier import ResolveIdentifier
|
|
28
29
|
from kfinance.tool_calling.shared_models import KfinanceTool
|
|
29
30
|
|
|
30
31
|
|
|
31
32
|
ALL_TOOLS: list[Type[KfinanceTool]] = [
|
|
32
33
|
GetLatest,
|
|
33
34
|
GetNQuartersAgo,
|
|
34
|
-
GetCompanyIdFromIdentifier,
|
|
35
|
-
GetSecurityIdFromIdentifier,
|
|
36
|
-
GetTradingItemIdFromIdentifier,
|
|
37
35
|
GetIsinFromTicker,
|
|
38
36
|
GetCusipFromTicker,
|
|
39
37
|
GetInfoFromIdentifier,
|
|
@@ -44,4 +42,6 @@ ALL_TOOLS: list[Type[KfinanceTool]] = [
|
|
|
44
42
|
GetFinancialStatementFromIdentifier,
|
|
45
43
|
GetFinancialLineItemFromIdentifier,
|
|
46
44
|
GetBusinessRelationshipFromIdentifier,
|
|
45
|
+
ResolveIdentifier,
|
|
46
|
+
GetSegmentsFromIdentifier,
|
|
47
47
|
]
|
|
@@ -8,7 +8,7 @@ from kfinance.tool_calling.shared_models import KfinanceTool, ToolArgsWithIdenti
|
|
|
8
8
|
|
|
9
9
|
class GetInfoFromIdentifier(KfinanceTool):
|
|
10
10
|
name: str = "get_info_from_identifier"
|
|
11
|
-
description: str = "Get the information associated with an identifier. Info includes company name, status, type, simple industry, number of employees, founding date, webpage, HQ address, HQ city, HQ zip code, HQ state, HQ country, and HQ country iso code."
|
|
11
|
+
description: str = "Get the information associated with an identifier. 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."
|
|
12
12
|
args_schema: Type[BaseModel] = ToolArgsWithIdentifier
|
|
13
13
|
required_permission: Permission | None = None
|
|
14
14
|
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from typing import Literal, Type
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
|
|
5
|
+
from kfinance.constants import PeriodType, Permission, SegmentType
|
|
6
|
+
from kfinance.tool_calling.shared_models import KfinanceTool, ToolArgsWithIdentifier
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class GetSegmentsFromIdentifierArgs(ToolArgsWithIdentifier):
|
|
10
|
+
# no description because the description for enum fields comes from the enum docstring.
|
|
11
|
+
segment_type: SegmentType
|
|
12
|
+
period_type: PeriodType | None = Field(default=None, description="The period type")
|
|
13
|
+
start_year: int | None = Field(default=None, description="The starting year for the data range")
|
|
14
|
+
end_year: int | None = Field(default=None, description="The ending year for the data range")
|
|
15
|
+
start_quarter: Literal[1, 2, 3, 4] | None = Field(default=None, description="Starting quarter")
|
|
16
|
+
end_quarter: Literal[1, 2, 3, 4] | None = Field(default=None, description="Ending quarter")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class GetSegmentsFromIdentifier(KfinanceTool):
|
|
20
|
+
name: str = "get_segments_from_identifier"
|
|
21
|
+
description: str = "Get the templated segments associated with an identifier."
|
|
22
|
+
args_schema: Type[BaseModel] = GetSegmentsFromIdentifierArgs
|
|
23
|
+
required_permission: Permission | None = Permission.StatementsPermission
|
|
24
|
+
|
|
25
|
+
def _run(
|
|
26
|
+
self,
|
|
27
|
+
identifier: str,
|
|
28
|
+
segment_type: SegmentType,
|
|
29
|
+
period_type: PeriodType | None = None,
|
|
30
|
+
start_year: int | None = None,
|
|
31
|
+
end_year: int | None = None,
|
|
32
|
+
start_quarter: Literal[1, 2, 3, 4] | None = None,
|
|
33
|
+
end_quarter: Literal[1, 2, 3, 4] | None = None,
|
|
34
|
+
) -> str:
|
|
35
|
+
ticker = self.kfinance_client.ticker(identifier)
|
|
36
|
+
return getattr(ticker, segment_type.value + "_segments")(
|
|
37
|
+
period_type=period_type,
|
|
38
|
+
start_year=start_year,
|
|
39
|
+
end_year=end_year,
|
|
40
|
+
start_quarter=start_quarter,
|
|
41
|
+
end_quarter=end_quarter,
|
|
42
|
+
)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from typing import Type
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
from kfinance.constants import Permission
|
|
6
|
+
from kfinance.tool_calling.shared_models import KfinanceTool, ToolArgsWithIdentifier
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ResolveIdentifier(KfinanceTool):
|
|
10
|
+
name: str = "resolve_identifier"
|
|
11
|
+
description: str = (
|
|
12
|
+
"Get the company_id, security_id, and trading_item_id associated with an identifier."
|
|
13
|
+
)
|
|
14
|
+
args_schema: Type[BaseModel] = ToolArgsWithIdentifier
|
|
15
|
+
required_permission: Permission | None = None
|
|
16
|
+
|
|
17
|
+
def _run(self, identifier: str) -> dict[str, int]:
|
|
18
|
+
return self.kfinance_client.ticker(identifier).id_triple._asdict()
|
kfinance/version.py
CHANGED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
from typing import Type
|
|
2
|
-
|
|
3
|
-
from pydantic import BaseModel
|
|
4
|
-
|
|
5
|
-
from kfinance.constants import Permission
|
|
6
|
-
from kfinance.tool_calling.shared_models import KfinanceTool, ToolArgsWithIdentifier
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class GetCompanyIdFromIdentifier(KfinanceTool):
|
|
10
|
-
name: str = "get_company_id_from_identifier"
|
|
11
|
-
description: str = "Get the company id associated with an identifier."
|
|
12
|
-
args_schema: Type[BaseModel] = ToolArgsWithIdentifier
|
|
13
|
-
required_permission: Permission | None = None
|
|
14
|
-
|
|
15
|
-
def _run(self, identifier: str) -> int:
|
|
16
|
-
return self.kfinance_client.ticker(identifier).company_id
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
from typing import Type
|
|
2
|
-
|
|
3
|
-
from pydantic import BaseModel
|
|
4
|
-
|
|
5
|
-
from kfinance.constants import Permission
|
|
6
|
-
from kfinance.tool_calling.shared_models import KfinanceTool, ToolArgsWithIdentifier
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class GetSecurityIdFromIdentifier(KfinanceTool):
|
|
10
|
-
name: str = "get_security_id_from_identifier"
|
|
11
|
-
description: str = "Get the security id associated with an identifier."
|
|
12
|
-
args_schema: Type[BaseModel] = ToolArgsWithIdentifier
|
|
13
|
-
required_permission: Permission | None = None
|
|
14
|
-
|
|
15
|
-
def _run(self, identifier: str) -> int:
|
|
16
|
-
return self.kfinance_client.ticker(identifier).security_id
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
from typing import Type
|
|
2
|
-
|
|
3
|
-
from pydantic import BaseModel
|
|
4
|
-
|
|
5
|
-
from kfinance.constants import Permission
|
|
6
|
-
from kfinance.tool_calling.shared_models import KfinanceTool, ToolArgsWithIdentifier
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class GetTradingItemIdFromIdentifier(KfinanceTool):
|
|
10
|
-
name: str = "get_trading_item_id_from_identifier"
|
|
11
|
-
description: str = "Get the trading item id associated with an identifier."
|
|
12
|
-
args_schema: Type[BaseModel] = ToolArgsWithIdentifier
|
|
13
|
-
required_permission: Permission | None = None
|
|
14
|
-
|
|
15
|
-
def _run(self, identifier: str) -> int:
|
|
16
|
-
return self.kfinance_client.ticker(identifier).trading_item_id
|
|
File without changes
|
|
File without changes
|
|
File without changes
|