crypticorn 2.5.3__py3-none-any.whl → 2.7.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 +320 -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/data_api.py +15 -12
- 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/hive/main.py +43 -2
- crypticorn/hive/utils.py +65 -0
- {crypticorn-2.5.3.dist-info → crypticorn-2.7.0.dist-info}/METADATA +2 -2
- {crypticorn-2.5.3.dist-info → crypticorn-2.7.0.dist-info}/RECORD +31 -26
- {crypticorn-2.5.3.dist-info → crypticorn-2.7.0.dist-info}/WHEEL +1 -1
- {crypticorn-2.5.3.dist-info → crypticorn-2.7.0.dist-info}/entry_points.txt +0 -0
- {crypticorn-2.5.3.dist-info → crypticorn-2.7.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,9 +56,11 @@ 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"
|
63
|
+
MODEL_NAME_NOT_UNIQUE = "model_name_not_unique"
|
64
64
|
NO_CREDENTIALS = "no_credentials"
|
65
65
|
NOW_API_DOWN = "now_api_down"
|
66
66
|
OBJECT_ALREADY_EXISTS = "object_already_exists"
|
@@ -97,7 +97,7 @@ class ApiErrorIdentifier(ExcludeEnumMixin, StrEnum):
|
|
97
97
|
URL_NOT_FOUND = "requested_resource_not_found"
|
98
98
|
|
99
99
|
@property
|
100
|
-
def
|
100
|
+
def get_error(self) -> "ApiError":
|
101
101
|
"""Get the corresponding ApiError."""
|
102
102
|
return ApiError[self.value]
|
103
103
|
|
@@ -124,11 +124,6 @@ class ApiError(ExcludeEnumMixin, Enum, metaclass=ApiErrorFallback):
|
|
124
124
|
ApiErrorType.USER_ERROR,
|
125
125
|
ApiErrorLevel.ERROR,
|
126
126
|
)
|
127
|
-
ALPHANUMERIC_CHARACTERS_ONLY = (
|
128
|
-
ApiErrorIdentifier.ALPHANUMERIC_CHARACTERS_ONLY,
|
129
|
-
ApiErrorType.USER_ERROR,
|
130
|
-
ApiErrorLevel.ERROR,
|
131
|
-
)
|
132
127
|
BLACK_SWAN = (
|
133
128
|
ApiErrorIdentifier.BLACK_SWAN,
|
134
129
|
ApiErrorType.USER_ERROR,
|
@@ -269,6 +264,11 @@ class ApiError(ExcludeEnumMixin, Enum, metaclass=ApiErrorFallback):
|
|
269
264
|
ApiErrorType.USER_ERROR,
|
270
265
|
ApiErrorLevel.ERROR,
|
271
266
|
)
|
267
|
+
INVALID_MODEL_NAME = (
|
268
|
+
ApiErrorIdentifier.INVALID_MODEL_NAME,
|
269
|
+
ApiErrorType.USER_ERROR,
|
270
|
+
ApiErrorLevel.ERROR,
|
271
|
+
)
|
272
272
|
INSUFFICIENT_SCOPES = (
|
273
273
|
ApiErrorIdentifier.INSUFFICIENT_SCOPES,
|
274
274
|
ApiErrorType.USER_ERROR,
|
@@ -319,6 +319,11 @@ class ApiError(ExcludeEnumMixin, Enum, metaclass=ApiErrorFallback):
|
|
319
319
|
ApiErrorType.SERVER_ERROR,
|
320
320
|
ApiErrorLevel.ERROR,
|
321
321
|
)
|
322
|
+
MODEL_NAME_NOT_UNIQUE = (
|
323
|
+
ApiErrorIdentifier.MODEL_NAME_NOT_UNIQUE,
|
324
|
+
ApiErrorType.USER_ERROR,
|
325
|
+
ApiErrorLevel.ERROR,
|
326
|
+
)
|
322
327
|
NO_CREDENTIALS = (
|
323
328
|
ApiErrorIdentifier.NO_CREDENTIALS,
|
324
329
|
ApiErrorType.USER_ERROR,
|
@@ -477,7 +482,7 @@ class ApiError(ExcludeEnumMixin, Enum, metaclass=ApiErrorFallback):
|
|
477
482
|
)
|
478
483
|
UNKNOWN_ERROR = (
|
479
484
|
ApiErrorIdentifier.UNKNOWN_ERROR,
|
480
|
-
ApiErrorType.
|
485
|
+
ApiErrorType.SERVER_ERROR,
|
481
486
|
ApiErrorLevel.ERROR,
|
482
487
|
)
|
483
488
|
URL_NOT_FOUND = (
|
@@ -502,102 +507,329 @@ class ApiError(ExcludeEnumMixin, Enum, metaclass=ApiErrorFallback):
|
|
502
507
|
return self.value[2]
|
503
508
|
|
504
509
|
@property
|
505
|
-
def
|
510
|
+
def http_code(self) -> int:
|
506
511
|
"""HTTP status code for the error."""
|
507
|
-
return
|
512
|
+
return StatusCodeMapper.get_http_code(self)
|
508
513
|
|
514
|
+
@property
|
515
|
+
def websocket_code(self) -> int:
|
516
|
+
"""WebSocket status code for the error."""
|
517
|
+
return StatusCodeMapper.get_websocket_code(self)
|
509
518
|
|
510
|
-
|
519
|
+
|
520
|
+
class StatusCodeMapper:
|
511
521
|
"""Map API errors to HTTP status codes."""
|
512
522
|
|
513
523
|
_mapping = {
|
514
524
|
# Authentication/Authorization
|
515
|
-
ApiError.EXPIRED_BEARER:
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
ApiError.
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
ApiError.
|
524
|
-
|
525
|
+
ApiError.EXPIRED_BEARER: (
|
526
|
+
status.HTTP_401_UNAUTHORIZED,
|
527
|
+
status.WS_1008_POLICY_VIOLATION,
|
528
|
+
),
|
529
|
+
ApiError.INVALID_BEARER: (
|
530
|
+
status.HTTP_401_UNAUTHORIZED,
|
531
|
+
status.WS_1008_POLICY_VIOLATION,
|
532
|
+
),
|
533
|
+
ApiError.EXPIRED_API_KEY: (
|
534
|
+
status.HTTP_401_UNAUTHORIZED,
|
535
|
+
status.WS_1008_POLICY_VIOLATION,
|
536
|
+
),
|
537
|
+
ApiError.INVALID_API_KEY: (
|
538
|
+
status.HTTP_401_UNAUTHORIZED,
|
539
|
+
status.WS_1008_POLICY_VIOLATION,
|
540
|
+
),
|
541
|
+
ApiError.NO_CREDENTIALS: (
|
542
|
+
status.HTTP_401_UNAUTHORIZED,
|
543
|
+
status.WS_1008_POLICY_VIOLATION,
|
544
|
+
),
|
545
|
+
ApiError.INSUFFICIENT_SCOPES: (
|
546
|
+
status.HTTP_403_FORBIDDEN,
|
547
|
+
status.WS_1008_POLICY_VIOLATION,
|
548
|
+
),
|
549
|
+
ApiError.EXCHANGE_PERMISSION_DENIED: (
|
550
|
+
status.HTTP_403_FORBIDDEN,
|
551
|
+
status.WS_1008_POLICY_VIOLATION,
|
552
|
+
),
|
553
|
+
ApiError.EXCHANGE_USER_FROZEN: (
|
554
|
+
status.HTTP_403_FORBIDDEN,
|
555
|
+
status.WS_1008_POLICY_VIOLATION,
|
556
|
+
),
|
557
|
+
ApiError.TRADING_LOCKED: (
|
558
|
+
status.HTTP_403_FORBIDDEN,
|
559
|
+
status.WS_1008_POLICY_VIOLATION,
|
560
|
+
),
|
561
|
+
ApiError.FORBIDDEN: (
|
562
|
+
status.HTTP_403_FORBIDDEN,
|
563
|
+
status.WS_1008_POLICY_VIOLATION,
|
564
|
+
),
|
525
565
|
# Not Found
|
526
|
-
ApiError.URL_NOT_FOUND:
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
ApiError.
|
566
|
+
ApiError.URL_NOT_FOUND: (
|
567
|
+
status.HTTP_404_NOT_FOUND,
|
568
|
+
status.WS_1008_POLICY_VIOLATION,
|
569
|
+
),
|
570
|
+
ApiError.OBJECT_NOT_FOUND: (
|
571
|
+
status.HTTP_404_NOT_FOUND,
|
572
|
+
status.WS_1008_POLICY_VIOLATION,
|
573
|
+
),
|
574
|
+
ApiError.ORDER_NOT_FOUND: (
|
575
|
+
status.HTTP_404_NOT_FOUND,
|
576
|
+
status.WS_1008_POLICY_VIOLATION,
|
577
|
+
),
|
578
|
+
ApiError.POSITION_NOT_FOUND: (
|
579
|
+
status.HTTP_404_NOT_FOUND,
|
580
|
+
status.WS_1008_POLICY_VIOLATION,
|
581
|
+
),
|
582
|
+
ApiError.SYMBOL_NOT_FOUND: (
|
583
|
+
status.HTTP_404_NOT_FOUND,
|
584
|
+
status.WS_1008_POLICY_VIOLATION,
|
585
|
+
),
|
531
586
|
# Conflicts/Duplicates
|
532
|
-
ApiError.CLIENT_ORDER_ID_REPEATED:
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
ApiError.
|
587
|
+
ApiError.CLIENT_ORDER_ID_REPEATED: (
|
588
|
+
status.HTTP_409_CONFLICT,
|
589
|
+
status.WS_1008_POLICY_VIOLATION,
|
590
|
+
),
|
591
|
+
ApiError.OBJECT_ALREADY_EXISTS: (
|
592
|
+
status.HTTP_409_CONFLICT,
|
593
|
+
status.WS_1008_POLICY_VIOLATION,
|
594
|
+
),
|
595
|
+
ApiError.EXCHANGE_KEY_ALREADY_EXISTS: (
|
596
|
+
status.HTTP_409_CONFLICT,
|
597
|
+
status.WS_1008_POLICY_VIOLATION,
|
598
|
+
),
|
599
|
+
ApiError.BOT_ALREADY_DELETED: (
|
600
|
+
status.HTTP_409_CONFLICT,
|
601
|
+
status.WS_1008_POLICY_VIOLATION,
|
602
|
+
),
|
603
|
+
ApiError.STRATEGY_ALREADY_EXISTS: (
|
604
|
+
status.HTTP_409_CONFLICT,
|
605
|
+
status.WS_1008_POLICY_VIOLATION,
|
606
|
+
),
|
607
|
+
ApiError.MODEL_NAME_NOT_UNIQUE: (
|
608
|
+
status.HTTP_409_CONFLICT,
|
609
|
+
status.WS_1008_POLICY_VIOLATION,
|
610
|
+
),
|
537
611
|
# Invalid Content
|
538
|
-
ApiError.CONTENT_TYPE_ERROR:
|
539
|
-
|
540
|
-
|
612
|
+
ApiError.CONTENT_TYPE_ERROR: (
|
613
|
+
status.HTTP_415_UNSUPPORTED_MEDIA_TYPE,
|
614
|
+
status.WS_1003_UNSUPPORTED_DATA,
|
615
|
+
),
|
616
|
+
ApiError.INVALID_DATA_REQUEST: (
|
617
|
+
status.HTTP_422_UNPROCESSABLE_ENTITY,
|
618
|
+
status.WS_1007_INVALID_FRAME_PAYLOAD_DATA,
|
619
|
+
),
|
620
|
+
ApiError.INVALID_DATA_RESPONSE: (
|
621
|
+
status.HTTP_422_UNPROCESSABLE_ENTITY,
|
622
|
+
status.WS_1007_INVALID_FRAME_PAYLOAD_DATA,
|
623
|
+
),
|
541
624
|
# Rate Limits
|
542
|
-
ApiError.EXCHANGE_RATE_LIMIT:
|
543
|
-
|
625
|
+
ApiError.EXCHANGE_RATE_LIMIT: (
|
626
|
+
status.HTTP_429_TOO_MANY_REQUESTS,
|
627
|
+
status.WS_1013_TRY_AGAIN_LATER,
|
628
|
+
),
|
629
|
+
ApiError.REQUEST_SCOPE_EXCEEDED: (
|
630
|
+
status.HTTP_429_TOO_MANY_REQUESTS,
|
631
|
+
status.WS_1013_TRY_AGAIN_LATER,
|
632
|
+
),
|
544
633
|
# Server Errors
|
545
|
-
ApiError.UNKNOWN_ERROR:
|
546
|
-
|
547
|
-
|
548
|
-
|
634
|
+
ApiError.UNKNOWN_ERROR: (
|
635
|
+
status.HTTP_500_INTERNAL_SERVER_ERROR,
|
636
|
+
status.WS_1011_INTERNAL_ERROR,
|
637
|
+
),
|
638
|
+
ApiError.EXCHANGE_SYSTEM_ERROR: (
|
639
|
+
status.HTTP_500_INTERNAL_SERVER_ERROR,
|
640
|
+
status.WS_1011_INTERNAL_ERROR,
|
641
|
+
),
|
642
|
+
ApiError.NOW_API_DOWN: (
|
643
|
+
status.HTTP_500_INTERNAL_SERVER_ERROR,
|
644
|
+
status.WS_1011_INTERNAL_ERROR,
|
645
|
+
),
|
646
|
+
ApiError.RPC_TIMEOUT: (
|
647
|
+
status.HTTP_500_INTERNAL_SERVER_ERROR,
|
648
|
+
status.WS_1011_INTERNAL_ERROR,
|
649
|
+
),
|
549
650
|
# Service Unavailable
|
550
|
-
ApiError.EXCHANGE_SERVICE_UNAVAILABLE:
|
551
|
-
|
552
|
-
|
553
|
-
|
554
|
-
ApiError.
|
555
|
-
|
651
|
+
ApiError.EXCHANGE_SERVICE_UNAVAILABLE: (
|
652
|
+
status.HTTP_503_SERVICE_UNAVAILABLE,
|
653
|
+
status.WS_1011_INTERNAL_ERROR,
|
654
|
+
),
|
655
|
+
ApiError.EXCHANGE_MAINTENANCE: (
|
656
|
+
status.HTTP_503_SERVICE_UNAVAILABLE,
|
657
|
+
status.WS_1011_INTERNAL_ERROR,
|
658
|
+
),
|
659
|
+
ApiError.EXCHANGE_SYSTEM_BUSY: (
|
660
|
+
status.HTTP_503_SERVICE_UNAVAILABLE,
|
661
|
+
status.WS_1011_INTERNAL_ERROR,
|
662
|
+
),
|
663
|
+
ApiError.SETTLEMENT_IN_PROGRESS: (
|
664
|
+
status.HTTP_503_SERVICE_UNAVAILABLE,
|
665
|
+
status.WS_1011_INTERNAL_ERROR,
|
666
|
+
),
|
667
|
+
ApiError.POSITION_SUSPENDED: (
|
668
|
+
status.HTTP_503_SERVICE_UNAVAILABLE,
|
669
|
+
status.WS_1011_INTERNAL_ERROR,
|
670
|
+
),
|
671
|
+
ApiError.TRADING_SUSPENDED: (
|
672
|
+
status.HTTP_503_SERVICE_UNAVAILABLE,
|
673
|
+
status.WS_1011_INTERNAL_ERROR,
|
674
|
+
),
|
556
675
|
# 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
|
-
|
676
|
+
ApiError.INVALID_MODEL_NAME: (
|
677
|
+
status.HTTP_400_BAD_REQUEST,
|
678
|
+
status.WS_1008_POLICY_VIOLATION,
|
679
|
+
),
|
680
|
+
ApiError.ALLOCATION_BELOW_EXPOSURE: (
|
681
|
+
status.HTTP_400_BAD_REQUEST,
|
682
|
+
status.WS_1008_POLICY_VIOLATION,
|
683
|
+
),
|
684
|
+
ApiError.ALLOCATION_BELOW_MINIMUM: (
|
685
|
+
status.HTTP_400_BAD_REQUEST,
|
686
|
+
status.WS_1008_POLICY_VIOLATION,
|
687
|
+
),
|
688
|
+
ApiError.BLACK_SWAN: (
|
689
|
+
status.HTTP_400_BAD_REQUEST,
|
690
|
+
status.WS_1008_POLICY_VIOLATION,
|
691
|
+
),
|
692
|
+
ApiError.BOT_DISABLED: (
|
693
|
+
status.HTTP_400_BAD_REQUEST,
|
694
|
+
status.WS_1008_POLICY_VIOLATION,
|
695
|
+
),
|
696
|
+
ApiError.DELETE_BOT_ERROR: (
|
697
|
+
status.HTTP_400_BAD_REQUEST,
|
698
|
+
status.WS_1008_POLICY_VIOLATION,
|
699
|
+
),
|
700
|
+
ApiError.EXCHANGE_INVALID_SIGNATURE: (
|
701
|
+
status.HTTP_400_BAD_REQUEST,
|
702
|
+
status.WS_1008_POLICY_VIOLATION,
|
703
|
+
),
|
704
|
+
ApiError.EXCHANGE_INVALID_TIMESTAMP: (
|
705
|
+
status.HTTP_400_BAD_REQUEST,
|
706
|
+
status.WS_1008_POLICY_VIOLATION,
|
707
|
+
),
|
708
|
+
ApiError.EXCHANGE_IP_RESTRICTED: (
|
709
|
+
status.HTTP_400_BAD_REQUEST,
|
710
|
+
status.WS_1008_POLICY_VIOLATION,
|
711
|
+
),
|
712
|
+
ApiError.EXCHANGE_KEY_IN_USE: (
|
713
|
+
status.HTTP_400_BAD_REQUEST,
|
714
|
+
status.WS_1008_POLICY_VIOLATION,
|
715
|
+
),
|
716
|
+
ApiError.EXCHANGE_SYSTEM_CONFIG_ERROR: (
|
717
|
+
status.HTTP_400_BAD_REQUEST,
|
718
|
+
status.WS_1008_POLICY_VIOLATION,
|
719
|
+
),
|
720
|
+
ApiError.HEDGE_MODE_NOT_ACTIVE: (
|
721
|
+
status.HTTP_400_BAD_REQUEST,
|
722
|
+
status.WS_1008_POLICY_VIOLATION,
|
723
|
+
),
|
724
|
+
ApiError.HTTP_ERROR: (
|
725
|
+
status.HTTP_400_BAD_REQUEST,
|
726
|
+
status.WS_1008_POLICY_VIOLATION,
|
727
|
+
),
|
728
|
+
ApiError.INSUFFICIENT_BALANCE: (
|
729
|
+
status.HTTP_400_BAD_REQUEST,
|
730
|
+
status.WS_1008_POLICY_VIOLATION,
|
731
|
+
),
|
571
732
|
ApiError.INSUFFICIENT_MARGIN: status.HTTP_400_BAD_REQUEST,
|
572
|
-
ApiError.INVALID_EXCHANGE_KEY:
|
573
|
-
|
574
|
-
|
575
|
-
|
733
|
+
ApiError.INVALID_EXCHANGE_KEY: (
|
734
|
+
status.HTTP_400_BAD_REQUEST,
|
735
|
+
status.WS_1008_POLICY_VIOLATION,
|
736
|
+
),
|
737
|
+
ApiError.INVALID_MARGIN_MODE: (
|
738
|
+
status.HTTP_400_BAD_REQUEST,
|
739
|
+
status.WS_1008_POLICY_VIOLATION,
|
740
|
+
),
|
741
|
+
ApiError.INVALID_PARAMETER: (
|
742
|
+
status.HTTP_400_BAD_REQUEST,
|
743
|
+
status.WS_1008_POLICY_VIOLATION,
|
744
|
+
),
|
745
|
+
ApiError.LEVERAGE_EXCEEDED: (
|
746
|
+
status.HTTP_400_BAD_REQUEST,
|
747
|
+
status.WS_1008_POLICY_VIOLATION,
|
748
|
+
),
|
576
749
|
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
|
-
|
750
|
+
ApiError.ORDER_ALREADY_FILLED: (
|
751
|
+
status.HTTP_400_BAD_REQUEST,
|
752
|
+
status.WS_1008_POLICY_VIOLATION,
|
753
|
+
),
|
754
|
+
ApiError.ORDER_IN_PROCESS: (
|
755
|
+
status.HTTP_400_BAD_REQUEST,
|
756
|
+
status.WS_1008_POLICY_VIOLATION,
|
757
|
+
),
|
758
|
+
ApiError.ORDER_LIMIT_EXCEEDED: (
|
759
|
+
status.HTTP_400_BAD_REQUEST,
|
760
|
+
status.WS_1008_POLICY_VIOLATION,
|
761
|
+
),
|
762
|
+
ApiError.ORDER_PRICE_INVALID: (
|
763
|
+
status.HTTP_400_BAD_REQUEST,
|
764
|
+
status.WS_1008_POLICY_VIOLATION,
|
765
|
+
),
|
766
|
+
ApiError.ORDER_SIZE_TOO_LARGE: (
|
767
|
+
status.HTTP_400_BAD_REQUEST,
|
768
|
+
status.WS_1008_POLICY_VIOLATION,
|
769
|
+
),
|
770
|
+
ApiError.ORDER_SIZE_TOO_SMALL: (
|
771
|
+
status.HTTP_400_BAD_REQUEST,
|
772
|
+
status.WS_1008_POLICY_VIOLATION,
|
773
|
+
),
|
774
|
+
ApiError.POSITION_LIMIT_EXCEEDED: (
|
775
|
+
status.HTTP_400_BAD_REQUEST,
|
776
|
+
status.WS_1008_POLICY_VIOLATION,
|
777
|
+
),
|
778
|
+
ApiError.POST_ONLY_REJECTED: (
|
779
|
+
status.HTTP_400_BAD_REQUEST,
|
780
|
+
status.WS_1008_POLICY_VIOLATION,
|
781
|
+
),
|
782
|
+
ApiError.RISK_LIMIT_EXCEEDED: (
|
783
|
+
status.HTTP_400_BAD_REQUEST,
|
784
|
+
status.WS_1008_POLICY_VIOLATION,
|
785
|
+
),
|
786
|
+
ApiError.STRATEGY_DISABLED: (
|
787
|
+
status.HTTP_400_BAD_REQUEST,
|
788
|
+
status.WS_1008_POLICY_VIOLATION,
|
789
|
+
),
|
790
|
+
ApiError.STRATEGY_LEVERAGE_MISMATCH: (
|
791
|
+
status.HTTP_400_BAD_REQUEST,
|
792
|
+
status.WS_1008_POLICY_VIOLATION,
|
793
|
+
),
|
794
|
+
ApiError.STRATEGY_NOT_SUPPORTING_EXCHANGE: (
|
795
|
+
status.HTTP_400_BAD_REQUEST,
|
796
|
+
status.WS_1008_POLICY_VIOLATION,
|
797
|
+
),
|
798
|
+
ApiError.TRADING_ACTION_EXPIRED: (
|
799
|
+
status.HTTP_400_BAD_REQUEST,
|
800
|
+
status.WS_1008_POLICY_VIOLATION,
|
801
|
+
),
|
802
|
+
ApiError.TRADING_ACTION_SKIPPED: (
|
803
|
+
status.HTTP_400_BAD_REQUEST,
|
804
|
+
status.WS_1008_POLICY_VIOLATION,
|
805
|
+
),
|
591
806
|
# Success cases
|
592
|
-
ApiError.SUCCESS: status.HTTP_200_OK,
|
593
|
-
ApiError.BOT_STOPPING_COMPLETED:
|
594
|
-
|
595
|
-
|
596
|
-
|
597
|
-
ApiError.
|
807
|
+
ApiError.SUCCESS: (status.HTTP_200_OK, status.WS_1000_NORMAL_CLOSURE),
|
808
|
+
ApiError.BOT_STOPPING_COMPLETED: (
|
809
|
+
status.HTTP_200_OK,
|
810
|
+
status.WS_1000_NORMAL_CLOSURE,
|
811
|
+
),
|
812
|
+
ApiError.BOT_STOPPING_STARTED: (
|
813
|
+
status.HTTP_200_OK,
|
814
|
+
status.WS_1000_NORMAL_CLOSURE,
|
815
|
+
),
|
816
|
+
ApiError.OBJECT_CREATED: (
|
817
|
+
status.HTTP_201_CREATED,
|
818
|
+
status.WS_1000_NORMAL_CLOSURE,
|
819
|
+
),
|
820
|
+
ApiError.OBJECT_UPDATED: (status.HTTP_200_OK, status.WS_1000_NORMAL_CLOSURE),
|
821
|
+
ApiError.OBJECT_DELETED: (
|
822
|
+
status.HTTP_204_NO_CONTENT,
|
823
|
+
status.WS_1000_NORMAL_CLOSURE,
|
824
|
+
),
|
598
825
|
}
|
599
826
|
|
600
827
|
@classmethod
|
601
|
-
def
|
828
|
+
def get_http_code(cls, error: ApiError) -> int:
|
602
829
|
"""Get the HTTP status code for the error. If the error is not in the mapping, return 500."""
|
603
|
-
return cls._mapping.get(error,
|
830
|
+
return cls._mapping.get(error, cls._mapping[ApiError.UNKNOWN_ERROR])[0]
|
831
|
+
|
832
|
+
@classmethod
|
833
|
+
def get_websocket_code(cls, error: ApiError) -> int:
|
834
|
+
"""Get the WebSocket status code for the error. If the error is not in the mapping, return 1008."""
|
835
|
+
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
|