crypticorn 2.16.0__py3-none-any.whl → 2.17.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 (164) hide show
  1. crypticorn/__init__.py +2 -2
  2. crypticorn/auth/client/api/admin_api.py +397 -13
  3. crypticorn/auth/client/api/auth_api.py +3610 -341
  4. crypticorn/auth/client/api/service_api.py +249 -7
  5. crypticorn/auth/client/api/user_api.py +2295 -179
  6. crypticorn/auth/client/api/wallet_api.py +1468 -81
  7. crypticorn/auth/client/configuration.py +2 -2
  8. crypticorn/auth/client/models/create_api_key_request.py +2 -1
  9. crypticorn/auth/client/models/get_api_keys200_response_inner.py +2 -1
  10. crypticorn/auth/client/rest.py +23 -4
  11. crypticorn/auth/main.py +8 -5
  12. crypticorn/cli/init.py +1 -1
  13. crypticorn/cli/templates/.env.docker.temp +3 -0
  14. crypticorn/cli/templates/.env.example.temp +4 -0
  15. crypticorn/cli/templates/Dockerfile +5 -2
  16. crypticorn/client.py +226 -59
  17. crypticorn/common/__init__.py +1 -0
  18. crypticorn/common/auth.py +45 -14
  19. crypticorn/common/decorators.py +1 -2
  20. crypticorn/common/enums.py +0 -2
  21. crypticorn/common/errors.py +10 -0
  22. crypticorn/common/metrics.py +30 -0
  23. crypticorn/common/middleware.py +94 -1
  24. crypticorn/common/pagination.py +252 -20
  25. crypticorn/common/router/admin_router.py +2 -2
  26. crypticorn/common/router/status_router.py +40 -2
  27. crypticorn/common/scopes.py +2 -2
  28. crypticorn/common/warnings.py +7 -0
  29. crypticorn/dex/__init__.py +6 -0
  30. crypticorn/dex/client/__init__.py +49 -0
  31. crypticorn/dex/client/api/__init__.py +6 -0
  32. crypticorn/dex/client/api/admin_api.py +2986 -0
  33. crypticorn/dex/client/api/signals_api.py +1798 -0
  34. crypticorn/dex/client/api/status_api.py +892 -0
  35. crypticorn/dex/client/api_client.py +758 -0
  36. crypticorn/dex/client/api_response.py +20 -0
  37. crypticorn/dex/client/configuration.py +620 -0
  38. crypticorn/dex/client/exceptions.py +220 -0
  39. crypticorn/dex/client/models/__init__.py +30 -0
  40. crypticorn/dex/client/models/api_error_identifier.py +121 -0
  41. crypticorn/dex/client/models/api_error_level.py +37 -0
  42. crypticorn/dex/client/models/api_error_type.py +37 -0
  43. crypticorn/dex/client/models/exception_detail.py +117 -0
  44. crypticorn/dex/client/models/log_level.py +38 -0
  45. crypticorn/dex/client/models/paginated_response_signal_with_token.py +134 -0
  46. crypticorn/dex/client/models/risk.py +86 -0
  47. crypticorn/dex/client/models/signal_overview_stats.py +158 -0
  48. crypticorn/dex/client/models/signal_volume.py +84 -0
  49. crypticorn/dex/client/models/signal_with_token.py +163 -0
  50. crypticorn/dex/client/models/token_data.py +127 -0
  51. crypticorn/dex/client/models/token_detail.py +116 -0
  52. crypticorn/dex/client/py.typed +0 -0
  53. crypticorn/dex/client/rest.py +217 -0
  54. crypticorn/dex/main.py +1 -0
  55. crypticorn/hive/client/api/admin_api.py +1173 -47
  56. crypticorn/hive/client/api/data_api.py +499 -17
  57. crypticorn/hive/client/api/models_api.py +1595 -87
  58. crypticorn/hive/client/api/status_api.py +397 -16
  59. crypticorn/hive/client/api_client.py +0 -5
  60. crypticorn/hive/client/models/api_error_identifier.py +1 -1
  61. crypticorn/hive/client/models/coin_info.py +1 -1
  62. crypticorn/hive/client/models/exception_detail.py +1 -1
  63. crypticorn/hive/client/models/target_info.py +1 -1
  64. crypticorn/hive/client/rest.py +23 -4
  65. crypticorn/hive/main.py +99 -25
  66. crypticorn/hive/utils.py +2 -2
  67. crypticorn/klines/client/api/admin_api.py +1173 -47
  68. crypticorn/klines/client/api/change_in_timeframe_api.py +269 -11
  69. crypticorn/klines/client/api/funding_rates_api.py +315 -11
  70. crypticorn/klines/client/api/ohlcv_data_api.py +390 -11
  71. crypticorn/klines/client/api/status_api.py +397 -16
  72. crypticorn/klines/client/api/symbols_api.py +216 -11
  73. crypticorn/klines/client/api/udf_api.py +1268 -51
  74. crypticorn/klines/client/api_client.py +0 -5
  75. crypticorn/klines/client/models/api_error_identifier.py +3 -1
  76. crypticorn/klines/client/models/exception_detail.py +1 -1
  77. crypticorn/klines/client/models/ohlcv.py +1 -1
  78. crypticorn/klines/client/models/symbol_group.py +1 -1
  79. crypticorn/klines/client/models/udf_config.py +1 -1
  80. crypticorn/klines/client/rest.py +23 -4
  81. crypticorn/klines/main.py +89 -12
  82. crypticorn/metrics/client/api/admin_api.py +1173 -47
  83. crypticorn/metrics/client/api/exchanges_api.py +1370 -145
  84. crypticorn/metrics/client/api/indicators_api.py +622 -17
  85. crypticorn/metrics/client/api/logs_api.py +296 -11
  86. crypticorn/metrics/client/api/marketcap_api.py +1207 -67
  87. crypticorn/metrics/client/api/markets_api.py +343 -11
  88. crypticorn/metrics/client/api/quote_currencies_api.py +228 -11
  89. crypticorn/metrics/client/api/status_api.py +397 -16
  90. crypticorn/metrics/client/api/tokens_api.py +382 -15
  91. crypticorn/metrics/client/api_client.py +0 -5
  92. crypticorn/metrics/client/configuration.py +4 -2
  93. crypticorn/metrics/client/models/exception_detail.py +1 -1
  94. crypticorn/metrics/client/models/exchange_mapping.py +1 -1
  95. crypticorn/metrics/client/models/marketcap_ranking.py +1 -1
  96. crypticorn/metrics/client/models/marketcap_symbol_ranking.py +1 -1
  97. crypticorn/metrics/client/models/ohlcv.py +1 -1
  98. crypticorn/metrics/client/rest.py +23 -4
  99. crypticorn/metrics/main.py +113 -19
  100. crypticorn/pay/client/api/admin_api.py +1585 -57
  101. crypticorn/pay/client/api/now_payments_api.py +961 -39
  102. crypticorn/pay/client/api/payments_api.py +562 -17
  103. crypticorn/pay/client/api/products_api.py +880 -30
  104. crypticorn/pay/client/api/status_api.py +397 -16
  105. crypticorn/pay/client/api_client.py +0 -5
  106. crypticorn/pay/client/configuration.py +2 -2
  107. crypticorn/pay/client/models/api_error_identifier.py +7 -7
  108. crypticorn/pay/client/models/exception_detail.py +1 -1
  109. crypticorn/pay/client/models/now_create_invoice_req.py +1 -1
  110. crypticorn/pay/client/models/now_create_invoice_res.py +1 -1
  111. crypticorn/pay/client/models/product.py +1 -1
  112. crypticorn/pay/client/models/product_create.py +1 -1
  113. crypticorn/pay/client/models/product_update.py +1 -1
  114. crypticorn/pay/client/models/scope.py +1 -0
  115. crypticorn/pay/client/rest.py +23 -4
  116. crypticorn/pay/main.py +10 -6
  117. crypticorn/trade/client/__init__.py +11 -1
  118. crypticorn/trade/client/api/__init__.py +0 -1
  119. crypticorn/trade/client/api/admin_api.py +1184 -55
  120. crypticorn/trade/client/api/api_keys_api.py +1678 -162
  121. crypticorn/trade/client/api/bots_api.py +7563 -187
  122. crypticorn/trade/client/api/exchanges_api.py +565 -19
  123. crypticorn/trade/client/api/notifications_api.py +1290 -116
  124. crypticorn/trade/client/api/orders_api.py +393 -55
  125. crypticorn/trade/client/api/status_api.py +397 -13
  126. crypticorn/trade/client/api/strategies_api.py +1133 -77
  127. crypticorn/trade/client/api/trading_actions_api.py +786 -65
  128. crypticorn/trade/client/models/__init__.py +11 -0
  129. crypticorn/trade/client/models/actions_count.py +88 -0
  130. crypticorn/trade/client/models/api_error_identifier.py +1 -0
  131. crypticorn/trade/client/models/bot.py +7 -18
  132. crypticorn/trade/client/models/bot_create.py +17 -1
  133. crypticorn/trade/client/models/bot_update.py +17 -1
  134. crypticorn/trade/client/models/exchange.py +6 -1
  135. crypticorn/trade/client/models/exchange_key.py +1 -1
  136. crypticorn/trade/client/models/exchange_key_balance.py +111 -0
  137. crypticorn/trade/client/models/exchange_key_create.py +17 -1
  138. crypticorn/trade/client/models/exchange_key_update.py +17 -1
  139. crypticorn/trade/client/models/execution_ids.py +1 -1
  140. crypticorn/trade/client/models/futures_balance.py +27 -25
  141. crypticorn/trade/client/models/notification.py +17 -1
  142. crypticorn/trade/client/models/notification_create.py +18 -2
  143. crypticorn/trade/client/models/notification_update.py +17 -1
  144. crypticorn/trade/client/models/orders_count.py +88 -0
  145. crypticorn/trade/client/models/paginated_response_futures_trading_action.py +134 -0
  146. crypticorn/trade/client/models/paginated_response_order.py +134 -0
  147. crypticorn/trade/client/models/pn_l.py +95 -0
  148. crypticorn/trade/client/models/post_futures_action.py +1 -1
  149. crypticorn/trade/client/models/spot_balance.py +109 -0
  150. crypticorn/trade/client/models/strategy.py +22 -4
  151. crypticorn/trade/client/models/strategy_create.py +23 -5
  152. crypticorn/trade/client/models/strategy_exchange_info.py +16 -4
  153. crypticorn/trade/client/models/strategy_update.py +19 -3
  154. crypticorn/trade/client/models/tpsl.py +4 -19
  155. crypticorn/trade/client/models/tpsl_create.py +6 -19
  156. crypticorn/trade/client/rest.py +23 -4
  157. crypticorn/trade/main.py +15 -12
  158. {crypticorn-2.16.0.dist-info → crypticorn-2.17.0.dist-info}/METADATA +65 -20
  159. {crypticorn-2.16.0.dist-info → crypticorn-2.17.0.dist-info}/RECORD +163 -128
  160. crypticorn/trade/client/api/futures_trading_panel_api.py +0 -1285
  161. {crypticorn-2.16.0.dist-info → crypticorn-2.17.0.dist-info}/WHEEL +0 -0
  162. {crypticorn-2.16.0.dist-info → crypticorn-2.17.0.dist-info}/entry_points.txt +0 -0
  163. {crypticorn-2.16.0.dist-info → crypticorn-2.17.0.dist-info}/licenses/LICENSE +0 -0
  164. {crypticorn-2.16.0.dist-info → crypticorn-2.17.0.dist-info}/top_level.txt +0 -0
@@ -1,8 +1,7 @@
1
- from datetime import datetime
2
1
  from typing import Optional, Type, Any, Tuple
3
2
  from copy import deepcopy
4
3
 
5
- from pydantic import BaseModel, create_model, field_validator, BeforeValidator
4
+ from pydantic import BaseModel, create_model
6
5
  from pydantic.fields import FieldInfo
7
6
 
8
7
 
@@ -5,9 +5,7 @@ try:
5
5
  except ImportError:
6
6
  from strenum import StrEnum
7
7
 
8
- from enum import Enum
9
8
  import warnings
10
- import typing_extensions
11
9
  from crypticorn.common.mixins import ValidateEnumMixin
12
10
  from crypticorn.common.warnings import CrypticornDeprecatedSince215
13
11
 
@@ -70,6 +70,7 @@ class ApiErrorIdentifier(StrEnum):
70
70
  INSUFFICIENT_MARGIN = "insufficient_margin"
71
71
  INSUFFICIENT_SCOPES = "insufficient_scopes"
72
72
  INVALID_API_KEY = "invalid_api_key"
73
+ INVALID_BASIC_AUTH = "invalid_basic_auth"
73
74
  INVALID_BEARER = "invalid_bearer"
74
75
  INVALID_DATA_REQUEST = "invalid_data"
75
76
  INVALID_DATA_RESPONSE = "invalid_data_response"
@@ -302,6 +303,11 @@ class ApiError(Enum, metaclass=ApiErrorFallback):
302
303
  ApiErrorType.USER_ERROR,
303
304
  ApiErrorLevel.ERROR,
304
305
  )
306
+ INVALID_BASIC_AUTH = (
307
+ ApiErrorIdentifier.INVALID_BASIC_AUTH,
308
+ ApiErrorType.USER_ERROR,
309
+ ApiErrorLevel.ERROR,
310
+ )
305
311
  INVALID_BEARER = (
306
312
  ApiErrorIdentifier.INVALID_BEARER,
307
313
  ApiErrorType.USER_ERROR,
@@ -569,6 +575,10 @@ class StatusCodeMapper:
569
575
  status.HTTP_401_UNAUTHORIZED,
570
576
  status.WS_1008_POLICY_VIOLATION,
571
577
  ),
578
+ ApiError.INVALID_BASIC_AUTH: (
579
+ status.HTTP_401_UNAUTHORIZED,
580
+ status.WS_1008_POLICY_VIOLATION,
581
+ ),
572
582
  ApiError.INVALID_BEARER: (
573
583
  status.HTTP_401_UNAUTHORIZED,
574
584
  status.WS_1008_POLICY_VIOLATION,
@@ -0,0 +1,30 @@
1
+ # metrics/registry.py
2
+ from prometheus_client import Counter, Histogram, CollectorRegistry
3
+
4
+ registry = CollectorRegistry()
5
+
6
+ HTTP_REQUESTS_COUNT = Counter(
7
+ "http_requests_total",
8
+ "Total HTTP requests",
9
+ ["method", "endpoint", "status_code", "auth_type"],
10
+ registry=registry,
11
+ )
12
+
13
+ HTTP_REQUEST_DURATION = Histogram(
14
+ "http_request_duration_seconds",
15
+ "HTTP request duration in seconds",
16
+ ["endpoint", "method"],
17
+ registry=registry,
18
+ )
19
+
20
+ REQUEST_SIZE = Histogram(
21
+ "http_request_size_bytes", "Size of HTTP request bodies", ["method", "endpoint"]
22
+ )
23
+
24
+
25
+ RESPONSE_SIZE = Histogram(
26
+ "http_response_size_bytes",
27
+ "Size of HTTP responses",
28
+ ["method", "endpoint"],
29
+ registry=registry,
30
+ )
@@ -1,10 +1,102 @@
1
- from fastapi import FastAPI
1
+ import time
2
+ from fastapi import FastAPI, Request
2
3
  from fastapi.middleware.cors import CORSMiddleware
4
+ from starlette.middleware.base import BaseHTTPMiddleware
3
5
  from crypticorn.common.logging import configure_logging
4
6
  from contextlib import asynccontextmanager
7
+ from typing_extensions import deprecated
8
+ import warnings
9
+ from crypticorn.common.warnings import CrypticornDeprecatedSince217
10
+ from crypticorn.common.metrics import (
11
+ HTTP_REQUESTS_COUNT,
12
+ HTTP_REQUEST_DURATION,
13
+ REQUEST_SIZE,
14
+ RESPONSE_SIZE,
15
+ )
5
16
 
6
17
 
18
+ class PrometheusMiddleware(BaseHTTPMiddleware):
19
+ async def dispatch(self, request: Request, call_next):
20
+
21
+ if "authorization" in request.headers:
22
+ auth_type = (
23
+ request.headers["authorization"].split()[0]
24
+ if " " in request.headers["authorization"]
25
+ else "none"
26
+ )
27
+ elif "x-api-key" in request.headers:
28
+ auth_type = "X-API-KEY"
29
+ else:
30
+ auth_type = "none"
31
+
32
+ try:
33
+ endpoint = request.get(
34
+ "route"
35
+ ).path # use /time/{type} instead of dynamic route to avoid high cardinality
36
+ except Exception:
37
+ endpoint = request.url.path
38
+
39
+ start = time.perf_counter()
40
+ response = await call_next(request)
41
+ duration = time.perf_counter() - start
42
+
43
+ HTTP_REQUESTS_COUNT.labels(
44
+ method=request.method,
45
+ endpoint=endpoint,
46
+ status_code=response.status_code,
47
+ auth_type=auth_type,
48
+ ).inc()
49
+
50
+ try:
51
+ body = await request.body()
52
+ size = len(body)
53
+ except Exception:
54
+ size = 0
55
+
56
+ REQUEST_SIZE.labels(
57
+ method=request.method,
58
+ endpoint=endpoint,
59
+ ).observe(size)
60
+
61
+ try:
62
+ body = await response.body()
63
+ size = len(body)
64
+ except Exception:
65
+ size = 0
66
+
67
+ RESPONSE_SIZE.labels(
68
+ method=request.method,
69
+ endpoint=endpoint,
70
+ ).observe(size)
71
+
72
+ HTTP_REQUEST_DURATION.labels(
73
+ endpoint=endpoint,
74
+ method=request.method,
75
+ ).observe(duration)
76
+
77
+ return response
78
+
79
+
80
+ @deprecated("Use add_middleware instead", category=None)
7
81
  def add_cors_middleware(app: "FastAPI"):
82
+ warnings.warn(
83
+ "add_cors_middleware is deprecated. Use add_middleware instead.",
84
+ CrypticornDeprecatedSince217,
85
+ )
86
+ app.add_middleware(
87
+ CORSMiddleware,
88
+ allow_origins=[
89
+ "http://localhost:5173", # vite dev server
90
+ "http://localhost:4173", # vite preview server
91
+ ],
92
+ allow_origin_regex="^https://([a-zA-Z0-9-]+.)*crypticorn.(dev|com)/?$", # matches (multiple or no) subdomains of crypticorn.dev and crypticorn.com
93
+ allow_credentials=True,
94
+ allow_methods=["*"],
95
+ allow_headers=["*"],
96
+ )
97
+
98
+
99
+ def add_middleware(app: "FastAPI"):
8
100
  app.add_middleware(
9
101
  CORSMiddleware,
10
102
  allow_origins=[
@@ -16,6 +108,7 @@ def add_cors_middleware(app: "FastAPI"):
16
108
  allow_methods=["*"],
17
109
  allow_headers=["*"],
18
110
  )
111
+ app.add_middleware(PrometheusMiddleware)
19
112
 
20
113
 
21
114
  @asynccontextmanager
@@ -1,53 +1,285 @@
1
- """Utilities for handling paginated API responses and cursor-based pagination."""
1
+ """Utilities for handling paginated API responses, cursor-based pagination, filtering, sorting, etc."""
2
2
 
3
- from typing import Generic, Type, TypeVar, Optional, Literal
3
+ from typing import Annotated, Any, Generic, Type, TypeVar, Optional, Literal
4
4
  from pydantic import BaseModel, Field, model_validator
5
+ import math
5
6
 
6
7
  T = TypeVar("T")
7
8
 
8
9
 
9
10
  class PaginatedResponse(BaseModel, Generic[T]):
10
11
  """Pydantic model for paginated response
11
- >>> PaginatedResponse[ItemModel](data=items, total=total_items, page=1, size=10, prev=None, next=2)
12
+ >>> @router.get("", operation_id="getOrders")
13
+ >>> async def get_orders(
14
+ >>> params: Annotated[PaginationParams, Query()],
15
+ >>> ) -> PaginatedResponse[Order]:
16
+ >>> ...
17
+ >>> return PaginatedResponse[Order](
18
+ data=orders,
19
+ total=count,
20
+ page=params.page,
21
+ page_size=params.page_size,
22
+ prev=PaginatedResponse.get_prev_page(params.page),
23
+ next=PaginatedResponse.get_next_page(count, params.page_size, params.page),
24
+ last=PaginatedResponse.get_last_page(count, params.page_size),
25
+ )
12
26
  """
13
27
 
14
28
  data: list[T]
15
29
  total: int = Field(description="The total number of items")
16
30
  page: int = Field(description="The current page number")
17
- size: int = Field(description="The number of items per page")
31
+ page_size: int = Field(description="The number of items per page")
18
32
  prev: Optional[int] = Field(None, description="The previous page number")
19
33
  next: Optional[int] = Field(None, description="The next page number")
34
+ last: Optional[int] = Field(None, description="The last page number")
35
+
36
+ @staticmethod
37
+ def get_next_page(total: int, page_size: int, page: int) -> Optional[int]:
38
+ """Get the next page number"""
39
+ if page < math.ceil(total / page_size):
40
+ return page + 1
41
+ return None
42
+
43
+ @staticmethod
44
+ def get_prev_page(page: int) -> Optional[int]:
45
+ """Get the previous page number"""
46
+ if page > 1:
47
+ return page - 1
48
+ return None
49
+
50
+ @staticmethod
51
+ def get_last_page(total: int, page_size: int) -> int:
52
+ """Get the last page number"""
53
+ return max(1, math.ceil(total / page_size))
20
54
 
21
55
 
22
56
  class PaginationParams(BaseModel, Generic[T]):
23
57
  """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.
24
- The default size is 10 items per page, but can be overridden:
25
- >>> class HeavyPaginationParams(PaginationParams[T]):
26
- >>> size: int = Field(default=100, description="The number of items per page")
58
+ You should inherit from this class when adding additional parameters. You should use this class in combination with `PaginatedResponse` to return the paginated response.
59
+ Usage:
60
+ >>> @router.get("", operation_id="getOrders")
61
+ >>> async def get_orders(
62
+ >>> params: Annotated[PaginationParams[Order], Query()],
63
+ >>> ) -> PaginatedResponse[Order]:
64
+ >>> ...
65
+
66
+ The default size is 10 items per page and there is a `HeavyPaginationParams` class with 100 items per page. You can override this default:
67
+ >>> class LightPaginationParams(PaginationParams):
68
+ >>> page_size: int = Field(default=5, description="The number of items per page")
69
+ """
70
+
71
+ page: Optional[int] = Field(default=1, description="The current page number")
72
+ page_size: Annotated[int, Field(ge=1, le=100)] = Field(
73
+ 10, description="The number of items per page. Default is 10, max is 100."
74
+ )
75
+
76
+
77
+ class HeavyPaginationParams(PaginationParams[T]):
78
+ """Pagination parameters with a higher default size. Refer to `PaginationParams` for usage examples."""
79
+
80
+ page_size: Annotated[int, Field(ge=1, le=1000)] = Field(
81
+ 100, description="The number of items per page. Default is 100, max is 1000."
82
+ )
83
+
84
+
85
+ class SortParams(BaseModel, Generic[T]):
86
+ """Standard sort 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.
87
+ You should inherit from this class when adding additional parameters.
88
+ Usage:
89
+ >>> @router.get("", operation_id="getOrders")
90
+ >>> async def get_orders(
91
+ >>> params: Annotated[SortParams[Order], Query()],
92
+ >>> ) -> PaginatedResponse[Order]:
93
+ >>> ...
27
94
  """
28
95
 
29
- page: int = Field(default=1, description="The current page number")
30
- size: int = Field(default=10, description="The number of items per page")
31
- order: Optional[Literal["asc", "desc"]] = Field(None, description="The order to sort by")
32
- sort: Optional[str] = Field(None, description="The field to sort by")
96
+ sort_order: Optional[Literal["asc", "desc"]] = Field(
97
+ None, description="The order to sort by"
98
+ )
99
+ sort_by: Optional[str] = Field(None, description="The field to sort by")
33
100
 
34
101
  @model_validator(mode="after")
35
- def validate(self):
102
+ def validate_sort(self):
36
103
  # Extract the generic argument type
37
104
  args: tuple = self.__pydantic_generic_metadata__.get("args")
38
105
  if not args or not issubclass(args[0], BaseModel):
39
106
  raise TypeError(
40
- "PaginationParams must be used with a Pydantic BaseModel as a generic parameter"
107
+ "SortParams must be used with a Pydantic BaseModel as a generic parameter"
41
108
  )
42
- if self.sort:
109
+ if self.sort_by:
43
110
  # check if the sort field is valid
44
111
  model: Type[BaseModel] = args[0]
45
- if self.sort and self.sort not in model.model_fields:
112
+ if self.sort_by not in model.model_fields:
113
+ raise ValueError(
114
+ f"Invalid field: '{self.sort_by}'. Must be one of: {list(model.model_fields)}"
115
+ )
116
+ if self.sort_order and self.sort_order not in ["asc", "desc"]:
117
+ raise ValueError(
118
+ f"Invalid order: '{self.sort_order}' — must be one of: ['asc', 'desc']"
119
+ )
120
+ if (
121
+ self.sort_order
122
+ and self.sort_by is None
123
+ or self.sort_by
124
+ and self.sort_order is None
125
+ ):
126
+ raise ValueError("sort_order and sort_by must be provided together")
127
+ return self
128
+
129
+
130
+ class FilterParams(BaseModel, Generic[T]):
131
+ """Standard filter 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.
132
+ You should inherit from this class when adding additional parameters.
133
+ Usage:
134
+ >>> @router.get("", operation_id="getOrders")
135
+ >>> async def get_orders(
136
+ >>> params: Annotated[FilterParams[Order], Query()],
137
+ >>> ) -> PaginatedResponse[Order]:
138
+ >>> ...
139
+ """
140
+
141
+ filter_by: Optional[str] = Field(None, description="The field to filter by")
142
+ filter_value: Optional[str] = Field(None, description="The value to filter with")
143
+ # currently openapi-gen does not support typing.Any in combo with None, so we use str
144
+ # this is fine since the input is a string anyways from the request and the correct type is enforced by the model validator from the filter_by field
145
+
146
+ @model_validator(mode="after")
147
+ def validate_filter(self):
148
+ if self.filter_by and not self.filter_value:
149
+ raise ValueError("filter_by and filter_value must be provided together")
150
+ if self.filter_by:
151
+ # Extract the generic argument type
152
+ args: tuple = self.__pydantic_generic_metadata__.get("args")
153
+ if not args or not issubclass(args[0], BaseModel):
154
+ raise TypeError(
155
+ "FilterParams must be used with a Pydantic BaseModel as a generic parameter"
156
+ )
157
+ # check if the filter field is valid
158
+ model: Type[BaseModel] = args[0]
159
+ if self.filter_by not in model.model_fields:
46
160
  raise ValueError(
47
- f"Invalid sort field: '{self.sort}' must be one of: {list(model.model_fields)}"
161
+ f"Invalid field: '{self.filter_by}'. Must be one of: {list(model.model_fields)}"
48
162
  )
49
- if self.order and self.order not in ["asc", "desc"]:
50
- raise ValueError(f"Invalid order: '{self.order}' — must be one of: ['asc', 'desc']")
51
- if self.order and not self.sort or self.sort and not self.order:
52
- raise ValueError("Sort and order must be provided together")
163
+ self.filter_value = _enforce_field_type(
164
+ model, self.filter_by, self.filter_value
165
+ )
53
166
  return self
167
+
168
+
169
+ class SortFilterParams(SortParams[T], FilterParams[T]):
170
+ """Combines sort and filter parameters. Just a convenience class for when you need to combine sort and filter parameters.
171
+ You should inherit from this class when adding additional parameters.
172
+ Usage:
173
+ >>> @router.get("", operation_id="getOrders")
174
+ >>> async def get_orders(
175
+ >>> params: Annotated[SortFilterParams[Order], Query()],
176
+ >>> ) -> PaginatedResponse[Order]:
177
+ >>> ...
178
+ """
179
+
180
+ @model_validator(mode="after")
181
+ def validate_sort_filter(self):
182
+ self.validate_sort()
183
+ self.validate_filter()
184
+ return self
185
+
186
+
187
+ class PageFilterParams(PaginationParams[T], FilterParams[T]):
188
+ """Combines pagination and filter parameters. Just a convenience class for when you need to combine pagination and filter parameters.
189
+ You should inherit from this class when adding additional parameters.
190
+ Usage:
191
+ >>> @router.get("", operation_id="getOrders")
192
+ >>> async def get_orders(
193
+ >>> params: Annotated[PageFilterParams[Order], Query()],
194
+ >>> ) -> PaginatedResponse[Order]:
195
+ >>> ...
196
+ """
197
+
198
+ @model_validator(mode="after")
199
+ def validate_page_filter(self):
200
+ self.validate_filter()
201
+ return self
202
+
203
+
204
+ class PageSortParams(PaginationParams[T], SortParams[T]):
205
+ """Combines pagination and sort parameters. Just a convenience class for when you need to combine pagination and sort parameters.
206
+ You should inherit from this class when adding additional parameters.
207
+ Usage:
208
+ >>> @router.get("", operation_id="getOrders")
209
+ >>> async def get_orders(
210
+ >>> params: Annotated[PageSortParams[Order], Query()],
211
+ >>> ) -> PaginatedResponse[Order]:
212
+ >>> ...
213
+ """
214
+
215
+ @model_validator(mode="after")
216
+ def validate_page_sort(self):
217
+ self.validate_sort()
218
+ return self
219
+
220
+
221
+ class PageSortFilterParams(
222
+ PaginationParams[T],
223
+ SortParams[T],
224
+ FilterParams[T],
225
+ ):
226
+ """Combines pagination, filter, and sort parameters. Just a convenience class for when you need to combine pagination, filter, and sort parameters.
227
+ You should inherit from this class when adding additional parameters.
228
+ Usage:
229
+ >>> @router.get("", operation_id="getOrders")
230
+ >>> async def get_orders(
231
+ >>> params: Annotated[PageSortFilterParams[Order], Query()],
232
+ >>> ) -> PaginatedResponse[Order]:
233
+ >>> ...
234
+ """
235
+
236
+ @model_validator(mode="after")
237
+ def validate_filter_combo(self):
238
+ self.validate_filter()
239
+ self.validate_sort()
240
+ return self
241
+
242
+
243
+ class HeavyPageSortFilterParams(
244
+ HeavyPaginationParams[T], FilterParams[T], SortParams[T]
245
+ ):
246
+ """Combines heavy pagination, filter, and sort parameters. Just a convenience class for when you need to combine heavy pagination, filter, and sort parameters.
247
+ You should inherit from this class when adding additional parameters.
248
+ Usage:
249
+ >>> @router.get("", operation_id="getOrders")
250
+ >>> async def get_orders(
251
+ >>> params: Annotated[HeavyPageSortFilterParams[Order], Query()],
252
+ >>> ) -> PaginatedResponse[Order]:
253
+ >>> ...
254
+ """
255
+
256
+ @model_validator(mode="after")
257
+ def validate_heavy_page_sort_filter(self):
258
+ self.validate_filter()
259
+ self.validate_sort()
260
+ return self
261
+
262
+
263
+ def _enforce_field_type(model: Type[BaseModel], field_name: str, value: Any) -> Any:
264
+ """
265
+ Coerce or validate `value` to match the type of `field_name` on the given `model`. Should be used after checking that the field is valid.
266
+
267
+ :param model: The Pydantic model.
268
+ :param field_name: The name of the field to match.
269
+ :param value: The value to validate or coerce.
270
+
271
+ :return: The value cast to the expected type.
272
+
273
+ :raises: ValueError: If the field doesn't exist or coercion fails.
274
+ """
275
+ expected_type = model.model_fields[field_name].annotation
276
+
277
+ if isinstance(value, expected_type):
278
+ return value
279
+
280
+ try:
281
+ return expected_type(value)
282
+ except Exception:
283
+ raise ValueError(
284
+ f"Expected {expected_type} for field {field_name}, got {type(value)}"
285
+ )
@@ -11,10 +11,10 @@ import threading
11
11
  import time
12
12
  import psutil
13
13
  import re
14
- from fastapi import APIRouter, Query
14
+ import logging
15
15
  from typing import Literal
16
+ from fastapi import APIRouter, Query
16
17
  from crypticorn.common.logging import LogLevel
17
- import logging
18
18
 
19
19
  router = APIRouter(tags=["Admin"], prefix="/admin")
20
20
 
@@ -2,14 +2,42 @@
2
2
  This module contains the status router for the API.
3
3
  It provides endpoints for checking the status of the API and get the server's time.
4
4
  SHOULD ALLOW ACCESS TO THIS ROUTER WITHOUT AUTH.
5
+ To enable metrics, pass enable_metrics=True and the auth_handler to the router.
6
+ >>> status_router.enable_metrics = True
7
+ >>> status_router.auth_handler = auth_handler
8
+ Then include the router in the FastAPI app.
5
9
  >>> app.include_router(status_router)
6
10
  """
7
11
 
8
12
  from datetime import datetime
9
- from typing import Literal
10
13
  from fastapi import APIRouter, Request
14
+ from typing import Annotated, Literal
15
+ from fastapi import APIRouter, Response, Depends
16
+ from fastapi.security import HTTPBasicCredentials
17
+ from prometheus_client import generate_latest, CONTENT_TYPE_LATEST
18
+ from crypticorn.common.metrics import registry
19
+ from crypticorn.common.auth import AuthHandler
11
20
 
12
- router = APIRouter(tags=["Status"], prefix="")
21
+
22
+ class EnhancedApiRouter(APIRouter):
23
+ def __init__(
24
+ self,
25
+ enable_metrics: bool = False,
26
+ auth_handler: AuthHandler = AuthHandler(),
27
+ *args,
28
+ **kwargs
29
+ ):
30
+ """
31
+ Enhanced API Router that allows for metrics and authentication.
32
+ If enable_metrics is True, the router will include the metrics endpoint.
33
+ If auth_handler is provided, the router will use the auth handler to authenticate requests with
34
+ """
35
+ super().__init__(*args, **kwargs)
36
+ self.enable_metrics = enable_metrics
37
+ self.auth_handler = auth_handler
38
+
39
+
40
+ router = EnhancedApiRouter(tags=["Status"], prefix="")
13
41
 
14
42
 
15
43
  @router.get("/", operation_id="ping")
@@ -29,3 +57,13 @@ async def time(type: Literal["iso", "unix"] = "iso") -> str:
29
57
  return datetime.now().isoformat()
30
58
  elif type == "unix":
31
59
  return str(int(datetime.now().timestamp()))
60
+
61
+
62
+ @router.get("/metrics", operation_id="getMetrics", include_in_schema=True)
63
+ def metrics(
64
+ username: Annotated[HTTPBasicCredentials, Depends(router.auth_handler.basic_auth)],
65
+ ):
66
+ """
67
+ Get Prometheus metrics for the application. Returns plain text.
68
+ """
69
+ return Response(generate_latest(registry), media_type=CONTENT_TYPE_LATEST)
@@ -16,7 +16,7 @@ class Scope(StrEnum):
16
16
 
17
17
  # Scopes that can be purchased - these actually exist in the jwt token
18
18
  READ_PREDICTIONS = "read:predictions"
19
- READ_DEXSIGNALS = "read:dexsignals"
19
+ READ_DEX_SIGNALS = "read:dex:signals"
20
20
 
21
21
  # Hive scopes
22
22
  READ_HIVE_MODEL = "read:hive:model"
@@ -88,5 +88,5 @@ class Scope(StrEnum):
88
88
  cls.READ_METRICS_MARKETS,
89
89
  cls.READ_KLINES,
90
90
  cls.READ_SENTIMENT,
91
- cls.READ_DEXSIGNALS,
91
+ cls.READ_DEX_SIGNALS,
92
92
  ]
@@ -63,6 +63,13 @@ class CrypticornDeprecatedSince215(CrypticornDeprecationWarning):
63
63
  super().__init__(message, *args, since=(2, 15), expected_removal=(3, 0))
64
64
 
65
65
 
66
+ class CrypticornDeprecatedSince217(CrypticornDeprecationWarning):
67
+ """A specific `CrypticornDeprecationWarning` subclass defining functionality deprecated since Crypticorn 2.17."""
68
+
69
+ def __init__(self, message: str, *args: object) -> None:
70
+ super().__init__(message, *args, since=(2, 17), expected_removal=(3, 0))
71
+
72
+
66
73
  class CrypticornExperimentalWarning(Warning):
67
74
  """A Crypticorn specific experimental functionality warning.
68
75
 
@@ -0,0 +1,6 @@
1
+ from crypticorn.dex.client import *
2
+ from crypticorn.dex.main import DexClient
3
+
4
+ __all__ = [
5
+ "DexClient",
6
+ ]
@@ -0,0 +1,49 @@
1
+ # coding: utf-8
2
+
3
+ # flake8: noqa
4
+
5
+ """
6
+ DEX AI API
7
+
8
+ API for DEX AI
9
+
10
+ The version of the OpenAPI document: 1.0.0
11
+ Generated by OpenAPI Generator (https://openapi-generator.tech)
12
+
13
+ Do not edit the class manually.
14
+ """ # noqa: E501
15
+
16
+
17
+ __version__ = "1.0.0"
18
+
19
+ # import apis into sdk package
20
+ from crypticorn.dex.client.api.admin_api import AdminApi
21
+ from crypticorn.dex.client.api.signals_api import SignalsApi
22
+ from crypticorn.dex.client.api.status_api import StatusApi
23
+
24
+ # import ApiClient
25
+ from crypticorn.dex.client.api_response import ApiResponse
26
+ from crypticorn.dex.client.api_client import ApiClient
27
+ from crypticorn.dex.client.configuration import Configuration
28
+ from crypticorn.dex.client.exceptions import OpenApiException
29
+ from crypticorn.dex.client.exceptions import ApiTypeError
30
+ from crypticorn.dex.client.exceptions import ApiValueError
31
+ from crypticorn.dex.client.exceptions import ApiKeyError
32
+ from crypticorn.dex.client.exceptions import ApiAttributeError
33
+ from crypticorn.dex.client.exceptions import ApiException
34
+
35
+ # import models into sdk package
36
+ from crypticorn.dex.client.models.api_error_identifier import ApiErrorIdentifier
37
+ from crypticorn.dex.client.models.api_error_level import ApiErrorLevel
38
+ from crypticorn.dex.client.models.api_error_type import ApiErrorType
39
+ from crypticorn.dex.client.models.exception_detail import ExceptionDetail
40
+ from crypticorn.dex.client.models.log_level import LogLevel
41
+ from crypticorn.dex.client.models.paginated_response_signal_with_token import (
42
+ PaginatedResponseSignalWithToken,
43
+ )
44
+ from crypticorn.dex.client.models.risk import Risk
45
+ from crypticorn.dex.client.models.signal_overview_stats import SignalOverviewStats
46
+ from crypticorn.dex.client.models.signal_volume import SignalVolume
47
+ from crypticorn.dex.client.models.signal_with_token import SignalWithToken
48
+ from crypticorn.dex.client.models.token_data import TokenData
49
+ from crypticorn.dex.client.models.token_detail import TokenDetail
@@ -0,0 +1,6 @@
1
+ # flake8: noqa
2
+
3
+ # import apis into api package
4
+ from crypticorn.dex.client.api.admin_api import AdminApi
5
+ from crypticorn.dex.client.api.signals_api import SignalsApi
6
+ from crypticorn.dex.client.api.status_api import StatusApi