crypticorn 2.16.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 -20
- crypticorn/common/router/admin_router.py +2 -2
- crypticorn/common/router/status_router.py +40 -2
- crypticorn/common/scopes.py +2 -2
- crypticorn/common/warnings.py +7 -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 +1 -0
- 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/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/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/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 -19
- crypticorn/trade/client/models/tpsl_create.py +6 -19
- crypticorn/trade/client/rest.py +23 -4
- crypticorn/trade/main.py +15 -12
- {crypticorn-2.16.0.dist-info → crypticorn-2.17.0.dist-info}/METADATA +65 -20
- {crypticorn-2.16.0.dist-info → crypticorn-2.17.0.dist-info}/RECORD +163 -128
- crypticorn/trade/client/api/futures_trading_panel_api.py +0 -1285
- {crypticorn-2.16.0.dist-info → crypticorn-2.17.0.dist-info}/WHEEL +0 -0
- {crypticorn-2.16.0.dist-info → crypticorn-2.17.0.dist-info}/entry_points.txt +0 -0
- {crypticorn-2.16.0.dist-info → crypticorn-2.17.0.dist-info}/licenses/LICENSE +0 -0
- {crypticorn-2.16.0.dist-info → crypticorn-2.17.0.dist-info}/top_level.txt +0 -0
crypticorn/common/decorators.py
CHANGED
@@ -1,8 +1,7 @@
|
|
1
|
-
from datetime import datetime
|
2
1
|
from typing import Optional, Type, Any, Tuple
|
3
2
|
from copy import deepcopy
|
4
3
|
|
5
|
-
from pydantic import BaseModel, create_model
|
4
|
+
from pydantic import BaseModel, create_model
|
6
5
|
from pydantic.fields import FieldInfo
|
7
6
|
|
8
7
|
|
crypticorn/common/enums.py
CHANGED
crypticorn/common/errors.py
CHANGED
@@ -70,6 +70,7 @@ class ApiErrorIdentifier(StrEnum):
|
|
70
70
|
INSUFFICIENT_MARGIN = "insufficient_margin"
|
71
71
|
INSUFFICIENT_SCOPES = "insufficient_scopes"
|
72
72
|
INVALID_API_KEY = "invalid_api_key"
|
73
|
+
INVALID_BASIC_AUTH = "invalid_basic_auth"
|
73
74
|
INVALID_BEARER = "invalid_bearer"
|
74
75
|
INVALID_DATA_REQUEST = "invalid_data"
|
75
76
|
INVALID_DATA_RESPONSE = "invalid_data_response"
|
@@ -302,6 +303,11 @@ class ApiError(Enum, metaclass=ApiErrorFallback):
|
|
302
303
|
ApiErrorType.USER_ERROR,
|
303
304
|
ApiErrorLevel.ERROR,
|
304
305
|
)
|
306
|
+
INVALID_BASIC_AUTH = (
|
307
|
+
ApiErrorIdentifier.INVALID_BASIC_AUTH,
|
308
|
+
ApiErrorType.USER_ERROR,
|
309
|
+
ApiErrorLevel.ERROR,
|
310
|
+
)
|
305
311
|
INVALID_BEARER = (
|
306
312
|
ApiErrorIdentifier.INVALID_BEARER,
|
307
313
|
ApiErrorType.USER_ERROR,
|
@@ -569,6 +575,10 @@ class StatusCodeMapper:
|
|
569
575
|
status.HTTP_401_UNAUTHORIZED,
|
570
576
|
status.WS_1008_POLICY_VIOLATION,
|
571
577
|
),
|
578
|
+
ApiError.INVALID_BASIC_AUTH: (
|
579
|
+
status.HTTP_401_UNAUTHORIZED,
|
580
|
+
status.WS_1008_POLICY_VIOLATION,
|
581
|
+
),
|
572
582
|
ApiError.INVALID_BEARER: (
|
573
583
|
status.HTTP_401_UNAUTHORIZED,
|
574
584
|
status.WS_1008_POLICY_VIOLATION,
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# metrics/registry.py
|
2
|
+
from prometheus_client import Counter, Histogram, CollectorRegistry
|
3
|
+
|
4
|
+
registry = CollectorRegistry()
|
5
|
+
|
6
|
+
HTTP_REQUESTS_COUNT = Counter(
|
7
|
+
"http_requests_total",
|
8
|
+
"Total HTTP requests",
|
9
|
+
["method", "endpoint", "status_code", "auth_type"],
|
10
|
+
registry=registry,
|
11
|
+
)
|
12
|
+
|
13
|
+
HTTP_REQUEST_DURATION = Histogram(
|
14
|
+
"http_request_duration_seconds",
|
15
|
+
"HTTP request duration in seconds",
|
16
|
+
["endpoint", "method"],
|
17
|
+
registry=registry,
|
18
|
+
)
|
19
|
+
|
20
|
+
REQUEST_SIZE = Histogram(
|
21
|
+
"http_request_size_bytes", "Size of HTTP request bodies", ["method", "endpoint"]
|
22
|
+
)
|
23
|
+
|
24
|
+
|
25
|
+
RESPONSE_SIZE = Histogram(
|
26
|
+
"http_response_size_bytes",
|
27
|
+
"Size of HTTP responses",
|
28
|
+
["method", "endpoint"],
|
29
|
+
registry=registry,
|
30
|
+
)
|
crypticorn/common/middleware.py
CHANGED
@@ -1,10 +1,102 @@
|
|
1
|
-
|
1
|
+
import time
|
2
|
+
from fastapi import FastAPI, Request
|
2
3
|
from fastapi.middleware.cors import CORSMiddleware
|
4
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
3
5
|
from crypticorn.common.logging import configure_logging
|
4
6
|
from contextlib import asynccontextmanager
|
7
|
+
from typing_extensions import deprecated
|
8
|
+
import warnings
|
9
|
+
from crypticorn.common.warnings import CrypticornDeprecatedSince217
|
10
|
+
from crypticorn.common.metrics import (
|
11
|
+
HTTP_REQUESTS_COUNT,
|
12
|
+
HTTP_REQUEST_DURATION,
|
13
|
+
REQUEST_SIZE,
|
14
|
+
RESPONSE_SIZE,
|
15
|
+
)
|
5
16
|
|
6
17
|
|
18
|
+
class PrometheusMiddleware(BaseHTTPMiddleware):
|
19
|
+
async def dispatch(self, request: Request, call_next):
|
20
|
+
|
21
|
+
if "authorization" in request.headers:
|
22
|
+
auth_type = (
|
23
|
+
request.headers["authorization"].split()[0]
|
24
|
+
if " " in request.headers["authorization"]
|
25
|
+
else "none"
|
26
|
+
)
|
27
|
+
elif "x-api-key" in request.headers:
|
28
|
+
auth_type = "X-API-KEY"
|
29
|
+
else:
|
30
|
+
auth_type = "none"
|
31
|
+
|
32
|
+
try:
|
33
|
+
endpoint = request.get(
|
34
|
+
"route"
|
35
|
+
).path # use /time/{type} instead of dynamic route to avoid high cardinality
|
36
|
+
except Exception:
|
37
|
+
endpoint = request.url.path
|
38
|
+
|
39
|
+
start = time.perf_counter()
|
40
|
+
response = await call_next(request)
|
41
|
+
duration = time.perf_counter() - start
|
42
|
+
|
43
|
+
HTTP_REQUESTS_COUNT.labels(
|
44
|
+
method=request.method,
|
45
|
+
endpoint=endpoint,
|
46
|
+
status_code=response.status_code,
|
47
|
+
auth_type=auth_type,
|
48
|
+
).inc()
|
49
|
+
|
50
|
+
try:
|
51
|
+
body = await request.body()
|
52
|
+
size = len(body)
|
53
|
+
except Exception:
|
54
|
+
size = 0
|
55
|
+
|
56
|
+
REQUEST_SIZE.labels(
|
57
|
+
method=request.method,
|
58
|
+
endpoint=endpoint,
|
59
|
+
).observe(size)
|
60
|
+
|
61
|
+
try:
|
62
|
+
body = await response.body()
|
63
|
+
size = len(body)
|
64
|
+
except Exception:
|
65
|
+
size = 0
|
66
|
+
|
67
|
+
RESPONSE_SIZE.labels(
|
68
|
+
method=request.method,
|
69
|
+
endpoint=endpoint,
|
70
|
+
).observe(size)
|
71
|
+
|
72
|
+
HTTP_REQUEST_DURATION.labels(
|
73
|
+
endpoint=endpoint,
|
74
|
+
method=request.method,
|
75
|
+
).observe(duration)
|
76
|
+
|
77
|
+
return response
|
78
|
+
|
79
|
+
|
80
|
+
@deprecated("Use add_middleware instead", category=None)
|
7
81
|
def add_cors_middleware(app: "FastAPI"):
|
82
|
+
warnings.warn(
|
83
|
+
"add_cors_middleware is deprecated. Use add_middleware instead.",
|
84
|
+
CrypticornDeprecatedSince217,
|
85
|
+
)
|
86
|
+
app.add_middleware(
|
87
|
+
CORSMiddleware,
|
88
|
+
allow_origins=[
|
89
|
+
"http://localhost:5173", # vite dev server
|
90
|
+
"http://localhost:4173", # vite preview server
|
91
|
+
],
|
92
|
+
allow_origin_regex="^https://([a-zA-Z0-9-]+.)*crypticorn.(dev|com)/?$", # matches (multiple or no) subdomains of crypticorn.dev and crypticorn.com
|
93
|
+
allow_credentials=True,
|
94
|
+
allow_methods=["*"],
|
95
|
+
allow_headers=["*"],
|
96
|
+
)
|
97
|
+
|
98
|
+
|
99
|
+
def add_middleware(app: "FastAPI"):
|
8
100
|
app.add_middleware(
|
9
101
|
CORSMiddleware,
|
10
102
|
allow_origins=[
|
@@ -16,6 +108,7 @@ def add_cors_middleware(app: "FastAPI"):
|
|
16
108
|
allow_methods=["*"],
|
17
109
|
allow_headers=["*"],
|
18
110
|
)
|
111
|
+
app.add_middleware(PrometheusMiddleware)
|
19
112
|
|
20
113
|
|
21
114
|
@asynccontextmanager
|
crypticorn/common/pagination.py
CHANGED
@@ -1,53 +1,285 @@
|
|
1
|
-
"""Utilities for handling paginated API responses
|
1
|
+
"""Utilities for handling paginated API responses, cursor-based pagination, filtering, sorting, etc."""
|
2
2
|
|
3
|
-
from typing import Generic, Type, TypeVar, Optional, Literal
|
3
|
+
from typing import Annotated, Any, Generic, Type, TypeVar, Optional, Literal
|
4
4
|
from pydantic import BaseModel, Field, model_validator
|
5
|
+
import math
|
5
6
|
|
6
7
|
T = TypeVar("T")
|
7
8
|
|
8
9
|
|
9
10
|
class PaginatedResponse(BaseModel, Generic[T]):
|
10
11
|
"""Pydantic model for paginated response
|
11
|
-
>>>
|
12
|
+
>>> @router.get("", operation_id="getOrders")
|
13
|
+
>>> async def get_orders(
|
14
|
+
>>> params: Annotated[PaginationParams, Query()],
|
15
|
+
>>> ) -> PaginatedResponse[Order]:
|
16
|
+
>>> ...
|
17
|
+
>>> return PaginatedResponse[Order](
|
18
|
+
data=orders,
|
19
|
+
total=count,
|
20
|
+
page=params.page,
|
21
|
+
page_size=params.page_size,
|
22
|
+
prev=PaginatedResponse.get_prev_page(params.page),
|
23
|
+
next=PaginatedResponse.get_next_page(count, params.page_size, params.page),
|
24
|
+
last=PaginatedResponse.get_last_page(count, params.page_size),
|
25
|
+
)
|
12
26
|
"""
|
13
27
|
|
14
28
|
data: list[T]
|
15
29
|
total: int = Field(description="The total number of items")
|
16
30
|
page: int = Field(description="The current page number")
|
17
|
-
|
31
|
+
page_size: int = Field(description="The number of items per page")
|
18
32
|
prev: Optional[int] = Field(None, description="The previous page number")
|
19
33
|
next: Optional[int] = Field(None, description="The next page number")
|
34
|
+
last: Optional[int] = Field(None, description="The last page number")
|
35
|
+
|
36
|
+
@staticmethod
|
37
|
+
def get_next_page(total: int, page_size: int, page: int) -> Optional[int]:
|
38
|
+
"""Get the next page number"""
|
39
|
+
if page < math.ceil(total / page_size):
|
40
|
+
return page + 1
|
41
|
+
return None
|
42
|
+
|
43
|
+
@staticmethod
|
44
|
+
def get_prev_page(page: int) -> Optional[int]:
|
45
|
+
"""Get the previous page number"""
|
46
|
+
if page > 1:
|
47
|
+
return page - 1
|
48
|
+
return None
|
49
|
+
|
50
|
+
@staticmethod
|
51
|
+
def get_last_page(total: int, page_size: int) -> int:
|
52
|
+
"""Get the last page number"""
|
53
|
+
return max(1, math.ceil(total / page_size))
|
20
54
|
|
21
55
|
|
22
56
|
class PaginationParams(BaseModel, Generic[T]):
|
23
57
|
"""Standard pagination parameters for usage in API endpoints. Check the [fastapi docs](https://fastapi.tiangolo.com/tutorial/query-param-models/?h=qu#query-parameters-with-a-pydantic-model) for usage examples.
|
24
|
-
|
25
|
-
|
26
|
-
>>>
|
58
|
+
You should inherit from this class when adding additional parameters. You should use this class in combination with `PaginatedResponse` to return the paginated response.
|
59
|
+
Usage:
|
60
|
+
>>> @router.get("", operation_id="getOrders")
|
61
|
+
>>> async def get_orders(
|
62
|
+
>>> params: Annotated[PaginationParams[Order], Query()],
|
63
|
+
>>> ) -> PaginatedResponse[Order]:
|
64
|
+
>>> ...
|
65
|
+
|
66
|
+
The default size is 10 items per page and there is a `HeavyPaginationParams` class with 100 items per page. You can override this default:
|
67
|
+
>>> class LightPaginationParams(PaginationParams):
|
68
|
+
>>> page_size: int = Field(default=5, description="The number of items per page")
|
69
|
+
"""
|
70
|
+
|
71
|
+
page: Optional[int] = Field(default=1, description="The current page number")
|
72
|
+
page_size: Annotated[int, Field(ge=1, le=100)] = Field(
|
73
|
+
10, description="The number of items per page. Default is 10, max is 100."
|
74
|
+
)
|
75
|
+
|
76
|
+
|
77
|
+
class HeavyPaginationParams(PaginationParams[T]):
|
78
|
+
"""Pagination parameters with a higher default size. Refer to `PaginationParams` for usage examples."""
|
79
|
+
|
80
|
+
page_size: Annotated[int, Field(ge=1, le=1000)] = Field(
|
81
|
+
100, description="The number of items per page. Default is 100, max is 1000."
|
82
|
+
)
|
83
|
+
|
84
|
+
|
85
|
+
class SortParams(BaseModel, Generic[T]):
|
86
|
+
"""Standard sort parameters for usage in API endpoints. Check the [fastapi docs](https://fastapi.tiangolo.com/tutorial/query-param-models/?h=qu#query-parameters-with-a-pydantic-model) for usage examples.
|
87
|
+
You should inherit from this class when adding additional parameters.
|
88
|
+
Usage:
|
89
|
+
>>> @router.get("", operation_id="getOrders")
|
90
|
+
>>> async def get_orders(
|
91
|
+
>>> params: Annotated[SortParams[Order], Query()],
|
92
|
+
>>> ) -> PaginatedResponse[Order]:
|
93
|
+
>>> ...
|
27
94
|
"""
|
28
95
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
96
|
+
sort_order: Optional[Literal["asc", "desc"]] = Field(
|
97
|
+
None, description="The order to sort by"
|
98
|
+
)
|
99
|
+
sort_by: Optional[str] = Field(None, description="The field to sort by")
|
33
100
|
|
34
101
|
@model_validator(mode="after")
|
35
|
-
def
|
102
|
+
def validate_sort(self):
|
36
103
|
# Extract the generic argument type
|
37
104
|
args: tuple = self.__pydantic_generic_metadata__.get("args")
|
38
105
|
if not args or not issubclass(args[0], BaseModel):
|
39
106
|
raise TypeError(
|
40
|
-
"
|
107
|
+
"SortParams must be used with a Pydantic BaseModel as a generic parameter"
|
41
108
|
)
|
42
|
-
if self.
|
109
|
+
if self.sort_by:
|
43
110
|
# check if the sort field is valid
|
44
111
|
model: Type[BaseModel] = args[0]
|
45
|
-
if self.
|
112
|
+
if self.sort_by not in model.model_fields:
|
113
|
+
raise ValueError(
|
114
|
+
f"Invalid field: '{self.sort_by}'. Must be one of: {list(model.model_fields)}"
|
115
|
+
)
|
116
|
+
if self.sort_order and self.sort_order not in ["asc", "desc"]:
|
117
|
+
raise ValueError(
|
118
|
+
f"Invalid order: '{self.sort_order}' — must be one of: ['asc', 'desc']"
|
119
|
+
)
|
120
|
+
if (
|
121
|
+
self.sort_order
|
122
|
+
and self.sort_by is None
|
123
|
+
or self.sort_by
|
124
|
+
and self.sort_order is None
|
125
|
+
):
|
126
|
+
raise ValueError("sort_order and sort_by must be provided together")
|
127
|
+
return self
|
128
|
+
|
129
|
+
|
130
|
+
class FilterParams(BaseModel, Generic[T]):
|
131
|
+
"""Standard filter parameters for usage in API endpoints. Check the [fastapi docs](https://fastapi.tiangolo.com/tutorial/query-param-models/?h=qu#query-parameters-with-a-pydantic-model) for usage examples.
|
132
|
+
You should inherit from this class when adding additional parameters.
|
133
|
+
Usage:
|
134
|
+
>>> @router.get("", operation_id="getOrders")
|
135
|
+
>>> async def get_orders(
|
136
|
+
>>> params: Annotated[FilterParams[Order], Query()],
|
137
|
+
>>> ) -> PaginatedResponse[Order]:
|
138
|
+
>>> ...
|
139
|
+
"""
|
140
|
+
|
141
|
+
filter_by: Optional[str] = Field(None, description="The field to filter by")
|
142
|
+
filter_value: Optional[str] = Field(None, description="The value to filter with")
|
143
|
+
# currently openapi-gen does not support typing.Any in combo with None, so we use str
|
144
|
+
# this is fine since the input is a string anyways from the request and the correct type is enforced by the model validator from the filter_by field
|
145
|
+
|
146
|
+
@model_validator(mode="after")
|
147
|
+
def validate_filter(self):
|
148
|
+
if self.filter_by and not self.filter_value:
|
149
|
+
raise ValueError("filter_by and filter_value must be provided together")
|
150
|
+
if self.filter_by:
|
151
|
+
# Extract the generic argument type
|
152
|
+
args: tuple = self.__pydantic_generic_metadata__.get("args")
|
153
|
+
if not args or not issubclass(args[0], BaseModel):
|
154
|
+
raise TypeError(
|
155
|
+
"FilterParams must be used with a Pydantic BaseModel as a generic parameter"
|
156
|
+
)
|
157
|
+
# check if the filter field is valid
|
158
|
+
model: Type[BaseModel] = args[0]
|
159
|
+
if self.filter_by not in model.model_fields:
|
46
160
|
raise ValueError(
|
47
|
-
f"Invalid
|
161
|
+
f"Invalid field: '{self.filter_by}'. Must be one of: {list(model.model_fields)}"
|
48
162
|
)
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
raise ValueError("Sort and order must be provided together")
|
163
|
+
self.filter_value = _enforce_field_type(
|
164
|
+
model, self.filter_by, self.filter_value
|
165
|
+
)
|
53
166
|
return self
|
167
|
+
|
168
|
+
|
169
|
+
class SortFilterParams(SortParams[T], FilterParams[T]):
|
170
|
+
"""Combines sort and filter parameters. Just a convenience class for when you need to combine sort and filter parameters.
|
171
|
+
You should inherit from this class when adding additional parameters.
|
172
|
+
Usage:
|
173
|
+
>>> @router.get("", operation_id="getOrders")
|
174
|
+
>>> async def get_orders(
|
175
|
+
>>> params: Annotated[SortFilterParams[Order], Query()],
|
176
|
+
>>> ) -> PaginatedResponse[Order]:
|
177
|
+
>>> ...
|
178
|
+
"""
|
179
|
+
|
180
|
+
@model_validator(mode="after")
|
181
|
+
def validate_sort_filter(self):
|
182
|
+
self.validate_sort()
|
183
|
+
self.validate_filter()
|
184
|
+
return self
|
185
|
+
|
186
|
+
|
187
|
+
class PageFilterParams(PaginationParams[T], FilterParams[T]):
|
188
|
+
"""Combines pagination and filter parameters. Just a convenience class for when you need to combine pagination and filter parameters.
|
189
|
+
You should inherit from this class when adding additional parameters.
|
190
|
+
Usage:
|
191
|
+
>>> @router.get("", operation_id="getOrders")
|
192
|
+
>>> async def get_orders(
|
193
|
+
>>> params: Annotated[PageFilterParams[Order], Query()],
|
194
|
+
>>> ) -> PaginatedResponse[Order]:
|
195
|
+
>>> ...
|
196
|
+
"""
|
197
|
+
|
198
|
+
@model_validator(mode="after")
|
199
|
+
def validate_page_filter(self):
|
200
|
+
self.validate_filter()
|
201
|
+
return self
|
202
|
+
|
203
|
+
|
204
|
+
class PageSortParams(PaginationParams[T], SortParams[T]):
|
205
|
+
"""Combines pagination and sort parameters. Just a convenience class for when you need to combine pagination and sort parameters.
|
206
|
+
You should inherit from this class when adding additional parameters.
|
207
|
+
Usage:
|
208
|
+
>>> @router.get("", operation_id="getOrders")
|
209
|
+
>>> async def get_orders(
|
210
|
+
>>> params: Annotated[PageSortParams[Order], Query()],
|
211
|
+
>>> ) -> PaginatedResponse[Order]:
|
212
|
+
>>> ...
|
213
|
+
"""
|
214
|
+
|
215
|
+
@model_validator(mode="after")
|
216
|
+
def validate_page_sort(self):
|
217
|
+
self.validate_sort()
|
218
|
+
return self
|
219
|
+
|
220
|
+
|
221
|
+
class PageSortFilterParams(
|
222
|
+
PaginationParams[T],
|
223
|
+
SortParams[T],
|
224
|
+
FilterParams[T],
|
225
|
+
):
|
226
|
+
"""Combines pagination, filter, and sort parameters. Just a convenience class for when you need to combine pagination, filter, and sort parameters.
|
227
|
+
You should inherit from this class when adding additional parameters.
|
228
|
+
Usage:
|
229
|
+
>>> @router.get("", operation_id="getOrders")
|
230
|
+
>>> async def get_orders(
|
231
|
+
>>> params: Annotated[PageSortFilterParams[Order], Query()],
|
232
|
+
>>> ) -> PaginatedResponse[Order]:
|
233
|
+
>>> ...
|
234
|
+
"""
|
235
|
+
|
236
|
+
@model_validator(mode="after")
|
237
|
+
def validate_filter_combo(self):
|
238
|
+
self.validate_filter()
|
239
|
+
self.validate_sort()
|
240
|
+
return self
|
241
|
+
|
242
|
+
|
243
|
+
class HeavyPageSortFilterParams(
|
244
|
+
HeavyPaginationParams[T], FilterParams[T], SortParams[T]
|
245
|
+
):
|
246
|
+
"""Combines heavy pagination, filter, and sort parameters. Just a convenience class for when you need to combine heavy pagination, filter, and sort parameters.
|
247
|
+
You should inherit from this class when adding additional parameters.
|
248
|
+
Usage:
|
249
|
+
>>> @router.get("", operation_id="getOrders")
|
250
|
+
>>> async def get_orders(
|
251
|
+
>>> params: Annotated[HeavyPageSortFilterParams[Order], Query()],
|
252
|
+
>>> ) -> PaginatedResponse[Order]:
|
253
|
+
>>> ...
|
254
|
+
"""
|
255
|
+
|
256
|
+
@model_validator(mode="after")
|
257
|
+
def validate_heavy_page_sort_filter(self):
|
258
|
+
self.validate_filter()
|
259
|
+
self.validate_sort()
|
260
|
+
return self
|
261
|
+
|
262
|
+
|
263
|
+
def _enforce_field_type(model: Type[BaseModel], field_name: str, value: Any) -> Any:
|
264
|
+
"""
|
265
|
+
Coerce or validate `value` to match the type of `field_name` on the given `model`. Should be used after checking that the field is valid.
|
266
|
+
|
267
|
+
:param model: The Pydantic model.
|
268
|
+
:param field_name: The name of the field to match.
|
269
|
+
:param value: The value to validate or coerce.
|
270
|
+
|
271
|
+
:return: The value cast to the expected type.
|
272
|
+
|
273
|
+
:raises: ValueError: If the field doesn't exist or coercion fails.
|
274
|
+
"""
|
275
|
+
expected_type = model.model_fields[field_name].annotation
|
276
|
+
|
277
|
+
if isinstance(value, expected_type):
|
278
|
+
return value
|
279
|
+
|
280
|
+
try:
|
281
|
+
return expected_type(value)
|
282
|
+
except Exception:
|
283
|
+
raise ValueError(
|
284
|
+
f"Expected {expected_type} for field {field_name}, got {type(value)}"
|
285
|
+
)
|
@@ -11,10 +11,10 @@ import threading
|
|
11
11
|
import time
|
12
12
|
import psutil
|
13
13
|
import re
|
14
|
-
|
14
|
+
import logging
|
15
15
|
from typing import Literal
|
16
|
+
from fastapi import APIRouter, Query
|
16
17
|
from crypticorn.common.logging import LogLevel
|
17
|
-
import logging
|
18
18
|
|
19
19
|
router = APIRouter(tags=["Admin"], prefix="/admin")
|
20
20
|
|
@@ -2,14 +2,42 @@
|
|
2
2
|
This module contains the status router for the API.
|
3
3
|
It provides endpoints for checking the status of the API and get the server's time.
|
4
4
|
SHOULD ALLOW ACCESS TO THIS ROUTER WITHOUT AUTH.
|
5
|
+
To enable metrics, pass enable_metrics=True and the auth_handler to the router.
|
6
|
+
>>> status_router.enable_metrics = True
|
7
|
+
>>> status_router.auth_handler = auth_handler
|
8
|
+
Then include the router in the FastAPI app.
|
5
9
|
>>> app.include_router(status_router)
|
6
10
|
"""
|
7
11
|
|
8
12
|
from datetime import datetime
|
9
|
-
from typing import Literal
|
10
13
|
from fastapi import APIRouter, Request
|
14
|
+
from typing import Annotated, Literal
|
15
|
+
from fastapi import APIRouter, Response, Depends
|
16
|
+
from fastapi.security import HTTPBasicCredentials
|
17
|
+
from prometheus_client import generate_latest, CONTENT_TYPE_LATEST
|
18
|
+
from crypticorn.common.metrics import registry
|
19
|
+
from crypticorn.common.auth import AuthHandler
|
11
20
|
|
12
|
-
|
21
|
+
|
22
|
+
class EnhancedApiRouter(APIRouter):
|
23
|
+
def __init__(
|
24
|
+
self,
|
25
|
+
enable_metrics: bool = False,
|
26
|
+
auth_handler: AuthHandler = AuthHandler(),
|
27
|
+
*args,
|
28
|
+
**kwargs
|
29
|
+
):
|
30
|
+
"""
|
31
|
+
Enhanced API Router that allows for metrics and authentication.
|
32
|
+
If enable_metrics is True, the router will include the metrics endpoint.
|
33
|
+
If auth_handler is provided, the router will use the auth handler to authenticate requests with
|
34
|
+
"""
|
35
|
+
super().__init__(*args, **kwargs)
|
36
|
+
self.enable_metrics = enable_metrics
|
37
|
+
self.auth_handler = auth_handler
|
38
|
+
|
39
|
+
|
40
|
+
router = EnhancedApiRouter(tags=["Status"], prefix="")
|
13
41
|
|
14
42
|
|
15
43
|
@router.get("/", operation_id="ping")
|
@@ -29,3 +57,13 @@ async def time(type: Literal["iso", "unix"] = "iso") -> str:
|
|
29
57
|
return datetime.now().isoformat()
|
30
58
|
elif type == "unix":
|
31
59
|
return str(int(datetime.now().timestamp()))
|
60
|
+
|
61
|
+
|
62
|
+
@router.get("/metrics", operation_id="getMetrics", include_in_schema=True)
|
63
|
+
def metrics(
|
64
|
+
username: Annotated[HTTPBasicCredentials, Depends(router.auth_handler.basic_auth)],
|
65
|
+
):
|
66
|
+
"""
|
67
|
+
Get Prometheus metrics for the application. Returns plain text.
|
68
|
+
"""
|
69
|
+
return Response(generate_latest(registry), media_type=CONTENT_TYPE_LATEST)
|
crypticorn/common/scopes.py
CHANGED
@@ -16,7 +16,7 @@ class Scope(StrEnum):
|
|
16
16
|
|
17
17
|
# Scopes that can be purchased - these actually exist in the jwt token
|
18
18
|
READ_PREDICTIONS = "read:predictions"
|
19
|
-
|
19
|
+
READ_DEX_SIGNALS = "read:dex:signals"
|
20
20
|
|
21
21
|
# Hive scopes
|
22
22
|
READ_HIVE_MODEL = "read:hive:model"
|
@@ -88,5 +88,5 @@ class Scope(StrEnum):
|
|
88
88
|
cls.READ_METRICS_MARKETS,
|
89
89
|
cls.READ_KLINES,
|
90
90
|
cls.READ_SENTIMENT,
|
91
|
-
cls.
|
91
|
+
cls.READ_DEX_SIGNALS,
|
92
92
|
]
|
crypticorn/common/warnings.py
CHANGED
@@ -63,6 +63,13 @@ class CrypticornDeprecatedSince215(CrypticornDeprecationWarning):
|
|
63
63
|
super().__init__(message, *args, since=(2, 15), expected_removal=(3, 0))
|
64
64
|
|
65
65
|
|
66
|
+
class CrypticornDeprecatedSince217(CrypticornDeprecationWarning):
|
67
|
+
"""A specific `CrypticornDeprecationWarning` subclass defining functionality deprecated since Crypticorn 2.17."""
|
68
|
+
|
69
|
+
def __init__(self, message: str, *args: object) -> None:
|
70
|
+
super().__init__(message, *args, since=(2, 17), expected_removal=(3, 0))
|
71
|
+
|
72
|
+
|
66
73
|
class CrypticornExperimentalWarning(Warning):
|
67
74
|
"""A Crypticorn specific experimental functionality warning.
|
68
75
|
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
# flake8: noqa
|
4
|
+
|
5
|
+
"""
|
6
|
+
DEX AI API
|
7
|
+
|
8
|
+
API for DEX AI
|
9
|
+
|
10
|
+
The version of the OpenAPI document: 1.0.0
|
11
|
+
Generated by OpenAPI Generator (https://openapi-generator.tech)
|
12
|
+
|
13
|
+
Do not edit the class manually.
|
14
|
+
""" # noqa: E501
|
15
|
+
|
16
|
+
|
17
|
+
__version__ = "1.0.0"
|
18
|
+
|
19
|
+
# import apis into sdk package
|
20
|
+
from crypticorn.dex.client.api.admin_api import AdminApi
|
21
|
+
from crypticorn.dex.client.api.signals_api import SignalsApi
|
22
|
+
from crypticorn.dex.client.api.status_api import StatusApi
|
23
|
+
|
24
|
+
# import ApiClient
|
25
|
+
from crypticorn.dex.client.api_response import ApiResponse
|
26
|
+
from crypticorn.dex.client.api_client import ApiClient
|
27
|
+
from crypticorn.dex.client.configuration import Configuration
|
28
|
+
from crypticorn.dex.client.exceptions import OpenApiException
|
29
|
+
from crypticorn.dex.client.exceptions import ApiTypeError
|
30
|
+
from crypticorn.dex.client.exceptions import ApiValueError
|
31
|
+
from crypticorn.dex.client.exceptions import ApiKeyError
|
32
|
+
from crypticorn.dex.client.exceptions import ApiAttributeError
|
33
|
+
from crypticorn.dex.client.exceptions import ApiException
|
34
|
+
|
35
|
+
# import models into sdk package
|
36
|
+
from crypticorn.dex.client.models.api_error_identifier import ApiErrorIdentifier
|
37
|
+
from crypticorn.dex.client.models.api_error_level import ApiErrorLevel
|
38
|
+
from crypticorn.dex.client.models.api_error_type import ApiErrorType
|
39
|
+
from crypticorn.dex.client.models.exception_detail import ExceptionDetail
|
40
|
+
from crypticorn.dex.client.models.log_level import LogLevel
|
41
|
+
from crypticorn.dex.client.models.paginated_response_signal_with_token import (
|
42
|
+
PaginatedResponseSignalWithToken,
|
43
|
+
)
|
44
|
+
from crypticorn.dex.client.models.risk import Risk
|
45
|
+
from crypticorn.dex.client.models.signal_overview_stats import SignalOverviewStats
|
46
|
+
from crypticorn.dex.client.models.signal_volume import SignalVolume
|
47
|
+
from crypticorn.dex.client.models.signal_with_token import SignalWithToken
|
48
|
+
from crypticorn.dex.client.models.token_data import TokenData
|
49
|
+
from crypticorn.dex.client.models.token_detail import TokenDetail
|