dojosdk 0.1.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.
- dojo/__init__.py +38 -0
- dojo/_compat.py +38 -0
- dojo/_exceptions.py +103 -0
- dojo/client/async_client.py +239 -0
- dojo/client/base.py +403 -0
- dojo/client/sync.py +235 -0
- dojo/datasource/__init__.py +15 -0
- dojo/datasource/api.py +10 -0
- dojo/datasource/base.py +18 -0
- dojo/datasource/config.py +37 -0
- dojo/datasource/huggingface.py +430 -0
- dojo/datasource/registry.py +158 -0
- dojo/datasource/upload.py +63 -0
- dojo/logging.py +14 -0
- dojo/resources/base.py +70 -0
- dojo/resources/benchmark.py +204 -0
- dojo/resources/cache.py +37 -0
- dojo/resources/concepts.py +157 -0
- dojo/resources/forex.py +143 -0
- dojo/resources/macro.py +409 -0
- dojo/resources/market_data.py +952 -0
- dojo/resources/news.py +462 -0
- dojo/resources/sectors.py +378 -0
- dojo/resources/stocks.py +1389 -0
- dojo/resources/strategy.py +104 -0
- dojo/resources/user.py +234 -0
- dojo/types/__init__.py +113 -0
- dojo/types/models.py +409 -0
- dojo/utils/parquet_utils.py +53 -0
- dojosdk-0.1.0.dist-info/METADATA +59 -0
- dojosdk-0.1.0.dist-info/RECORD +34 -0
- dojosdk-0.1.0.dist-info/WHEEL +5 -0
- dojosdk-0.1.0.dist-info/licenses/LICENSE +201 -0
- dojosdk-0.1.0.dist-info/top_level.txt +1 -0
dojo/__init__.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from dojo.client.sync import Dojo as Dojo
|
|
2
|
+
from dojo.client.async_client import AsyncDojo as AsyncDojo
|
|
3
|
+
|
|
4
|
+
from dojo._exceptions import (
|
|
5
|
+
DojoError as DojoError,
|
|
6
|
+
APIError as APIError,
|
|
7
|
+
APIConnectionError as APIConnectionError,
|
|
8
|
+
APITimeoutError as APITimeoutError,
|
|
9
|
+
APIResponseValidationError as APIResponseValidationError,
|
|
10
|
+
APIStatusError as APIStatusError,
|
|
11
|
+
BadRequestError as BadRequestError,
|
|
12
|
+
AuthenticationError as AuthenticationError,
|
|
13
|
+
PermissionDeniedError as PermissionDeniedError,
|
|
14
|
+
NotFoundError as NotFoundError,
|
|
15
|
+
ConflictError as ConflictError,
|
|
16
|
+
UnprocessableEntityError as UnprocessableEntityError,
|
|
17
|
+
RateLimitError as RateLimitError,
|
|
18
|
+
InternalServerError as InternalServerError,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"Dojo",
|
|
23
|
+
"AsyncDojo",
|
|
24
|
+
"DojoError",
|
|
25
|
+
"APIError",
|
|
26
|
+
"APIConnectionError",
|
|
27
|
+
"APITimeoutError",
|
|
28
|
+
"APIResponseValidationError",
|
|
29
|
+
"APIStatusError",
|
|
30
|
+
"BadRequestError",
|
|
31
|
+
"AuthenticationError",
|
|
32
|
+
"PermissionDeniedError",
|
|
33
|
+
"NotFoundError",
|
|
34
|
+
"ConflictError",
|
|
35
|
+
"UnprocessableEntityError",
|
|
36
|
+
"RateLimitError",
|
|
37
|
+
"InternalServerError",
|
|
38
|
+
]
|
dojo/_compat.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from typing import Any, Type, TypeVar
|
|
2
|
+
|
|
3
|
+
try:
|
|
4
|
+
# Pydantic v2
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
|
+
PYDANTIC_V1 = False
|
|
8
|
+
except ImportError:
|
|
9
|
+
# Pydantic v1 fallback
|
|
10
|
+
from pydantic import BaseModel # type: ignore[no-redef]
|
|
11
|
+
|
|
12
|
+
PYDANTIC_V1 = True
|
|
13
|
+
|
|
14
|
+
_T = TypeVar("_T", bound=BaseModel)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def model_dump(model: BaseModel, **kwargs: Any) -> dict[str, Any]:
|
|
18
|
+
"""Dump model to a dict, compatible across Pydantic v1 and v2."""
|
|
19
|
+
if PYDANTIC_V1:
|
|
20
|
+
return getattr(model, "dict")(**kwargs) # type: ignore
|
|
21
|
+
else:
|
|
22
|
+
return getattr(model, "model_dump")(**kwargs)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def model_validate(model_cls: Type[_T], obj: Any) -> _T:
|
|
26
|
+
"""Validate and construct a model from a dict/object, compatible across Pydantic v1 and v2."""
|
|
27
|
+
if PYDANTIC_V1:
|
|
28
|
+
return getattr(model_cls, "parse_obj")(obj) # type: ignore
|
|
29
|
+
else:
|
|
30
|
+
return getattr(model_cls, "model_validate")(obj)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def model_construct(model_cls: Type[_T], **kwargs: Any) -> _T:
|
|
34
|
+
"""Construct a model without validation, compatible across Pydantic v1 and v2."""
|
|
35
|
+
if PYDANTIC_V1:
|
|
36
|
+
return getattr(model_cls, "construct")(**kwargs) # type: ignore
|
|
37
|
+
else:
|
|
38
|
+
return getattr(model_cls, "model_construct")(**kwargs)
|
dojo/_exceptions.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import httpx
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class DojoError(Exception):
|
|
6
|
+
"""Base exception for all DojoSDK errors."""
|
|
7
|
+
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class APIError(DojoError):
|
|
12
|
+
"""Base class for errors related to interacting with the Dojo API."""
|
|
13
|
+
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class APIConnectionError(APIError):
|
|
18
|
+
"""Raised when the SDK fails to connect to the Dojo API service."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, message: str = "Connection error.", *, request: httpx.Request | None = None) -> None:
|
|
21
|
+
super().__init__(message)
|
|
22
|
+
self.request = request
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class APITimeoutError(APIConnectionError):
|
|
26
|
+
"""Raised when a request to the Dojo API times out."""
|
|
27
|
+
|
|
28
|
+
def __init__(self, message: str = "Request timed out.", *, request: httpx.Request | None = None) -> None:
|
|
29
|
+
super().__init__(message, request=request)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class APIResponseValidationError(APIError):
|
|
33
|
+
"""Raised when the API response does not conform to the expected Pydantic schema."""
|
|
34
|
+
|
|
35
|
+
def __init__(self, response: httpx.Response, body: Any, message: str = "API response did not match expected schema.") -> None:
|
|
36
|
+
super().__init__(message)
|
|
37
|
+
self.response = response
|
|
38
|
+
self.status_code = response.status_code
|
|
39
|
+
self.body = body
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class APIStatusError(APIError):
|
|
43
|
+
"""Raised when the Dojo API returns a non-2xx status code."""
|
|
44
|
+
|
|
45
|
+
def __init__(self, message: str, *, response: httpx.Response, body: Any) -> None:
|
|
46
|
+
super().__init__(message)
|
|
47
|
+
self.response = response
|
|
48
|
+
self.status_code = response.status_code
|
|
49
|
+
self.body = body
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class BadRequestError(APIStatusError):
|
|
53
|
+
"""HTTP 400 Bad Request."""
|
|
54
|
+
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class AuthenticationError(APIStatusError):
|
|
59
|
+
"""HTTP 401 Unauthorized."""
|
|
60
|
+
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class PermissionDeniedError(APIStatusError):
|
|
65
|
+
"""HTTP 403 Forbidden."""
|
|
66
|
+
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class NotFoundError(APIStatusError):
|
|
71
|
+
"""HTTP 404 Not Found."""
|
|
72
|
+
|
|
73
|
+
pass
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class ConflictError(APIStatusError):
|
|
77
|
+
"""HTTP 409 Conflict."""
|
|
78
|
+
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class UnprocessableEntityError(APIStatusError):
|
|
83
|
+
"""HTTP 422 Unprocessable Entity."""
|
|
84
|
+
|
|
85
|
+
pass
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class RateLimitError(APIStatusError):
|
|
89
|
+
"""HTTP 429 Rate Limit Exceeded."""
|
|
90
|
+
|
|
91
|
+
pass
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class InternalServerError(APIStatusError):
|
|
95
|
+
"""HTTP 500+ Internal Server Error."""
|
|
96
|
+
|
|
97
|
+
pass
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class OfflineDataNotAvailableError(DojoError):
|
|
101
|
+
"""Offline data is not available for the requested endpoint or parameters."""
|
|
102
|
+
|
|
103
|
+
pass
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import Mapping, Union, TYPE_CHECKING
|
|
5
|
+
from functools import cached_property
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from dojo.resources.stocks import AsyncStocks
|
|
10
|
+
from dojo.resources.market_data import AsyncMarketData, AsyncIndices, AsyncInstruments
|
|
11
|
+
from dojo.resources.macro import AsyncMacro
|
|
12
|
+
from dojo.resources.benchmark import AsyncBenchmark
|
|
13
|
+
from dojo.resources.concepts import AsyncConcepts
|
|
14
|
+
from dojo.resources.news import AsyncNews
|
|
15
|
+
from dojo.resources.forex import AsyncForex
|
|
16
|
+
from dojo.resources.user import AsyncUser
|
|
17
|
+
from dojo.resources.sectors import AsyncSectors
|
|
18
|
+
from dojo.resources.strategy import AsyncStrategy
|
|
19
|
+
from dojo.resources.cache import AsyncCache
|
|
20
|
+
|
|
21
|
+
from dojo.client.base import AsyncAPIClient
|
|
22
|
+
from dojo._exceptions import DojoError
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class AsyncDojo(AsyncAPIClient):
|
|
26
|
+
"""Asynchronous Dojo API Client."""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
*,
|
|
31
|
+
api_key: str | None = None,
|
|
32
|
+
base_url: str | None = None,
|
|
33
|
+
timeout: Union[float, httpx.Timeout] = 300.0,
|
|
34
|
+
max_retries: int = 1,
|
|
35
|
+
http_client: httpx.AsyncClient | None = None,
|
|
36
|
+
default_headers: Mapping[str, str] | None = None,
|
|
37
|
+
default_query: Mapping[str, object] | None = None,
|
|
38
|
+
return_raw_data: bool = True,
|
|
39
|
+
) -> None:
|
|
40
|
+
"""Constructs a new asynchronous Dojo client instance.
|
|
41
|
+
|
|
42
|
+
Parameters
|
|
43
|
+
----------
|
|
44
|
+
api_key : str, optional
|
|
45
|
+
The token used to authenticate requests. If not provided, it is loaded
|
|
46
|
+
from the DOJO_API_KEY environment variable.
|
|
47
|
+
base_url : str, optional
|
|
48
|
+
The base URL of the Dojo API. Defaults to https://api.flowhale.ai.
|
|
49
|
+
timeout : float or httpx.Timeout, default 300.0
|
|
50
|
+
Maximum time (in seconds) to wait for an HTTP request before timing out.
|
|
51
|
+
max_retries : int, default 1
|
|
52
|
+
Number of times to retry failed requests on connection errors/rate limits.
|
|
53
|
+
http_client : httpx.AsyncClient, optional
|
|
54
|
+
A custom httpx AsyncClient instance to use for sending requests.
|
|
55
|
+
default_headers : dict, optional
|
|
56
|
+
A dictionary of custom HTTP headers to send with every request.
|
|
57
|
+
default_query : dict, optional
|
|
58
|
+
A dictionary of custom query parameters to send with every request.
|
|
59
|
+
return_raw_data : bool, default True
|
|
60
|
+
If True, API responses are returned as native Python dictionaries/lists,
|
|
61
|
+
skipping Pydantic model validation.
|
|
62
|
+
"""
|
|
63
|
+
from dojo.datasource.config import is_online, HFConfig
|
|
64
|
+
from dojo.datasource.huggingface import HuggingFaceKlineDataSource
|
|
65
|
+
|
|
66
|
+
self._online = is_online()
|
|
67
|
+
|
|
68
|
+
api_key = api_key or os.environ.get("DOJO_API_KEY")
|
|
69
|
+
if self._online and not api_key:
|
|
70
|
+
raise DojoError("Missing authentication credentials. Please pass an `api_key` or set " "the `DOJO_API_KEY` environment variable.")
|
|
71
|
+
self.api_key = api_key
|
|
72
|
+
|
|
73
|
+
self._data_source = None
|
|
74
|
+
if not self._online:
|
|
75
|
+
self._data_source = HuggingFaceKlineDataSource(HFConfig.from_env())
|
|
76
|
+
|
|
77
|
+
base_url = base_url or os.environ.get("DOJO_BASE_URL") or "https://api.flowhale.ai"
|
|
78
|
+
|
|
79
|
+
if http_client is None:
|
|
80
|
+
# Default pool limits
|
|
81
|
+
limits = httpx.Limits(max_connections=100, max_keepalive_connections=20)
|
|
82
|
+
http_client = httpx.AsyncClient(
|
|
83
|
+
limits=limits,
|
|
84
|
+
timeout=timeout,
|
|
85
|
+
follow_redirects=True,
|
|
86
|
+
)
|
|
87
|
+
self._client = http_client
|
|
88
|
+
|
|
89
|
+
super().__init__(
|
|
90
|
+
base_url=base_url,
|
|
91
|
+
max_retries=max_retries,
|
|
92
|
+
custom_headers=default_headers,
|
|
93
|
+
custom_query=default_query,
|
|
94
|
+
return_raw_data=return_raw_data,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def auth_headers(self) -> dict[str, str]:
|
|
99
|
+
"""Returns the authorization headers containing the Bearer token."""
|
|
100
|
+
return {"Authorization": f"Bearer {self.api_key}"}
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def default_headers(self) -> dict[str, str]:
|
|
104
|
+
"""Returns the default headers containing auth and telemetry details."""
|
|
105
|
+
headers = super().default_headers
|
|
106
|
+
headers.update(self.auth_headers)
|
|
107
|
+
return headers
|
|
108
|
+
|
|
109
|
+
def start_background_sync(self, interval_days: int = 1) -> None:
|
|
110
|
+
"""Starts a background daemon thread that refreshes offline datasets periodically.
|
|
111
|
+
|
|
112
|
+
Parameters
|
|
113
|
+
----------
|
|
114
|
+
interval_days : int
|
|
115
|
+
The number of days between each refresh. Defaults to 1.
|
|
116
|
+
"""
|
|
117
|
+
if self._data_source is not None and hasattr(self._data_source, "start_background_sync"):
|
|
118
|
+
self._data_source.start_background_sync(interval_days=interval_days)
|
|
119
|
+
|
|
120
|
+
def stop_background_sync(self) -> None:
|
|
121
|
+
"""Stops the background dataset refresh thread."""
|
|
122
|
+
if self._data_source is not None and hasattr(self._data_source, "stop_background_sync"):
|
|
123
|
+
self._data_source.stop_background_sync()
|
|
124
|
+
|
|
125
|
+
async def preload_offline_data(self, paths: list[str] | None = None) -> None:
|
|
126
|
+
"""Preload specific offline data resources into memory to avoid latency on first request."""
|
|
127
|
+
if self._data_source is not None and hasattr(self._data_source, "preload"):
|
|
128
|
+
import asyncio
|
|
129
|
+
|
|
130
|
+
if paths is None:
|
|
131
|
+
from dojo.datasource.registry import HF_REGISTRY
|
|
132
|
+
|
|
133
|
+
paths = list(HF_REGISTRY.keys())
|
|
134
|
+
await asyncio.to_thread(self._data_source.preload, paths)
|
|
135
|
+
|
|
136
|
+
async def upload_dataset(self, dataset_name: str, local_folder: str, token: str | None = None) -> None:
|
|
137
|
+
"""Uploads a local folder as a dataset to HuggingFace Hub using a background thread."""
|
|
138
|
+
import asyncio
|
|
139
|
+
from dojo.datasource.upload import upload_dataset as sync_upload_dataset
|
|
140
|
+
|
|
141
|
+
await asyncio.to_thread(sync_upload_dataset, dataset_name, local_folder, token)
|
|
142
|
+
|
|
143
|
+
async def download_dataset(self, dataset_name: str, local_folder: str, token: str | None = None) -> None:
|
|
144
|
+
"""Downloads a dataset from HuggingFace Hub to a local folder using a background thread."""
|
|
145
|
+
import asyncio
|
|
146
|
+
from dojo.datasource.upload import download_dataset as sync_download_dataset
|
|
147
|
+
|
|
148
|
+
await asyncio.to_thread(sync_download_dataset, dataset_name, local_folder, token)
|
|
149
|
+
|
|
150
|
+
@cached_property
|
|
151
|
+
def stocks(self) -> AsyncStocks:
|
|
152
|
+
"""Access the stocks resource namespace for stock quotes, fundamentals, news, and metrics."""
|
|
153
|
+
from dojo.resources.stocks import AsyncStocks
|
|
154
|
+
|
|
155
|
+
return AsyncStocks(self)
|
|
156
|
+
|
|
157
|
+
@cached_property
|
|
158
|
+
def market_data(self) -> AsyncMarketData:
|
|
159
|
+
"""Access the market_data resource namespace for public real-time tickers, order books, and klines."""
|
|
160
|
+
from dojo.resources.market_data import AsyncMarketData
|
|
161
|
+
|
|
162
|
+
return AsyncMarketData(self)
|
|
163
|
+
|
|
164
|
+
@cached_property
|
|
165
|
+
def macro(self) -> AsyncMacro:
|
|
166
|
+
"""Access the macro resource namespace for macroeconomic data, news, and metrics."""
|
|
167
|
+
from dojo.resources.macro import AsyncMacro
|
|
168
|
+
|
|
169
|
+
return AsyncMacro(self)
|
|
170
|
+
|
|
171
|
+
@cached_property
|
|
172
|
+
def benchmark(self) -> AsyncBenchmark:
|
|
173
|
+
"""Access the benchmark resource namespace for index benchmarks and index klines."""
|
|
174
|
+
from dojo.resources.benchmark import AsyncBenchmark
|
|
175
|
+
|
|
176
|
+
return AsyncBenchmark(self)
|
|
177
|
+
|
|
178
|
+
@cached_property
|
|
179
|
+
def concepts(self) -> AsyncConcepts:
|
|
180
|
+
"""Access the concepts resource namespace for concept themes and constituents."""
|
|
181
|
+
from dojo.resources.concepts import AsyncConcepts
|
|
182
|
+
|
|
183
|
+
return AsyncConcepts(self)
|
|
184
|
+
|
|
185
|
+
@cached_property
|
|
186
|
+
def news(self) -> AsyncNews:
|
|
187
|
+
"""Access the news resource namespace for general news, news sentiment scores, and events."""
|
|
188
|
+
from dojo.resources.news import AsyncNews
|
|
189
|
+
|
|
190
|
+
return AsyncNews(self)
|
|
191
|
+
|
|
192
|
+
@cached_property
|
|
193
|
+
def forex(self) -> AsyncForex:
|
|
194
|
+
"""Access the forex resource namespace for forex quotes, klines, and symbol lists."""
|
|
195
|
+
from dojo.resources.forex import AsyncForex
|
|
196
|
+
|
|
197
|
+
return AsyncForex(self)
|
|
198
|
+
|
|
199
|
+
@cached_property
|
|
200
|
+
def user(self) -> AsyncUser:
|
|
201
|
+
"""Access the user resource namespace for user traits management and analytics."""
|
|
202
|
+
from dojo.resources.user import AsyncUser
|
|
203
|
+
|
|
204
|
+
return AsyncUser(self)
|
|
205
|
+
|
|
206
|
+
@cached_property
|
|
207
|
+
def sectors(self) -> AsyncSectors:
|
|
208
|
+
"""Access the sectors resource namespace for sector/industry listings and metrics."""
|
|
209
|
+
from dojo.resources.sectors import AsyncSectors
|
|
210
|
+
|
|
211
|
+
return AsyncSectors(self)
|
|
212
|
+
|
|
213
|
+
@cached_property
|
|
214
|
+
def strategy(self) -> AsyncStrategy:
|
|
215
|
+
"""Access the strategy resource namespace for classic strategy demos and backtest results."""
|
|
216
|
+
from dojo.resources.strategy import AsyncStrategy
|
|
217
|
+
|
|
218
|
+
return AsyncStrategy(self)
|
|
219
|
+
|
|
220
|
+
@cached_property
|
|
221
|
+
def cache(self) -> AsyncCache:
|
|
222
|
+
"""Access the cache resource namespace for resetting/clearing cached data."""
|
|
223
|
+
from dojo.resources.cache import AsyncCache
|
|
224
|
+
|
|
225
|
+
return AsyncCache(self)
|
|
226
|
+
|
|
227
|
+
@cached_property
|
|
228
|
+
def indices(self) -> AsyncIndices:
|
|
229
|
+
"""Access the indices resource namespace for standard index list retrieval."""
|
|
230
|
+
from dojo.resources.market_data import AsyncIndices
|
|
231
|
+
|
|
232
|
+
return AsyncIndices(self)
|
|
233
|
+
|
|
234
|
+
@cached_property
|
|
235
|
+
def instruments(self) -> AsyncInstruments:
|
|
236
|
+
"""Access the instruments resource namespace for exchange tradable instruments listing."""
|
|
237
|
+
from dojo.resources.market_data import AsyncInstruments
|
|
238
|
+
|
|
239
|
+
return AsyncInstruments(self)
|