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.
Files changed (31) 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 +320 -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/data_api.py +15 -12
  19. crypticorn/hive/client/api/models_api.py +343 -56
  20. crypticorn/hive/client/models/__init__.py +2 -0
  21. crypticorn/hive/client/models/data_info.py +44 -12
  22. crypticorn/hive/client/models/data_version_info.py +89 -0
  23. crypticorn/hive/client/models/model.py +2 -3
  24. crypticorn/hive/client/models/target_info.py +94 -0
  25. crypticorn/hive/main.py +43 -2
  26. crypticorn/hive/utils.py +65 -0
  27. {crypticorn-2.5.3.dist-info → crypticorn-2.7.0.dist-info}/METADATA +2 -2
  28. {crypticorn-2.5.3.dist-info → crypticorn-2.7.0.dist-info}/RECORD +31 -26
  29. {crypticorn-2.5.3.dist-info → crypticorn-2.7.0.dist-info}/WHEEL +1 -1
  30. {crypticorn-2.5.3.dist-info → crypticorn-2.7.0.dist-info}/entry_points.txt +0 -0
  31. {crypticorn-2.5.3.dist-info → crypticorn-2.7.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,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 get_error_code(self) -> str:
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.EXCHANGE_ERROR,
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 status_code(self) -> int:
510
+ def http_code(self) -> int:
506
511
  """HTTP status code for the error."""
507
- return HttpStatusMapper.get_status_code(self)
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
- class HttpStatusMapper:
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: 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,
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: 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,
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: 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,
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: 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,
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: status.HTTP_429_TOO_MANY_REQUESTS,
543
- ApiError.REQUEST_SCOPE_EXCEEDED: status.HTTP_429_TOO_MANY_REQUESTS,
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: 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,
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: 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,
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.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,
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: 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,
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: 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,
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: 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,
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 get_status_code(cls, error: ApiError) -> int:
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, status.HTTP_500_INTERNAL_SERVER_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]
@@ -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