kensho-kfinance 1.1.0a0__tar.gz → 1.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of kensho-kfinance might be problematic. Click here for more details.

Files changed (48) hide show
  1. {kensho_kfinance-1.1.0a0 → kensho_kfinance-1.2.2}/.readthedocs.yaml +2 -0
  2. {kensho_kfinance-1.1.0a0 → kensho_kfinance-1.2.2}/PKG-INFO +10 -7
  3. {kensho_kfinance-1.1.0a0 → kensho_kfinance-1.2.2}/kensho_kfinance.egg-info/PKG-INFO +10 -7
  4. {kensho_kfinance-1.1.0a0 → kensho_kfinance-1.2.2}/kensho_kfinance.egg-info/SOURCES.txt +2 -0
  5. {kensho_kfinance-1.1.0a0 → kensho_kfinance-1.2.2}/kensho_kfinance.egg-info/requires.txt +9 -6
  6. {kensho_kfinance-1.1.0a0 → kensho_kfinance-1.2.2}/kfinance/CHANGELOG.md +10 -0
  7. kensho_kfinance-1.2.2/kfinance/batch_request_handling.py +137 -0
  8. {kensho_kfinance-1.1.0a0 → kensho_kfinance-1.2.2}/kfinance/constants.py +11 -0
  9. {kensho_kfinance-1.1.0a0 → kensho_kfinance-1.2.2}/kfinance/fetch.py +124 -28
  10. {kensho_kfinance-1.1.0a0 → kensho_kfinance-1.2.2}/kfinance/kfinance.py +21 -2
  11. kensho_kfinance-1.2.2/kfinance/tests/test_batch_requests.py +256 -0
  12. {kensho_kfinance-1.1.0a0 → kensho_kfinance-1.2.2}/kfinance/tests/test_fetch.py +0 -20
  13. {kensho_kfinance-1.1.0a0 → kensho_kfinance-1.2.2}/kfinance/version.py +2 -2
  14. {kensho_kfinance-1.1.0a0 → kensho_kfinance-1.2.2}/pyproject.toml +10 -6
  15. {kensho_kfinance-1.1.0a0 → kensho_kfinance-1.2.2}/.coveragerc +0 -0
  16. {kensho_kfinance-1.1.0a0 → kensho_kfinance-1.2.2}/.github/workflows/ci-lint.yml +0 -0
  17. {kensho_kfinance-1.1.0a0 → kensho_kfinance-1.2.2}/.github/workflows/ci-test.yml +0 -0
  18. {kensho_kfinance-1.1.0a0 → kensho_kfinance-1.2.2}/.github/workflows/python-publish.yml +0 -0
  19. {kensho_kfinance-1.1.0a0 → kensho_kfinance-1.2.2}/.gitignore +0 -0
  20. {kensho_kfinance-1.1.0a0 → kensho_kfinance-1.2.2}/AUTHORS.md +0 -0
  21. {kensho_kfinance-1.1.0a0 → kensho_kfinance-1.2.2}/CODE_OF_CONDUCT.md +0 -0
  22. {kensho_kfinance-1.1.0a0 → kensho_kfinance-1.2.2}/CONTRIBUTING.md +0 -0
  23. {kensho_kfinance-1.1.0a0 → kensho_kfinance-1.2.2}/LICENSE +0 -0
  24. {kensho_kfinance-1.1.0a0 → kensho_kfinance-1.2.2}/README.md +0 -0
  25. {kensho_kfinance-1.1.0a0 → kensho_kfinance-1.2.2}/docs/conf.py +0 -0
  26. {kensho_kfinance-1.1.0a0 → kensho_kfinance-1.2.2}/docs/index.rst +0 -0
  27. {kensho_kfinance-1.1.0a0 → kensho_kfinance-1.2.2}/docs/kfinance.rst +0 -0
  28. {kensho_kfinance-1.1.0a0 → kensho_kfinance-1.2.2}/docs/llm_tools.rst +0 -0
  29. {kensho_kfinance-1.1.0a0 → kensho_kfinance-1.2.2}/docs/requirements.txt +0 -0
  30. {kensho_kfinance-1.1.0a0 → kensho_kfinance-1.2.2}/docs/templates/apidoc/package.rst_t +0 -0
  31. {kensho_kfinance-1.1.0a0 → kensho_kfinance-1.2.2}/docs/templates/apidoc/toc.rst_t +0 -0
  32. {kensho_kfinance-1.1.0a0 → kensho_kfinance-1.2.2}/justfile +0 -0
  33. {kensho_kfinance-1.1.0a0 → kensho_kfinance-1.2.2}/kensho_kfinance.egg-info/dependency_links.txt +0 -0
  34. {kensho_kfinance-1.1.0a0 → kensho_kfinance-1.2.2}/kensho_kfinance.egg-info/top_level.txt +0 -0
  35. {kensho_kfinance-1.1.0a0 → kensho_kfinance-1.2.2}/kfinance/__init__.py +0 -0
  36. {kensho_kfinance-1.1.0a0 → kensho_kfinance-1.2.2}/kfinance/llm_tools.py +0 -0
  37. {kensho_kfinance-1.1.0a0 → kensho_kfinance-1.2.2}/kfinance/meta_classes.py +0 -0
  38. {kensho_kfinance-1.1.0a0 → kensho_kfinance-1.2.2}/kfinance/prompt.py +0 -0
  39. {kensho_kfinance-1.1.0a0 → kensho_kfinance-1.2.2}/kfinance/py.typed +0 -0
  40. {kensho_kfinance-1.1.0a0 → kensho_kfinance-1.2.2}/kfinance/server_thread.py +0 -0
  41. {kensho_kfinance-1.1.0a0 → kensho_kfinance-1.2.2}/kfinance/tests/__init__.py +0 -0
  42. {kensho_kfinance-1.1.0a0 → kensho_kfinance-1.2.2}/kfinance/tests/test_objects.py +0 -0
  43. {kensho_kfinance-1.1.0a0 → kensho_kfinance-1.2.2}/kfinance/tool_schemas.py +0 -0
  44. {kensho_kfinance-1.1.0a0 → kensho_kfinance-1.2.2}/scripts/copyright_line_check.sh +0 -0
  45. {kensho_kfinance-1.1.0a0 → kensho_kfinance-1.2.2}/scripts/lint.sh +0 -0
  46. {kensho_kfinance-1.1.0a0 → kensho_kfinance-1.2.2}/scripts/test.sh +0 -0
  47. {kensho_kfinance-1.1.0a0 → kensho_kfinance-1.2.2}/setup.cfg +0 -0
  48. {kensho_kfinance-1.1.0a0 → kensho_kfinance-1.2.2}/setup.py +0 -0
@@ -18,4 +18,6 @@ sphinx:
18
18
  # Dependencies required to build your docs
19
19
  python:
20
20
  install:
21
+ - method: pip
22
+ path: .
21
23
  - requirements: docs/requirements.txt
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kensho-kfinance
3
- Version: 1.1.0a0
3
+ Version: 1.2.2
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
@@ -12,21 +12,24 @@ Requires-Python: >=3.10
12
12
  Description-Content-Type: text/markdown
13
13
  License-File: LICENSE
14
14
  License-File: AUTHORS.md
15
- Requires-Dist: python-dateutil<2.9,>=2.8.2
16
- Requires-Dist: requests<3,>=2.22.0
17
- Requires-Dist: urllib3>=1.21.1
18
- Requires-Dist: pyjwt>=2.8.0
15
+ Requires-Dist: langchain-core>=0.3.15
19
16
  Requires-Dist: numpy>=1.22.4
20
17
  Requires-Dist: pandas>=2.0.0
21
- Requires-Dist: types-requests<3,>=2.22.0
22
18
  Requires-Dist: pillow>=10
23
- Requires-Dist: langchain-core>=0.3.15
19
+ Requires-Dist: pydantic<3,>=2.10.0
20
+ Requires-Dist: pyjwt>=2.8.0
21
+ Requires-Dist: python-dateutil<2.9,>=2.8.2
24
22
  Requires-Dist: strenum>=0.4.15
23
+ Requires-Dist: tabulate>=0.9.0
24
+ Requires-Dist: types-requests<3,>=2.22.0
25
+ Requires-Dist: requests<3,>=2.22.0
26
+ Requires-Dist: urllib3>=1.21.1
25
27
  Provides-Extra: dev
26
28
  Requires-Dist: coverage<8,>=7.6.10; extra == "dev"
27
29
  Requires-Dist: mypy<2,>=1.15.0; extra == "dev"
28
30
  Requires-Dist: pytest<7,>=6.1.2; extra == "dev"
29
31
  Requires-Dist: pytest-cov<7,>=6.0.0; extra == "dev"
32
+ Requires-Dist: requests_mock<1.2,>=1.1; extra == "dev"
30
33
  Requires-Dist: ruff<1,>=0.9.4; extra == "dev"
31
34
  Dynamic: license-file
32
35
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kensho-kfinance
3
- Version: 1.1.0a0
3
+ Version: 1.2.2
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
@@ -12,21 +12,24 @@ Requires-Python: >=3.10
12
12
  Description-Content-Type: text/markdown
13
13
  License-File: LICENSE
14
14
  License-File: AUTHORS.md
15
- Requires-Dist: python-dateutil<2.9,>=2.8.2
16
- Requires-Dist: requests<3,>=2.22.0
17
- Requires-Dist: urllib3>=1.21.1
18
- Requires-Dist: pyjwt>=2.8.0
15
+ Requires-Dist: langchain-core>=0.3.15
19
16
  Requires-Dist: numpy>=1.22.4
20
17
  Requires-Dist: pandas>=2.0.0
21
- Requires-Dist: types-requests<3,>=2.22.0
22
18
  Requires-Dist: pillow>=10
23
- Requires-Dist: langchain-core>=0.3.15
19
+ Requires-Dist: pydantic<3,>=2.10.0
20
+ Requires-Dist: pyjwt>=2.8.0
21
+ Requires-Dist: python-dateutil<2.9,>=2.8.2
24
22
  Requires-Dist: strenum>=0.4.15
23
+ Requires-Dist: tabulate>=0.9.0
24
+ Requires-Dist: types-requests<3,>=2.22.0
25
+ Requires-Dist: requests<3,>=2.22.0
26
+ Requires-Dist: urllib3>=1.21.1
25
27
  Provides-Extra: dev
26
28
  Requires-Dist: coverage<8,>=7.6.10; extra == "dev"
27
29
  Requires-Dist: mypy<2,>=1.15.0; extra == "dev"
28
30
  Requires-Dist: pytest<7,>=6.1.2; extra == "dev"
29
31
  Requires-Dist: pytest-cov<7,>=6.0.0; extra == "dev"
32
+ Requires-Dist: requests_mock<1.2,>=1.1; extra == "dev"
30
33
  Requires-Dist: ruff<1,>=0.9.4; extra == "dev"
31
34
  Dynamic: license-file
32
35
 
@@ -26,6 +26,7 @@ kensho_kfinance.egg-info/requires.txt
26
26
  kensho_kfinance.egg-info/top_level.txt
27
27
  kfinance/CHANGELOG.md
28
28
  kfinance/__init__.py
29
+ kfinance/batch_request_handling.py
29
30
  kfinance/constants.py
30
31
  kfinance/fetch.py
31
32
  kfinance/kfinance.py
@@ -37,6 +38,7 @@ kfinance/server_thread.py
37
38
  kfinance/tool_schemas.py
38
39
  kfinance/version.py
39
40
  kfinance/tests/__init__.py
41
+ kfinance/tests/test_batch_requests.py
40
42
  kfinance/tests/test_fetch.py
41
43
  kfinance/tests/test_objects.py
42
44
  scripts/copyright_line_check.sh
@@ -1,17 +1,20 @@
1
- python-dateutil<2.9,>=2.8.2
2
- requests<3,>=2.22.0
3
- urllib3>=1.21.1
4
- pyjwt>=2.8.0
1
+ langchain-core>=0.3.15
5
2
  numpy>=1.22.4
6
3
  pandas>=2.0.0
7
- types-requests<3,>=2.22.0
8
4
  pillow>=10
9
- langchain-core>=0.3.15
5
+ pydantic<3,>=2.10.0
6
+ pyjwt>=2.8.0
7
+ python-dateutil<2.9,>=2.8.2
10
8
  strenum>=0.4.15
9
+ tabulate>=0.9.0
10
+ types-requests<3,>=2.22.0
11
+ requests<3,>=2.22.0
12
+ urllib3>=1.21.1
11
13
 
12
14
  [dev]
13
15
  coverage<8,>=7.6.10
14
16
  mypy<2,>=1.15.0
15
17
  pytest<7,>=6.1.2
16
18
  pytest-cov<7,>=6.0.0
19
+ requests_mock<1.2,>=1.1
17
20
  ruff<1,>=0.9.4
@@ -1,4 +1,14 @@
1
1
  # Changelog
2
+
3
+ ## v1.2.2
4
+ - Add tabulate and pydantic as dependencies
5
+
6
+ ## v1.2.1
7
+ - Add fetch for industry_code, industry_classification and gics_code.
8
+
9
+ ## v1.2.0
10
+ - Add batch requests for iterable classes
11
+
2
12
  ## v1.1.0
3
13
  - Add market cap, TEV, and shares outstanding
4
14
 
@@ -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
@@ -76,6 +76,17 @@ class LatestPeriods(TypedDict):
76
76
  now: CurrentPeriod
77
77
 
78
78
 
79
+ class IndustryClassification(StrEnum):
80
+ sic = "sic"
81
+ naics = "naics"
82
+ nace = "nace"
83
+ anzsic = "anzsic"
84
+ spcapiqetf = "spcapiqetf"
85
+ spratings = "spratings"
86
+ gics = "gics"
87
+ simple = "simple"
88
+
89
+
79
90
  # all of these values must be lower case keys
80
91
  LINE_ITEMS: list[LineItemType] = [
81
92
  {
@@ -1,10 +1,13 @@
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
6
9
 
7
- from .constants import BusinessRelationshipType, IdentificationTriple
10
+ from .constants import BusinessRelationshipType, IdentificationTriple, IndustryClassification
8
11
 
9
12
 
10
13
  # version.py gets autogenerated by setuptools-scm and is not available
@@ -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()
@@ -305,26 +368,6 @@ class KFinanceApiClient:
305
368
  country_iso_code=country_iso_code, state_iso_code=state_iso_code, fetch_ticker=False
306
369
  )
307
370
 
308
- def fetch_simple_industry_groups(
309
- self, simple_industry: str, fetch_ticker: bool = True
310
- ) -> dict[str, list]:
311
- """Fetch simple industry groups"""
312
- url = f"{self.url_base}{'ticker_groups' if fetch_ticker else 'company_groups'}/industry/simple/{simple_industry}"
313
- return self.fetch(url)
314
-
315
- def fetch_ticker_simple_industry_groups(
316
- self, simple_industry: str
317
- ) -> dict[str, list[IdentificationTriple]]:
318
- """Fetch ticker simple industry groups"""
319
- return self.fetch_simple_industry_groups(simple_industry=simple_industry, fetch_ticker=True)
320
-
321
- def fetch_company_simple_industry_groups(self, simple_industry: str) -> dict[str, list[int]]:
322
- """Fetch company simple industry groups"""
323
- return self.fetch_simple_industry_groups(
324
- simple_industry=simple_industry,
325
- fetch_ticker=False,
326
- )
327
-
328
371
  def fetch_exchange_groups(
329
372
  self, exchange_code: str, fetch_ticker: bool = True
330
373
  ) -> dict[str, list]:
@@ -393,3 +436,56 @@ class KFinanceApiClient:
393
436
  """
394
437
  url = f"{self.url_base}relationship/{company_id}/{relationship_type}"
395
438
  return self.fetch(url)
439
+
440
+ def fetch_from_industry_code(
441
+ self,
442
+ industry_code: str,
443
+ industry_classification: IndustryClassification,
444
+ fetch_ticker: bool = True,
445
+ ) -> dict[str, list]:
446
+ """Fetches a list of companies or identification triples that are classified in the given industry_code and industry_classification."""
447
+
448
+ url = f"{self.url_base}{'ticker_groups' if fetch_ticker else 'company_groups'}/industry/{industry_classification}/{industry_code}"
449
+ return self.fetch(url)
450
+
451
+ def fetch_ticker_from_industry_code(
452
+ self,
453
+ industry_code: str,
454
+ industry_classification: IndustryClassification,
455
+ ) -> dict[str, list[IdentificationTriple]]:
456
+ """Fetches a list of identification triples that are classified in the given industry_code and industry_classification.
457
+
458
+ Returns a dictionary of shape {"tickers": List[{“company_id”: <company_id>, “security_id”: <security_id>, “trading_item_id”: <trading_item_id>}]}.
459
+ :param industry_code: The industry_code to filter on. The industry_code is a string corresponding to the Industry classifications ontology.
460
+ :type industry_code: str
461
+ :param industry_classification: The type of industry_classification to filter on.
462
+ :type industry_classification: IndustryClassification
463
+ :return: A dictionary containing the list of identification triple [company_id, security_id, trading_item_id] that are classified in the given industry_code and industry_classification.
464
+ :rtype: dict[str, list[IdentificationTriple]]
465
+ """
466
+ return self.fetch_from_industry_code(
467
+ industry_code=industry_code,
468
+ industry_classification=industry_classification,
469
+ fetch_ticker=True,
470
+ )
471
+
472
+ def fetch_company_from_industry_code(
473
+ self,
474
+ industry_code: str,
475
+ industry_classification: IndustryClassification,
476
+ ) -> dict[str, list[int]]:
477
+ """Fetches a list of companies that are classified in the given industry_code and industry_classification.
478
+
479
+ Returns a dictionary of shape {"companies": List[<company_id>]}.
480
+ :param industry_code: The industry_code to filter on. The industry_code is a string corresponding to the Industry classifications ontology.
481
+ :type industry_code: str
482
+ :param industry_classification: The type of industry_classification to filter on.
483
+ :type industry_classification: IndustryClassification
484
+ :return: A dictionary containing the list of companies that are classified in the given industry_code and industry_classification.
485
+ :rtype: dict[str, list[int]]
486
+ """
487
+ return self.fetch_from_industry_code(
488
+ industry_code=industry_code,
489
+ industry_classification=industry_classification,
490
+ fetch_ticker=False,
491
+ )
@@ -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 CompanyFunctionsMetaClass, DelegatedCompanyFunctionsMetaClass
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 TickerSet class for representing a set of Tickers"""
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
 
@@ -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)
@@ -157,26 +157,6 @@ class TestFetchItem(TestCase):
157
157
  )
158
158
  self.kfinance_api_client.fetch.assert_called_with(expected_fetch_url)
159
159
 
160
- def test_fetch_ticker_industry_simple_groups(self) -> None:
161
- simple_industry = "media"
162
- expected_fetch_url = (
163
- f"{self.kfinance_api_client.url_base}ticker_groups/industry/simple/{simple_industry}"
164
- )
165
- self.kfinance_api_client.fetch_ticker_simple_industry_groups(
166
- simple_industry=simple_industry
167
- )
168
- self.kfinance_api_client.fetch.assert_called_once_with(expected_fetch_url)
169
-
170
- def test_fetch_company_industry_simple_groups(self) -> None:
171
- simple_industry = "media"
172
- expected_fetch_url = (
173
- f"{self.kfinance_api_client.url_base}company_groups/industry/simple/{simple_industry}"
174
- )
175
- self.kfinance_api_client.fetch_company_simple_industry_groups(
176
- simple_industry=simple_industry
177
- )
178
- self.kfinance_api_client.fetch.assert_called_once_with(expected_fetch_url)
179
-
180
160
  def test_fetch_ticker_exchange_groups(self) -> None:
181
161
  exchange_code = "NYSE"
182
162
  expected_fetch_url = (
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '1.1.0a0'
21
- __version_tuple__ = version_tuple = (1, 1, 0)
20
+ __version__ = version = '1.2.2'
21
+ __version_tuple__ = version_tuple = (1, 2, 2)
@@ -16,17 +16,20 @@ classifiers = [
16
16
  "Operating System :: OS Independent",
17
17
  ]
18
18
 
19
+
19
20
  dependencies = [
20
- "python-dateutil>=2.8.2,<2.9",
21
- "requests>=2.22.0,<3",
22
- "urllib3>=1.21.1",
23
- "pyjwt>=2.8.0",
21
+ "langchain-core>=0.3.15",
24
22
  "numpy>=1.22.4",
25
23
  "pandas>=2.0.0",
26
- "types-requests>=2.22.0,<3",
27
24
  "pillow>=10",
28
- "langchain-core>=0.3.15",
25
+ "pydantic>=2.10.0,<3",
26
+ "pyjwt>=2.8.0",
27
+ "python-dateutil>=2.8.2,<2.9",
29
28
  "strenum>=0.4.15",
29
+ "tabulate>=0.9.0", # required for turning dataframes into markdown
30
+ "types-requests>=2.22.0,<3",
31
+ "requests>=2.22.0,<3",
32
+ "urllib3>=1.21.1",
30
33
  ]
31
34
 
32
35
  [project.optional-dependencies]
@@ -35,6 +38,7 @@ dev = [
35
38
  "mypy>=1.15.0,<2",
36
39
  "pytest>=6.1.2,<7",
37
40
  "pytest-cov>=6.0.0,<7",
41
+ "requests_mock>=1.1,<1.2",
38
42
  "ruff>=0.9.4,<1",
39
43
  ]
40
44