crypticorn 2.5.3__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/auth/client/models/user_by_username200_response.py +91 -0
- 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/common/__init__.py +1 -0
- crypticorn/common/auth.py +13 -9
- crypticorn/common/errors.py +310 -88
- crypticorn/common/exceptions.py +38 -4
- crypticorn/common/pagination.py +49 -0
- crypticorn/common/scopes.py +24 -5
- crypticorn/hive/client/__init__.py +2 -0
- crypticorn/hive/client/api/models_api.py +343 -56
- crypticorn/hive/client/models/__init__.py +2 -0
- crypticorn/hive/client/models/data_info.py +44 -12
- crypticorn/hive/client/models/data_version_info.py +89 -0
- crypticorn/hive/client/models/model.py +2 -3
- crypticorn/hive/client/models/target_info.py +94 -0
- {crypticorn-2.5.3.dist-info → crypticorn-2.6.0.dist-info}/METADATA +1 -1
- {crypticorn-2.5.3.dist-info → crypticorn-2.6.0.dist-info}/RECORD +28 -24
- {crypticorn-2.5.3.dist-info → crypticorn-2.6.0.dist-info}/WHEEL +1 -1
- {crypticorn-2.5.3.dist-info → crypticorn-2.6.0.dist-info}/entry_points.txt +0 -0
- {crypticorn-2.5.3.dist-info → crypticorn-2.6.0.dist-info}/top_level.txt +0 -0
crypticorn/common/errors.py
CHANGED
@@ -1,5 +1,4 @@
|
|
1
|
-
from enum import Enum,
|
2
|
-
import logging
|
1
|
+
from enum import Enum, StrEnum
|
3
2
|
from fastapi import status
|
4
3
|
from crypticorn.common.mixins import ExcludeEnumMixin, ApiErrorFallback
|
5
4
|
|
@@ -22,7 +21,6 @@ class ApiErrorIdentifier(ExcludeEnumMixin, StrEnum):
|
|
22
21
|
|
23
22
|
ALLOCATION_BELOW_EXPOSURE = "allocation_below_current_exposure"
|
24
23
|
ALLOCATION_BELOW_MINIMUM = "allocation_below_min_amount"
|
25
|
-
ALPHANUMERIC_CHARACTERS_ONLY = "alphanumeric_characters_only"
|
26
24
|
BLACK_SWAN = "black_swan"
|
27
25
|
BOT_ALREADY_DELETED = "bot_already_deleted"
|
28
26
|
BOT_DISABLED = "bot_disabled"
|
@@ -58,6 +56,7 @@ class ApiErrorIdentifier(ExcludeEnumMixin, StrEnum):
|
|
58
56
|
INVALID_DATA_RESPONSE = "invalid_data_response"
|
59
57
|
INVALID_EXCHANGE_KEY = "invalid_exchange_key"
|
60
58
|
INVALID_MARGIN_MODE = "invalid_margin_mode"
|
59
|
+
INVALID_MODEL_NAME = "invalid_model_name"
|
61
60
|
INVALID_PARAMETER = "invalid_parameter_provided"
|
62
61
|
LEVERAGE_EXCEEDED = "leverage_limit_exceeded"
|
63
62
|
LIQUIDATION_PRICE_VIOLATION = "order_violates_liquidation_price_constraints"
|
@@ -97,7 +96,7 @@ class ApiErrorIdentifier(ExcludeEnumMixin, StrEnum):
|
|
97
96
|
URL_NOT_FOUND = "requested_resource_not_found"
|
98
97
|
|
99
98
|
@property
|
100
|
-
def
|
99
|
+
def get_error(self) -> "ApiError":
|
101
100
|
"""Get the corresponding ApiError."""
|
102
101
|
return ApiError[self.value]
|
103
102
|
|
@@ -124,11 +123,6 @@ class ApiError(ExcludeEnumMixin, Enum, metaclass=ApiErrorFallback):
|
|
124
123
|
ApiErrorType.USER_ERROR,
|
125
124
|
ApiErrorLevel.ERROR,
|
126
125
|
)
|
127
|
-
ALPHANUMERIC_CHARACTERS_ONLY = (
|
128
|
-
ApiErrorIdentifier.ALPHANUMERIC_CHARACTERS_ONLY,
|
129
|
-
ApiErrorType.USER_ERROR,
|
130
|
-
ApiErrorLevel.ERROR,
|
131
|
-
)
|
132
126
|
BLACK_SWAN = (
|
133
127
|
ApiErrorIdentifier.BLACK_SWAN,
|
134
128
|
ApiErrorType.USER_ERROR,
|
@@ -269,6 +263,11 @@ class ApiError(ExcludeEnumMixin, Enum, metaclass=ApiErrorFallback):
|
|
269
263
|
ApiErrorType.USER_ERROR,
|
270
264
|
ApiErrorLevel.ERROR,
|
271
265
|
)
|
266
|
+
INVALID_MODEL_NAME = (
|
267
|
+
ApiErrorIdentifier.INVALID_MODEL_NAME,
|
268
|
+
ApiErrorType.USER_ERROR,
|
269
|
+
ApiErrorLevel.ERROR,
|
270
|
+
)
|
272
271
|
INSUFFICIENT_SCOPES = (
|
273
272
|
ApiErrorIdentifier.INSUFFICIENT_SCOPES,
|
274
273
|
ApiErrorType.USER_ERROR,
|
@@ -477,7 +476,7 @@ class ApiError(ExcludeEnumMixin, Enum, metaclass=ApiErrorFallback):
|
|
477
476
|
)
|
478
477
|
UNKNOWN_ERROR = (
|
479
478
|
ApiErrorIdentifier.UNKNOWN_ERROR,
|
480
|
-
ApiErrorType.
|
479
|
+
ApiErrorType.SERVER_ERROR,
|
481
480
|
ApiErrorLevel.ERROR,
|
482
481
|
)
|
483
482
|
URL_NOT_FOUND = (
|
@@ -502,102 +501,325 @@ class ApiError(ExcludeEnumMixin, Enum, metaclass=ApiErrorFallback):
|
|
502
501
|
return self.value[2]
|
503
502
|
|
504
503
|
@property
|
505
|
-
def
|
504
|
+
def http_code(self) -> int:
|
506
505
|
"""HTTP status code for the error."""
|
507
|
-
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)
|
508
512
|
|
509
513
|
|
510
|
-
class
|
514
|
+
class StatusCodeMapper:
|
511
515
|
"""Map API errors to HTTP status codes."""
|
512
516
|
|
513
517
|
_mapping = {
|
514
518
|
# Authentication/Authorization
|
515
|
-
ApiError.EXPIRED_BEARER:
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
ApiError.
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
ApiError.
|
524
|
-
|
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
|
+
),
|
525
559
|
# Not Found
|
526
|
-
ApiError.URL_NOT_FOUND:
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
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
|
+
),
|
531
580
|
# Conflicts/Duplicates
|
532
|
-
ApiError.CLIENT_ORDER_ID_REPEATED:
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
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
|
+
),
|
537
601
|
# Invalid Content
|
538
|
-
ApiError.CONTENT_TYPE_ERROR:
|
539
|
-
|
540
|
-
|
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
|
+
),
|
541
614
|
# Rate Limits
|
542
|
-
ApiError.EXCHANGE_RATE_LIMIT:
|
543
|
-
|
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
|
+
),
|
544
623
|
# Server Errors
|
545
|
-
ApiError.UNKNOWN_ERROR:
|
546
|
-
|
547
|
-
|
548
|
-
|
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
|
+
),
|
549
640
|
# Service Unavailable
|
550
|
-
ApiError.EXCHANGE_SERVICE_UNAVAILABLE:
|
551
|
-
|
552
|
-
|
553
|
-
|
554
|
-
ApiError.
|
555
|
-
|
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
|
+
),
|
556
665
|
# Bad Requests (400) - Invalid parameters or states
|
557
|
-
ApiError.
|
558
|
-
|
559
|
-
|
560
|
-
|
561
|
-
ApiError.
|
562
|
-
|
563
|
-
|
564
|
-
|
565
|
-
ApiError.
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
ApiError.
|
570
|
-
|
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
|
+
),
|
571
722
|
ApiError.INSUFFICIENT_MARGIN: status.HTTP_400_BAD_REQUEST,
|
572
|
-
ApiError.INVALID_EXCHANGE_KEY:
|
573
|
-
|
574
|
-
|
575
|
-
|
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
|
+
),
|
576
739
|
ApiError.LIQUIDATION_PRICE_VIOLATION: status.HTTP_400_BAD_REQUEST,
|
577
|
-
ApiError.ORDER_ALREADY_FILLED:
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
ApiError.
|
582
|
-
|
583
|
-
|
584
|
-
|
585
|
-
ApiError.
|
586
|
-
|
587
|
-
|
588
|
-
|
589
|
-
ApiError.
|
590
|
-
|
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
|
+
),
|
591
796
|
# Success cases
|
592
|
-
ApiError.SUCCESS: status.HTTP_200_OK,
|
593
|
-
ApiError.BOT_STOPPING_COMPLETED:
|
594
|
-
|
595
|
-
|
596
|
-
|
597
|
-
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
|
+
),
|
598
815
|
}
|
599
816
|
|
600
817
|
@classmethod
|
601
|
-
def
|
818
|
+
def get_http_code(cls, error: ApiError) -> int:
|
602
819
|
"""Get the HTTP status code for the error. If the error is not in the mapping, return 500."""
|
603
|
-
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)
|
@@ -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
|
crypticorn/common/scopes.py
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
from enum import StrEnum
|
2
|
-
|
2
|
+
|
3
3
|
|
4
4
|
class Scope(StrEnum):
|
5
5
|
"""
|
@@ -8,10 +8,6 @@ class Scope(StrEnum):
|
|
8
8
|
|
9
9
|
# If you update anything here, also update the scopes in the auth-service repository
|
10
10
|
|
11
|
-
@classmethod
|
12
|
-
def from_str(cls, value: str) -> "Scope":
|
13
|
-
return cls(value)
|
14
|
-
|
15
11
|
# Scopes that can be purchased - these actually exist in the jwt token
|
16
12
|
READ_PREDICTIONS = "read:predictions"
|
17
13
|
|
@@ -52,3 +48,26 @@ class Scope(StrEnum):
|
|
52
48
|
|
53
49
|
# Sentiment scopes
|
54
50
|
READ_SENTIMENT = "read:sentiment"
|
51
|
+
|
52
|
+
@classmethod
|
53
|
+
def admin_scopes(cls) -> list["Scope"]:
|
54
|
+
"""Scopes that are only available to admins"""
|
55
|
+
return [
|
56
|
+
cls.WRITE_TRADE_STRATEGIES,
|
57
|
+
cls.WRITE_PAY_PRODUCTS,
|
58
|
+
cls.WRITE_PAY_NOW,
|
59
|
+
]
|
60
|
+
|
61
|
+
@classmethod
|
62
|
+
def internal_scopes(cls) -> list["Scope"]:
|
63
|
+
"""Scopes that are only available to internal services"""
|
64
|
+
return [
|
65
|
+
cls.WRITE_TRADE_ACTIONS,
|
66
|
+
]
|
67
|
+
|
68
|
+
@classmethod
|
69
|
+
def purchaseable_scopes(cls) -> list["Scope"]:
|
70
|
+
"""Scopes that can be purchased"""
|
71
|
+
return [
|
72
|
+
cls.READ_PREDICTIONS,
|
73
|
+
]
|
@@ -40,6 +40,7 @@ from crypticorn.hive.client.models.data_value_value_value_inner import (
|
|
40
40
|
DataValueValueValueInner,
|
41
41
|
)
|
42
42
|
from crypticorn.hive.client.models.data_version import DataVersion
|
43
|
+
from crypticorn.hive.client.models.data_version_info import DataVersionInfo
|
43
44
|
from crypticorn.hive.client.models.download_links import DownloadLinks
|
44
45
|
from crypticorn.hive.client.models.evaluation import Evaluation
|
45
46
|
from crypticorn.hive.client.models.evaluation_response import EvaluationResponse
|
@@ -50,4 +51,5 @@ from crypticorn.hive.client.models.model_create import ModelCreate
|
|
50
51
|
from crypticorn.hive.client.models.model_status import ModelStatus
|
51
52
|
from crypticorn.hive.client.models.model_update import ModelUpdate
|
52
53
|
from crypticorn.hive.client.models.target import Target
|
54
|
+
from crypticorn.hive.client.models.target_info import TargetInfo
|
53
55
|
from crypticorn.hive.client.models.target_type import TargetType
|