crypticorn-utils 0.1.0rc1__py3-none-any.whl → 1.0.0rc1__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.
@@ -1,16 +1,50 @@
1
- from crypticorn_utils.ansi_colors import *
2
- from crypticorn_utils.auth import *
3
- from crypticorn_utils.decorators import *
4
- from crypticorn_utils.enums import *
5
- from crypticorn_utils.errors import *
6
- from crypticorn_utils.exceptions import *
7
- from crypticorn_utils.logging import *
8
- from crypticorn_utils.metrics import *
9
- from crypticorn_utils.middleware import *
10
- from crypticorn_utils.mixins import *
11
- from crypticorn_utils.openapi import *
12
- from crypticorn_utils.pagination import *
13
- from crypticorn_utils.router.admin_router import router as admin_router
14
- from crypticorn_utils.router.status_router import router as status_router
15
- from crypticorn_utils.utils import *
16
- from crypticorn_utils.warnings import *
1
+ from crypticorn_utils.auth import AuthHandler
2
+ from crypticorn_utils.decorators import partial_model
3
+ from crypticorn_utils.enums import ApiEnv, BaseUrl, Exchange, MarketType
4
+ from crypticorn_utils.errors import ErrorLevel, ErrorType
5
+ from crypticorn_utils.exceptions import BaseError, ExceptionHandler, ExceptionType
6
+ from crypticorn_utils.logging import configure_logging, disable_logging
7
+ from crypticorn_utils.middleware import add_middleware
8
+ from crypticorn_utils.pagination import (
9
+ FilterParams,
10
+ HeavyPageSortFilterParams,
11
+ HeavyPaginationParams,
12
+ PageFilterParams,
13
+ PageSortFilterParams,
14
+ PageSortParams,
15
+ PaginatedResponse,
16
+ PaginationParams,
17
+ SortFilterParams,
18
+ SortParams,
19
+ )
20
+ from crypticorn_utils.utils import datetime_to_timestamp, gen_random_id, optional_import
21
+
22
+ __all__ = [
23
+ "AuthHandler",
24
+ "partial_model",
25
+ "Exchange",
26
+ "MarketType",
27
+ "ApiEnv",
28
+ "BaseUrl",
29
+ "ErrorType",
30
+ "ErrorLevel",
31
+ "BaseError",
32
+ "ExceptionHandler",
33
+ "ExceptionType",
34
+ "configure_logging",
35
+ "disable_logging",
36
+ "add_middleware",
37
+ "PaginatedResponse",
38
+ "PaginationParams",
39
+ "HeavyPaginationParams",
40
+ "SortParams",
41
+ "FilterParams",
42
+ "SortFilterParams",
43
+ "PageFilterParams",
44
+ "PageSortParams",
45
+ "PageSortFilterParams",
46
+ "HeavyPageSortFilterParams",
47
+ "gen_random_id",
48
+ "datetime_to_timestamp",
49
+ "optional_import",
50
+ ]
@@ -1,5 +1,6 @@
1
1
  # This file is used to check if the crypticorn version is greater than 2.18
2
2
  # This is to be compatible with this new utils library
3
+ # otherwise prometheus reqisters metrics twice, resulting in an exception
3
4
 
4
5
  import importlib.metadata
5
6
  from importlib.metadata import PackageNotFoundError
@@ -9,4 +10,4 @@ try:
9
10
  parts = crypticorn_version.split(".")
10
11
  has_migrated = parts[0] >= "2" and parts[1] > "18"
11
12
  except PackageNotFoundError:
12
- has_migrated = False
13
+ has_migrated = True
@@ -1,7 +1,4 @@
1
- try:
2
- from enum import StrEnum
3
- except ImportError:
4
- from strenum import StrEnum
1
+ from enum import StrEnum
5
2
 
6
3
 
7
4
  class AnsiColors(StrEnum):
crypticorn_utils/auth.py CHANGED
@@ -1,11 +1,17 @@
1
1
  import json
2
+ from enum import StrEnum
2
3
  from typing import Union
3
4
 
4
5
  from crypticorn.auth import AuthClient, Configuration, Verify200Response
5
6
  from crypticorn.auth.client.exceptions import ApiException
6
- from crypticorn_utils.enums import Scope, BaseUrl
7
- from crypticorn_utils.exceptions import ApiError, ExceptionContent, HTTPException
8
- from fastapi import Depends, Query
7
+ from crypticorn_utils.enums import BaseUrl
8
+ from crypticorn_utils.exceptions import (
9
+ BaseError,
10
+ ErrorLevel,
11
+ ErrorType,
12
+ ExceptionHandler,
13
+ )
14
+ from fastapi import Depends, HTTPException, Query, status
9
15
  from fastapi.security import (
10
16
  APIKeyHeader,
11
17
  HTTPAuthorizationCredentials,
@@ -16,6 +22,79 @@ from fastapi.security import (
16
22
  )
17
23
  from typing_extensions import Annotated
18
24
 
25
+
26
+ class AuthErrorCodes(StrEnum):
27
+ INVALID_API_KEY = "invalid_api_key"
28
+ EXPIRED_API_KEY = "expired_api_key"
29
+ INVALID_BEARER = "invalid_bearer"
30
+ EXPIRED_BEARER = "expired_bearer"
31
+ INVALID_BASIC_AUTH = "invalid_basic_auth"
32
+ NO_CREDENTIALS = "no_credentials"
33
+ INSUFFICIENT_SCOPES = "insufficient_scopes"
34
+ UNKNOWN_ERROR = "unknown_error"
35
+
36
+
37
+ class AuthError(BaseError):
38
+ INVALID_API_KEY = (
39
+ AuthErrorCodes.INVALID_API_KEY,
40
+ ErrorType.USER_ERROR,
41
+ ErrorLevel.ERROR,
42
+ status.HTTP_401_UNAUTHORIZED,
43
+ status.WS_1008_POLICY_VIOLATION,
44
+ )
45
+ INVALID_BASIC_AUTH = (
46
+ AuthErrorCodes.INVALID_BASIC_AUTH,
47
+ ErrorType.USER_ERROR,
48
+ ErrorLevel.ERROR,
49
+ status.HTTP_401_UNAUTHORIZED,
50
+ status.WS_1008_POLICY_VIOLATION,
51
+ )
52
+ INVALID_BEARER = (
53
+ AuthErrorCodes.INVALID_BEARER,
54
+ ErrorType.USER_ERROR,
55
+ ErrorLevel.ERROR,
56
+ status.HTTP_401_UNAUTHORIZED,
57
+ status.WS_1008_POLICY_VIOLATION,
58
+ )
59
+ EXPIRED_API_KEY = (
60
+ AuthErrorCodes.EXPIRED_API_KEY,
61
+ ErrorType.USER_ERROR,
62
+ ErrorLevel.ERROR,
63
+ status.HTTP_401_UNAUTHORIZED,
64
+ status.WS_1008_POLICY_VIOLATION,
65
+ )
66
+ EXPIRED_BEARER = (
67
+ AuthErrorCodes.EXPIRED_BEARER,
68
+ ErrorType.USER_ERROR,
69
+ ErrorLevel.ERROR,
70
+ status.HTTP_401_UNAUTHORIZED,
71
+ status.WS_1008_POLICY_VIOLATION,
72
+ )
73
+ NO_CREDENTIALS = (
74
+ AuthErrorCodes.NO_CREDENTIALS,
75
+ ErrorType.USER_ERROR,
76
+ ErrorLevel.ERROR,
77
+ status.HTTP_401_UNAUTHORIZED,
78
+ status.WS_1008_POLICY_VIOLATION,
79
+ )
80
+ INSUFFICIENT_SCOPES = (
81
+ AuthErrorCodes.INSUFFICIENT_SCOPES,
82
+ ErrorType.USER_ERROR,
83
+ ErrorLevel.ERROR,
84
+ status.HTTP_403_FORBIDDEN,
85
+ status.WS_1008_POLICY_VIOLATION,
86
+ )
87
+ UNKNOWN_ERROR = (
88
+ AuthErrorCodes.UNKNOWN_ERROR,
89
+ ErrorType.SERVER_ERROR,
90
+ ErrorLevel.ERROR,
91
+ status.HTTP_500_INTERNAL_SERVER_ERROR,
92
+ status.WS_1011_INTERNAL_ERROR,
93
+ )
94
+
95
+
96
+ handler = ExceptionHandler(callback=AuthError.from_identifier)
97
+
19
98
  AUTHENTICATE_HEADER = "WWW-Authenticate"
20
99
  BEARER_AUTH_SCHEME = "Bearer"
21
100
  APIKEY_AUTH_SCHEME = "X-API-Key"
@@ -80,19 +159,17 @@ class AuthHandler:
80
159
  return await self.client.login.verify_basic_auth(basic.username, basic.password)
81
160
 
82
161
  async def _validate_scopes(
83
- self, api_scopes: list[Scope], user_scopes: list[Scope]
84
- ) -> bool:
162
+ self, api_scopes: list[str], user_scopes: list[str]
163
+ ) -> None:
85
164
  """
86
165
  Checks if the required scopes are a subset of the user scopes.
87
166
  """
88
167
  if not set(api_scopes).issubset(user_scopes):
89
- raise HTTPException(
90
- content=ExceptionContent(
91
- error=ApiError.INSUFFICIENT_SCOPES,
92
- message="Insufficient scopes to access this resource (required: "
93
- + ", ".join(api_scopes)
94
- + ")",
95
- ),
168
+ raise handler.build_exception(
169
+ AuthErrorCodes.INSUFFICIENT_SCOPES,
170
+ "Insufficient scopes to access this resource (required: "
171
+ + ", ".join(api_scopes)
172
+ + ")",
96
173
  )
97
174
 
98
175
  async def _extract_message(self, e: ApiException) -> str:
@@ -119,33 +196,24 @@ class AuthHandler:
119
196
  # Unfortunately, we cannot share the error messages defined in python/crypticorn/common/errors.py with the typescript client
120
197
  message = await self._extract_message(e)
121
198
  if message == "Invalid API key":
122
- error = ApiError.INVALID_API_KEY
199
+ error = AuthErrorCodes.INVALID_API_KEY
123
200
  elif message == "API key expired":
124
- error = ApiError.EXPIRED_API_KEY
201
+ error = AuthErrorCodes.EXPIRED_API_KEY
125
202
  elif message == "jwt expired":
126
- error = ApiError.EXPIRED_BEARER
203
+ error = AuthErrorCodes.EXPIRED_BEARER
127
204
  elif message == "Invalid basic authentication credentials":
128
- error = ApiError.INVALID_BASIC_AUTH
205
+ error = AuthErrorCodes.INVALID_BASIC_AUTH
129
206
  else:
130
207
  message = "Invalid bearer token"
131
208
  error = (
132
- ApiError.INVALID_BEARER
209
+ AuthErrorCodes.INVALID_BEARER
133
210
  ) # jwt malformed, jwt not active (https://www.npmjs.com/package/jsonwebtoken#errors--codes)
134
- return HTTPException(
135
- content=ExceptionContent(
136
- error=error,
137
- message=message,
138
- ),
139
- )
211
+ return handler.build_exception(error, message)
212
+
140
213
  elif isinstance(e, HTTPException):
141
214
  return e
142
215
  else:
143
- return HTTPException(
144
- content=ExceptionContent(
145
- error=ApiError.UNKNOWN_ERROR,
146
- message=str(e),
147
- ),
148
- )
216
+ return handler.build_exception(AuthErrorCodes.UNKNOWN_ERROR, str(e))
149
217
 
150
218
  async def api_key_auth(
151
219
  self,
@@ -162,11 +230,9 @@ class AuthHandler:
162
230
  bearer=None, api_key=api_key, basic=None, sec=sec
163
231
  )
164
232
  except HTTPException as e:
165
- raise HTTPException(
166
- content=ExceptionContent(
167
- error=ApiError.from_json(e.detail),
168
- message=e.detail.get("message"),
169
- ),
233
+ raise handler.build_exception(
234
+ e.detail.get("code"),
235
+ e.detail.get("message"),
170
236
  headers={AUTHENTICATE_HEADER: APIKEY_AUTH_SCHEME},
171
237
  )
172
238
 
@@ -188,11 +254,9 @@ class AuthHandler:
188
254
  bearer=bearer, api_key=None, basic=None, sec=sec
189
255
  )
190
256
  except HTTPException as e:
191
- raise HTTPException(
192
- content=ExceptionContent(
193
- error=ApiError.from_json(e.detail),
194
- message=e.detail.get("message"),
195
- ),
257
+ raise handler.build_exception(
258
+ e.detail.get("code"),
259
+ e.detail.get("message"),
196
260
  headers={AUTHENTICATE_HEADER: BEARER_AUTH_SCHEME},
197
261
  )
198
262
 
@@ -208,11 +272,9 @@ class AuthHandler:
208
272
  basic=credentials, bearer=None, api_key=None, sec=None
209
273
  )
210
274
  except HTTPException as e:
211
- raise HTTPException(
212
- content=ExceptionContent(
213
- error=ApiError.from_json(e.detail),
214
- message=e.detail.get("message"),
215
- ),
275
+ raise handler.build_exception(
276
+ e.detail.get("code"),
277
+ e.detail.get("message"),
216
278
  headers={AUTHENTICATE_HEADER: BASIC_AUTH_SCHEME},
217
279
  )
218
280
 
@@ -235,11 +297,9 @@ class AuthHandler:
235
297
  basic=None, bearer=bearer, api_key=api_key, sec=sec
236
298
  )
237
299
  except HTTPException as e:
238
- raise HTTPException(
239
- content=ExceptionContent(
240
- error=ApiError.from_json(e.detail),
241
- message=e.detail.get("message"),
242
- ),
300
+ raise handler.build_exception(
301
+ e.detail.get("code"),
302
+ e.detail.get("message"),
243
303
  headers={
244
304
  AUTHENTICATE_HEADER: f"{BEARER_AUTH_SCHEME}, {APIKEY_AUTH_SCHEME}"
245
305
  },
@@ -287,11 +347,9 @@ class AuthHandler:
287
347
  if last_error:
288
348
  raise last_error
289
349
  else:
290
- raise HTTPException(
291
- content=ExceptionContent(
292
- error=ApiError.NO_CREDENTIALS,
293
- message="No credentials provided. Check the WWW-Authenticate header for the available authentication methods.",
294
- ),
350
+ raise handler.build_exception(
351
+ AuthErrorCodes.NO_CREDENTIALS,
352
+ "No credentials provided. Check the WWW-Authenticate header for the available authentication methods.",
295
353
  headers={
296
354
  AUTHENTICATE_HEADER: f"{BEARER_AUTH_SCHEME}, {APIKEY_AUTH_SCHEME}, {BASIC_AUTH_SCHEME}"
297
355
  },
crypticorn_utils/enums.py CHANGED
@@ -1,109 +1,9 @@
1
1
  """Defines common enumerations used throughout the codebase for type safety and consistency."""
2
2
 
3
- try:
4
- from enum import StrEnum
5
- except ImportError:
6
- from strenum import StrEnum
3
+ from enum import StrEnum
7
4
 
8
- import warnings
9
5
 
10
- from crypticorn_utils.mixins import ValidateEnumMixin
11
- from crypticorn_utils.warnings import CrypticornDeprecatedSince215
12
-
13
-
14
- class Scope(StrEnum):
15
- """
16
- The permission scopes for the API.
17
- """
18
-
19
- # If you update anything here, also update the scopes in the auth-service repository
20
-
21
- WRITE_ADMIN = "write:admin"
22
- READ_ADMIN = "read:admin"
23
-
24
- # Scopes that can be purchased - these actually exist in the jwt token
25
- READ_PREDICTIONS = "read:predictions"
26
- READ_DEX_SIGNALS = "read:dex:signals"
27
-
28
- # Hive scopes
29
- READ_HIVE_MODEL = "read:hive:model"
30
- READ_HIVE_DATA = "read:hive:data"
31
- WRITE_HIVE_MODEL = "write:hive:model"
32
-
33
- # Trade scopes
34
- READ_TRADE_BOTS = "read:trade:bots"
35
- WRITE_TRADE_BOTS = "write:trade:bots"
36
- READ_TRADE_EXCHANGEKEYS = "read:trade:exchangekeys"
37
- WRITE_TRADE_EXCHANGEKEYS = "write:trade:exchangekeys"
38
- READ_TRADE_ORDERS = "read:trade:orders"
39
- READ_TRADE_ACTIONS = "read:trade:actions"
40
- WRITE_TRADE_ACTIONS = "write:trade:actions"
41
- READ_TRADE_EXCHANGES = "read:trade:exchanges"
42
- READ_TRADE_FUTURES = "read:trade:futures"
43
- WRITE_TRADE_FUTURES = "write:trade:futures"
44
- READ_TRADE_NOTIFICATIONS = "read:trade:notifications"
45
- WRITE_TRADE_NOTIFICATIONS = "write:trade:notifications"
46
- READ_TRADE_STRATEGIES = "read:trade:strategies"
47
- WRITE_TRADE_STRATEGIES = "write:trade:strategies"
48
-
49
- # Payment scopes
50
- READ_PAY_PAYMENTS = "read:pay:payments"
51
- READ_PAY_PRODUCTS = "read:pay:products"
52
- WRITE_PAY_PRODUCTS = "write:pay:products"
53
- READ_PAY_NOW = "read:pay:now"
54
- WRITE_PAY_NOW = "write:pay:now"
55
- WRITE_PAY_COUPONS = "write:pay:coupons"
56
- READ_PAY_COUPONS = "read:pay:coupons"
57
-
58
- # Metrics scopes
59
- READ_METRICS_MARKETCAP = "read:metrics:marketcap"
60
- READ_METRICS_INDICATORS = "read:metrics:indicators"
61
- READ_METRICS_EXCHANGES = "read:metrics:exchanges"
62
- READ_METRICS_TOKENS = "read:metrics:tokens"
63
- READ_METRICS_MARKETS = "read:metrics:markets"
64
-
65
- # Sentiment scopes
66
- READ_SENTIMENT = "read:sentiment"
67
-
68
- # Klines scopes
69
- READ_KLINES = "read:klines"
70
-
71
- @classmethod
72
- def admin_scopes(cls) -> list["Scope"]:
73
- """Scopes that are only available to admins."""
74
- return [
75
- cls.WRITE_TRADE_STRATEGIES,
76
- cls.WRITE_PAY_PRODUCTS,
77
- cls.WRITE_PAY_COUPONS,
78
- cls.READ_PAY_COUPONS,
79
- cls.WRITE_ADMIN,
80
- cls.READ_ADMIN,
81
- ]
82
-
83
- @classmethod
84
- def internal_scopes(cls) -> list["Scope"]:
85
- """Scopes that are only available to internal services."""
86
- return [
87
- cls.WRITE_TRADE_ACTIONS,
88
- ]
89
-
90
- @classmethod
91
- def purchaseable_scopes(cls) -> list["Scope"]:
92
- """Scopes that can be purchased."""
93
- return [
94
- cls.READ_PREDICTIONS,
95
- cls.READ_METRICS_MARKETCAP,
96
- cls.READ_METRICS_INDICATORS,
97
- cls.READ_METRICS_EXCHANGES,
98
- cls.READ_METRICS_TOKENS,
99
- cls.READ_METRICS_MARKETS,
100
- cls.READ_KLINES,
101
- cls.READ_SENTIMENT,
102
- cls.READ_DEX_SIGNALS,
103
- ]
104
-
105
-
106
- class Exchange(ValidateEnumMixin, StrEnum):
6
+ class Exchange(StrEnum):
107
7
  """All exchanges used in the crypticorn ecosystem. Refer to the APIs for support for a specific usecase (data, trading, etc.)."""
108
8
 
109
9
  KUCOIN = "kucoin"
@@ -116,26 +16,7 @@ class Exchange(ValidateEnumMixin, StrEnum):
116
16
  BITSTAMP = "bitstamp"
117
17
 
118
18
 
119
- class InternalExchange(ValidateEnumMixin, StrEnum):
120
- """All exchanges we are using, including public (Exchange)"""
121
-
122
- KUCOIN = "kucoin"
123
- BINGX = "bingx"
124
- BINANCE = "binance"
125
- BYBIT = "bybit"
126
- HYPERLIQUID = "hyperliquid"
127
- BITGET = "bitget"
128
-
129
- @classmethod
130
- def __getattr__(cls, name):
131
- warnings.warn(
132
- "The `InternalExchange` enum is deprecated; use `Exchange` instead.",
133
- category=CrypticornDeprecatedSince215,
134
- )
135
- return super().__getattr__(name)
136
-
137
-
138
- class MarketType(ValidateEnumMixin, StrEnum):
19
+ class MarketType(StrEnum):
139
20
  """
140
21
  Market types
141
22
  """
@@ -172,4 +53,3 @@ class BaseUrl(StrEnum):
172
53
  return cls.LOCAL
173
54
  elif env == ApiEnv.DOCKER:
174
55
  return cls.DOCKER
175
-