crypticorn 2.15.0__py3-none-any.whl → 2.17.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.
- crypticorn/__init__.py +2 -2
- crypticorn/auth/client/api/admin_api.py +397 -13
- crypticorn/auth/client/api/auth_api.py +3610 -341
- crypticorn/auth/client/api/service_api.py +249 -7
- crypticorn/auth/client/api/user_api.py +2295 -179
- crypticorn/auth/client/api/wallet_api.py +1468 -81
- crypticorn/auth/client/configuration.py +2 -2
- crypticorn/auth/client/models/create_api_key_request.py +2 -1
- crypticorn/auth/client/models/get_api_keys200_response_inner.py +2 -1
- crypticorn/auth/client/rest.py +23 -4
- crypticorn/auth/main.py +8 -5
- crypticorn/cli/init.py +1 -1
- crypticorn/cli/templates/.env.docker.temp +3 -0
- crypticorn/cli/templates/.env.example.temp +4 -0
- crypticorn/cli/templates/Dockerfile +5 -2
- crypticorn/client.py +226 -59
- crypticorn/common/__init__.py +1 -0
- crypticorn/common/auth.py +45 -14
- crypticorn/common/decorators.py +1 -2
- crypticorn/common/enums.py +0 -2
- crypticorn/common/errors.py +10 -0
- crypticorn/common/metrics.py +30 -0
- crypticorn/common/middleware.py +94 -1
- crypticorn/common/pagination.py +252 -18
- crypticorn/common/router/admin_router.py +2 -2
- crypticorn/common/router/status_router.py +40 -2
- crypticorn/common/scopes.py +2 -0
- crypticorn/common/warnings.py +8 -0
- crypticorn/dex/__init__.py +6 -0
- crypticorn/dex/client/__init__.py +49 -0
- crypticorn/dex/client/api/__init__.py +6 -0
- crypticorn/dex/client/api/admin_api.py +2986 -0
- crypticorn/dex/client/api/signals_api.py +1798 -0
- crypticorn/dex/client/api/status_api.py +892 -0
- crypticorn/dex/client/api_client.py +758 -0
- crypticorn/dex/client/api_response.py +20 -0
- crypticorn/dex/client/configuration.py +620 -0
- crypticorn/dex/client/exceptions.py +220 -0
- crypticorn/dex/client/models/__init__.py +30 -0
- crypticorn/dex/client/models/api_error_identifier.py +121 -0
- crypticorn/dex/client/models/api_error_level.py +37 -0
- crypticorn/dex/client/models/api_error_type.py +37 -0
- crypticorn/dex/client/models/exception_detail.py +117 -0
- crypticorn/dex/client/models/log_level.py +38 -0
- crypticorn/dex/client/models/paginated_response_signal_with_token.py +134 -0
- crypticorn/dex/client/models/risk.py +86 -0
- crypticorn/dex/client/models/signal_overview_stats.py +158 -0
- crypticorn/dex/client/models/signal_volume.py +84 -0
- crypticorn/dex/client/models/signal_with_token.py +163 -0
- crypticorn/dex/client/models/token_data.py +127 -0
- crypticorn/dex/client/models/token_detail.py +116 -0
- crypticorn/dex/client/py.typed +0 -0
- crypticorn/dex/client/rest.py +217 -0
- crypticorn/dex/main.py +1 -0
- crypticorn/hive/client/api/admin_api.py +1173 -47
- crypticorn/hive/client/api/data_api.py +499 -17
- crypticorn/hive/client/api/models_api.py +1595 -87
- crypticorn/hive/client/api/status_api.py +397 -16
- crypticorn/hive/client/api_client.py +0 -5
- crypticorn/hive/client/models/api_error_identifier.py +1 -1
- crypticorn/hive/client/models/coin_info.py +1 -1
- crypticorn/hive/client/models/exception_detail.py +1 -1
- crypticorn/hive/client/models/target_info.py +1 -1
- crypticorn/hive/client/rest.py +23 -4
- crypticorn/hive/main.py +99 -25
- crypticorn/hive/utils.py +2 -2
- crypticorn/klines/client/api/admin_api.py +1173 -47
- crypticorn/klines/client/api/change_in_timeframe_api.py +269 -11
- crypticorn/klines/client/api/funding_rates_api.py +315 -11
- crypticorn/klines/client/api/ohlcv_data_api.py +390 -11
- crypticorn/klines/client/api/status_api.py +397 -16
- crypticorn/klines/client/api/symbols_api.py +216 -11
- crypticorn/klines/client/api/udf_api.py +1268 -51
- crypticorn/klines/client/api_client.py +0 -5
- crypticorn/klines/client/models/api_error_identifier.py +3 -1
- crypticorn/klines/client/models/exception_detail.py +1 -1
- crypticorn/klines/client/models/ohlcv.py +1 -1
- crypticorn/klines/client/models/symbol_group.py +1 -1
- crypticorn/klines/client/models/udf_config.py +1 -1
- crypticorn/klines/client/rest.py +23 -4
- crypticorn/klines/main.py +89 -12
- crypticorn/metrics/client/api/admin_api.py +1173 -47
- crypticorn/metrics/client/api/exchanges_api.py +1370 -145
- crypticorn/metrics/client/api/indicators_api.py +622 -17
- crypticorn/metrics/client/api/logs_api.py +296 -11
- crypticorn/metrics/client/api/marketcap_api.py +1207 -67
- crypticorn/metrics/client/api/markets_api.py +343 -11
- crypticorn/metrics/client/api/quote_currencies_api.py +228 -11
- crypticorn/metrics/client/api/status_api.py +397 -16
- crypticorn/metrics/client/api/tokens_api.py +382 -15
- crypticorn/metrics/client/api_client.py +0 -5
- crypticorn/metrics/client/configuration.py +4 -2
- crypticorn/metrics/client/models/exception_detail.py +1 -1
- crypticorn/metrics/client/models/exchange_mapping.py +1 -1
- crypticorn/metrics/client/models/marketcap_ranking.py +1 -1
- crypticorn/metrics/client/models/marketcap_symbol_ranking.py +1 -1
- crypticorn/metrics/client/models/ohlcv.py +1 -1
- crypticorn/metrics/client/rest.py +23 -4
- crypticorn/metrics/main.py +113 -19
- crypticorn/pay/client/api/admin_api.py +1585 -57
- crypticorn/pay/client/api/now_payments_api.py +961 -39
- crypticorn/pay/client/api/payments_api.py +562 -17
- crypticorn/pay/client/api/products_api.py +880 -30
- crypticorn/pay/client/api/status_api.py +397 -16
- crypticorn/pay/client/api_client.py +0 -5
- crypticorn/pay/client/configuration.py +2 -2
- crypticorn/pay/client/models/api_error_identifier.py +7 -7
- crypticorn/pay/client/models/exception_detail.py +1 -1
- crypticorn/pay/client/models/now_create_invoice_req.py +1 -1
- crypticorn/pay/client/models/now_create_invoice_res.py +1 -1
- crypticorn/pay/client/models/product.py +1 -1
- crypticorn/pay/client/models/product_create.py +1 -1
- crypticorn/pay/client/models/product_update.py +1 -1
- crypticorn/pay/client/models/scope.py +1 -0
- crypticorn/pay/client/rest.py +23 -4
- crypticorn/pay/main.py +10 -6
- crypticorn/trade/client/__init__.py +11 -1
- crypticorn/trade/client/api/__init__.py +0 -1
- crypticorn/trade/client/api/admin_api.py +1184 -55
- crypticorn/trade/client/api/api_keys_api.py +1678 -162
- crypticorn/trade/client/api/bots_api.py +7563 -187
- crypticorn/trade/client/api/exchanges_api.py +565 -19
- crypticorn/trade/client/api/notifications_api.py +1290 -116
- crypticorn/trade/client/api/orders_api.py +393 -55
- crypticorn/trade/client/api/status_api.py +397 -13
- crypticorn/trade/client/api/strategies_api.py +1133 -77
- crypticorn/trade/client/api/trading_actions_api.py +786 -65
- crypticorn/trade/client/models/__init__.py +11 -0
- crypticorn/trade/client/models/actions_count.py +88 -0
- crypticorn/trade/client/models/api_error_identifier.py +8 -7
- crypticorn/trade/client/models/bot.py +7 -18
- crypticorn/trade/client/models/bot_create.py +17 -1
- crypticorn/trade/client/models/bot_update.py +17 -1
- crypticorn/trade/client/models/exchange.py +6 -1
- crypticorn/trade/client/models/exchange_key.py +1 -1
- crypticorn/trade/client/models/exchange_key_balance.py +111 -0
- crypticorn/trade/client/models/exchange_key_create.py +17 -1
- crypticorn/trade/client/models/exchange_key_update.py +17 -1
- crypticorn/trade/client/models/execution_ids.py +1 -1
- crypticorn/trade/client/models/futures_balance.py +27 -25
- crypticorn/trade/client/models/futures_trading_action.py +6 -28
- crypticorn/trade/client/models/futures_trading_action_create.py +10 -13
- crypticorn/trade/client/models/notification.py +17 -1
- crypticorn/trade/client/models/notification_create.py +18 -2
- crypticorn/trade/client/models/notification_update.py +17 -1
- crypticorn/trade/client/models/order.py +2 -14
- crypticorn/trade/client/models/orders_count.py +88 -0
- crypticorn/trade/client/models/paginated_response_futures_trading_action.py +134 -0
- crypticorn/trade/client/models/paginated_response_order.py +134 -0
- crypticorn/trade/client/models/pn_l.py +95 -0
- crypticorn/trade/client/models/post_futures_action.py +1 -1
- crypticorn/trade/client/models/spot_balance.py +109 -0
- crypticorn/trade/client/models/spot_trading_action_create.py +4 -1
- crypticorn/trade/client/models/strategy.py +22 -4
- crypticorn/trade/client/models/strategy_create.py +23 -5
- crypticorn/trade/client/models/strategy_exchange_info.py +16 -4
- crypticorn/trade/client/models/strategy_update.py +19 -3
- crypticorn/trade/client/models/tpsl.py +4 -27
- crypticorn/trade/client/models/tpsl_create.py +6 -19
- crypticorn/trade/client/rest.py +23 -4
- crypticorn/trade/main.py +15 -12
- {crypticorn-2.15.0.dist-info → crypticorn-2.17.0.dist-info}/METADATA +65 -20
- {crypticorn-2.15.0.dist-info → crypticorn-2.17.0.dist-info}/RECORD +167 -132
- crypticorn/trade/client/api/futures_trading_panel_api.py +0 -1285
- {crypticorn-2.15.0.dist-info → crypticorn-2.17.0.dist-info}/WHEEL +0 -0
- {crypticorn-2.15.0.dist-info → crypticorn-2.17.0.dist-info}/entry_points.txt +0 -0
- {crypticorn-2.15.0.dist-info → crypticorn-2.17.0.dist-info}/licenses/LICENSE +0 -0
- {crypticorn-2.15.0.dist-info → crypticorn-2.17.0.dist-info}/top_level.txt +0 -0
@@ -195,7 +195,7 @@ class Configuration:
|
|
195
195
|
debug: Optional[bool] = None,
|
196
196
|
) -> None:
|
197
197
|
"""Constructor"""
|
198
|
-
self._base_path = "
|
198
|
+
self._base_path = "http://localhost/v1/auth" if host is None else host
|
199
199
|
"""Default Base url
|
200
200
|
"""
|
201
201
|
self.server_index = 0 if server_index is None and host is None else server_index
|
@@ -528,7 +528,7 @@ class Configuration:
|
|
528
528
|
"""
|
529
529
|
return [
|
530
530
|
{
|
531
|
-
"url": "
|
531
|
+
"url": "http://localhost/v1/auth",
|
532
532
|
"description": "No description provided",
|
533
533
|
}
|
534
534
|
]
|
@@ -50,6 +50,7 @@ class CreateApiKeyRequest(BaseModel):
|
|
50
50
|
if i not in set(
|
51
51
|
[
|
52
52
|
"read:predictions",
|
53
|
+
"read:dex:signals",
|
53
54
|
"read:hive:model",
|
54
55
|
"read:hive:data",
|
55
56
|
"write:hive:model",
|
@@ -84,7 +85,7 @@ class CreateApiKeyRequest(BaseModel):
|
|
84
85
|
]
|
85
86
|
):
|
86
87
|
raise ValueError(
|
87
|
-
"each list item must be one of ('read:predictions', 'read:hive:model', 'read:hive:data', 'write:hive:model', 'read:trade:bots', 'write:trade:bots', 'read:trade:exchangekeys', 'write:trade:exchangekeys', 'read:trade:orders', 'read:trade:actions', 'write:trade:actions', 'read:trade:exchanges', 'read:trade:futures', 'write:trade:futures', 'read:trade:notifications', 'write:trade:notifications', 'read:trade:strategies', 'write:trade:strategies', 'read:pay:payments', 'read:pay:products', 'write:pay:products', 'read:pay:now', 'write:pay:now', 'read:metrics:marketcap', 'read:metrics:indicators', 'read:metrics:exchanges', 'read:metrics:tokens', 'read:metrics:markets', 'read:sentiment', 'read:klines', 'read:admin', 'write:admin')"
|
88
|
+
"each list item must be one of ('read:predictions', 'read:dex:signals', 'read:hive:model', 'read:hive:data', 'write:hive:model', 'read:trade:bots', 'write:trade:bots', 'read:trade:exchangekeys', 'write:trade:exchangekeys', 'read:trade:orders', 'read:trade:actions', 'write:trade:actions', 'read:trade:exchanges', 'read:trade:futures', 'write:trade:futures', 'read:trade:notifications', 'write:trade:notifications', 'read:trade:strategies', 'write:trade:strategies', 'read:pay:payments', 'read:pay:products', 'write:pay:products', 'read:pay:now', 'write:pay:now', 'read:metrics:marketcap', 'read:metrics:indicators', 'read:metrics:exchanges', 'read:metrics:tokens', 'read:metrics:markets', 'read:sentiment', 'read:klines', 'read:admin', 'write:admin')"
|
88
89
|
)
|
89
90
|
return value
|
90
91
|
|
@@ -58,6 +58,7 @@ class GetApiKeys200ResponseInner(BaseModel):
|
|
58
58
|
if i not in set(
|
59
59
|
[
|
60
60
|
"read:predictions",
|
61
|
+
"read:dex:signals",
|
61
62
|
"read:hive:model",
|
62
63
|
"read:hive:data",
|
63
64
|
"write:hive:model",
|
@@ -92,7 +93,7 @@ class GetApiKeys200ResponseInner(BaseModel):
|
|
92
93
|
]
|
93
94
|
):
|
94
95
|
raise ValueError(
|
95
|
-
"each list item must be one of ('read:predictions', 'read:hive:model', 'read:hive:data', 'write:hive:model', 'read:trade:bots', 'write:trade:bots', 'read:trade:exchangekeys', 'write:trade:exchangekeys', 'read:trade:orders', 'read:trade:actions', 'write:trade:actions', 'read:trade:exchanges', 'read:trade:futures', 'write:trade:futures', 'read:trade:notifications', 'write:trade:notifications', 'read:trade:strategies', 'write:trade:strategies', 'read:pay:payments', 'read:pay:products', 'write:pay:products', 'read:pay:now', 'write:pay:now', 'read:metrics:marketcap', 'read:metrics:indicators', 'read:metrics:exchanges', 'read:metrics:tokens', 'read:metrics:markets', 'read:sentiment', 'read:klines', 'read:admin', 'write:admin')"
|
96
|
+
"each list item must be one of ('read:predictions', 'read:dex:signals', 'read:hive:model', 'read:hive:data', 'write:hive:model', 'read:trade:bots', 'write:trade:bots', 'read:trade:exchangekeys', 'write:trade:exchangekeys', 'read:trade:orders', 'read:trade:actions', 'write:trade:actions', 'read:trade:exchanges', 'read:trade:futures', 'write:trade:futures', 'read:trade:notifications', 'write:trade:notifications', 'read:trade:strategies', 'write:trade:strategies', 'read:pay:payments', 'read:pay:products', 'write:pay:products', 'read:pay:now', 'write:pay:now', 'read:metrics:marketcap', 'read:metrics:indicators', 'read:metrics:exchanges', 'read:metrics:tokens', 'read:metrics:markets', 'read:sentiment', 'read:klines', 'read:admin', 'write:admin')"
|
96
97
|
)
|
97
98
|
return value
|
98
99
|
|
crypticorn/auth/client/rest.py
CHANGED
@@ -77,6 +77,7 @@ class RESTClientObject:
|
|
77
77
|
|
78
78
|
self.pool_manager: Optional[aiohttp.ClientSession] = None
|
79
79
|
self.retry_client: Optional[aiohttp_retry.RetryClient] = None
|
80
|
+
self.is_sync: bool = False # Track whether this is sync or async mode
|
80
81
|
|
81
82
|
async def close(self) -> None:
|
82
83
|
if self.pool_manager:
|
@@ -170,7 +171,9 @@ class RESTClientObject:
|
|
170
171
|
|
171
172
|
pool_manager: Union[aiohttp.ClientSession, aiohttp_retry.RetryClient]
|
172
173
|
|
173
|
-
#
|
174
|
+
# For sync operations, always use a fresh session
|
175
|
+
should_close_session = False
|
176
|
+
|
174
177
|
if self.pool_manager is None:
|
175
178
|
self.pool_manager = aiohttp.ClientSession(
|
176
179
|
connector=aiohttp.TCPConnector(
|
@@ -178,6 +181,9 @@ class RESTClientObject:
|
|
178
181
|
),
|
179
182
|
trust_env=True,
|
180
183
|
)
|
184
|
+
# Only close session automatically in sync mode
|
185
|
+
should_close_session = self.is_sync
|
186
|
+
|
181
187
|
pool_manager = self.pool_manager
|
182
188
|
|
183
189
|
if self.retries is not None and method in ALLOW_RETRY_METHODS:
|
@@ -193,6 +199,19 @@ class RESTClientObject:
|
|
193
199
|
)
|
194
200
|
pool_manager = self.retry_client
|
195
201
|
|
196
|
-
|
197
|
-
|
198
|
-
|
202
|
+
try:
|
203
|
+
r = await pool_manager.request(**args)
|
204
|
+
# For sessions we're about to close, read the data immediately
|
205
|
+
if should_close_session:
|
206
|
+
response = RESTResponse(r)
|
207
|
+
await response.read() # Read data before closing session
|
208
|
+
return response
|
209
|
+
else:
|
210
|
+
return RESTResponse(r)
|
211
|
+
finally:
|
212
|
+
if should_close_session:
|
213
|
+
if self.retry_client is not None:
|
214
|
+
await self.retry_client.close()
|
215
|
+
self.retry_client = None
|
216
|
+
await self.pool_manager.close()
|
217
|
+
self.pool_manager = None
|
crypticorn/auth/main.py
CHANGED
@@ -25,14 +25,17 @@ class AuthClient:
|
|
25
25
|
self,
|
26
26
|
config: Configuration,
|
27
27
|
http_client: Optional[ClientSession] = None,
|
28
|
+
is_sync: bool = False,
|
28
29
|
):
|
29
30
|
self.config = config
|
30
31
|
self.base_client = ApiClient(configuration=self.config)
|
31
32
|
if http_client is not None:
|
32
33
|
self.base_client.rest_client.pool_manager = http_client
|
34
|
+
# Pass sync context to REST client for proper session management
|
35
|
+
self.base_client.rest_client.is_sync = is_sync
|
33
36
|
# Instantiate all the endpoint clients
|
34
|
-
self.admin = AdminApi(self.base_client)
|
35
|
-
self.service = ServiceApi(self.base_client)
|
36
|
-
self.user = UserApi(self.base_client)
|
37
|
-
self.wallet = WalletApi(self.base_client)
|
38
|
-
self.login = AuthApi(self.base_client)
|
37
|
+
self.admin = AdminApi(self.base_client, is_sync=is_sync)
|
38
|
+
self.service = ServiceApi(self.base_client, is_sync=is_sync)
|
39
|
+
self.user = UserApi(self.base_client, is_sync=is_sync)
|
40
|
+
self.wallet = WalletApi(self.base_client, is_sync=is_sync)
|
41
|
+
self.login = AuthApi(self.base_client, is_sync=is_sync)
|
crypticorn/cli/init.py
CHANGED
@@ -33,7 +33,7 @@ def copy_template(template_name: str, target_path: Path):
|
|
33
33
|
|
34
34
|
def check_file_exists(path: Path, force: bool):
|
35
35
|
if path.exists() and not force:
|
36
|
-
click.secho(
|
36
|
+
click.secho("File already exists, use --force / -f to overwrite", fg="red")
|
37
37
|
return False
|
38
38
|
return True
|
39
39
|
|
@@ -19,14 +19,17 @@ WORKDIR /code
|
|
19
19
|
# and to the workflow:
|
20
20
|
# env:
|
21
21
|
# API_ENV: ${{ github.ref == 'refs/heads/main' && 'prod' || 'dev' }}
|
22
|
-
|
22
|
+
# and in the docker build:
|
23
|
+
# build-args: |
|
24
|
+
# CACHEBUST=${{ github.run_id }} # invalidates dependency cache on every workflow run
|
23
25
|
|
24
|
-
|
26
|
+
ARG API_ENV
|
25
27
|
|
26
28
|
RUN if [ "$API_ENV" != "prod" ]; then \
|
27
29
|
pip install --pre crypticorn; \
|
28
30
|
fi
|
29
31
|
|
32
|
+
RUN pip install --no-cache-dir -r requirements.txt
|
30
33
|
|
31
34
|
COPY ./src /code/src
|
32
35
|
|
crypticorn/client.py
CHANGED
@@ -1,22 +1,30 @@
|
|
1
1
|
from typing import TypeVar, Optional
|
2
|
+
import warnings
|
2
3
|
from aiohttp import ClientSession, ClientTimeout, TCPConnector
|
3
4
|
from crypticorn.hive import HiveClient
|
4
5
|
from crypticorn.klines import KlinesClient
|
5
6
|
from crypticorn.pay import PayClient
|
7
|
+
|
6
8
|
from crypticorn.trade import TradeClient
|
7
9
|
from crypticorn.metrics import MetricsClient
|
8
10
|
from crypticorn.auth import AuthClient
|
9
|
-
from crypticorn.common import
|
11
|
+
from crypticorn.common import (
|
12
|
+
BaseUrl,
|
13
|
+
ApiVersion,
|
14
|
+
Service,
|
15
|
+
apikey_header as aph,
|
16
|
+
CrypticornDeprecatedSince217,
|
17
|
+
)
|
10
18
|
from importlib.metadata import version
|
19
|
+
from typing_extensions import deprecated
|
11
20
|
|
12
21
|
ConfigT = TypeVar("ConfigT")
|
13
22
|
SubClient = TypeVar("SubClient")
|
14
23
|
|
15
24
|
|
16
|
-
class
|
25
|
+
class BaseAsyncClient:
|
17
26
|
"""
|
18
|
-
|
19
|
-
It is consisting of multiple microservices covering the whole stack of the Crypticorn project.
|
27
|
+
Base class for Crypticorn API clients containing shared functionality.
|
20
28
|
"""
|
21
29
|
|
22
30
|
def __init__(
|
@@ -24,20 +32,23 @@ class ApiClient:
|
|
24
32
|
api_key: Optional[str] = None,
|
25
33
|
jwt: Optional[str] = None,
|
26
34
|
base_url: BaseUrl = BaseUrl.PROD,
|
27
|
-
|
35
|
+
is_sync: bool = False,
|
28
36
|
http_client: Optional[ClientSession] = None,
|
29
37
|
):
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
38
|
+
"""
|
39
|
+
:param api_key: The API key to use for authentication (recommended).
|
40
|
+
:param jwt: The JWT to use for authentication (not recommended).
|
41
|
+
:param base_url: The base URL the client will use to connect to the API.
|
42
|
+
:param is_sync: Whether this client should operate in synchronous mode.
|
43
|
+
:param http_client: Optional aiohttp ClientSession to use for HTTP requests.
|
44
|
+
"""
|
45
|
+
self._base_url = base_url
|
46
|
+
self._api_key = api_key
|
47
|
+
self._jwt = jwt
|
48
|
+
self._is_sync = is_sync
|
39
49
|
self._http_client = http_client
|
40
50
|
self._owns_http_client = http_client is None # whether we own the http client
|
51
|
+
|
41
52
|
self._service_classes: dict[Service, type[SubClient]] = {
|
42
53
|
Service.HIVE: HiveClient,
|
43
54
|
Service.TRADE: TradeClient,
|
@@ -46,12 +57,69 @@ class ApiClient:
|
|
46
57
|
Service.METRICS: MetricsClient,
|
47
58
|
Service.AUTH: AuthClient,
|
48
59
|
}
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
}
|
60
|
+
|
61
|
+
self._services: dict[Service, SubClient] = self._create_services()
|
62
|
+
|
63
|
+
def _create_services(self) -> dict[Service, SubClient]:
|
64
|
+
"""Create services with the appropriate configuration based on sync/async mode."""
|
65
|
+
services = {}
|
66
|
+
for service, client_class in self._service_classes.items():
|
67
|
+
config = self._get_default_config(service)
|
68
|
+
# For sync clients, don't pass the persistent http_client
|
69
|
+
# Let each operation manage its own session
|
70
|
+
if self._is_sync:
|
71
|
+
services[service] = client_class(
|
72
|
+
config, http_client=None, is_sync=self._is_sync
|
73
|
+
)
|
74
|
+
else:
|
75
|
+
services[service] = client_class(
|
76
|
+
config, http_client=self._http_client, is_sync=self._is_sync
|
77
|
+
)
|
78
|
+
return services
|
79
|
+
|
80
|
+
@property
|
81
|
+
def base_url(self) -> BaseUrl:
|
82
|
+
"""
|
83
|
+
The base URL the client will use to connect to the API.
|
84
|
+
"""
|
85
|
+
return self._base_url
|
86
|
+
|
87
|
+
@property
|
88
|
+
def api_key(self) -> Optional[str]:
|
89
|
+
"""
|
90
|
+
The API key the client will use to connect to the API.
|
91
|
+
This is the preferred way to authenticate.
|
92
|
+
"""
|
93
|
+
return self._api_key
|
94
|
+
|
95
|
+
@property
|
96
|
+
def jwt(self) -> Optional[str]:
|
97
|
+
"""
|
98
|
+
The JWT the client will use to connect to the API.
|
99
|
+
This is the not the preferred way to authenticate.
|
100
|
+
"""
|
101
|
+
return self._jwt
|
102
|
+
|
103
|
+
@property
|
104
|
+
def version(self) -> str:
|
105
|
+
"""
|
106
|
+
The version of the client.
|
107
|
+
"""
|
108
|
+
return version("crypticorn")
|
109
|
+
|
110
|
+
@property
|
111
|
+
def is_sync(self) -> bool:
|
112
|
+
"""
|
113
|
+
Whether this client operates in synchronous mode.
|
114
|
+
"""
|
115
|
+
return self._is_sync
|
116
|
+
|
117
|
+
@property
|
118
|
+
def http_client(self) -> Optional[ClientSession]:
|
119
|
+
"""
|
120
|
+
The HTTP client session being used, if any.
|
121
|
+
"""
|
122
|
+
return self._http_client
|
55
123
|
|
56
124
|
@property
|
57
125
|
def hive(self) -> HiveClient:
|
@@ -95,17 +163,67 @@ class ApiClient:
|
|
95
163
|
"""
|
96
164
|
return self._services[Service.AUTH]
|
97
165
|
|
166
|
+
def configure(self, config: ConfigT, service: Service) -> None:
|
167
|
+
"""
|
168
|
+
Update a sub-client's configuration by overriding with the values set in the new config.
|
169
|
+
Useful for testing a specific service against a local server instead of the default proxy.
|
170
|
+
|
171
|
+
:param config: The new configuration to use for the sub-client.
|
172
|
+
:param service: The service to configure.
|
173
|
+
|
174
|
+
Example:
|
175
|
+
>>> # For async client
|
176
|
+
>>> async with AsyncClient() as client:
|
177
|
+
... client.configure(config=HiveConfig(host="http://localhost:8000"), service=Service.HIVE)
|
178
|
+
>>>
|
179
|
+
>>> # For sync client
|
180
|
+
>>> with SyncClient() as client:
|
181
|
+
... client.configure(config=HiveConfig(host="http://localhost:8000"), service=Service.HIVE)
|
182
|
+
"""
|
183
|
+
assert Service.validate(service), f"Invalid service: {service}"
|
184
|
+
client = self._services[service]
|
185
|
+
new_config = client.config
|
186
|
+
for attr in vars(config):
|
187
|
+
new_value = getattr(config, attr)
|
188
|
+
if new_value:
|
189
|
+
setattr(new_config, attr, new_value)
|
190
|
+
|
191
|
+
# Recreate service with new config and appropriate parameters
|
192
|
+
if self._is_sync:
|
193
|
+
self._services[service] = type(client)(
|
194
|
+
new_config, is_sync=self._is_sync, http_client=self._http_client
|
195
|
+
)
|
196
|
+
else:
|
197
|
+
self._services[service] = type(client)(
|
198
|
+
new_config, http_client=self._http_client
|
199
|
+
)
|
200
|
+
|
201
|
+
def _get_default_config(self, service, version=None):
|
202
|
+
if version is None:
|
203
|
+
version = ApiVersion.V1
|
204
|
+
config_class = self._service_classes[service].config_class
|
205
|
+
return config_class(
|
206
|
+
host=f"{self.base_url}/{version}/{service}",
|
207
|
+
access_token=self.jwt,
|
208
|
+
api_key={aph.scheme_name: self.api_key} if self.api_key else None,
|
209
|
+
)
|
210
|
+
|
98
211
|
async def close(self):
|
99
|
-
|
212
|
+
"""Close the client and clean up resources."""
|
213
|
+
# close each service
|
100
214
|
for service in self._services.values():
|
101
|
-
if
|
215
|
+
if (
|
216
|
+
hasattr(service, "base_client")
|
217
|
+
and hasattr(service.base_client, "close")
|
218
|
+
and self._owns_http_client
|
219
|
+
):
|
102
220
|
await service.base_client.close()
|
103
|
-
# close shared
|
221
|
+
# close shared http client if we own it
|
104
222
|
if self._http_client and self._owns_http_client:
|
105
223
|
await self._http_client.close()
|
106
224
|
self._http_client = None
|
107
225
|
|
108
|
-
|
226
|
+
def _ensure_session(self) -> None:
|
109
227
|
"""
|
110
228
|
Lazily create the shared HTTP client when first needed and pass it to all subclients.
|
111
229
|
"""
|
@@ -115,48 +233,97 @@ class ApiClient:
|
|
115
233
|
connector=TCPConnector(limit=100, limit_per_host=20),
|
116
234
|
headers={"User-Agent": f"crypticorn/python/{self.version}"},
|
117
235
|
)
|
118
|
-
|
119
|
-
|
120
|
-
service.base_client, "rest_client"
|
121
|
-
):
|
122
|
-
service.base_client.rest_client.pool_manager = self._http_client
|
236
|
+
# Update services to use the new session
|
237
|
+
self._services = self._create_services()
|
123
238
|
|
124
|
-
def _get_default_config(self, service, version=None):
|
125
|
-
if version is None:
|
126
|
-
version = ApiVersion.V1
|
127
|
-
config_class = self._service_classes[service].config_class
|
128
|
-
return config_class(
|
129
|
-
host=f"{self.base_url}/{version}/{service}",
|
130
|
-
access_token=self.jwt,
|
131
|
-
api_key={aph.scheme_name: self.api_key} if self.api_key else None,
|
132
|
-
)
|
133
|
-
|
134
|
-
def configure(self, config: ConfigT, service: Service) -> None:
|
135
|
-
"""
|
136
|
-
Update a sub-client's configuration by overriding with the values set in the new config.
|
137
|
-
Useful for testing a specific service against a local server instead of the default proxy.
|
138
239
|
|
139
|
-
|
140
|
-
|
240
|
+
class AsyncClient(BaseAsyncClient):
|
241
|
+
"""
|
242
|
+
The official async Python client for interacting with the Crypticorn API.
|
243
|
+
It is consisting of multiple microservices covering the whole stack of the Crypticorn project.
|
244
|
+
"""
|
141
245
|
|
142
|
-
|
143
|
-
|
144
|
-
|
246
|
+
def __init__(
|
247
|
+
self,
|
248
|
+
api_key: Optional[str] = None,
|
249
|
+
jwt: Optional[str] = None,
|
250
|
+
base_url: BaseUrl = BaseUrl.PROD,
|
251
|
+
*,
|
252
|
+
http_client: Optional[ClientSession] = None,
|
253
|
+
):
|
145
254
|
"""
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
for
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
)
|
255
|
+
:param api_key: The API key to use for authentication (recommended).
|
256
|
+
:param jwt: The JWT to use for authentication (not recommended).
|
257
|
+
:param base_url: The base URL the client will use to connect to the API.
|
258
|
+
:param http_client: The HTTP client to use for the client.
|
259
|
+
"""
|
260
|
+
# Initialize as async client
|
261
|
+
super().__init__(api_key, jwt, base_url, is_sync=False, http_client=http_client)
|
262
|
+
|
263
|
+
async def close(self):
|
264
|
+
await super().close()
|
156
265
|
|
157
266
|
async def __aenter__(self):
|
158
|
-
|
267
|
+
self._ensure_session()
|
159
268
|
return self
|
160
269
|
|
161
270
|
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
162
271
|
await self.close()
|
272
|
+
|
273
|
+
|
274
|
+
@deprecated("Use AsyncClient instead", category=None)
|
275
|
+
class ApiClient(AsyncClient):
|
276
|
+
def __init__(self, *args, **kwargs):
|
277
|
+
warnings.warn(
|
278
|
+
"ApiClient is deprecated. Use AsyncClient instead.",
|
279
|
+
CrypticornDeprecatedSince217,
|
280
|
+
)
|
281
|
+
super().__init__(*args, **kwargs)
|
282
|
+
|
283
|
+
|
284
|
+
class SyncClient(BaseAsyncClient):
|
285
|
+
"""
|
286
|
+
The official synchronous Python client for interacting with the Crypticorn API.
|
287
|
+
"""
|
288
|
+
|
289
|
+
def __init__(
|
290
|
+
self,
|
291
|
+
api_key: Optional[str] = None,
|
292
|
+
jwt: Optional[str] = None,
|
293
|
+
base_url: BaseUrl = BaseUrl.PROD,
|
294
|
+
*,
|
295
|
+
http_client: Optional[ClientSession] = None,
|
296
|
+
):
|
297
|
+
"""
|
298
|
+
:param http_client: Optional aiohttp ClientSession to use for HTTP requests.
|
299
|
+
Note: For sync client, session management is handled automatically.
|
300
|
+
"""
|
301
|
+
super().__init__(api_key, jwt, base_url, is_sync=True, http_client=http_client)
|
302
|
+
|
303
|
+
def close(self):
|
304
|
+
"""Close the client and clean up resources."""
|
305
|
+
# For sync client, don't maintain persistent sessions
|
306
|
+
# Each operation creates its own session within async_to_sync
|
307
|
+
self._http_client = None
|
308
|
+
|
309
|
+
def _ensure_session(self) -> None:
|
310
|
+
"""
|
311
|
+
For sync client, don't create persistent sessions.
|
312
|
+
Let each async_to_sync call handle its own session.
|
313
|
+
"""
|
314
|
+
# Don't create persistent sessions in sync mode
|
315
|
+
# Each API call will handle session creation/cleanup within async_to_sync
|
316
|
+
pass
|
317
|
+
|
318
|
+
def __enter__(self):
|
319
|
+
return self
|
320
|
+
|
321
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
322
|
+
self.close()
|
323
|
+
|
324
|
+
def __del__(self):
|
325
|
+
"""Automatic cleanup when the object is garbage collected."""
|
326
|
+
try:
|
327
|
+
self.close()
|
328
|
+
except Exception:
|
329
|
+
pass
|
crypticorn/common/__init__.py
CHANGED
@@ -13,5 +13,6 @@ from crypticorn.common.ansi_colors import *
|
|
13
13
|
from crypticorn.common.middleware import *
|
14
14
|
from crypticorn.common.warnings import *
|
15
15
|
from crypticorn.common.openapi import *
|
16
|
+
from crypticorn.common.metrics import *
|
16
17
|
from crypticorn.common.router.status_router import router as status_router
|
17
18
|
from crypticorn.common.router.admin_router import router as admin_router
|
crypticorn/common/auth.py
CHANGED
@@ -7,18 +7,19 @@ from crypticorn.common.exceptions import (
|
|
7
7
|
ApiError,
|
8
8
|
HTTPException,
|
9
9
|
ExceptionContent,
|
10
|
-
WebSocketException,
|
11
10
|
)
|
12
11
|
from crypticorn.common.urls import BaseUrl, Service, ApiVersion
|
13
|
-
from fastapi import Depends, Query
|
12
|
+
from fastapi import Depends, Query, Request
|
14
13
|
from fastapi.security import (
|
15
14
|
HTTPAuthorizationCredentials,
|
16
15
|
SecurityScopes,
|
17
16
|
HTTPBearer,
|
18
17
|
APIKeyHeader,
|
18
|
+
HTTPBasic,
|
19
19
|
)
|
20
20
|
from typing_extensions import Annotated
|
21
21
|
from typing import Union
|
22
|
+
from fastapi.security import HTTPBasicCredentials
|
22
23
|
|
23
24
|
# Auth Schemes
|
24
25
|
http_bearer = HTTPBearer(
|
@@ -33,6 +34,12 @@ apikey_header = APIKeyHeader(
|
|
33
34
|
description="The API key to use for authentication.",
|
34
35
|
)
|
35
36
|
|
37
|
+
basic_auth = HTTPBasic(
|
38
|
+
scheme_name="Basic",
|
39
|
+
auto_error=False,
|
40
|
+
description="The username and password to use for authentication. Only used in /admin/metrics",
|
41
|
+
)
|
42
|
+
|
36
43
|
|
37
44
|
# Auth Handler
|
38
45
|
class AuthHandler:
|
@@ -112,6 +119,7 @@ class AuthHandler:
|
|
112
119
|
elif message == "jwt expired":
|
113
120
|
error = ApiError.EXPIRED_BEARER
|
114
121
|
else:
|
122
|
+
message = "Invalid bearer token"
|
115
123
|
error = (
|
116
124
|
ApiError.INVALID_BEARER
|
117
125
|
) # jwt malformed, jwt not active (https://www.npmjs.com/package/jsonwebtoken#errors--codes)
|
@@ -150,6 +158,7 @@ class AuthHandler:
|
|
150
158
|
error=ApiError.NO_API_KEY,
|
151
159
|
message="No credentials provided. API key is required",
|
152
160
|
),
|
161
|
+
headers={"WWW-Authenticate": "X-API-Key"},
|
153
162
|
)
|
154
163
|
raise e
|
155
164
|
|
@@ -175,6 +184,7 @@ class AuthHandler:
|
|
175
184
|
error=ApiError.NO_BEARER,
|
176
185
|
message="No credentials provided. Bearer token is required",
|
177
186
|
),
|
187
|
+
headers={"WWW-Authenticate": "Bearer"},
|
178
188
|
)
|
179
189
|
raise e
|
180
190
|
|
@@ -222,6 +232,7 @@ class AuthHandler:
|
|
222
232
|
error=ApiError.NO_CREDENTIALS,
|
223
233
|
message="No credentials provided. Either API key or bearer token is required.",
|
224
234
|
),
|
235
|
+
headers={"WWW-Authenticate": "Bearer, X-API-Key"},
|
225
236
|
)
|
226
237
|
|
227
238
|
async def ws_api_key_auth(
|
@@ -234,10 +245,7 @@ class AuthHandler:
|
|
234
245
|
Use this function if you only want to allow access via the API key.
|
235
246
|
This function is used for WebSocket connections.
|
236
247
|
"""
|
237
|
-
|
238
|
-
return await self.api_key_auth(api_key=api_key, sec=sec)
|
239
|
-
except HTTPException as e:
|
240
|
-
raise WebSocketException.from_http_exception(e)
|
248
|
+
return await self.api_key_auth(api_key=api_key, sec=sec)
|
241
249
|
|
242
250
|
async def ws_bearer_auth(
|
243
251
|
self,
|
@@ -249,10 +257,8 @@ class AuthHandler:
|
|
249
257
|
Use this function if you only want to allow access via the bearer token.
|
250
258
|
This function is used for WebSocket connections.
|
251
259
|
"""
|
252
|
-
|
253
|
-
|
254
|
-
except HTTPException as e:
|
255
|
-
raise WebSocketException.from_http_exception(e)
|
260
|
+
credentials = HTTPAuthorizationCredentials(scheme="Bearer", credentials=bearer)
|
261
|
+
return await self.bearer_auth(bearer=credentials, sec=sec)
|
256
262
|
|
257
263
|
async def ws_combined_auth(
|
258
264
|
self,
|
@@ -270,9 +276,34 @@ class AuthHandler:
|
|
270
276
|
if bearer
|
271
277
|
else None
|
272
278
|
)
|
279
|
+
return await self.combined_auth(bearer=credentials, api_key=api_key, sec=sec)
|
280
|
+
|
281
|
+
async def basic_auth(
|
282
|
+
self,
|
283
|
+
request: Request,
|
284
|
+
credentials: Annotated[Union[HTTPBasicCredentials, None], Depends(basic_auth)],
|
285
|
+
):
|
286
|
+
"""
|
287
|
+
Verifies the basic authentication credentials. This authentication method should just be used for special cases like /admin/metrics, where JWT and API key authentication are not desired or not possible.
|
288
|
+
"""
|
289
|
+
if not credentials:
|
290
|
+
raise HTTPException(
|
291
|
+
content=ExceptionContent(
|
292
|
+
error=ApiError.NO_CREDENTIALS,
|
293
|
+
message="No credentials provided. Basic authentication credentials are required.",
|
294
|
+
),
|
295
|
+
)
|
296
|
+
|
273
297
|
try:
|
274
|
-
|
275
|
-
|
298
|
+
await self.client.login.verify_basic_auth_without_preload_content(
|
299
|
+
credentials.username, credentials.password
|
276
300
|
)
|
277
|
-
except
|
278
|
-
raise
|
301
|
+
except ApiException as e:
|
302
|
+
raise HTTPException(
|
303
|
+
content=ExceptionContent(
|
304
|
+
error=ApiError.INVALID_BASIC_AUTH,
|
305
|
+
message="Invalid basic authentication credentials",
|
306
|
+
),
|
307
|
+
headers={"WWW-Authenticate": "Basic"},
|
308
|
+
)
|
309
|
+
return credentials.username
|