crypticorn 2.5.2__py3-none-any.whl → 2.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) 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/{hive/client/models/http_validation_error.py → auth/client/models/user_by_username200_response.py} +14 -22
  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/cli/init.py +3 -0
  12. crypticorn/common/__init__.py +2 -1
  13. crypticorn/common/auth.py +13 -9
  14. crypticorn/common/errors.py +312 -105
  15. crypticorn/common/exceptions.py +38 -4
  16. crypticorn/common/mixins.py +19 -0
  17. crypticorn/common/pagination.py +49 -0
  18. crypticorn/common/scopes.py +27 -24
  19. crypticorn/common/status_router.py +9 -7
  20. crypticorn/common/utils.py +12 -6
  21. crypticorn/hive/client/__init__.py +3 -5
  22. crypticorn/hive/client/api/data_api.py +1 -33
  23. crypticorn/hive/client/api/models_api.py +351 -160
  24. crypticorn/hive/client/api/status_api.py +481 -9
  25. crypticorn/hive/client/configuration.py +12 -4
  26. crypticorn/hive/client/models/__init__.py +3 -5
  27. crypticorn/hive/client/models/coins.py +0 -1
  28. crypticorn/hive/client/models/data_info.py +44 -12
  29. crypticorn/hive/client/models/data_version.py +0 -1
  30. crypticorn/{pay/client/models/now_api_status_res.py → hive/client/models/data_version_info.py} +17 -11
  31. crypticorn/hive/client/models/exception_detail.py +114 -0
  32. crypticorn/hive/client/models/model.py +2 -3
  33. crypticorn/hive/client/models/{validation_error.py → target_info.py} +14 -25
  34. crypticorn/hive/client/rest.py +4 -1
  35. crypticorn/klines/client/api/status_api.py +481 -6
  36. crypticorn/klines/client/api/udf_api.py +0 -227
  37. crypticorn/metrics/client/api/status_api.py +476 -1
  38. crypticorn/pay/client/__init__.py +3 -8
  39. crypticorn/pay/client/api/now_payments_api.py +14 -17
  40. crypticorn/pay/client/api/payments_api.py +2 -11
  41. crypticorn/pay/client/api/products_api.py +2 -11
  42. crypticorn/pay/client/api/status_api.py +483 -8
  43. crypticorn/pay/client/api_client.py +2 -2
  44. crypticorn/pay/client/configuration.py +3 -3
  45. crypticorn/pay/client/exceptions.py +2 -2
  46. crypticorn/pay/client/models/__init__.py +3 -8
  47. crypticorn/pay/client/models/{validation_error.py → exception_detail.py} +37 -28
  48. crypticorn/pay/client/models/now_create_invoice_req.py +2 -2
  49. crypticorn/pay/client/models/now_create_invoice_res.py +2 -2
  50. crypticorn/pay/client/models/payment.py +2 -2
  51. crypticorn/pay/client/models/payment_status.py +2 -2
  52. crypticorn/pay/client/models/product_create.py +2 -2
  53. crypticorn/pay/client/models/product_read.py +2 -2
  54. crypticorn/pay/client/models/product_sub_read.py +2 -2
  55. crypticorn/pay/client/models/product_update.py +2 -2
  56. crypticorn/pay/client/models/scope.py +2 -2
  57. crypticorn/pay/client/models/services.py +2 -2
  58. crypticorn/pay/client/rest.py +2 -2
  59. crypticorn/trade/client/__init__.py +3 -7
  60. crypticorn/trade/client/api/api_keys_api.py +5 -20
  61. crypticorn/trade/client/api/bots_api.py +7 -19
  62. crypticorn/trade/client/api/exchanges_api.py +2 -2
  63. crypticorn/trade/client/api/futures_trading_panel_api.py +10 -22
  64. crypticorn/trade/client/api/notifications_api.py +10 -25
  65. crypticorn/trade/client/api/orders_api.py +2 -5
  66. crypticorn/trade/client/api/status_api.py +483 -8
  67. crypticorn/trade/client/api/strategies_api.py +5 -17
  68. crypticorn/trade/client/api/trading_actions_api.py +2 -11
  69. crypticorn/trade/client/api_client.py +2 -2
  70. crypticorn/trade/client/configuration.py +3 -3
  71. crypticorn/trade/client/exceptions.py +2 -2
  72. crypticorn/trade/client/models/__init__.py +3 -7
  73. crypticorn/trade/client/models/action_model.py +2 -2
  74. crypticorn/trade/client/models/bot_model.py +2 -2
  75. crypticorn/trade/client/models/bot_status.py +2 -2
  76. crypticorn/trade/client/models/{validation_error.py → exception_detail.py} +37 -28
  77. crypticorn/trade/client/models/exchange_key_model.py +2 -2
  78. crypticorn/trade/client/models/execution_ids.py +2 -2
  79. crypticorn/trade/client/models/futures_balance.py +2 -2
  80. crypticorn/trade/client/models/futures_trading_action.py +2 -2
  81. crypticorn/trade/client/models/margin_mode.py +2 -2
  82. crypticorn/trade/client/models/notification_model.py +2 -2
  83. crypticorn/trade/client/models/order_model.py +2 -2
  84. crypticorn/trade/client/models/order_status.py +2 -2
  85. crypticorn/trade/client/models/post_futures_action.py +2 -2
  86. crypticorn/trade/client/models/spot_trading_action.py +2 -2
  87. crypticorn/trade/client/models/strategy_exchange_info.py +2 -2
  88. crypticorn/trade/client/models/strategy_model_input.py +2 -2
  89. crypticorn/trade/client/models/strategy_model_output.py +2 -2
  90. crypticorn/trade/client/models/tpsl.py +2 -2
  91. crypticorn/trade/client/models/trading_action_type.py +2 -2
  92. crypticorn/trade/client/rest.py +2 -2
  93. {crypticorn-2.5.2.dist-info → crypticorn-2.6.0.dist-info}/METADATA +1 -1
  94. {crypticorn-2.5.2.dist-info → crypticorn-2.6.0.dist-info}/RECORD +97 -100
  95. {crypticorn-2.5.2.dist-info → crypticorn-2.6.0.dist-info}/WHEEL +1 -1
  96. crypticorn/hive/client/models/validation_error_loc_inner.py +0 -159
  97. crypticorn/pay/client/models/http_validation_error.py +0 -99
  98. crypticorn/pay/client/models/validation_error_loc_inner.py +0 -159
  99. crypticorn/trade/client/models/http_validation_error.py +0 -99
  100. crypticorn/trade/client/models/validation_error_loc_inner.py +0 -159
  101. {crypticorn-2.5.2.dist-info → crypticorn-2.6.0.dist-info}/entry_points.txt +0 -0
  102. {crypticorn-2.5.2.dist-info → crypticorn-2.6.0.dist-info}/top_level.txt +0 -0
@@ -1,22 +1,6 @@
1
- from enum import Enum, EnumMeta, StrEnum
2
- import logging
1
+ from enum import Enum, StrEnum
3
2
  from fastapi import status
4
- from crypticorn.common.mixins import ExcludeEnumMixin
5
-
6
- logger = logging.getLogger(__name__)
7
-
8
-
9
- class Fallback(EnumMeta):
10
- """Fallback to UNKNOWN_ERROR for error codes not yet published to PyPI."""
11
-
12
- def __getattr__(cls, name):
13
- # Let Pydantic/internal stuff pass silently ! fragile
14
- if name.startswith("__"):
15
- raise AttributeError(name)
16
- logger.warning(
17
- f"Unknown error code '{name}' - update crypticorn package or check for typos"
18
- )
19
- return cls.UNKNOWN_ERROR
3
+ from crypticorn.common.mixins import ExcludeEnumMixin, ApiErrorFallback
20
4
 
21
5
 
22
6
  class ApiErrorType(ExcludeEnumMixin, StrEnum):
@@ -37,7 +21,6 @@ class ApiErrorIdentifier(ExcludeEnumMixin, StrEnum):
37
21
 
38
22
  ALLOCATION_BELOW_EXPOSURE = "allocation_below_current_exposure"
39
23
  ALLOCATION_BELOW_MINIMUM = "allocation_below_min_amount"
40
- ALPHANUMERIC_CHARACTERS_ONLY = "alphanumeric_characters_only"
41
24
  BLACK_SWAN = "black_swan"
42
25
  BOT_ALREADY_DELETED = "bot_already_deleted"
43
26
  BOT_DISABLED = "bot_disabled"
@@ -73,6 +56,7 @@ class ApiErrorIdentifier(ExcludeEnumMixin, StrEnum):
73
56
  INVALID_DATA_RESPONSE = "invalid_data_response"
74
57
  INVALID_EXCHANGE_KEY = "invalid_exchange_key"
75
58
  INVALID_MARGIN_MODE = "invalid_margin_mode"
59
+ INVALID_MODEL_NAME = "invalid_model_name"
76
60
  INVALID_PARAMETER = "invalid_parameter_provided"
77
61
  LEVERAGE_EXCEEDED = "leverage_limit_exceeded"
78
62
  LIQUIDATION_PRICE_VIOLATION = "order_violates_liquidation_price_constraints"
@@ -112,7 +96,7 @@ class ApiErrorIdentifier(ExcludeEnumMixin, StrEnum):
112
96
  URL_NOT_FOUND = "requested_resource_not_found"
113
97
 
114
98
  @property
115
- def get_error_code(self) -> str:
99
+ def get_error(self) -> "ApiError":
116
100
  """Get the corresponding ApiError."""
117
101
  return ApiError[self.value]
118
102
 
@@ -126,7 +110,7 @@ class ApiErrorLevel(ExcludeEnumMixin, StrEnum):
126
110
  WARNING = "warning"
127
111
 
128
112
 
129
- class ApiError(ExcludeEnumMixin, Enum, metaclass=Fallback):
113
+ class ApiError(ExcludeEnumMixin, Enum, metaclass=ApiErrorFallback):
130
114
  """API error codes. Fallback to UNKNOWN_ERROR for error codes not yet published to PyPI."""
131
115
 
132
116
  ALLOCATION_BELOW_EXPOSURE = (
@@ -139,11 +123,6 @@ class ApiError(ExcludeEnumMixin, Enum, metaclass=Fallback):
139
123
  ApiErrorType.USER_ERROR,
140
124
  ApiErrorLevel.ERROR,
141
125
  )
142
- ALPHANUMERIC_CHARACTERS_ONLY = (
143
- ApiErrorIdentifier.ALPHANUMERIC_CHARACTERS_ONLY,
144
- ApiErrorType.USER_ERROR,
145
- ApiErrorLevel.ERROR,
146
- )
147
126
  BLACK_SWAN = (
148
127
  ApiErrorIdentifier.BLACK_SWAN,
149
128
  ApiErrorType.USER_ERROR,
@@ -284,6 +263,11 @@ class ApiError(ExcludeEnumMixin, Enum, metaclass=Fallback):
284
263
  ApiErrorType.USER_ERROR,
285
264
  ApiErrorLevel.ERROR,
286
265
  )
266
+ INVALID_MODEL_NAME = (
267
+ ApiErrorIdentifier.INVALID_MODEL_NAME,
268
+ ApiErrorType.USER_ERROR,
269
+ ApiErrorLevel.ERROR,
270
+ )
287
271
  INSUFFICIENT_SCOPES = (
288
272
  ApiErrorIdentifier.INSUFFICIENT_SCOPES,
289
273
  ApiErrorType.USER_ERROR,
@@ -492,7 +476,7 @@ class ApiError(ExcludeEnumMixin, Enum, metaclass=Fallback):
492
476
  )
493
477
  UNKNOWN_ERROR = (
494
478
  ApiErrorIdentifier.UNKNOWN_ERROR,
495
- ApiErrorType.EXCHANGE_ERROR,
479
+ ApiErrorType.SERVER_ERROR,
496
480
  ApiErrorLevel.ERROR,
497
481
  )
498
482
  URL_NOT_FOUND = (
@@ -517,102 +501,325 @@ class ApiError(ExcludeEnumMixin, Enum, metaclass=Fallback):
517
501
  return self.value[2]
518
502
 
519
503
  @property
520
- def status_code(self) -> int:
504
+ def http_code(self) -> int:
521
505
  """HTTP status code for the error."""
522
- 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)
523
512
 
524
513
 
525
- class HttpStatusMapper:
514
+ class StatusCodeMapper:
526
515
  """Map API errors to HTTP status codes."""
527
516
 
528
517
  _mapping = {
529
518
  # Authentication/Authorization
530
- ApiError.EXPIRED_BEARER: status.HTTP_401_UNAUTHORIZED,
531
- ApiError.INVALID_BEARER: status.HTTP_401_UNAUTHORIZED,
532
- ApiError.EXPIRED_API_KEY: status.HTTP_401_UNAUTHORIZED,
533
- ApiError.INVALID_API_KEY: status.HTTP_401_UNAUTHORIZED,
534
- ApiError.NO_CREDENTIALS: status.HTTP_401_UNAUTHORIZED,
535
- ApiError.INSUFFICIENT_SCOPES: status.HTTP_403_FORBIDDEN,
536
- ApiError.EXCHANGE_PERMISSION_DENIED: status.HTTP_403_FORBIDDEN,
537
- ApiError.EXCHANGE_USER_FROZEN: status.HTTP_403_FORBIDDEN,
538
- ApiError.TRADING_LOCKED: status.HTTP_403_FORBIDDEN,
539
- 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
+ ),
540
559
  # Not Found
541
- ApiError.URL_NOT_FOUND: status.HTTP_404_NOT_FOUND,
542
- ApiError.OBJECT_NOT_FOUND: status.HTTP_404_NOT_FOUND,
543
- ApiError.ORDER_NOT_FOUND: status.HTTP_404_NOT_FOUND,
544
- ApiError.POSITION_NOT_FOUND: status.HTTP_404_NOT_FOUND,
545
- 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
+ ),
546
580
  # Conflicts/Duplicates
547
- ApiError.CLIENT_ORDER_ID_REPEATED: status.HTTP_409_CONFLICT,
548
- ApiError.OBJECT_ALREADY_EXISTS: status.HTTP_409_CONFLICT,
549
- ApiError.EXCHANGE_KEY_ALREADY_EXISTS: status.HTTP_409_CONFLICT,
550
- ApiError.BOT_ALREADY_DELETED: status.HTTP_409_CONFLICT,
551
- 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
+ ),
552
601
  # Invalid Content
553
- ApiError.CONTENT_TYPE_ERROR: status.HTTP_415_UNSUPPORTED_MEDIA_TYPE,
554
- ApiError.INVALID_DATA_REQUEST: status.HTTP_422_UNPROCESSABLE_ENTITY,
555
- 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
+ ),
556
614
  # Rate Limits
557
- ApiError.EXCHANGE_RATE_LIMIT: status.HTTP_429_TOO_MANY_REQUESTS,
558
- 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
+ ),
559
623
  # Server Errors
560
- ApiError.UNKNOWN_ERROR: status.HTTP_500_INTERNAL_SERVER_ERROR,
561
- ApiError.EXCHANGE_SYSTEM_ERROR: status.HTTP_500_INTERNAL_SERVER_ERROR,
562
- ApiError.NOW_API_DOWN: status.HTTP_500_INTERNAL_SERVER_ERROR,
563
- 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
+ ),
564
640
  # Service Unavailable
565
- ApiError.EXCHANGE_SERVICE_UNAVAILABLE: status.HTTP_503_SERVICE_UNAVAILABLE,
566
- ApiError.EXCHANGE_MAINTENANCE: status.HTTP_503_SERVICE_UNAVAILABLE,
567
- ApiError.EXCHANGE_SYSTEM_BUSY: status.HTTP_503_SERVICE_UNAVAILABLE,
568
- ApiError.SETTLEMENT_IN_PROGRESS: status.HTTP_503_SERVICE_UNAVAILABLE,
569
- ApiError.POSITION_SUSPENDED: status.HTTP_503_SERVICE_UNAVAILABLE,
570
- 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
+ ),
571
665
  # Bad Requests (400) - Invalid parameters or states
572
- ApiError.ALPHANUMERIC_CHARACTERS_ONLY: status.HTTP_400_BAD_REQUEST,
573
- ApiError.ALLOCATION_BELOW_EXPOSURE: status.HTTP_400_BAD_REQUEST,
574
- ApiError.ALLOCATION_BELOW_MINIMUM: status.HTTP_400_BAD_REQUEST,
575
- ApiError.BLACK_SWAN: status.HTTP_400_BAD_REQUEST,
576
- ApiError.BOT_DISABLED: status.HTTP_400_BAD_REQUEST,
577
- ApiError.DELETE_BOT_ERROR: status.HTTP_400_BAD_REQUEST,
578
- ApiError.EXCHANGE_INVALID_SIGNATURE: status.HTTP_400_BAD_REQUEST,
579
- ApiError.EXCHANGE_INVALID_TIMESTAMP: status.HTTP_400_BAD_REQUEST,
580
- ApiError.EXCHANGE_IP_RESTRICTED: status.HTTP_400_BAD_REQUEST,
581
- ApiError.EXCHANGE_KEY_IN_USE: status.HTTP_400_BAD_REQUEST,
582
- ApiError.EXCHANGE_SYSTEM_CONFIG_ERROR: status.HTTP_400_BAD_REQUEST,
583
- ApiError.HEDGE_MODE_NOT_ACTIVE: status.HTTP_400_BAD_REQUEST,
584
- ApiError.HTTP_ERROR: status.HTTP_400_BAD_REQUEST,
585
- 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
+ ),
586
722
  ApiError.INSUFFICIENT_MARGIN: status.HTTP_400_BAD_REQUEST,
587
- ApiError.INVALID_EXCHANGE_KEY: status.HTTP_400_BAD_REQUEST,
588
- ApiError.INVALID_MARGIN_MODE: status.HTTP_400_BAD_REQUEST,
589
- ApiError.INVALID_PARAMETER: status.HTTP_400_BAD_REQUEST,
590
- 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
+ ),
591
739
  ApiError.LIQUIDATION_PRICE_VIOLATION: status.HTTP_400_BAD_REQUEST,
592
- ApiError.ORDER_ALREADY_FILLED: status.HTTP_400_BAD_REQUEST,
593
- ApiError.ORDER_IN_PROCESS: status.HTTP_400_BAD_REQUEST,
594
- ApiError.ORDER_LIMIT_EXCEEDED: status.HTTP_400_BAD_REQUEST,
595
- ApiError.ORDER_PRICE_INVALID: status.HTTP_400_BAD_REQUEST,
596
- ApiError.ORDER_SIZE_TOO_LARGE: status.HTTP_400_BAD_REQUEST,
597
- ApiError.ORDER_SIZE_TOO_SMALL: status.HTTP_400_BAD_REQUEST,
598
- ApiError.POSITION_LIMIT_EXCEEDED: status.HTTP_400_BAD_REQUEST,
599
- ApiError.POST_ONLY_REJECTED: status.HTTP_400_BAD_REQUEST,
600
- ApiError.RISK_LIMIT_EXCEEDED: status.HTTP_400_BAD_REQUEST,
601
- ApiError.STRATEGY_DISABLED: status.HTTP_400_BAD_REQUEST,
602
- ApiError.STRATEGY_LEVERAGE_MISMATCH: status.HTTP_400_BAD_REQUEST,
603
- ApiError.STRATEGY_NOT_SUPPORTING_EXCHANGE: status.HTTP_400_BAD_REQUEST,
604
- ApiError.TRADING_ACTION_EXPIRED: status.HTTP_400_BAD_REQUEST,
605
- 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
+ ),
606
796
  # Success cases
607
- ApiError.SUCCESS: status.HTTP_200_OK,
608
- ApiError.BOT_STOPPING_COMPLETED: status.HTTP_200_OK,
609
- ApiError.BOT_STOPPING_STARTED: status.HTTP_200_OK,
610
- ApiError.OBJECT_CREATED: status.HTTP_201_CREATED,
611
- ApiError.OBJECT_UPDATED: status.HTTP_200_OK,
612
- 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
+ ),
613
815
  }
614
816
 
615
817
  @classmethod
616
- def get_status_code(cls, error: ApiError) -> int:
818
+ def get_http_code(cls, error: ApiError) -> int:
617
819
  """Get the HTTP status code for the error. If the error is not in the mapping, return 500."""
618
- return cls._mapping.get(error, 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)
@@ -1,3 +1,9 @@
1
+ from enum import EnumMeta
2
+ import logging
3
+
4
+ logger = logging.getLogger("uvicorn")
5
+
6
+
1
7
  class ValidateEnumMixin:
2
8
  """
3
9
  Mixin for validating enum values manually.
@@ -35,3 +41,16 @@ class ExcludeEnumMixin:
35
41
  schema = handler(core_schema)
36
42
  schema.pop("enum", None)
37
43
  return schema
44
+
45
+
46
+ class ApiErrorFallback(EnumMeta):
47
+ """Fallback for enum members that are not yet published to PyPI."""
48
+
49
+ def __getattr__(cls, name):
50
+ # Let Pydantic/internal stuff pass silently ! fragile
51
+ if name.startswith("__"):
52
+ raise AttributeError(name)
53
+ logger.warning(
54
+ f"Unknown enum member '{name}' - update crypticorn package or check for typos"
55
+ )
56
+ return cls.UNKNOWN_ERROR
@@ -0,0 +1,49 @@
1
+ from typing import Generic, Type, TypeVar, List, Optional, Literal
2
+ from pydantic import BaseModel, Field, model_validator
3
+
4
+ T = TypeVar("T")
5
+
6
+
7
+ class PaginatedResponse(BaseModel, Generic[T]):
8
+ """Pydantic model for paginated response
9
+ >>> PaginatedResponse[ItemModel](data=items, total=total_items, page=1, size=10, prev=None, next=2)
10
+ """
11
+
12
+ data: List[T]
13
+ total: int = Field(description="The total number of items")
14
+ page: int = Field(description="The current page number")
15
+ size: int = Field(description="The number of items per page")
16
+ prev: Optional[int] = Field(None, description="The previous page number")
17
+ next: Optional[int] = Field(None, description="The next page number")
18
+
19
+
20
+ class PaginationParams(BaseModel, Generic[T]):
21
+ """Standard pagination parameters for usage in API endpoints. Check the [fastapi docs](https://fastapi.tiangolo.com/tutorial/query-param-models/?h=qu#query-parameters-with-a-pydantic-model) for usage examples.
22
+ The default size is 10 items per page, but can be overridden:
23
+ >>> class HeavyPaginationParams(PaginationParams[T]):
24
+ >>> size: int = Field(default=100, description="The number of items per page")
25
+ """
26
+
27
+ page: int = Field(default=1, description="The current page number")
28
+ size: int = Field(default=10, description="The number of items per page")
29
+ order: Literal["asc", "desc"] = Field(
30
+ default="asc", description="The order to sort by"
31
+ )
32
+ sort: Optional[str] = Field(None, description="The field to sort by")
33
+
34
+ @model_validator(mode="after")
35
+ def validate(self):
36
+ # Extract the generic argument type
37
+ args: tuple = self.__pydantic_generic_metadata__.get("args")
38
+ if not args or not issubclass(args[0], BaseModel):
39
+ raise TypeError(
40
+ "PaginationParams must be used with a Pydantic BaseModel as a generic parameter"
41
+ )
42
+ if self.sort:
43
+ # check if the sort field is valid
44
+ model: Type[BaseModel] = args[0]
45
+ if self.sort and self.sort not in model.model_fields:
46
+ raise ValueError(
47
+ f"Invalid sort field: '{self.sort}' — must be one of: {list(model.model_fields)}"
48
+ )
49
+ return self