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.
Files changed (28) hide show
  1. crypticorn/auth/client/__init__.py +9 -0
  2. crypticorn/auth/client/api/auth_api.py +8 -5
  3. crypticorn/auth/client/api/user_api.py +247 -0
  4. crypticorn/auth/client/models/__init__.py +9 -0
  5. crypticorn/auth/client/models/create_api_key_request.py +2 -1
  6. crypticorn/auth/client/models/get_api_keys200_response_inner.py +2 -1
  7. crypticorn/auth/client/models/user_by_username200_response.py +91 -0
  8. crypticorn/auth/client/models/verify200_response.py +14 -1
  9. crypticorn/auth/client/models/verify_email200_response_auth_auth.py +14 -1
  10. crypticorn/auth/client/models/whoami200_response.py +6 -1
  11. crypticorn/common/__init__.py +1 -0
  12. crypticorn/common/auth.py +13 -9
  13. crypticorn/common/errors.py +310 -88
  14. crypticorn/common/exceptions.py +38 -4
  15. crypticorn/common/pagination.py +49 -0
  16. crypticorn/common/scopes.py +24 -5
  17. crypticorn/hive/client/__init__.py +2 -0
  18. crypticorn/hive/client/api/models_api.py +343 -56
  19. crypticorn/hive/client/models/__init__.py +2 -0
  20. crypticorn/hive/client/models/data_info.py +44 -12
  21. crypticorn/hive/client/models/data_version_info.py +89 -0
  22. crypticorn/hive/client/models/model.py +2 -3
  23. crypticorn/hive/client/models/target_info.py +94 -0
  24. {crypticorn-2.5.3.dist-info → crypticorn-2.6.0.dist-info}/METADATA +1 -1
  25. {crypticorn-2.5.3.dist-info → crypticorn-2.6.0.dist-info}/RECORD +28 -24
  26. {crypticorn-2.5.3.dist-info → crypticorn-2.6.0.dist-info}/WHEEL +1 -1
  27. {crypticorn-2.5.3.dist-info → crypticorn-2.6.0.dist-info}/entry_points.txt +0 -0
  28. {crypticorn-2.5.3.dist-info → crypticorn-2.6.0.dist-info}/top_level.txt +0 -0
@@ -1,5 +1,4 @@
1
- from enum import Enum, EnumMeta, StrEnum
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 get_error_code(self) -> str:
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.EXCHANGE_ERROR,
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 status_code(self) -> int:
504
+ def http_code(self) -> int:
506
505
  """HTTP status code for the error."""
507
- return HttpStatusMapper.get_status_code(self)
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 HttpStatusMapper:
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: status.HTTP_401_UNAUTHORIZED,
516
- ApiError.INVALID_BEARER: status.HTTP_401_UNAUTHORIZED,
517
- ApiError.EXPIRED_API_KEY: status.HTTP_401_UNAUTHORIZED,
518
- ApiError.INVALID_API_KEY: status.HTTP_401_UNAUTHORIZED,
519
- ApiError.NO_CREDENTIALS: status.HTTP_401_UNAUTHORIZED,
520
- ApiError.INSUFFICIENT_SCOPES: status.HTTP_403_FORBIDDEN,
521
- ApiError.EXCHANGE_PERMISSION_DENIED: status.HTTP_403_FORBIDDEN,
522
- ApiError.EXCHANGE_USER_FROZEN: status.HTTP_403_FORBIDDEN,
523
- ApiError.TRADING_LOCKED: status.HTTP_403_FORBIDDEN,
524
- ApiError.FORBIDDEN: status.HTTP_403_FORBIDDEN,
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: status.HTTP_404_NOT_FOUND,
527
- ApiError.OBJECT_NOT_FOUND: status.HTTP_404_NOT_FOUND,
528
- ApiError.ORDER_NOT_FOUND: status.HTTP_404_NOT_FOUND,
529
- ApiError.POSITION_NOT_FOUND: status.HTTP_404_NOT_FOUND,
530
- ApiError.SYMBOL_NOT_FOUND: status.HTTP_404_NOT_FOUND,
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: status.HTTP_409_CONFLICT,
533
- ApiError.OBJECT_ALREADY_EXISTS: status.HTTP_409_CONFLICT,
534
- ApiError.EXCHANGE_KEY_ALREADY_EXISTS: status.HTTP_409_CONFLICT,
535
- ApiError.BOT_ALREADY_DELETED: status.HTTP_409_CONFLICT,
536
- ApiError.STRATEGY_ALREADY_EXISTS: status.HTTP_409_CONFLICT,
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: status.HTTP_415_UNSUPPORTED_MEDIA_TYPE,
539
- ApiError.INVALID_DATA_REQUEST: status.HTTP_422_UNPROCESSABLE_ENTITY,
540
- ApiError.INVALID_DATA_RESPONSE: status.HTTP_422_UNPROCESSABLE_ENTITY,
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: status.HTTP_429_TOO_MANY_REQUESTS,
543
- ApiError.REQUEST_SCOPE_EXCEEDED: status.HTTP_429_TOO_MANY_REQUESTS,
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: status.HTTP_500_INTERNAL_SERVER_ERROR,
546
- ApiError.EXCHANGE_SYSTEM_ERROR: status.HTTP_500_INTERNAL_SERVER_ERROR,
547
- ApiError.NOW_API_DOWN: status.HTTP_500_INTERNAL_SERVER_ERROR,
548
- ApiError.RPC_TIMEOUT: status.HTTP_500_INTERNAL_SERVER_ERROR,
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: status.HTTP_503_SERVICE_UNAVAILABLE,
551
- ApiError.EXCHANGE_MAINTENANCE: status.HTTP_503_SERVICE_UNAVAILABLE,
552
- ApiError.EXCHANGE_SYSTEM_BUSY: status.HTTP_503_SERVICE_UNAVAILABLE,
553
- ApiError.SETTLEMENT_IN_PROGRESS: status.HTTP_503_SERVICE_UNAVAILABLE,
554
- ApiError.POSITION_SUSPENDED: status.HTTP_503_SERVICE_UNAVAILABLE,
555
- ApiError.TRADING_SUSPENDED: status.HTTP_503_SERVICE_UNAVAILABLE,
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.ALPHANUMERIC_CHARACTERS_ONLY: status.HTTP_400_BAD_REQUEST,
558
- ApiError.ALLOCATION_BELOW_EXPOSURE: status.HTTP_400_BAD_REQUEST,
559
- ApiError.ALLOCATION_BELOW_MINIMUM: status.HTTP_400_BAD_REQUEST,
560
- ApiError.BLACK_SWAN: status.HTTP_400_BAD_REQUEST,
561
- ApiError.BOT_DISABLED: status.HTTP_400_BAD_REQUEST,
562
- ApiError.DELETE_BOT_ERROR: status.HTTP_400_BAD_REQUEST,
563
- ApiError.EXCHANGE_INVALID_SIGNATURE: status.HTTP_400_BAD_REQUEST,
564
- ApiError.EXCHANGE_INVALID_TIMESTAMP: status.HTTP_400_BAD_REQUEST,
565
- ApiError.EXCHANGE_IP_RESTRICTED: status.HTTP_400_BAD_REQUEST,
566
- ApiError.EXCHANGE_KEY_IN_USE: status.HTTP_400_BAD_REQUEST,
567
- ApiError.EXCHANGE_SYSTEM_CONFIG_ERROR: status.HTTP_400_BAD_REQUEST,
568
- ApiError.HEDGE_MODE_NOT_ACTIVE: status.HTTP_400_BAD_REQUEST,
569
- ApiError.HTTP_ERROR: status.HTTP_400_BAD_REQUEST,
570
- ApiError.INSUFFICIENT_BALANCE: status.HTTP_400_BAD_REQUEST,
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: status.HTTP_400_BAD_REQUEST,
573
- ApiError.INVALID_MARGIN_MODE: status.HTTP_400_BAD_REQUEST,
574
- ApiError.INVALID_PARAMETER: status.HTTP_400_BAD_REQUEST,
575
- ApiError.LEVERAGE_EXCEEDED: status.HTTP_400_BAD_REQUEST,
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: status.HTTP_400_BAD_REQUEST,
578
- ApiError.ORDER_IN_PROCESS: status.HTTP_400_BAD_REQUEST,
579
- ApiError.ORDER_LIMIT_EXCEEDED: status.HTTP_400_BAD_REQUEST,
580
- ApiError.ORDER_PRICE_INVALID: status.HTTP_400_BAD_REQUEST,
581
- ApiError.ORDER_SIZE_TOO_LARGE: status.HTTP_400_BAD_REQUEST,
582
- ApiError.ORDER_SIZE_TOO_SMALL: status.HTTP_400_BAD_REQUEST,
583
- ApiError.POSITION_LIMIT_EXCEEDED: status.HTTP_400_BAD_REQUEST,
584
- ApiError.POST_ONLY_REJECTED: status.HTTP_400_BAD_REQUEST,
585
- ApiError.RISK_LIMIT_EXCEEDED: status.HTTP_400_BAD_REQUEST,
586
- ApiError.STRATEGY_DISABLED: status.HTTP_400_BAD_REQUEST,
587
- ApiError.STRATEGY_LEVERAGE_MISMATCH: status.HTTP_400_BAD_REQUEST,
588
- ApiError.STRATEGY_NOT_SUPPORTING_EXCHANGE: status.HTTP_400_BAD_REQUEST,
589
- ApiError.TRADING_ACTION_EXPIRED: status.HTTP_400_BAD_REQUEST,
590
- ApiError.TRADING_ACTION_SKIPPED: status.HTTP_400_BAD_REQUEST,
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: status.HTTP_200_OK,
594
- ApiError.BOT_STOPPING_STARTED: status.HTTP_200_OK,
595
- ApiError.OBJECT_CREATED: status.HTTP_201_CREATED,
596
- ApiError.OBJECT_UPDATED: status.HTTP_200_OK,
597
- ApiError.OBJECT_DELETED: status.HTTP_204_NO_CONTENT,
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 get_status_code(cls, error: ApiError) -> int:
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, status.HTTP_500_INTERNAL_SERVER_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]
@@ -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(self) -> ExceptionDetail:
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=self.error.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
@@ -1,5 +1,5 @@
1
1
  from enum import StrEnum
2
- import logging
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