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 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)