kensho-kfinance 1.1.0__py3-none-any.whl → 1.2.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-1.1.0.dist-info → kensho_kfinance-1.2.0.dist-info}/METADATA +2 -1
- kensho_kfinance-1.2.0.dist-info/RECORD +23 -0
- kfinance/CHANGELOG.md +3 -0
- kfinance/batch_request_handling.py +137 -0
- kfinance/fetch.py +70 -7
- kfinance/kfinance.py +21 -2
- kfinance/llm_tools.py +24 -20
- kfinance/tests/test_batch_requests.py +256 -0
- kfinance/tool_schemas.py +3 -1
- kfinance/version.py +2 -2
- kensho_kfinance-1.1.0.dist-info/RECORD +0 -21
- {kensho_kfinance-1.1.0.dist-info → kensho_kfinance-1.2.0.dist-info}/WHEEL +0 -0
- {kensho_kfinance-1.1.0.dist-info → kensho_kfinance-1.2.0.dist-info}/licenses/AUTHORS.md +0 -0
- {kensho_kfinance-1.1.0.dist-info → kensho_kfinance-1.2.0.dist-info}/licenses/LICENSE +0 -0
- {kensho_kfinance-1.1.0.dist-info → kensho_kfinance-1.2.0.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kensho-kfinance
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0
|
|
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,6 +28,7 @@ Requires-Dist: mypy<2,>=1.15.0; extra == "dev"
|
|
|
28
28
|
Requires-Dist: pytest<7,>=6.1.2; extra == "dev"
|
|
29
29
|
Requires-Dist: pytest-cov<7,>=6.0.0; extra == "dev"
|
|
30
30
|
Requires-Dist: ruff<1,>=0.9.4; extra == "dev"
|
|
31
|
+
Requires-Dist: requests_mock<1.2,>=1.1; extra == "dev"
|
|
31
32
|
Dynamic: license-file
|
|
32
33
|
|
|
33
34
|
# kFinance
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
kensho_kfinance-1.2.0.dist-info/licenses/AUTHORS.md,sha256=0h9ClbI0pu1oKj1M28ROUsaxrbZg-6ukQGl6X4y9noI,68
|
|
2
|
+
kensho_kfinance-1.2.0.dist-info/licenses/LICENSE,sha256=bsY4blvSgq6o0FMQ3RXa2NCgco--nHCCchLXzxr6kms,83
|
|
3
|
+
kfinance/CHANGELOG.md,sha256=rPeMYaICQiwXrL3vPgLjanjulnxVyovt27dLFqqZ_RA,443
|
|
4
|
+
kfinance/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
kfinance/batch_request_handling.py,sha256=s0uXRY4CC-GnShI0i8PLPgLddRdqLyQ8jnqDKdmaZzs,5531
|
|
6
|
+
kfinance/constants.py,sha256=SJfI3K2gS0N-M3-rHIMizcxjyTz_iWBc7UYtauUOjDg,47338
|
|
7
|
+
kfinance/fetch.py,sha256=tDPFQQSPMAAnH8gFlT1dgERaHRfQjkW2zcUYcRFbXEY,18130
|
|
8
|
+
kfinance/kfinance.py,sha256=PGJzfq_AdX7IZsY4BpNuni90R6SGa6nrNLicJi_QEec,45643
|
|
9
|
+
kfinance/llm_tools.py,sha256=t3i-5y34AzeFujjwOJVWY7OFI5aOAWG8rt-9KgR_0-E,30683
|
|
10
|
+
kfinance/meta_classes.py,sha256=ryncusGDe48gG31a-Ldx1ApJ-ZPRFJvjC18jry7TOVE,15980
|
|
11
|
+
kfinance/prompt.py,sha256=PtVB8c_FcSlVdyGgByAnIFGzuUuBaEjciCqnBJl1hSQ,25133
|
|
12
|
+
kfinance/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
+
kfinance/server_thread.py,sha256=jUnt1YGoYDkqqz1MbCwd44zJs1T_Z2BCgvj75bdtLgA,2574
|
|
14
|
+
kfinance/tool_schemas.py,sha256=JrFSbb6i2lOQaI24QqnhGhZ3e-pWLhjNqZloY9WGlnM,5643
|
|
15
|
+
kfinance/version.py,sha256=V2bJXGFUmn_IdFy3HF4zr3V9woAW6i1X0GXwu8-ZCDs,511
|
|
16
|
+
kfinance/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
17
|
+
kfinance/tests/test_batch_requests.py,sha256=NsldoZqjjQpv0QKM2th80F1nru2tIF4LJeXU-RamQvA,9836
|
|
18
|
+
kfinance/tests/test_fetch.py,sha256=gYSYDSsMUHpwwMODN0_g7kQQAEWq_jsucIRiqhBjAcA,12981
|
|
19
|
+
kfinance/tests/test_objects.py,sha256=egXkhfoK2MepZdXtrBxzWxWni7f-zVCefUbnyDnWDOE,22554
|
|
20
|
+
kensho_kfinance-1.2.0.dist-info/METADATA,sha256=ACI6Utx34JrW1rSNX97cMyDlHq8L_rsQAh7SH9PXWB8,2869
|
|
21
|
+
kensho_kfinance-1.2.0.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
|
22
|
+
kensho_kfinance-1.2.0.dist-info/top_level.txt,sha256=kT_kNwVhfQoOAecY8W7uYah5xaHMoHoAdBIvXh6DaKM,9
|
|
23
|
+
kensho_kfinance-1.2.0.dist-info/RECORD,,
|
kfinance/CHANGELOG.md
CHANGED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
2
|
+
import functools
|
|
3
|
+
from functools import cached_property
|
|
4
|
+
import threading
|
|
5
|
+
from typing import Any, Callable, Iterable, Protocol, Sized, Type, TypeVar
|
|
6
|
+
|
|
7
|
+
from requests.exceptions import HTTPError
|
|
8
|
+
|
|
9
|
+
from .fetch import KFinanceApiClient
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
T = TypeVar("T")
|
|
13
|
+
|
|
14
|
+
MAX_WORKERS_CAP: int = 10
|
|
15
|
+
|
|
16
|
+
throttle = threading.Semaphore(MAX_WORKERS_CAP)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def add_methods_of_singular_class_to_iterable_class(singular_cls: Type[T]) -> Callable:
|
|
20
|
+
"""Returns a decorator that sets each method, property, and cached_property of"""
|
|
21
|
+
"[singular_cls] as an attribute of the decorated class."
|
|
22
|
+
|
|
23
|
+
class IterableKfinanceClass(Protocol, Sized, Iterable[T]):
|
|
24
|
+
"""A protocol to represent a iterable Kfinance classes like Tickers and Companies.
|
|
25
|
+
|
|
26
|
+
Each of these classes has a kfinance_api_client attribute.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
kfinance_api_client: KFinanceApiClient
|
|
30
|
+
|
|
31
|
+
def decorator(iterable_cls: Type[IterableKfinanceClass]) -> Type[IterableKfinanceClass]:
|
|
32
|
+
"""Adds functions from a singular class to an iterable class.
|
|
33
|
+
|
|
34
|
+
This decorator modifies the [iterable_cls] so that when an attribute
|
|
35
|
+
(method, property, or cached property) added by the decorator is accessed,
|
|
36
|
+
it returns a dictionary. This dictionary maps each object in [iterable_cls]
|
|
37
|
+
to the result of invoking the attribute on that specific object.
|
|
38
|
+
|
|
39
|
+
For example, consider a `Company` class with a `city` property and a
|
|
40
|
+
`Companies` class that is an iterable of `Company` instances. When the
|
|
41
|
+
`Companies` class is decorated, it gains a `city` property. Accessing this
|
|
42
|
+
property will yield a dictionary where each key is a `Company` instance
|
|
43
|
+
and the corresponding value is the city of that instance. The resulting
|
|
44
|
+
dictionary might look like:
|
|
45
|
+
|
|
46
|
+
{<kfinance.kfinance.Company object>: 'Some City'}
|
|
47
|
+
|
|
48
|
+
Error Handling:
|
|
49
|
+
- If the result is a 404 HTTP error, the corresponding value
|
|
50
|
+
for that object in the dictionary will be set to None.
|
|
51
|
+
- For any other HTTP error, the error is raised and bubbles up.
|
|
52
|
+
|
|
53
|
+
Note:
|
|
54
|
+
This decorator requires [iterable_cls] to be an iterable of
|
|
55
|
+
instances of [singular_cls].
|
|
56
|
+
"""
|
|
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
|
+
def process_in_batch(
|
|
73
|
+
method: Callable, self: IterableKfinanceClass, *args: Any, **kwargs: Any
|
|
74
|
+
) -> dict:
|
|
75
|
+
results = {}
|
|
76
|
+
kfinance_api_client = self.kfinance_api_client
|
|
77
|
+
with kfinance_api_client.batch_request_header(batch_size=len(self)):
|
|
78
|
+
with kfinance_api_client.thread_pool as executor:
|
|
79
|
+
results = dict(
|
|
80
|
+
zip(
|
|
81
|
+
self,
|
|
82
|
+
[
|
|
83
|
+
process_in_thread_pool(executor, method, obj, *args, **kwargs)
|
|
84
|
+
for obj in self
|
|
85
|
+
],
|
|
86
|
+
)
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
return results
|
|
90
|
+
|
|
91
|
+
for method_name in dir(singular_cls):
|
|
92
|
+
method = getattr(singular_cls, method_name)
|
|
93
|
+
if method_name.startswith("__") or method_name.startswith("set_"):
|
|
94
|
+
continue
|
|
95
|
+
if callable(method):
|
|
96
|
+
|
|
97
|
+
def create_method_wrapper(method: Callable) -> Callable:
|
|
98
|
+
@functools.wraps(method)
|
|
99
|
+
def method_wrapper(
|
|
100
|
+
self: IterableKfinanceClass, *args: Any, **kwargs: Any
|
|
101
|
+
) -> dict:
|
|
102
|
+
return process_in_batch(method, self, *args, **kwargs)
|
|
103
|
+
|
|
104
|
+
return method_wrapper
|
|
105
|
+
|
|
106
|
+
setattr(iterable_cls, method_name, create_method_wrapper(method))
|
|
107
|
+
|
|
108
|
+
elif isinstance(method, property):
|
|
109
|
+
|
|
110
|
+
def create_prop_wrapper(method: property) -> Callable:
|
|
111
|
+
assert method.fget is not None
|
|
112
|
+
|
|
113
|
+
@functools.wraps(method.fget)
|
|
114
|
+
def prop_wrapper(self: IterableKfinanceClass) -> Any:
|
|
115
|
+
assert method.fget is not None
|
|
116
|
+
return process_in_batch(method.fget, self)
|
|
117
|
+
|
|
118
|
+
return prop_wrapper
|
|
119
|
+
|
|
120
|
+
setattr(iterable_cls, method_name, property(create_prop_wrapper(method)))
|
|
121
|
+
|
|
122
|
+
elif isinstance(method, cached_property):
|
|
123
|
+
|
|
124
|
+
def create_cached_prop_wrapper(method: cached_property) -> cached_property:
|
|
125
|
+
@functools.wraps(method.func)
|
|
126
|
+
def cached_prop_wrapper(self: IterableKfinanceClass) -> Any:
|
|
127
|
+
return process_in_batch(method.func, self)
|
|
128
|
+
|
|
129
|
+
wrapped_cached_property = cached_property(cached_prop_wrapper)
|
|
130
|
+
wrapped_cached_property.__set_name__(iterable_cls, method_name)
|
|
131
|
+
return wrapped_cached_property
|
|
132
|
+
|
|
133
|
+
setattr(iterable_cls, method_name, create_cached_prop_wrapper(method))
|
|
134
|
+
|
|
135
|
+
return iterable_cls
|
|
136
|
+
|
|
137
|
+
return decorator
|
kfinance/fetch.py
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
2
|
+
from contextlib import contextmanager
|
|
1
3
|
from time import time
|
|
2
|
-
from typing import Callable, Optional
|
|
4
|
+
from typing import Callable, Generator, Optional
|
|
5
|
+
from uuid import uuid4
|
|
3
6
|
|
|
4
7
|
import jwt
|
|
5
8
|
import requests
|
|
@@ -19,6 +22,7 @@ DEFAULT_API_HOST: str = "https://kfinance.kensho.com"
|
|
|
19
22
|
DEFAULT_API_VERSION: int = 1
|
|
20
23
|
DEFAULT_OKTA_HOST: str = "https://kensho.okta.com"
|
|
21
24
|
DEFAULT_OKTA_AUTH_SERVER: str = "default"
|
|
25
|
+
DEFAULT_MAX_WORKERS: int = 10
|
|
22
26
|
|
|
23
27
|
|
|
24
28
|
class KFinanceApiClient:
|
|
@@ -27,12 +31,33 @@ class KFinanceApiClient:
|
|
|
27
31
|
refresh_token: Optional[str] = None,
|
|
28
32
|
client_id: Optional[str] = None,
|
|
29
33
|
private_key: Optional[str] = None,
|
|
34
|
+
thread_pool: Optional[ThreadPoolExecutor] = None,
|
|
30
35
|
api_host: str = DEFAULT_API_HOST,
|
|
31
36
|
api_version: int = DEFAULT_API_VERSION,
|
|
32
37
|
okta_host: str = DEFAULT_OKTA_HOST,
|
|
33
38
|
okta_auth_server: str = DEFAULT_OKTA_AUTH_SERVER,
|
|
34
39
|
):
|
|
35
|
-
"""Configuration of KFinance Client.
|
|
40
|
+
"""Configuration of KFinance Client.
|
|
41
|
+
|
|
42
|
+
:param refresh_token: users refresh token
|
|
43
|
+
:type refresh_token: str, Optional
|
|
44
|
+
:param client_id: users client id will be provided by support@kensho.com
|
|
45
|
+
:type client_id: str, Optional
|
|
46
|
+
:param private_key: users private key that corresponds to the registered public sent to support@kensho.com
|
|
47
|
+
:type private_key: str, Optional
|
|
48
|
+
:param thread_pool: the thread pool used to execute batch requests. The number of concurrent requests is
|
|
49
|
+
capped at 10. If no thread pool is provided, a thread pool with 10 max workers will be created when batch
|
|
50
|
+
requests are made.
|
|
51
|
+
:type thread_pool: ThreadPoolExecutor, Optional
|
|
52
|
+
:param api_host: the api host URL
|
|
53
|
+
:type api_host: str
|
|
54
|
+
:param api_version: the api version number
|
|
55
|
+
:type api_version: int
|
|
56
|
+
:param okta_host: the okta host URL
|
|
57
|
+
:type okta_host: str
|
|
58
|
+
:param okta_auth_server: the okta route for authentication
|
|
59
|
+
:type okta_auth_server: str
|
|
60
|
+
"""
|
|
36
61
|
if refresh_token is not None:
|
|
37
62
|
self.refresh_token = refresh_token
|
|
38
63
|
self._access_token_refresh_func: Callable[..., str] = (
|
|
@@ -48,10 +73,40 @@ class KFinanceApiClient:
|
|
|
48
73
|
self.api_version = api_version
|
|
49
74
|
self.okta_host = okta_host
|
|
50
75
|
self.okta_auth_server = okta_auth_server
|
|
76
|
+
self._thread_pool = thread_pool
|
|
51
77
|
self.url_base = f"{self.api_host}/api/v{self.api_version}/"
|
|
52
78
|
self._access_token_expiry = 0
|
|
53
79
|
self._access_token: str | None = None
|
|
54
80
|
self.user_agent_source = "object_oriented"
|
|
81
|
+
self._batch_id: str | None = None
|
|
82
|
+
self._batch_size: str | None = None
|
|
83
|
+
|
|
84
|
+
@contextmanager
|
|
85
|
+
def batch_request_header(self, batch_size: int) -> Generator:
|
|
86
|
+
"""Set batch id and batch size for batch request request headers"""
|
|
87
|
+
batch_id = str(uuid4())
|
|
88
|
+
|
|
89
|
+
self._batch_id = batch_id
|
|
90
|
+
self._batch_size = str(batch_size)
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
yield
|
|
94
|
+
finally:
|
|
95
|
+
self._batch_id = None
|
|
96
|
+
self._batch_size = None
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def thread_pool(self) -> ThreadPoolExecutor:
|
|
100
|
+
"""Returns the thread pool used to execute batch requests.
|
|
101
|
+
|
|
102
|
+
If the thread pool is not set, a thread pool with 10 max workers will be created
|
|
103
|
+
and returned.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
if self._thread_pool is None:
|
|
107
|
+
self._thread_pool = ThreadPoolExecutor(max_workers=DEFAULT_MAX_WORKERS)
|
|
108
|
+
|
|
109
|
+
return self._thread_pool
|
|
55
110
|
|
|
56
111
|
@property
|
|
57
112
|
def access_token(self) -> str:
|
|
@@ -110,13 +165,21 @@ class KFinanceApiClient:
|
|
|
110
165
|
|
|
111
166
|
def fetch(self, url: str) -> dict:
|
|
112
167
|
"""Does the request and auth"""
|
|
168
|
+
|
|
169
|
+
headers = {
|
|
170
|
+
"Content-Type": "application/json",
|
|
171
|
+
"Authorization": f"Bearer {self.access_token}",
|
|
172
|
+
"User-Agent": f"kfinance/{kfinance_version} {self.user_agent_source}",
|
|
173
|
+
}
|
|
174
|
+
if self._batch_id is not None:
|
|
175
|
+
assert self._batch_size is not None
|
|
176
|
+
headers.update(
|
|
177
|
+
{"Kfinance-Batch-Id": self._batch_id, "Kfinance-Batch-Size": self._batch_size}
|
|
178
|
+
)
|
|
179
|
+
|
|
113
180
|
response = requests.get(
|
|
114
181
|
url,
|
|
115
|
-
headers=
|
|
116
|
-
"Content-Type": "application/json",
|
|
117
|
-
"Authorization": f"Bearer {self.access_token}",
|
|
118
|
-
"User-Agent": f"kfinance/{kfinance_version} {self.user_agent_source}",
|
|
119
|
-
},
|
|
182
|
+
headers=headers,
|
|
120
183
|
timeout=60,
|
|
121
184
|
)
|
|
122
185
|
response.raise_for_status()
|
kfinance/kfinance.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
3
4
|
from datetime import date, datetime, timezone
|
|
4
5
|
from functools import cached_property
|
|
5
6
|
from io import BytesIO
|
|
@@ -14,6 +15,7 @@ import numpy as np
|
|
|
14
15
|
import pandas as pd
|
|
15
16
|
from PIL.Image import Image, open as image_open
|
|
16
17
|
|
|
18
|
+
from .batch_request_handling import add_methods_of_singular_class_to_iterable_class
|
|
17
19
|
from .constants import HistoryMetadata, IdentificationTriple, LatestPeriods, YearAndQuarter
|
|
18
20
|
from .fetch import (
|
|
19
21
|
DEFAULT_API_HOST,
|
|
@@ -31,7 +33,10 @@ from .llm_tools import (
|
|
|
31
33
|
langchain_tools,
|
|
32
34
|
openai_tool_descriptions,
|
|
33
35
|
)
|
|
34
|
-
from .meta_classes import
|
|
36
|
+
from .meta_classes import (
|
|
37
|
+
CompanyFunctionsMetaClass,
|
|
38
|
+
DelegatedCompanyFunctionsMetaClass,
|
|
39
|
+
)
|
|
35
40
|
from .prompt import PROMPT
|
|
36
41
|
from .server_thread import ServerThread
|
|
37
42
|
|
|
@@ -917,6 +922,7 @@ class BusinessRelationships(NamedTuple):
|
|
|
917
922
|
return f"{type(self).__module__}.{type(self).__qualname__} of {str(dictionary)}"
|
|
918
923
|
|
|
919
924
|
|
|
925
|
+
@add_methods_of_singular_class_to_iterable_class(Company)
|
|
920
926
|
class Companies(set):
|
|
921
927
|
"""Base class for representing a set of Companies"""
|
|
922
928
|
|
|
@@ -928,9 +934,11 @@ class Companies(set):
|
|
|
928
934
|
:param company_ids: An iterable of S&P CIQ Company ids
|
|
929
935
|
:type company_ids: Iterable[int]
|
|
930
936
|
"""
|
|
937
|
+
self.kfinance_api_client = kfinance_api_client
|
|
931
938
|
super().__init__(Company(kfinance_api_client, company_id) for company_id in company_ids)
|
|
932
939
|
|
|
933
940
|
|
|
941
|
+
@add_methods_of_singular_class_to_iterable_class(Security)
|
|
934
942
|
class Securities(set):
|
|
935
943
|
"""Base class for representing a set of Securities"""
|
|
936
944
|
|
|
@@ -945,6 +953,7 @@ class Securities(set):
|
|
|
945
953
|
super().__init__(Security(kfinance_api_client, security_id) for security_id in security_ids)
|
|
946
954
|
|
|
947
955
|
|
|
956
|
+
@add_methods_of_singular_class_to_iterable_class(TradingItem)
|
|
948
957
|
class TradingItems(set):
|
|
949
958
|
"""Base class for representing a set of Trading Items"""
|
|
950
959
|
|
|
@@ -958,14 +967,16 @@ class TradingItems(set):
|
|
|
958
967
|
:param company_ids: An iterable of S&P CIQ Company ids
|
|
959
968
|
:type company_ids: Iterable[int]
|
|
960
969
|
"""
|
|
970
|
+
self.kfinance_api_client = kfinance_api_client
|
|
961
971
|
super().__init__(
|
|
962
972
|
TradingItem(kfinance_api_client, trading_item_id)
|
|
963
973
|
for trading_item_id in trading_item_ids
|
|
964
974
|
)
|
|
965
975
|
|
|
966
976
|
|
|
977
|
+
@add_methods_of_singular_class_to_iterable_class(Ticker)
|
|
967
978
|
class Tickers(set):
|
|
968
|
-
"""Base
|
|
979
|
+
"""Base class for representing a set of Tickers"""
|
|
969
980
|
|
|
970
981
|
def __init__(
|
|
971
982
|
self,
|
|
@@ -1041,6 +1052,7 @@ class Client:
|
|
|
1041
1052
|
refresh_token: Optional[str] = None,
|
|
1042
1053
|
client_id: Optional[str] = None,
|
|
1043
1054
|
private_key: Optional[str] = None,
|
|
1055
|
+
thread_pool: Optional[ThreadPoolExecutor] = None,
|
|
1044
1056
|
api_host: str = DEFAULT_API_HOST,
|
|
1045
1057
|
api_version: int = DEFAULT_API_VERSION,
|
|
1046
1058
|
okta_host: str = DEFAULT_OKTA_HOST,
|
|
@@ -1054,6 +1066,10 @@ class Client:
|
|
|
1054
1066
|
:type client_id: str, Optional
|
|
1055
1067
|
:param private_key: users private key that corresponds to the registered public sent to support@kensho.com
|
|
1056
1068
|
:type private_key: str, Optional
|
|
1069
|
+
:param thread_pool: the thread pool used to execute batch requests. The number of concurrent requests is
|
|
1070
|
+
capped at 10. If no thread pool is provided, a thread pool with 10 max workers will be created when batch
|
|
1071
|
+
requests are made.
|
|
1072
|
+
:type thread_pool: ThreadPoolExecutor, Optional
|
|
1057
1073
|
:param api_host: the api host URL
|
|
1058
1074
|
:type api_host: str
|
|
1059
1075
|
:param api_version: the api version number
|
|
@@ -1071,6 +1087,7 @@ class Client:
|
|
|
1071
1087
|
api_host=api_host,
|
|
1072
1088
|
api_version=api_version,
|
|
1073
1089
|
okta_host=okta_host,
|
|
1090
|
+
thread_pool=thread_pool,
|
|
1074
1091
|
)
|
|
1075
1092
|
# method 2 keypair
|
|
1076
1093
|
elif client_id is not None and private_key is not None:
|
|
@@ -1081,6 +1098,7 @@ class Client:
|
|
|
1081
1098
|
api_version=api_version,
|
|
1082
1099
|
okta_host=okta_host,
|
|
1083
1100
|
okta_auth_server=okta_auth_server,
|
|
1101
|
+
thread_pool=thread_pool,
|
|
1084
1102
|
)
|
|
1085
1103
|
# method 3 automatic login getting a refresh token
|
|
1086
1104
|
else:
|
|
@@ -1099,6 +1117,7 @@ class Client:
|
|
|
1099
1117
|
api_host=api_host,
|
|
1100
1118
|
api_version=api_version,
|
|
1101
1119
|
okta_host=okta_host,
|
|
1120
|
+
thread_pool=thread_pool,
|
|
1102
1121
|
)
|
|
1103
1122
|
stdout.write("Login credentials received.\n")
|
|
1104
1123
|
|
kfinance/llm_tools.py
CHANGED
|
@@ -31,23 +31,25 @@ class Model(Enum):
|
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
def get_latest(use_local_timezone: bool = True) -> LatestPeriods:
|
|
34
|
-
"""Get the latest annual reporting year, latest quarterly reporting quarter and year, and current date.
|
|
34
|
+
"""Get the latest annual reporting year, latest quarterly reporting quarter and year, and current date.
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
"
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
"
|
|
47
|
-
|
|
48
|
-
|
|
36
|
+
The output is a dictionary with the following schema::
|
|
37
|
+
|
|
38
|
+
{
|
|
39
|
+
"annual": {
|
|
40
|
+
"latest_year": int
|
|
41
|
+
},
|
|
42
|
+
"quarterly": {
|
|
43
|
+
"latest_quarter": int,
|
|
44
|
+
"latest_year": int
|
|
45
|
+
},
|
|
46
|
+
"now": {
|
|
47
|
+
"current_year": int,
|
|
48
|
+
"current_quarter": int,
|
|
49
|
+
"current_month": int,
|
|
50
|
+
"current_date": str # in format Y-m-d
|
|
51
|
+
}
|
|
49
52
|
}
|
|
50
|
-
}
|
|
51
53
|
|
|
52
54
|
Args:
|
|
53
55
|
use_local_timezone: whether to use the local timezone of the user
|
|
@@ -86,12 +88,14 @@ def get_latest(use_local_timezone: bool = True) -> LatestPeriods:
|
|
|
86
88
|
|
|
87
89
|
|
|
88
90
|
def get_n_quarters_ago(n: int) -> YearAndQuarter:
|
|
89
|
-
"""Get the year and quarter corresponding to [n] quarters before the current quarter.
|
|
91
|
+
"""Get the year and quarter corresponding to [n] quarters before the current quarter.
|
|
90
92
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
The output is a dictionary with the following schema::
|
|
94
|
+
|
|
95
|
+
{
|
|
96
|
+
"year": int,
|
|
97
|
+
"quarter": int
|
|
98
|
+
}
|
|
95
99
|
|
|
96
100
|
Args:
|
|
97
101
|
n: number of quarters before the current quarter
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
2
|
+
from typing import Any, Dict
|
|
3
|
+
from unittest import TestCase
|
|
4
|
+
from unittest.mock import patch
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
import pandas as pd
|
|
8
|
+
import pytest
|
|
9
|
+
import requests
|
|
10
|
+
import requests_mock
|
|
11
|
+
|
|
12
|
+
from kfinance.fetch import KFinanceApiClient
|
|
13
|
+
from kfinance.kfinance import Companies, Company, Ticker, TradingItems
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@pytest.fixture(autouse=True)
|
|
17
|
+
def mock_method():
|
|
18
|
+
with patch("kfinance.fetch.KFinanceApiClient.access_token", return_value="fake_access_token"):
|
|
19
|
+
yield
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TestTradingItem(TestCase):
|
|
23
|
+
def setUp(self):
|
|
24
|
+
self.kfinance_api_client = KFinanceApiClient(refresh_token="fake_refresh_token")
|
|
25
|
+
self.kfinance_api_client_with_thread_pool = KFinanceApiClient(
|
|
26
|
+
refresh_token="fake_refresh_token", thread_pool=ThreadPoolExecutor(100)
|
|
27
|
+
)
|
|
28
|
+
self.test_ticker = Ticker(self.kfinance_api_client, "test")
|
|
29
|
+
|
|
30
|
+
def company_object_keys_as_company_id(self, company_dict: Dict[Company, Any]):
|
|
31
|
+
return dict(map(lambda company: (company.company_id, company_dict[company]), company_dict))
|
|
32
|
+
|
|
33
|
+
@requests_mock.Mocker()
|
|
34
|
+
def test_batch_request_property(self, m):
|
|
35
|
+
"""GIVEN a kfinance group object like Companies
|
|
36
|
+
WHEN we batch request a property for each object in the group
|
|
37
|
+
THEN the batch request completes successfully and we get back a mapping of
|
|
38
|
+
company objects to the corresponding values."""
|
|
39
|
+
|
|
40
|
+
m.get(
|
|
41
|
+
"https://kfinance.kensho.com/api/v1/info/1001",
|
|
42
|
+
json={
|
|
43
|
+
"name": "Mock Company A, Inc.",
|
|
44
|
+
"city": "Mock City A",
|
|
45
|
+
},
|
|
46
|
+
)
|
|
47
|
+
m.get(
|
|
48
|
+
"https://kfinance.kensho.com/api/v1/info/1002",
|
|
49
|
+
json={
|
|
50
|
+
"name": "Mock Company B, Inc.",
|
|
51
|
+
"city": "Mock City B",
|
|
52
|
+
},
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
companies = Companies(self.kfinance_api_client, [1001, 1002])
|
|
56
|
+
result = companies.city
|
|
57
|
+
id_based_result = self.company_object_keys_as_company_id(result)
|
|
58
|
+
|
|
59
|
+
expected_id_based_result = {1001: "Mock City A", 1002: "Mock City B"}
|
|
60
|
+
self.assertDictEqual(id_based_result, expected_id_based_result)
|
|
61
|
+
|
|
62
|
+
@requests_mock.Mocker()
|
|
63
|
+
def test_batch_request_cached_properties(self, m):
|
|
64
|
+
"""GIVEN a kfinance group object like Companies
|
|
65
|
+
WHEN we batch request a cached property for each object in the group
|
|
66
|
+
THEN the batch request completes successfully and we get back a mapping of
|
|
67
|
+
company objects to the corresponding values."""
|
|
68
|
+
|
|
69
|
+
m.get(
|
|
70
|
+
"https://kfinance.kensho.com/api/v1/securities/1001",
|
|
71
|
+
json={"securities": [101, 102, 103]},
|
|
72
|
+
)
|
|
73
|
+
m.get(
|
|
74
|
+
"https://kfinance.kensho.com/api/v1/securities/1002",
|
|
75
|
+
json={"securities": [104, 105, 106, 107]},
|
|
76
|
+
)
|
|
77
|
+
m.get("https://kfinance.kensho.com/api/v1/securities/1005", json={"securities": [108, 109]})
|
|
78
|
+
|
|
79
|
+
companies = Companies(self.kfinance_api_client, [1001, 1002, 1005])
|
|
80
|
+
result = companies.securities
|
|
81
|
+
|
|
82
|
+
id_based_result = self.company_object_keys_as_company_id(result)
|
|
83
|
+
for k, v in id_based_result.items():
|
|
84
|
+
id_based_result[k] = set(map(lambda s: s.security_id, v))
|
|
85
|
+
|
|
86
|
+
expected_id_based_result = {
|
|
87
|
+
1001: set([101, 102, 103]),
|
|
88
|
+
1002: set([104, 105, 106, 107]),
|
|
89
|
+
1005: set([108, 109]),
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
self.assertDictEqual(id_based_result, expected_id_based_result)
|
|
93
|
+
|
|
94
|
+
@requests_mock.Mocker()
|
|
95
|
+
def test_batch_request_function(self, m):
|
|
96
|
+
"""GIVEN a kfinance group object like TradingItems
|
|
97
|
+
WHEN we batch request a function for each object in the group
|
|
98
|
+
THEN the batch request completes successfully and we get back a mapping of
|
|
99
|
+
trading item objects to the corresponding values."""
|
|
100
|
+
|
|
101
|
+
m.get(
|
|
102
|
+
"https://kfinance.kensho.com/api/v1/pricing/2/none/none/day/adjusted",
|
|
103
|
+
json={
|
|
104
|
+
"prices": [
|
|
105
|
+
{"date": "2024-01-01", "close": "100.000000"},
|
|
106
|
+
{"date": "2024-01-02", "close": "101.000000"},
|
|
107
|
+
]
|
|
108
|
+
},
|
|
109
|
+
)
|
|
110
|
+
m.get(
|
|
111
|
+
"https://kfinance.kensho.com/api/v1/pricing/3/none/none/day/adjusted",
|
|
112
|
+
json={
|
|
113
|
+
"prices": [
|
|
114
|
+
{"date": "2024-01-01", "close": "200.000000"},
|
|
115
|
+
{"date": "2024-01-02", "close": "201.000000"},
|
|
116
|
+
]
|
|
117
|
+
},
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
trading_items = TradingItems(self.kfinance_api_client, [2, 3])
|
|
121
|
+
|
|
122
|
+
result = trading_items.history()
|
|
123
|
+
expected_dictionary_based_result = {
|
|
124
|
+
2: [
|
|
125
|
+
{"date": "2024-01-01", "close": "100.000000"},
|
|
126
|
+
{"date": "2024-01-02", "close": "101.000000"},
|
|
127
|
+
],
|
|
128
|
+
3: [
|
|
129
|
+
{"date": "2024-01-01", "close": "200.000000"},
|
|
130
|
+
{"date": "2024-01-02", "close": "201.000000"},
|
|
131
|
+
],
|
|
132
|
+
}
|
|
133
|
+
self.assertEqual(len(result), len(expected_dictionary_based_result))
|
|
134
|
+
|
|
135
|
+
for k, v in result.items():
|
|
136
|
+
trading_item_id = k.trading_item_id
|
|
137
|
+
pd.testing.assert_frame_equal(
|
|
138
|
+
v,
|
|
139
|
+
pd.DataFrame(expected_dictionary_based_result[trading_item_id])
|
|
140
|
+
.set_index("date")
|
|
141
|
+
.apply(pd.to_numeric)
|
|
142
|
+
.replace(np.nan, None),
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
@requests_mock.Mocker()
|
|
146
|
+
def test_large_batch_request_property(self, m):
|
|
147
|
+
"""GIVEN a kfinance group object like Companies with a very large size
|
|
148
|
+
WHEN we batch request a property for each object in the group
|
|
149
|
+
THEN the batch request completes successfully and we get back a mapping of
|
|
150
|
+
company objects to the corresponding values."""
|
|
151
|
+
|
|
152
|
+
m.get(
|
|
153
|
+
"https://kfinance.kensho.com/api/v1/info/1000",
|
|
154
|
+
json={
|
|
155
|
+
"name": "Test Inc.",
|
|
156
|
+
"city": "Test City",
|
|
157
|
+
},
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
BATCH_SIZE = 100
|
|
161
|
+
companies = Companies(self.kfinance_api_client, [1000] * BATCH_SIZE)
|
|
162
|
+
result = list(companies.city.values())
|
|
163
|
+
expected_result = ["Test City"] * BATCH_SIZE
|
|
164
|
+
self.assertEqual(result, expected_result)
|
|
165
|
+
|
|
166
|
+
@requests_mock.Mocker()
|
|
167
|
+
def test_batch_request_property_404(self, m):
|
|
168
|
+
"""GIVEN a kfinance group object like Companies
|
|
169
|
+
WHEN we batch request a property for each object in the group and one of the
|
|
170
|
+
property requests returns a 404
|
|
171
|
+
THEN the batch request completes successfully and we get back a mapping of
|
|
172
|
+
company objects to the corresponding property value or None when the request for
|
|
173
|
+
that property returns a 404"""
|
|
174
|
+
|
|
175
|
+
m.get(
|
|
176
|
+
"https://kfinance.kensho.com/api/v1/info/1001",
|
|
177
|
+
json={
|
|
178
|
+
"name": "Mock Company A, Inc.",
|
|
179
|
+
"city": "Mock City A",
|
|
180
|
+
},
|
|
181
|
+
)
|
|
182
|
+
m.get("https://kfinance.kensho.com/api/v1/info/1002", status_code=404)
|
|
183
|
+
|
|
184
|
+
companies = Companies(self.kfinance_api_client, [1001, 1002])
|
|
185
|
+
result = companies.city
|
|
186
|
+
id_based_result = self.company_object_keys_as_company_id(result)
|
|
187
|
+
|
|
188
|
+
expected_id_based_result = {1001: "Mock City A", 1002: None}
|
|
189
|
+
self.assertDictEqual(id_based_result, expected_id_based_result)
|
|
190
|
+
|
|
191
|
+
@requests_mock.Mocker()
|
|
192
|
+
def test_batch_request_400(self, m):
|
|
193
|
+
"""GIVEN a kfinance group object like Companies
|
|
194
|
+
WHEN we batch request a property for each object in the group and one of the
|
|
195
|
+
property requests returns a 400
|
|
196
|
+
THEN the batch request returns a 400"""
|
|
197
|
+
|
|
198
|
+
m.get(
|
|
199
|
+
"https://kfinance.kensho.com/api/v1/info/1001",
|
|
200
|
+
json={
|
|
201
|
+
"name": "Mock Company A, Inc.",
|
|
202
|
+
"city": "Mock City A",
|
|
203
|
+
},
|
|
204
|
+
)
|
|
205
|
+
m.get("https://kfinance.kensho.com/api/v1/info/1002", status_code=400)
|
|
206
|
+
|
|
207
|
+
with self.assertRaises(requests.exceptions.HTTPError) as e:
|
|
208
|
+
companies = Companies(self.kfinance_api_client, [1001, 1002])
|
|
209
|
+
_ = companies.city
|
|
210
|
+
|
|
211
|
+
self.assertEqual(e.exception.response.status_code, 400)
|
|
212
|
+
|
|
213
|
+
@requests_mock.Mocker()
|
|
214
|
+
def test_batch_request_500(self, m):
|
|
215
|
+
"""GIVEN a kfinance group object like Companies
|
|
216
|
+
WHEN we batch request a property for each object in the group and one of the
|
|
217
|
+
property requests returns a 500
|
|
218
|
+
THEN the batch request returns a 500"""
|
|
219
|
+
|
|
220
|
+
m.get(
|
|
221
|
+
"https://kfinance.kensho.com/api/v1/info/1001",
|
|
222
|
+
json={
|
|
223
|
+
"name": "Mock Company A, Inc.",
|
|
224
|
+
"city": "Mock City A",
|
|
225
|
+
},
|
|
226
|
+
)
|
|
227
|
+
m.get("https://kfinance.kensho.com/api/v1/info/1002", status_code=500)
|
|
228
|
+
|
|
229
|
+
with self.assertRaises(requests.exceptions.HTTPError) as e:
|
|
230
|
+
companies = Companies(self.kfinance_api_client, [1001, 1002])
|
|
231
|
+
_ = companies.city
|
|
232
|
+
|
|
233
|
+
self.assertEqual(e.exception.response.status_code, 500)
|
|
234
|
+
|
|
235
|
+
@requests_mock.Mocker()
|
|
236
|
+
def test_batch_request_property_with_thread_pool(self, m):
|
|
237
|
+
"""GIVEN a kfinance group object like Companies and an api client instantiated
|
|
238
|
+
with a passed-in ThreadPool
|
|
239
|
+
WHEN we batch request a property for each object in the group
|
|
240
|
+
THEN the batch request completes successfully and we get back a mapping of
|
|
241
|
+
company objects to corresponding values"""
|
|
242
|
+
|
|
243
|
+
m.get(
|
|
244
|
+
"https://kfinance.kensho.com/api/v1/info/1001",
|
|
245
|
+
json={
|
|
246
|
+
"name": "Mock Company A, Inc.",
|
|
247
|
+
"city": "Mock City A",
|
|
248
|
+
},
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
companies = Companies(self.kfinance_api_client_with_thread_pool, [1001])
|
|
252
|
+
result = companies.city
|
|
253
|
+
id_based_result = self.company_object_keys_as_company_id(result)
|
|
254
|
+
|
|
255
|
+
expected_id_based_result = {1001: "Mock City A"}
|
|
256
|
+
self.assertDictEqual(id_based_result, expected_id_based_result)
|
kfinance/tool_schemas.py
CHANGED
|
@@ -16,7 +16,9 @@ class GetNQuartersAgoInput(BaseModel):
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
class GetCompanyIdFromIdentifier(BaseModel):
|
|
19
|
-
|
|
19
|
+
identifier: str = Field(
|
|
20
|
+
description="The identifier, which can be a ticker symbol, ISIN, or CUSIP"
|
|
21
|
+
)
|
|
20
22
|
|
|
21
23
|
|
|
22
24
|
class GetSecurityIdFromIdentifier(BaseModel):
|
kfinance/version.py
CHANGED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
kensho_kfinance-1.1.0.dist-info/licenses/AUTHORS.md,sha256=0h9ClbI0pu1oKj1M28ROUsaxrbZg-6ukQGl6X4y9noI,68
|
|
2
|
-
kensho_kfinance-1.1.0.dist-info/licenses/LICENSE,sha256=bsY4blvSgq6o0FMQ3RXa2NCgco--nHCCchLXzxr6kms,83
|
|
3
|
-
kfinance/CHANGELOG.md,sha256=HZt-UygSOIhqMa27Xxffm_R3TlmIPHmdt3jBYvaZPxY,390
|
|
4
|
-
kfinance/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
-
kfinance/constants.py,sha256=SJfI3K2gS0N-M3-rHIMizcxjyTz_iWBc7UYtauUOjDg,47338
|
|
6
|
-
kfinance/fetch.py,sha256=n34L_uM53FVN0cWz8aVqnst2bfFL2D9zIUwZTXze-tY,15736
|
|
7
|
-
kfinance/kfinance.py,sha256=4yVNoi1PMEwMUwYrcz4AypHu-TXfTPaZDzuykDB4Qs0,44669
|
|
8
|
-
kfinance/llm_tools.py,sha256=PehZgJN1qLXOu96Rp5CpZ4nrT9rLrzuedugTfBxHZr8,30597
|
|
9
|
-
kfinance/meta_classes.py,sha256=ryncusGDe48gG31a-Ldx1ApJ-ZPRFJvjC18jry7TOVE,15980
|
|
10
|
-
kfinance/prompt.py,sha256=PtVB8c_FcSlVdyGgByAnIFGzuUuBaEjciCqnBJl1hSQ,25133
|
|
11
|
-
kfinance/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
-
kfinance/server_thread.py,sha256=jUnt1YGoYDkqqz1MbCwd44zJs1T_Z2BCgvj75bdtLgA,2574
|
|
13
|
-
kfinance/tool_schemas.py,sha256=YaIXISqeE5WX2moWPXopwqF9X6hr5u-0rRVJuBVcaTo,5579
|
|
14
|
-
kfinance/version.py,sha256=UNO7UaKPam6Zugzw6NMt_RQBnRzrv9GVeKUjWt-tl6I,511
|
|
15
|
-
kfinance/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
|
-
kfinance/tests/test_fetch.py,sha256=gYSYDSsMUHpwwMODN0_g7kQQAEWq_jsucIRiqhBjAcA,12981
|
|
17
|
-
kfinance/tests/test_objects.py,sha256=egXkhfoK2MepZdXtrBxzWxWni7f-zVCefUbnyDnWDOE,22554
|
|
18
|
-
kensho_kfinance-1.1.0.dist-info/METADATA,sha256=rjS8yKw0aB18MiJg34AA0kjJlpbNzvRudvVSrevYSwM,2814
|
|
19
|
-
kensho_kfinance-1.1.0.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
|
20
|
-
kensho_kfinance-1.1.0.dist-info/top_level.txt,sha256=kT_kNwVhfQoOAecY8W7uYah5xaHMoHoAdBIvXh6DaKM,9
|
|
21
|
-
kensho_kfinance-1.1.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|