crypticorn 2.5.2__py3-none-any.whl → 2.6.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/auth/client/__init__.py +9 -0
- crypticorn/auth/client/api/auth_api.py +8 -5
- crypticorn/auth/client/api/user_api.py +247 -0
- crypticorn/auth/client/models/__init__.py +9 -0
- crypticorn/auth/client/models/create_api_key_request.py +2 -1
- crypticorn/auth/client/models/get_api_keys200_response_inner.py +2 -1
- crypticorn/{hive/client/models/http_validation_error.py → auth/client/models/user_by_username200_response.py} +14 -22
- crypticorn/auth/client/models/verify200_response.py +14 -1
- crypticorn/auth/client/models/verify_email200_response_auth_auth.py +14 -1
- crypticorn/auth/client/models/whoami200_response.py +6 -1
- crypticorn/cli/init.py +3 -0
- crypticorn/common/__init__.py +2 -1
- crypticorn/common/auth.py +13 -9
- crypticorn/common/errors.py +312 -105
- crypticorn/common/exceptions.py +38 -4
- crypticorn/common/mixins.py +19 -0
- crypticorn/common/pagination.py +49 -0
- crypticorn/common/scopes.py +27 -24
- crypticorn/common/status_router.py +9 -7
- crypticorn/common/utils.py +12 -6
- crypticorn/hive/client/__init__.py +3 -5
- crypticorn/hive/client/api/data_api.py +1 -33
- crypticorn/hive/client/api/models_api.py +351 -160
- crypticorn/hive/client/api/status_api.py +481 -9
- crypticorn/hive/client/configuration.py +12 -4
- crypticorn/hive/client/models/__init__.py +3 -5
- crypticorn/hive/client/models/coins.py +0 -1
- crypticorn/hive/client/models/data_info.py +44 -12
- crypticorn/hive/client/models/data_version.py +0 -1
- crypticorn/{pay/client/models/now_api_status_res.py → hive/client/models/data_version_info.py} +17 -11
- crypticorn/hive/client/models/exception_detail.py +114 -0
- crypticorn/hive/client/models/model.py +2 -3
- crypticorn/hive/client/models/{validation_error.py → target_info.py} +14 -25
- crypticorn/hive/client/rest.py +4 -1
- crypticorn/klines/client/api/status_api.py +481 -6
- crypticorn/klines/client/api/udf_api.py +0 -227
- crypticorn/metrics/client/api/status_api.py +476 -1
- crypticorn/pay/client/__init__.py +3 -8
- crypticorn/pay/client/api/now_payments_api.py +14 -17
- crypticorn/pay/client/api/payments_api.py +2 -11
- crypticorn/pay/client/api/products_api.py +2 -11
- crypticorn/pay/client/api/status_api.py +483 -8
- crypticorn/pay/client/api_client.py +2 -2
- crypticorn/pay/client/configuration.py +3 -3
- crypticorn/pay/client/exceptions.py +2 -2
- crypticorn/pay/client/models/__init__.py +3 -8
- crypticorn/pay/client/models/{validation_error.py → exception_detail.py} +37 -28
- crypticorn/pay/client/models/now_create_invoice_req.py +2 -2
- crypticorn/pay/client/models/now_create_invoice_res.py +2 -2
- crypticorn/pay/client/models/payment.py +2 -2
- crypticorn/pay/client/models/payment_status.py +2 -2
- crypticorn/pay/client/models/product_create.py +2 -2
- crypticorn/pay/client/models/product_read.py +2 -2
- crypticorn/pay/client/models/product_sub_read.py +2 -2
- crypticorn/pay/client/models/product_update.py +2 -2
- crypticorn/pay/client/models/scope.py +2 -2
- crypticorn/pay/client/models/services.py +2 -2
- crypticorn/pay/client/rest.py +2 -2
- crypticorn/trade/client/__init__.py +3 -7
- crypticorn/trade/client/api/api_keys_api.py +5 -20
- crypticorn/trade/client/api/bots_api.py +7 -19
- crypticorn/trade/client/api/exchanges_api.py +2 -2
- crypticorn/trade/client/api/futures_trading_panel_api.py +10 -22
- crypticorn/trade/client/api/notifications_api.py +10 -25
- crypticorn/trade/client/api/orders_api.py +2 -5
- crypticorn/trade/client/api/status_api.py +483 -8
- crypticorn/trade/client/api/strategies_api.py +5 -17
- crypticorn/trade/client/api/trading_actions_api.py +2 -11
- crypticorn/trade/client/api_client.py +2 -2
- crypticorn/trade/client/configuration.py +3 -3
- crypticorn/trade/client/exceptions.py +2 -2
- crypticorn/trade/client/models/__init__.py +3 -7
- crypticorn/trade/client/models/action_model.py +2 -2
- crypticorn/trade/client/models/bot_model.py +2 -2
- crypticorn/trade/client/models/bot_status.py +2 -2
- crypticorn/trade/client/models/{validation_error.py → exception_detail.py} +37 -28
- crypticorn/trade/client/models/exchange_key_model.py +2 -2
- crypticorn/trade/client/models/execution_ids.py +2 -2
- crypticorn/trade/client/models/futures_balance.py +2 -2
- crypticorn/trade/client/models/futures_trading_action.py +2 -2
- crypticorn/trade/client/models/margin_mode.py +2 -2
- crypticorn/trade/client/models/notification_model.py +2 -2
- crypticorn/trade/client/models/order_model.py +2 -2
- crypticorn/trade/client/models/order_status.py +2 -2
- crypticorn/trade/client/models/post_futures_action.py +2 -2
- crypticorn/trade/client/models/spot_trading_action.py +2 -2
- crypticorn/trade/client/models/strategy_exchange_info.py +2 -2
- crypticorn/trade/client/models/strategy_model_input.py +2 -2
- crypticorn/trade/client/models/strategy_model_output.py +2 -2
- crypticorn/trade/client/models/tpsl.py +2 -2
- crypticorn/trade/client/models/trading_action_type.py +2 -2
- crypticorn/trade/client/rest.py +2 -2
- {crypticorn-2.5.2.dist-info → crypticorn-2.6.0.dist-info}/METADATA +1 -1
- {crypticorn-2.5.2.dist-info → crypticorn-2.6.0.dist-info}/RECORD +97 -100
- {crypticorn-2.5.2.dist-info → crypticorn-2.6.0.dist-info}/WHEEL +1 -1
- crypticorn/hive/client/models/validation_error_loc_inner.py +0 -159
- crypticorn/pay/client/models/http_validation_error.py +0 -99
- crypticorn/pay/client/models/validation_error_loc_inner.py +0 -159
- crypticorn/trade/client/models/http_validation_error.py +0 -99
- crypticorn/trade/client/models/validation_error_loc_inner.py +0 -159
- {crypticorn-2.5.2.dist-info → crypticorn-2.6.0.dist-info}/entry_points.txt +0 -0
- {crypticorn-2.5.2.dist-info → crypticorn-2.6.0.dist-info}/top_level.txt +0 -0
crypticorn/common/errors.py
CHANGED
@@ -1,22 +1,6 @@
|
|
1
|
-
from enum import Enum,
|
2
|
-
import logging
|
1
|
+
from enum import Enum, StrEnum
|
3
2
|
from fastapi import status
|
4
|
-
from crypticorn.common.mixins import ExcludeEnumMixin
|
5
|
-
|
6
|
-
logger = logging.getLogger(__name__)
|
7
|
-
|
8
|
-
|
9
|
-
class Fallback(EnumMeta):
|
10
|
-
"""Fallback to UNKNOWN_ERROR for error codes not yet published to PyPI."""
|
11
|
-
|
12
|
-
def __getattr__(cls, name):
|
13
|
-
# Let Pydantic/internal stuff pass silently ! fragile
|
14
|
-
if name.startswith("__"):
|
15
|
-
raise AttributeError(name)
|
16
|
-
logger.warning(
|
17
|
-
f"Unknown error code '{name}' - update crypticorn package or check for typos"
|
18
|
-
)
|
19
|
-
return cls.UNKNOWN_ERROR
|
3
|
+
from crypticorn.common.mixins import ExcludeEnumMixin, ApiErrorFallback
|
20
4
|
|
21
5
|
|
22
6
|
class ApiErrorType(ExcludeEnumMixin, StrEnum):
|
@@ -37,7 +21,6 @@ class ApiErrorIdentifier(ExcludeEnumMixin, StrEnum):
|
|
37
21
|
|
38
22
|
ALLOCATION_BELOW_EXPOSURE = "allocation_below_current_exposure"
|
39
23
|
ALLOCATION_BELOW_MINIMUM = "allocation_below_min_amount"
|
40
|
-
ALPHANUMERIC_CHARACTERS_ONLY = "alphanumeric_characters_only"
|
41
24
|
BLACK_SWAN = "black_swan"
|
42
25
|
BOT_ALREADY_DELETED = "bot_already_deleted"
|
43
26
|
BOT_DISABLED = "bot_disabled"
|
@@ -73,6 +56,7 @@ class ApiErrorIdentifier(ExcludeEnumMixin, StrEnum):
|
|
73
56
|
INVALID_DATA_RESPONSE = "invalid_data_response"
|
74
57
|
INVALID_EXCHANGE_KEY = "invalid_exchange_key"
|
75
58
|
INVALID_MARGIN_MODE = "invalid_margin_mode"
|
59
|
+
INVALID_MODEL_NAME = "invalid_model_name"
|
76
60
|
INVALID_PARAMETER = "invalid_parameter_provided"
|
77
61
|
LEVERAGE_EXCEEDED = "leverage_limit_exceeded"
|
78
62
|
LIQUIDATION_PRICE_VIOLATION = "order_violates_liquidation_price_constraints"
|
@@ -112,7 +96,7 @@ class ApiErrorIdentifier(ExcludeEnumMixin, StrEnum):
|
|
112
96
|
URL_NOT_FOUND = "requested_resource_not_found"
|
113
97
|
|
114
98
|
@property
|
115
|
-
def
|
99
|
+
def get_error(self) -> "ApiError":
|
116
100
|
"""Get the corresponding ApiError."""
|
117
101
|
return ApiError[self.value]
|
118
102
|
|
@@ -126,7 +110,7 @@ class ApiErrorLevel(ExcludeEnumMixin, StrEnum):
|
|
126
110
|
WARNING = "warning"
|
127
111
|
|
128
112
|
|
129
|
-
class ApiError(ExcludeEnumMixin, Enum, metaclass=
|
113
|
+
class ApiError(ExcludeEnumMixin, Enum, metaclass=ApiErrorFallback):
|
130
114
|
"""API error codes. Fallback to UNKNOWN_ERROR for error codes not yet published to PyPI."""
|
131
115
|
|
132
116
|
ALLOCATION_BELOW_EXPOSURE = (
|
@@ -139,11 +123,6 @@ class ApiError(ExcludeEnumMixin, Enum, metaclass=Fallback):
|
|
139
123
|
ApiErrorType.USER_ERROR,
|
140
124
|
ApiErrorLevel.ERROR,
|
141
125
|
)
|
142
|
-
ALPHANUMERIC_CHARACTERS_ONLY = (
|
143
|
-
ApiErrorIdentifier.ALPHANUMERIC_CHARACTERS_ONLY,
|
144
|
-
ApiErrorType.USER_ERROR,
|
145
|
-
ApiErrorLevel.ERROR,
|
146
|
-
)
|
147
126
|
BLACK_SWAN = (
|
148
127
|
ApiErrorIdentifier.BLACK_SWAN,
|
149
128
|
ApiErrorType.USER_ERROR,
|
@@ -284,6 +263,11 @@ class ApiError(ExcludeEnumMixin, Enum, metaclass=Fallback):
|
|
284
263
|
ApiErrorType.USER_ERROR,
|
285
264
|
ApiErrorLevel.ERROR,
|
286
265
|
)
|
266
|
+
INVALID_MODEL_NAME = (
|
267
|
+
ApiErrorIdentifier.INVALID_MODEL_NAME,
|
268
|
+
ApiErrorType.USER_ERROR,
|
269
|
+
ApiErrorLevel.ERROR,
|
270
|
+
)
|
287
271
|
INSUFFICIENT_SCOPES = (
|
288
272
|
ApiErrorIdentifier.INSUFFICIENT_SCOPES,
|
289
273
|
ApiErrorType.USER_ERROR,
|
@@ -492,7 +476,7 @@ class ApiError(ExcludeEnumMixin, Enum, metaclass=Fallback):
|
|
492
476
|
)
|
493
477
|
UNKNOWN_ERROR = (
|
494
478
|
ApiErrorIdentifier.UNKNOWN_ERROR,
|
495
|
-
ApiErrorType.
|
479
|
+
ApiErrorType.SERVER_ERROR,
|
496
480
|
ApiErrorLevel.ERROR,
|
497
481
|
)
|
498
482
|
URL_NOT_FOUND = (
|
@@ -517,102 +501,325 @@ class ApiError(ExcludeEnumMixin, Enum, metaclass=Fallback):
|
|
517
501
|
return self.value[2]
|
518
502
|
|
519
503
|
@property
|
520
|
-
def
|
504
|
+
def http_code(self) -> int:
|
521
505
|
"""HTTP status code for the error."""
|
522
|
-
return
|
506
|
+
return StatusCodeMapper.get_http_code(self)
|
507
|
+
|
508
|
+
@property
|
509
|
+
def websocket_code(self) -> int:
|
510
|
+
"""WebSocket status code for the error."""
|
511
|
+
return StatusCodeMapper.get_websocket_code(self)
|
523
512
|
|
524
513
|
|
525
|
-
class
|
514
|
+
class StatusCodeMapper:
|
526
515
|
"""Map API errors to HTTP status codes."""
|
527
516
|
|
528
517
|
_mapping = {
|
529
518
|
# Authentication/Authorization
|
530
|
-
ApiError.EXPIRED_BEARER:
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
ApiError.
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
ApiError.
|
539
|
-
|
519
|
+
ApiError.EXPIRED_BEARER: (
|
520
|
+
status.HTTP_401_UNAUTHORIZED,
|
521
|
+
status.WS_1008_POLICY_VIOLATION,
|
522
|
+
),
|
523
|
+
ApiError.INVALID_BEARER: (
|
524
|
+
status.HTTP_401_UNAUTHORIZED,
|
525
|
+
status.WS_1008_POLICY_VIOLATION,
|
526
|
+
),
|
527
|
+
ApiError.EXPIRED_API_KEY: (
|
528
|
+
status.HTTP_401_UNAUTHORIZED,
|
529
|
+
status.WS_1008_POLICY_VIOLATION,
|
530
|
+
),
|
531
|
+
ApiError.INVALID_API_KEY: (
|
532
|
+
status.HTTP_401_UNAUTHORIZED,
|
533
|
+
status.WS_1008_POLICY_VIOLATION,
|
534
|
+
),
|
535
|
+
ApiError.NO_CREDENTIALS: (
|
536
|
+
status.HTTP_401_UNAUTHORIZED,
|
537
|
+
status.WS_1008_POLICY_VIOLATION,
|
538
|
+
),
|
539
|
+
ApiError.INSUFFICIENT_SCOPES: (
|
540
|
+
status.HTTP_403_FORBIDDEN,
|
541
|
+
status.WS_1008_POLICY_VIOLATION,
|
542
|
+
),
|
543
|
+
ApiError.EXCHANGE_PERMISSION_DENIED: (
|
544
|
+
status.HTTP_403_FORBIDDEN,
|
545
|
+
status.WS_1008_POLICY_VIOLATION,
|
546
|
+
),
|
547
|
+
ApiError.EXCHANGE_USER_FROZEN: (
|
548
|
+
status.HTTP_403_FORBIDDEN,
|
549
|
+
status.WS_1008_POLICY_VIOLATION,
|
550
|
+
),
|
551
|
+
ApiError.TRADING_LOCKED: (
|
552
|
+
status.HTTP_403_FORBIDDEN,
|
553
|
+
status.WS_1008_POLICY_VIOLATION,
|
554
|
+
),
|
555
|
+
ApiError.FORBIDDEN: (
|
556
|
+
status.HTTP_403_FORBIDDEN,
|
557
|
+
status.WS_1008_POLICY_VIOLATION,
|
558
|
+
),
|
540
559
|
# Not Found
|
541
|
-
ApiError.URL_NOT_FOUND:
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
ApiError.
|
560
|
+
ApiError.URL_NOT_FOUND: (
|
561
|
+
status.HTTP_404_NOT_FOUND,
|
562
|
+
status.WS_1008_POLICY_VIOLATION,
|
563
|
+
),
|
564
|
+
ApiError.OBJECT_NOT_FOUND: (
|
565
|
+
status.HTTP_404_NOT_FOUND,
|
566
|
+
status.WS_1008_POLICY_VIOLATION,
|
567
|
+
),
|
568
|
+
ApiError.ORDER_NOT_FOUND: (
|
569
|
+
status.HTTP_404_NOT_FOUND,
|
570
|
+
status.WS_1008_POLICY_VIOLATION,
|
571
|
+
),
|
572
|
+
ApiError.POSITION_NOT_FOUND: (
|
573
|
+
status.HTTP_404_NOT_FOUND,
|
574
|
+
status.WS_1008_POLICY_VIOLATION,
|
575
|
+
),
|
576
|
+
ApiError.SYMBOL_NOT_FOUND: (
|
577
|
+
status.HTTP_404_NOT_FOUND,
|
578
|
+
status.WS_1008_POLICY_VIOLATION,
|
579
|
+
),
|
546
580
|
# Conflicts/Duplicates
|
547
|
-
ApiError.CLIENT_ORDER_ID_REPEATED:
|
548
|
-
|
549
|
-
|
550
|
-
|
551
|
-
ApiError.
|
581
|
+
ApiError.CLIENT_ORDER_ID_REPEATED: (
|
582
|
+
status.HTTP_409_CONFLICT,
|
583
|
+
status.WS_1008_POLICY_VIOLATION,
|
584
|
+
),
|
585
|
+
ApiError.OBJECT_ALREADY_EXISTS: (
|
586
|
+
status.HTTP_409_CONFLICT,
|
587
|
+
status.WS_1008_POLICY_VIOLATION,
|
588
|
+
),
|
589
|
+
ApiError.EXCHANGE_KEY_ALREADY_EXISTS: (
|
590
|
+
status.HTTP_409_CONFLICT,
|
591
|
+
status.WS_1008_POLICY_VIOLATION,
|
592
|
+
),
|
593
|
+
ApiError.BOT_ALREADY_DELETED: (
|
594
|
+
status.HTTP_409_CONFLICT,
|
595
|
+
status.WS_1008_POLICY_VIOLATION,
|
596
|
+
),
|
597
|
+
ApiError.STRATEGY_ALREADY_EXISTS: (
|
598
|
+
status.HTTP_409_CONFLICT,
|
599
|
+
status.WS_1008_POLICY_VIOLATION,
|
600
|
+
),
|
552
601
|
# Invalid Content
|
553
|
-
ApiError.CONTENT_TYPE_ERROR:
|
554
|
-
|
555
|
-
|
602
|
+
ApiError.CONTENT_TYPE_ERROR: (
|
603
|
+
status.HTTP_415_UNSUPPORTED_MEDIA_TYPE,
|
604
|
+
status.WS_1003_UNSUPPORTED_DATA,
|
605
|
+
),
|
606
|
+
ApiError.INVALID_DATA_REQUEST: (
|
607
|
+
status.HTTP_422_UNPROCESSABLE_ENTITY,
|
608
|
+
status.WS_1007_INVALID_FRAME_PAYLOAD_DATA,
|
609
|
+
),
|
610
|
+
ApiError.INVALID_DATA_RESPONSE: (
|
611
|
+
status.HTTP_422_UNPROCESSABLE_ENTITY,
|
612
|
+
status.WS_1007_INVALID_FRAME_PAYLOAD_DATA,
|
613
|
+
),
|
556
614
|
# Rate Limits
|
557
|
-
ApiError.EXCHANGE_RATE_LIMIT:
|
558
|
-
|
615
|
+
ApiError.EXCHANGE_RATE_LIMIT: (
|
616
|
+
status.HTTP_429_TOO_MANY_REQUESTS,
|
617
|
+
status.WS_1013_TRY_AGAIN_LATER,
|
618
|
+
),
|
619
|
+
ApiError.REQUEST_SCOPE_EXCEEDED: (
|
620
|
+
status.HTTP_429_TOO_MANY_REQUESTS,
|
621
|
+
status.WS_1013_TRY_AGAIN_LATER,
|
622
|
+
),
|
559
623
|
# Server Errors
|
560
|
-
ApiError.UNKNOWN_ERROR:
|
561
|
-
|
562
|
-
|
563
|
-
|
624
|
+
ApiError.UNKNOWN_ERROR: (
|
625
|
+
status.HTTP_500_INTERNAL_SERVER_ERROR,
|
626
|
+
status.WS_1011_INTERNAL_ERROR,
|
627
|
+
),
|
628
|
+
ApiError.EXCHANGE_SYSTEM_ERROR: (
|
629
|
+
status.HTTP_500_INTERNAL_SERVER_ERROR,
|
630
|
+
status.WS_1011_INTERNAL_ERROR,
|
631
|
+
),
|
632
|
+
ApiError.NOW_API_DOWN: (
|
633
|
+
status.HTTP_500_INTERNAL_SERVER_ERROR,
|
634
|
+
status.WS_1011_INTERNAL_ERROR,
|
635
|
+
),
|
636
|
+
ApiError.RPC_TIMEOUT: (
|
637
|
+
status.HTTP_500_INTERNAL_SERVER_ERROR,
|
638
|
+
status.WS_1011_INTERNAL_ERROR,
|
639
|
+
),
|
564
640
|
# Service Unavailable
|
565
|
-
ApiError.EXCHANGE_SERVICE_UNAVAILABLE:
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
ApiError.
|
570
|
-
|
641
|
+
ApiError.EXCHANGE_SERVICE_UNAVAILABLE: (
|
642
|
+
status.HTTP_503_SERVICE_UNAVAILABLE,
|
643
|
+
status.WS_1011_INTERNAL_ERROR,
|
644
|
+
),
|
645
|
+
ApiError.EXCHANGE_MAINTENANCE: (
|
646
|
+
status.HTTP_503_SERVICE_UNAVAILABLE,
|
647
|
+
status.WS_1011_INTERNAL_ERROR,
|
648
|
+
),
|
649
|
+
ApiError.EXCHANGE_SYSTEM_BUSY: (
|
650
|
+
status.HTTP_503_SERVICE_UNAVAILABLE,
|
651
|
+
status.WS_1011_INTERNAL_ERROR,
|
652
|
+
),
|
653
|
+
ApiError.SETTLEMENT_IN_PROGRESS: (
|
654
|
+
status.HTTP_503_SERVICE_UNAVAILABLE,
|
655
|
+
status.WS_1011_INTERNAL_ERROR,
|
656
|
+
),
|
657
|
+
ApiError.POSITION_SUSPENDED: (
|
658
|
+
status.HTTP_503_SERVICE_UNAVAILABLE,
|
659
|
+
status.WS_1011_INTERNAL_ERROR,
|
660
|
+
),
|
661
|
+
ApiError.TRADING_SUSPENDED: (
|
662
|
+
status.HTTP_503_SERVICE_UNAVAILABLE,
|
663
|
+
status.WS_1011_INTERNAL_ERROR,
|
664
|
+
),
|
571
665
|
# Bad Requests (400) - Invalid parameters or states
|
572
|
-
ApiError.
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
ApiError.
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
ApiError.
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
ApiError.
|
585
|
-
|
666
|
+
ApiError.INVALID_MODEL_NAME: (
|
667
|
+
status.HTTP_400_BAD_REQUEST,
|
668
|
+
status.WS_1008_POLICY_VIOLATION,
|
669
|
+
),
|
670
|
+
ApiError.ALLOCATION_BELOW_EXPOSURE: (
|
671
|
+
status.HTTP_400_BAD_REQUEST,
|
672
|
+
status.WS_1008_POLICY_VIOLATION,
|
673
|
+
),
|
674
|
+
ApiError.ALLOCATION_BELOW_MINIMUM: (
|
675
|
+
status.HTTP_400_BAD_REQUEST,
|
676
|
+
status.WS_1008_POLICY_VIOLATION,
|
677
|
+
),
|
678
|
+
ApiError.BLACK_SWAN: (
|
679
|
+
status.HTTP_400_BAD_REQUEST,
|
680
|
+
status.WS_1008_POLICY_VIOLATION,
|
681
|
+
),
|
682
|
+
ApiError.BOT_DISABLED: (
|
683
|
+
status.HTTP_400_BAD_REQUEST,
|
684
|
+
status.WS_1008_POLICY_VIOLATION,
|
685
|
+
),
|
686
|
+
ApiError.DELETE_BOT_ERROR: (
|
687
|
+
status.HTTP_400_BAD_REQUEST,
|
688
|
+
status.WS_1008_POLICY_VIOLATION,
|
689
|
+
),
|
690
|
+
ApiError.EXCHANGE_INVALID_SIGNATURE: (
|
691
|
+
status.HTTP_400_BAD_REQUEST,
|
692
|
+
status.WS_1008_POLICY_VIOLATION,
|
693
|
+
),
|
694
|
+
ApiError.EXCHANGE_INVALID_TIMESTAMP: (
|
695
|
+
status.HTTP_400_BAD_REQUEST,
|
696
|
+
status.WS_1008_POLICY_VIOLATION,
|
697
|
+
),
|
698
|
+
ApiError.EXCHANGE_IP_RESTRICTED: (
|
699
|
+
status.HTTP_400_BAD_REQUEST,
|
700
|
+
status.WS_1008_POLICY_VIOLATION,
|
701
|
+
),
|
702
|
+
ApiError.EXCHANGE_KEY_IN_USE: (
|
703
|
+
status.HTTP_400_BAD_REQUEST,
|
704
|
+
status.WS_1008_POLICY_VIOLATION,
|
705
|
+
),
|
706
|
+
ApiError.EXCHANGE_SYSTEM_CONFIG_ERROR: (
|
707
|
+
status.HTTP_400_BAD_REQUEST,
|
708
|
+
status.WS_1008_POLICY_VIOLATION,
|
709
|
+
),
|
710
|
+
ApiError.HEDGE_MODE_NOT_ACTIVE: (
|
711
|
+
status.HTTP_400_BAD_REQUEST,
|
712
|
+
status.WS_1008_POLICY_VIOLATION,
|
713
|
+
),
|
714
|
+
ApiError.HTTP_ERROR: (
|
715
|
+
status.HTTP_400_BAD_REQUEST,
|
716
|
+
status.WS_1008_POLICY_VIOLATION,
|
717
|
+
),
|
718
|
+
ApiError.INSUFFICIENT_BALANCE: (
|
719
|
+
status.HTTP_400_BAD_REQUEST,
|
720
|
+
status.WS_1008_POLICY_VIOLATION,
|
721
|
+
),
|
586
722
|
ApiError.INSUFFICIENT_MARGIN: status.HTTP_400_BAD_REQUEST,
|
587
|
-
ApiError.INVALID_EXCHANGE_KEY:
|
588
|
-
|
589
|
-
|
590
|
-
|
723
|
+
ApiError.INVALID_EXCHANGE_KEY: (
|
724
|
+
status.HTTP_400_BAD_REQUEST,
|
725
|
+
status.WS_1008_POLICY_VIOLATION,
|
726
|
+
),
|
727
|
+
ApiError.INVALID_MARGIN_MODE: (
|
728
|
+
status.HTTP_400_BAD_REQUEST,
|
729
|
+
status.WS_1008_POLICY_VIOLATION,
|
730
|
+
),
|
731
|
+
ApiError.INVALID_PARAMETER: (
|
732
|
+
status.HTTP_400_BAD_REQUEST,
|
733
|
+
status.WS_1008_POLICY_VIOLATION,
|
734
|
+
),
|
735
|
+
ApiError.LEVERAGE_EXCEEDED: (
|
736
|
+
status.HTTP_400_BAD_REQUEST,
|
737
|
+
status.WS_1008_POLICY_VIOLATION,
|
738
|
+
),
|
591
739
|
ApiError.LIQUIDATION_PRICE_VIOLATION: status.HTTP_400_BAD_REQUEST,
|
592
|
-
ApiError.ORDER_ALREADY_FILLED:
|
593
|
-
|
594
|
-
|
595
|
-
|
596
|
-
ApiError.
|
597
|
-
|
598
|
-
|
599
|
-
|
600
|
-
ApiError.
|
601
|
-
|
602
|
-
|
603
|
-
|
604
|
-
ApiError.
|
605
|
-
|
740
|
+
ApiError.ORDER_ALREADY_FILLED: (
|
741
|
+
status.HTTP_400_BAD_REQUEST,
|
742
|
+
status.WS_1008_POLICY_VIOLATION,
|
743
|
+
),
|
744
|
+
ApiError.ORDER_IN_PROCESS: (
|
745
|
+
status.HTTP_400_BAD_REQUEST,
|
746
|
+
status.WS_1008_POLICY_VIOLATION,
|
747
|
+
),
|
748
|
+
ApiError.ORDER_LIMIT_EXCEEDED: (
|
749
|
+
status.HTTP_400_BAD_REQUEST,
|
750
|
+
status.WS_1008_POLICY_VIOLATION,
|
751
|
+
),
|
752
|
+
ApiError.ORDER_PRICE_INVALID: (
|
753
|
+
status.HTTP_400_BAD_REQUEST,
|
754
|
+
status.WS_1008_POLICY_VIOLATION,
|
755
|
+
),
|
756
|
+
ApiError.ORDER_SIZE_TOO_LARGE: (
|
757
|
+
status.HTTP_400_BAD_REQUEST,
|
758
|
+
status.WS_1008_POLICY_VIOLATION,
|
759
|
+
),
|
760
|
+
ApiError.ORDER_SIZE_TOO_SMALL: (
|
761
|
+
status.HTTP_400_BAD_REQUEST,
|
762
|
+
status.WS_1008_POLICY_VIOLATION,
|
763
|
+
),
|
764
|
+
ApiError.POSITION_LIMIT_EXCEEDED: (
|
765
|
+
status.HTTP_400_BAD_REQUEST,
|
766
|
+
status.WS_1008_POLICY_VIOLATION,
|
767
|
+
),
|
768
|
+
ApiError.POST_ONLY_REJECTED: (
|
769
|
+
status.HTTP_400_BAD_REQUEST,
|
770
|
+
status.WS_1008_POLICY_VIOLATION,
|
771
|
+
),
|
772
|
+
ApiError.RISK_LIMIT_EXCEEDED: (
|
773
|
+
status.HTTP_400_BAD_REQUEST,
|
774
|
+
status.WS_1008_POLICY_VIOLATION,
|
775
|
+
),
|
776
|
+
ApiError.STRATEGY_DISABLED: (
|
777
|
+
status.HTTP_400_BAD_REQUEST,
|
778
|
+
status.WS_1008_POLICY_VIOLATION,
|
779
|
+
),
|
780
|
+
ApiError.STRATEGY_LEVERAGE_MISMATCH: (
|
781
|
+
status.HTTP_400_BAD_REQUEST,
|
782
|
+
status.WS_1008_POLICY_VIOLATION,
|
783
|
+
),
|
784
|
+
ApiError.STRATEGY_NOT_SUPPORTING_EXCHANGE: (
|
785
|
+
status.HTTP_400_BAD_REQUEST,
|
786
|
+
status.WS_1008_POLICY_VIOLATION,
|
787
|
+
),
|
788
|
+
ApiError.TRADING_ACTION_EXPIRED: (
|
789
|
+
status.HTTP_400_BAD_REQUEST,
|
790
|
+
status.WS_1008_POLICY_VIOLATION,
|
791
|
+
),
|
792
|
+
ApiError.TRADING_ACTION_SKIPPED: (
|
793
|
+
status.HTTP_400_BAD_REQUEST,
|
794
|
+
status.WS_1008_POLICY_VIOLATION,
|
795
|
+
),
|
606
796
|
# Success cases
|
607
|
-
ApiError.SUCCESS: status.HTTP_200_OK,
|
608
|
-
ApiError.BOT_STOPPING_COMPLETED:
|
609
|
-
|
610
|
-
|
611
|
-
|
612
|
-
ApiError.
|
797
|
+
ApiError.SUCCESS: (status.HTTP_200_OK, status.WS_1000_NORMAL_CLOSURE),
|
798
|
+
ApiError.BOT_STOPPING_COMPLETED: (
|
799
|
+
status.HTTP_200_OK,
|
800
|
+
status.WS_1000_NORMAL_CLOSURE,
|
801
|
+
),
|
802
|
+
ApiError.BOT_STOPPING_STARTED: (
|
803
|
+
status.HTTP_200_OK,
|
804
|
+
status.WS_1000_NORMAL_CLOSURE,
|
805
|
+
),
|
806
|
+
ApiError.OBJECT_CREATED: (
|
807
|
+
status.HTTP_201_CREATED,
|
808
|
+
status.WS_1000_NORMAL_CLOSURE,
|
809
|
+
),
|
810
|
+
ApiError.OBJECT_UPDATED: (status.HTTP_200_OK, status.WS_1000_NORMAL_CLOSURE),
|
811
|
+
ApiError.OBJECT_DELETED: (
|
812
|
+
status.HTTP_204_NO_CONTENT,
|
813
|
+
status.WS_1000_NORMAL_CLOSURE,
|
814
|
+
),
|
613
815
|
}
|
614
816
|
|
615
817
|
@classmethod
|
616
|
-
def
|
818
|
+
def get_http_code(cls, error: ApiError) -> int:
|
617
819
|
"""Get the HTTP status code for the error. If the error is not in the mapping, return 500."""
|
618
|
-
return cls._mapping.get(error,
|
820
|
+
return cls._mapping.get(error, cls._mapping[ApiError.UNKNOWN_ERROR])[0]
|
821
|
+
|
822
|
+
@classmethod
|
823
|
+
def get_websocket_code(cls, error: ApiError) -> int:
|
824
|
+
"""Get the WebSocket status code for the error. If the error is not in the mapping, return 1008."""
|
825
|
+
return cls._mapping.get(error, cls._mapping[ApiError.UNKNOWN_ERROR])[1]
|
crypticorn/common/exceptions.py
CHANGED
@@ -1,4 +1,5 @@
|
|
1
|
-
from typing import Optional, Dict, Any
|
1
|
+
from typing import Optional, Dict, Any, Literal
|
2
|
+
from enum import StrEnum
|
2
3
|
from pydantic import BaseModel, Field
|
3
4
|
from fastapi import HTTPException as FastAPIHTTPException, Request, FastAPI
|
4
5
|
from fastapi.exceptions import RequestValidationError, ResponseValidationError
|
@@ -6,6 +7,11 @@ from fastapi.responses import JSONResponse
|
|
6
7
|
from crypticorn.common import ApiError, ApiErrorIdentifier, ApiErrorType, ApiErrorLevel
|
7
8
|
|
8
9
|
|
10
|
+
class ExceptionType(StrEnum):
|
11
|
+
HTTP = "http"
|
12
|
+
WEBSOCKET = "websocket"
|
13
|
+
|
14
|
+
|
9
15
|
class ExceptionDetail(BaseModel):
|
10
16
|
"""This is the detail of the exception. It is used to enrich the exception with additional information by unwrapping the ApiError into its components."""
|
11
17
|
|
@@ -24,13 +30,19 @@ class ExceptionContent(BaseModel):
|
|
24
30
|
message: Optional[str] = Field(None, description="An additional error message")
|
25
31
|
details: Any = Field(None, description="Additional details about the error")
|
26
32
|
|
27
|
-
def enrich(
|
33
|
+
def enrich(
|
34
|
+
self, _type: Optional[ExceptionType] = ExceptionType.HTTP
|
35
|
+
) -> ExceptionDetail:
|
28
36
|
return ExceptionDetail(
|
29
37
|
message=self.message,
|
30
38
|
code=self.error.identifier,
|
31
39
|
type=self.error.type,
|
32
40
|
level=self.error.level,
|
33
|
-
status_code=
|
41
|
+
status_code=(
|
42
|
+
self.error.http_code
|
43
|
+
if _type == ExceptionType.HTTP
|
44
|
+
else self.error.websocket_code
|
45
|
+
),
|
34
46
|
details=self.details,
|
35
47
|
)
|
36
48
|
|
@@ -45,9 +57,12 @@ class HTTPException(FastAPIHTTPException):
|
|
45
57
|
self,
|
46
58
|
content: ExceptionContent,
|
47
59
|
headers: Optional[Dict[str, str]] = None,
|
60
|
+
_type: Optional[ExceptionType] = ExceptionType.HTTP,
|
48
61
|
):
|
62
|
+
self.content = content
|
63
|
+
self.headers = headers
|
49
64
|
assert isinstance(content, ExceptionContent)
|
50
|
-
body = content.enrich()
|
65
|
+
body = content.enrich(_type)
|
51
66
|
super().__init__(
|
52
67
|
status_code=body.status_code,
|
53
68
|
detail=body.model_dump(mode="json"),
|
@@ -55,6 +70,25 @@ class HTTPException(FastAPIHTTPException):
|
|
55
70
|
)
|
56
71
|
|
57
72
|
|
73
|
+
class WebSocketException(HTTPException):
|
74
|
+
"""A WebSocketException is to be used for WebSocket connections. It is a wrapper around the HTTPException class to maintain the same structure, but using a different status code.
|
75
|
+
To be used in the same way as the HTTPException.
|
76
|
+
"""
|
77
|
+
|
78
|
+
def __init__(
|
79
|
+
self, content: ExceptionContent, headers: Optional[Dict[str, str]] = None
|
80
|
+
):
|
81
|
+
super().__init__(content, headers, _type=ExceptionType.WEBSOCKET)
|
82
|
+
|
83
|
+
@classmethod
|
84
|
+
def from_http_exception(cls, http_exception: HTTPException):
|
85
|
+
"""This is a helper method to convert an HTTPException to a WebSocketException."""
|
86
|
+
return WebSocketException(
|
87
|
+
content=http_exception.content,
|
88
|
+
headers=http_exception.headers,
|
89
|
+
)
|
90
|
+
|
91
|
+
|
58
92
|
async def general_handler(request: Request, exc: Exception):
|
59
93
|
"""This is the default exception handler for all exceptions."""
|
60
94
|
body = ExceptionContent(message=str(exc), error=ApiError.UNKNOWN_ERROR)
|
crypticorn/common/mixins.py
CHANGED
@@ -1,3 +1,9 @@
|
|
1
|
+
from enum import EnumMeta
|
2
|
+
import logging
|
3
|
+
|
4
|
+
logger = logging.getLogger("uvicorn")
|
5
|
+
|
6
|
+
|
1
7
|
class ValidateEnumMixin:
|
2
8
|
"""
|
3
9
|
Mixin for validating enum values manually.
|
@@ -35,3 +41,16 @@ class ExcludeEnumMixin:
|
|
35
41
|
schema = handler(core_schema)
|
36
42
|
schema.pop("enum", None)
|
37
43
|
return schema
|
44
|
+
|
45
|
+
|
46
|
+
class ApiErrorFallback(EnumMeta):
|
47
|
+
"""Fallback for enum members that are not yet published to PyPI."""
|
48
|
+
|
49
|
+
def __getattr__(cls, name):
|
50
|
+
# Let Pydantic/internal stuff pass silently ! fragile
|
51
|
+
if name.startswith("__"):
|
52
|
+
raise AttributeError(name)
|
53
|
+
logger.warning(
|
54
|
+
f"Unknown enum member '{name}' - update crypticorn package or check for typos"
|
55
|
+
)
|
56
|
+
return cls.UNKNOWN_ERROR
|
@@ -0,0 +1,49 @@
|
|
1
|
+
from typing import Generic, Type, TypeVar, List, Optional, Literal
|
2
|
+
from pydantic import BaseModel, Field, model_validator
|
3
|
+
|
4
|
+
T = TypeVar("T")
|
5
|
+
|
6
|
+
|
7
|
+
class PaginatedResponse(BaseModel, Generic[T]):
|
8
|
+
"""Pydantic model for paginated response
|
9
|
+
>>> PaginatedResponse[ItemModel](data=items, total=total_items, page=1, size=10, prev=None, next=2)
|
10
|
+
"""
|
11
|
+
|
12
|
+
data: List[T]
|
13
|
+
total: int = Field(description="The total number of items")
|
14
|
+
page: int = Field(description="The current page number")
|
15
|
+
size: int = Field(description="The number of items per page")
|
16
|
+
prev: Optional[int] = Field(None, description="The previous page number")
|
17
|
+
next: Optional[int] = Field(None, description="The next page number")
|
18
|
+
|
19
|
+
|
20
|
+
class PaginationParams(BaseModel, Generic[T]):
|
21
|
+
"""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.
|
22
|
+
The default size is 10 items per page, but can be overridden:
|
23
|
+
>>> class HeavyPaginationParams(PaginationParams[T]):
|
24
|
+
>>> size: int = Field(default=100, description="The number of items per page")
|
25
|
+
"""
|
26
|
+
|
27
|
+
page: int = Field(default=1, description="The current page number")
|
28
|
+
size: int = Field(default=10, description="The number of items per page")
|
29
|
+
order: Literal["asc", "desc"] = Field(
|
30
|
+
default="asc", description="The order to sort by"
|
31
|
+
)
|
32
|
+
sort: Optional[str] = Field(None, description="The field to sort by")
|
33
|
+
|
34
|
+
@model_validator(mode="after")
|
35
|
+
def validate(self):
|
36
|
+
# Extract the generic argument type
|
37
|
+
args: tuple = self.__pydantic_generic_metadata__.get("args")
|
38
|
+
if not args or not issubclass(args[0], BaseModel):
|
39
|
+
raise TypeError(
|
40
|
+
"PaginationParams must be used with a Pydantic BaseModel as a generic parameter"
|
41
|
+
)
|
42
|
+
if self.sort:
|
43
|
+
# check if the sort field is valid
|
44
|
+
model: Type[BaseModel] = args[0]
|
45
|
+
if self.sort and self.sort not in model.model_fields:
|
46
|
+
raise ValueError(
|
47
|
+
f"Invalid sort field: '{self.sort}' — must be one of: {list(model.model_fields)}"
|
48
|
+
)
|
49
|
+
return self
|