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.
- crypticorn_utils/__init__.py +50 -16
- crypticorn_utils/_migration.py +2 -1
- crypticorn_utils/ansi_colors.py +1 -4
- crypticorn_utils/auth.py +112 -54
- crypticorn_utils/enums.py +3 -123
- crypticorn_utils/errors.py +3 -893
- crypticorn_utils/exceptions.py +244 -139
- crypticorn_utils/logging.py +5 -19
- crypticorn_utils/metrics.py +1 -2
- crypticorn_utils/middleware.py +7 -40
- crypticorn_utils/utils.py +1 -52
- crypticorn_utils/warnings.py +0 -28
- {crypticorn_utils-0.1.0rc1.dist-info → crypticorn_utils-1.0.0rc1.dist-info}/METADATA +20 -5
- crypticorn_utils-1.0.0rc1.dist-info/RECORD +21 -0
- {crypticorn_utils-0.1.0rc1.dist-info → crypticorn_utils-1.0.0rc1.dist-info}/licenses/LICENSE +1 -1
- crypticorn_utils/cli/__init__.py +0 -4
- crypticorn_utils/cli/__main__.py +0 -17
- crypticorn_utils/cli/init.py +0 -127
- crypticorn_utils/cli/templates/auth.py +0 -33
- crypticorn_utils/cli/version.py +0 -8
- crypticorn_utils/mixins.py +0 -68
- crypticorn_utils/openapi.py +0 -10
- crypticorn_utils/router/admin_router.py +0 -117
- crypticorn_utils/router/status_router.py +0 -36
- crypticorn_utils-0.1.0rc1.dist-info/RECORD +0 -30
- /crypticorn_utils/{cli/templates/__init__.py → py.typed} +0 -0
- {crypticorn_utils-0.1.0rc1.dist-info → crypticorn_utils-1.0.0rc1.dist-info}/WHEEL +0 -0
- {crypticorn_utils-0.1.0rc1.dist-info → crypticorn_utils-1.0.0rc1.dist-info}/entry_points.txt +0 -0
- {crypticorn_utils-0.1.0rc1.dist-info → crypticorn_utils-1.0.0rc1.dist-info}/top_level.txt +0 -0
crypticorn_utils/exceptions.py
CHANGED
@@ -1,29 +1,34 @@
|
|
1
|
-
import json
|
2
1
|
import logging
|
3
|
-
from
|
2
|
+
from enum import Enum, StrEnum
|
3
|
+
from typing import Any, Callable, Optional, Self, TypedDict, Union
|
4
4
|
|
5
5
|
from crypticorn_utils.errors import (
|
6
|
-
|
7
|
-
|
8
|
-
ApiErrorLevel,
|
9
|
-
ApiErrorType,
|
6
|
+
ErrorLevel,
|
7
|
+
ErrorType,
|
10
8
|
)
|
11
9
|
from fastapi import FastAPI
|
10
|
+
from fastapi import HTTPException
|
12
11
|
from fastapi import HTTPException as FastAPIHTTPException
|
13
|
-
from fastapi import Request
|
12
|
+
from fastapi import Request, WebSocketException
|
14
13
|
from fastapi.exceptions import RequestValidationError, ResponseValidationError
|
15
14
|
from fastapi.responses import JSONResponse
|
16
15
|
from pydantic import BaseModel, Field
|
17
16
|
|
18
|
-
try:
|
19
|
-
from enum import StrEnum
|
20
|
-
except ImportError:
|
21
|
-
from strenum import StrEnum
|
22
|
-
|
23
17
|
_logger = logging.getLogger("crypticorn")
|
24
18
|
|
19
|
+
TError = TypedDict(
|
20
|
+
"TError",
|
21
|
+
{
|
22
|
+
"identifier": str,
|
23
|
+
"type": ErrorType,
|
24
|
+
"level": ErrorLevel,
|
25
|
+
"http_code": int,
|
26
|
+
"websocket_code": int,
|
27
|
+
},
|
28
|
+
)
|
29
|
+
|
25
30
|
|
26
|
-
class
|
31
|
+
class ExceptionType(StrEnum):
|
27
32
|
"""The protocol the exception is called from"""
|
28
33
|
|
29
34
|
HTTP = "http"
|
@@ -34,150 +39,250 @@ class ExceptionDetail(BaseModel):
|
|
34
39
|
"""Exception details returned to the client."""
|
35
40
|
|
36
41
|
message: Optional[str] = Field(None, description="An additional error message")
|
37
|
-
code:
|
38
|
-
type:
|
39
|
-
level:
|
42
|
+
code: str = Field(..., description="The unique error code")
|
43
|
+
type: ErrorType = Field(..., description="The type of error")
|
44
|
+
level: ErrorLevel = Field(..., description="The level of the error")
|
40
45
|
status_code: int = Field(..., description="The HTTP status code")
|
41
46
|
details: Any = Field(None, description="Additional details about the error")
|
42
47
|
|
43
48
|
|
44
|
-
class
|
45
|
-
"""
|
49
|
+
class ExceptionHandler:
|
50
|
+
"""This class is used to handle errors and exceptions. It is used to build exceptions and raise them.
|
46
51
|
|
47
|
-
|
48
|
-
|
49
|
-
|
52
|
+
- Register the exception handlers to the FastAPI app.
|
53
|
+
- Configure the instance with a callback to get the error object from the error identifier.
|
54
|
+
- Build exceptions from error codes defined in the client code.
|
50
55
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
level=self.error.level,
|
59
|
-
status_code=(
|
60
|
-
self.error.http_code
|
61
|
-
if _type == _ExceptionType.HTTP
|
62
|
-
else self.error.websocket_code
|
63
|
-
),
|
64
|
-
details=self.details,
|
65
|
-
)
|
56
|
+
Example for the client code implementation:
|
57
|
+
|
58
|
+
```python
|
59
|
+
from crypticorn_utils import ExceptionHandler, BaseError, ApiError
|
60
|
+
|
61
|
+
handler = ExceptionHandler(callback=ApiError.from_identifier)
|
62
|
+
handler.register_exception_handlers(app)
|
66
63
|
|
64
|
+
@app.get("/")
|
65
|
+
def get_root():
|
66
|
+
raise handler.build_exception(ApiErrorIdentifier.UNKNOWN_ERROR)
|
67
67
|
|
68
|
-
class
|
69
|
-
|
70
|
-
|
71
|
-
|
68
|
+
class ApiErrorIdentifier(StrEnum):
|
69
|
+
...
|
70
|
+
|
71
|
+
class ApiError(BaseError):
|
72
|
+
...
|
73
|
+
```
|
72
74
|
"""
|
73
75
|
|
74
76
|
def __init__(
|
75
77
|
self,
|
76
|
-
|
77
|
-
|
78
|
-
_type: Optional[_ExceptionType] = _ExceptionType.HTTP,
|
78
|
+
callback: Callable[[str], "BaseError"],
|
79
|
+
type: Optional[ExceptionType] = ExceptionType.HTTP,
|
79
80
|
):
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
81
|
+
"""
|
82
|
+
:param callback: The callback to use to get the error object from the error identifier.
|
83
|
+
:param type: The type of exception to raise. Defaults to HTTP.
|
84
|
+
"""
|
85
|
+
self.callback = callback
|
86
|
+
self.type = type
|
87
|
+
|
88
|
+
def _http_exception(
|
89
|
+
self, content: ExceptionDetail, headers: Optional[dict[str, str]] = None
|
90
|
+
) -> HTTPException:
|
91
|
+
return HTTPException(
|
92
|
+
detail=content.model_dump(mode="json"),
|
87
93
|
headers=headers,
|
94
|
+
status_code=content.status_code,
|
88
95
|
)
|
89
96
|
|
97
|
+
def _websocket_exception(self, content: ExceptionDetail) -> WebSocketException:
|
98
|
+
return WebSocketException(
|
99
|
+
reason=content.model_dump(mode="json"),
|
100
|
+
code=content.status_code,
|
101
|
+
)
|
90
102
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
103
|
+
def build_exception( # type: ignore[return]
|
104
|
+
self,
|
105
|
+
code: str,
|
106
|
+
message: Optional[str] = None,
|
107
|
+
headers: Optional[dict[str, str]] = None,
|
108
|
+
details: Any = None,
|
109
|
+
) -> Union[HTTPException, WebSocketException]:
|
110
|
+
"""Build an exception, without raising it.
|
111
|
+
:param code: The error code to raise.
|
112
|
+
:param message: The message to include in the error.
|
113
|
+
:param headers: The headers to include in the error.
|
114
|
+
:param details: The details to include in the error.
|
115
|
+
|
116
|
+
:return: The exception to raise, either an HTTPException or a WebSocketException.
|
117
|
+
|
118
|
+
```python
|
119
|
+
@app.get("/")
|
120
|
+
def get_root():
|
121
|
+
raise handler.build_exception(ApiErrorIdentifier.UNKNOWN_ERROR)
|
122
|
+
```
|
123
|
+
"""
|
124
|
+
error = self.callback(code)
|
125
|
+
content = ExceptionDetail(
|
126
|
+
message=message,
|
127
|
+
code=error.identifier,
|
128
|
+
type=error.type,
|
129
|
+
level=error.level,
|
130
|
+
status_code=error.http_code,
|
131
|
+
details=details,
|
132
|
+
)
|
133
|
+
if self.type == ExceptionType.HTTP:
|
134
|
+
return self._http_exception(content, headers)
|
135
|
+
elif self.type == ExceptionType.WEBSOCKET:
|
136
|
+
return self._websocket_exception(content)
|
137
|
+
|
138
|
+
async def _general_handler(request: Request, exc: Exception) -> JSONResponse:
|
139
|
+
"""Default exception handler for all exceptions."""
|
140
|
+
body = ExceptionDetail(
|
141
|
+
message=str(exc),
|
142
|
+
code="unknown_error",
|
143
|
+
type=ErrorType.SERVER_ERROR,
|
144
|
+
level=ErrorLevel.ERROR,
|
145
|
+
status_code=500,
|
146
|
+
)
|
147
|
+
res = JSONResponse(
|
148
|
+
status_code=body.status_code,
|
149
|
+
content=body.model_dump(mode="json"),
|
150
|
+
headers=None,
|
151
|
+
)
|
152
|
+
_logger.error(f"General error: {str(exc)}")
|
153
|
+
return res
|
154
|
+
|
155
|
+
async def _request_validation_handler(
|
156
|
+
request: Request, exc: RequestValidationError
|
157
|
+
) -> JSONResponse:
|
158
|
+
"""Exception handler for all request validation errors."""
|
159
|
+
body = ExceptionDetail(
|
160
|
+
message=str(exc),
|
161
|
+
code="invalid_data_request",
|
162
|
+
type=ErrorType.USER_ERROR,
|
163
|
+
level=ErrorLevel.ERROR,
|
164
|
+
status_code=400,
|
165
|
+
)
|
166
|
+
res = JSONResponse(
|
167
|
+
status_code=body.status_code,
|
168
|
+
content=body.model_dump(mode="json"),
|
169
|
+
headers=None,
|
170
|
+
)
|
171
|
+
_logger.error(f"Request validation error: {str(exc)}")
|
172
|
+
return res
|
173
|
+
|
174
|
+
async def _response_validation_handler(
|
175
|
+
request: Request, exc: ResponseValidationError
|
176
|
+
) -> JSONResponse:
|
177
|
+
"""Exception handler for all response validation errors."""
|
178
|
+
body = ExceptionDetail(
|
179
|
+
message=str(exc),
|
180
|
+
code="invalid_data_response",
|
181
|
+
type=ErrorType.USER_ERROR,
|
182
|
+
level=ErrorLevel.ERROR,
|
183
|
+
status_code=400,
|
184
|
+
)
|
185
|
+
res = JSONResponse(
|
186
|
+
status_code=body.status_code,
|
187
|
+
content=body.model_dump(mode="json"),
|
188
|
+
headers=None,
|
189
|
+
)
|
190
|
+
_logger.error(f"Response validation error: {str(exc)}")
|
191
|
+
return res
|
100
192
|
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
193
|
+
async def _http_handler(request: Request, exc: HTTPException) -> JSONResponse:
|
194
|
+
"""Exception handler for HTTPExceptions. It unwraps the HTTPException and returns the detail in a flat JSON response."""
|
195
|
+
res = JSONResponse(
|
196
|
+
status_code=exc.status_code, content=exc.detail, headers=exc.headers
|
197
|
+
)
|
198
|
+
_logger.error(f"HTTP error: {str(exc)}")
|
199
|
+
return res
|
200
|
+
|
201
|
+
def register_exception_handlers(self, app: FastAPI):
|
202
|
+
"""Utility to register serveral exception handlers in one go. Catches Exception, HTTPException and Data Validation errors, logs them and responds with a unified json body.
|
203
|
+
|
204
|
+
```python
|
205
|
+
handler.register_exception_handlers(app)
|
206
|
+
```
|
207
|
+
"""
|
208
|
+
app.add_exception_handler(Exception, self._general_handler)
|
209
|
+
app.add_exception_handler(FastAPIHTTPException, self._http_handler)
|
210
|
+
app.add_exception_handler(
|
211
|
+
RequestValidationError, self._request_validation_handler
|
212
|
+
)
|
213
|
+
app.add_exception_handler(
|
214
|
+
ResponseValidationError, self._response_validation_handler
|
107
215
|
)
|
108
216
|
|
109
217
|
|
110
|
-
|
111
|
-
"""
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
)
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
def __str__(self):
|
183
|
-
return f"{self.error.identifier}: {self.message}"
|
218
|
+
class BaseError(Enum):
|
219
|
+
"""Base API error for the API."""
|
220
|
+
|
221
|
+
@property
|
222
|
+
def identifier(self) -> str:
|
223
|
+
return self.value[0]
|
224
|
+
|
225
|
+
@property
|
226
|
+
def type(self) -> ErrorType:
|
227
|
+
return self.value[1]
|
228
|
+
|
229
|
+
@property
|
230
|
+
def level(self) -> ErrorLevel:
|
231
|
+
return self.value[2]
|
232
|
+
|
233
|
+
@property
|
234
|
+
def http_code(self) -> int:
|
235
|
+
return self.value[3]
|
236
|
+
|
237
|
+
@property
|
238
|
+
def websocket_code(self) -> int:
|
239
|
+
return self.value[4]
|
240
|
+
|
241
|
+
@classmethod
|
242
|
+
def from_identifier(cls, identifier: str) -> Self:
|
243
|
+
return next(error for error in cls if error.identifier == identifier)
|
244
|
+
|
245
|
+
|
246
|
+
## Since enums don't support inheritance, you can copy these values to your own enum.
|
247
|
+
|
248
|
+
# UNKNOWN_ERROR = "unknown_error"
|
249
|
+
# INVALID_DATA_REQUEST = "invalid_data"
|
250
|
+
# INVALID_DATA_RESPONSE = "invalid_data_response"
|
251
|
+
# OBJECT_ALREADY_EXISTS = "object_already_exists"
|
252
|
+
# OBJECT_NOT_FOUND = "object_not_found"
|
253
|
+
|
254
|
+
# UNKNOWN_ERROR = (
|
255
|
+
# ErrorCodes.UNKNOWN_ERROR,
|
256
|
+
# ErrorType.SERVER_ERROR,
|
257
|
+
# ErrorLevel.ERROR,
|
258
|
+
# status.HTTP_500_INTERNAL_SERVER_ERROR,
|
259
|
+
# status.WS_1011_INTERNAL_ERROR,
|
260
|
+
# )
|
261
|
+
# INVALID_DATA_REQUEST = (
|
262
|
+
# ErrorCodes.INVALID_DATA_REQUEST,
|
263
|
+
# ErrorType.USER_ERROR,
|
264
|
+
# ErrorLevel.ERROR,
|
265
|
+
# status.HTTP_422_UNPROCESSABLE_ENTITY,
|
266
|
+
# status.WS_1007_INVALID_FRAME_PAYLOAD_DATA,
|
267
|
+
# )
|
268
|
+
# INVALID_DATA_RESPONSE = (
|
269
|
+
# ErrorCodes.INVALID_DATA_RESPONSE,
|
270
|
+
# ErrorType.SERVER_ERROR,
|
271
|
+
# ErrorLevel.ERROR,
|
272
|
+
# status.HTTP_422_UNPROCESSABLE_ENTITY,
|
273
|
+
# status.WS_1007_INVALID_FRAME_PAYLOAD_DATA,
|
274
|
+
# )
|
275
|
+
# OBJECT_ALREADY_EXISTS = (
|
276
|
+
# ErrorCodes.OBJECT_ALREADY_EXISTS,
|
277
|
+
# ErrorType.USER_ERROR,
|
278
|
+
# ErrorLevel.ERROR,
|
279
|
+
# status.HTTP_409_CONFLICT,
|
280
|
+
# status.WS_1008_POLICY_VIOLATION,
|
281
|
+
# )
|
282
|
+
# OBJECT_NOT_FOUND = (
|
283
|
+
# ErrorCodes.OBJECT_NOT_FOUND,
|
284
|
+
# ErrorType.USER_ERROR,
|
285
|
+
# ErrorLevel.ERROR,
|
286
|
+
# status.HTTP_404_NOT_FOUND,
|
287
|
+
# status.WS_1008_POLICY_VIOLATION,
|
288
|
+
# )
|
crypticorn_utils/logging.py
CHANGED
@@ -4,18 +4,14 @@ import logging
|
|
4
4
|
import os
|
5
5
|
import sys
|
6
6
|
from datetime import datetime
|
7
|
+
from enum import StrEnum
|
7
8
|
from logging.handlers import RotatingFileHandler
|
9
|
+
from typing import Optional
|
8
10
|
|
9
11
|
from crypticorn_utils.ansi_colors import AnsiColors as C
|
10
|
-
from crypticorn_utils.mixins import ValidateEnumMixin
|
11
12
|
|
12
|
-
try:
|
13
|
-
from enum import StrEnum
|
14
|
-
except ImportError:
|
15
|
-
from strenum import StrEnum
|
16
13
|
|
17
|
-
|
18
|
-
class LogLevel(ValidateEnumMixin, StrEnum):
|
14
|
+
class LogLevel(StrEnum):
|
19
15
|
DEBUG = "DEBUG"
|
20
16
|
INFO = "INFO"
|
21
17
|
WARNING = "WARNING"
|
@@ -38,16 +34,6 @@ class LogLevel(ValidateEnumMixin, StrEnum):
|
|
38
34
|
else:
|
39
35
|
return C.RESET
|
40
36
|
|
41
|
-
@staticmethod
|
42
|
-
def get_level(level: "LogLevel") -> int:
|
43
|
-
"""Get the integer value from a log level name."""
|
44
|
-
return logging._nameToLevel.get(level, logging.INFO)
|
45
|
-
|
46
|
-
@staticmethod
|
47
|
-
def get_name(level: int) -> "LogLevel":
|
48
|
-
"""Get the level name from the integer value of a log level."""
|
49
|
-
return LogLevel(logging._levelToName.get(level, "INFO"))
|
50
|
-
|
51
37
|
|
52
38
|
_LOGFORMAT = (
|
53
39
|
f"{C.CYAN_BOLD}%(asctime)s{C.RESET} - "
|
@@ -74,12 +60,12 @@ class _CustomFormatter(logging.Formatter):
|
|
74
60
|
|
75
61
|
|
76
62
|
def configure_logging(
|
77
|
-
name: str = None,
|
63
|
+
name: Optional[str] = None,
|
78
64
|
fmt: str = _LOGFORMAT,
|
79
65
|
datefmt: str = _DATEFMT,
|
80
66
|
stdout_level: int = logging.INFO,
|
81
67
|
file_level: int = logging.INFO,
|
82
|
-
log_file: str = None,
|
68
|
+
log_file: Optional[str] = None,
|
83
69
|
filters: list[logging.Filter] = [],
|
84
70
|
) -> None:
|
85
71
|
"""Configures the logging for the application.
|
crypticorn_utils/metrics.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# metrics/registry.py
|
2
|
-
from prometheus_client import CollectorRegistry, Counter, Histogram
|
3
2
|
from crypticorn_utils._migration import has_migrated
|
3
|
+
from prometheus_client import CollectorRegistry, Counter, Histogram
|
4
4
|
|
5
5
|
registry = CollectorRegistry()
|
6
6
|
|
@@ -23,7 +23,6 @@ if has_migrated:
|
|
23
23
|
"http_request_size_bytes", "Size of HTTP request bodies", ["method", "endpoint"]
|
24
24
|
)
|
25
25
|
|
26
|
-
|
27
26
|
RESPONSE_SIZE = Histogram(
|
28
27
|
"http_response_size_bytes",
|
29
28
|
"Size of HTTP responses",
|
crypticorn_utils/middleware.py
CHANGED
@@ -1,22 +1,18 @@
|
|
1
1
|
import time
|
2
|
-
import warnings
|
3
|
-
from contextlib import asynccontextmanager
|
4
2
|
|
5
|
-
from crypticorn_utils.
|
6
|
-
from crypticorn_utils.warnings import CrypticornDeprecatedSince217
|
3
|
+
from crypticorn_utils.metrics import has_migrated
|
7
4
|
from fastapi import FastAPI, Request
|
8
5
|
from fastapi.middleware.cors import CORSMiddleware
|
9
6
|
from starlette.middleware.base import BaseHTTPMiddleware
|
10
|
-
from typing_extensions import deprecated
|
11
|
-
from crypticorn_utils.metrics import has_migrated
|
12
7
|
|
13
8
|
if has_migrated:
|
14
9
|
from crypticorn_utils.metrics import (
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
)
|
10
|
+
HTTP_REQUEST_DURATION,
|
11
|
+
HTTP_REQUESTS_COUNT,
|
12
|
+
REQUEST_SIZE,
|
13
|
+
RESPONSE_SIZE,
|
14
|
+
)
|
15
|
+
|
20
16
|
# otherwise prometheus reqisters metrics twice, resulting in an exception
|
21
17
|
class PrometheusMiddleware(BaseHTTPMiddleware):
|
22
18
|
async def dispatch(self, request: Request, call_next):
|
@@ -80,25 +76,6 @@ if has_migrated:
|
|
80
76
|
return response
|
81
77
|
|
82
78
|
|
83
|
-
@deprecated("Use add_middleware instead", category=None)
|
84
|
-
def add_cors_middleware(app: "FastAPI"):
|
85
|
-
warnings.warn(
|
86
|
-
"add_cors_middleware is deprecated. Use add_middleware instead.",
|
87
|
-
CrypticornDeprecatedSince217,
|
88
|
-
)
|
89
|
-
app.add_middleware(
|
90
|
-
CORSMiddleware,
|
91
|
-
allow_origins=[
|
92
|
-
"http://localhost:5173", # vite dev server
|
93
|
-
"http://localhost:4173", # vite preview server
|
94
|
-
],
|
95
|
-
allow_origin_regex="^https://([a-zA-Z0-9-]+.)*crypticorn.(dev|com)/?$", # matches (multiple or no) subdomains of crypticorn.dev and crypticorn.com
|
96
|
-
allow_credentials=True,
|
97
|
-
allow_methods=["*"],
|
98
|
-
allow_headers=["*"],
|
99
|
-
)
|
100
|
-
|
101
|
-
|
102
79
|
def add_middleware(app: "FastAPI"):
|
103
80
|
app.add_middleware(
|
104
81
|
CORSMiddleware,
|
@@ -113,13 +90,3 @@ def add_middleware(app: "FastAPI"):
|
|
113
90
|
)
|
114
91
|
if has_migrated:
|
115
92
|
app.add_middleware(PrometheusMiddleware)
|
116
|
-
|
117
|
-
|
118
|
-
@asynccontextmanager
|
119
|
-
async def default_lifespan(app: FastAPI):
|
120
|
-
"""Default lifespan for the applications.
|
121
|
-
This is used to configure the logging for the application.
|
122
|
-
To override this, pass a different lifespan to the FastAPI constructor or call this lifespan within a custom lifespan.
|
123
|
-
"""
|
124
|
-
configure_logging()
|
125
|
-
yield
|
crypticorn_utils/utils.py
CHANGED
@@ -2,32 +2,8 @@
|
|
2
2
|
|
3
3
|
import random
|
4
4
|
import string
|
5
|
-
import warnings
|
6
5
|
from datetime import datetime
|
7
|
-
from
|
8
|
-
from typing import Any, Union
|
9
|
-
|
10
|
-
import typing_extensions
|
11
|
-
from crypticorn_utils.exceptions import ApiError, ExceptionContent, HTTPException
|
12
|
-
from crypticorn_utils.warnings import CrypticornDeprecatedSince25
|
13
|
-
|
14
|
-
|
15
|
-
def throw_if_none(
|
16
|
-
value: Any,
|
17
|
-
message: str = "Object not found",
|
18
|
-
) -> None:
|
19
|
-
"""Throws an FastAPI HTTPException if the value is None. https://docs.python.org/3/library/stdtypes.html#truth-value-testing"""
|
20
|
-
if value is None:
|
21
|
-
raise HTTPException(content=ExceptionContent(error=ApiError.OBJECT_NOT_FOUND, message=message))
|
22
|
-
|
23
|
-
|
24
|
-
def throw_if_falsy(
|
25
|
-
value: Any,
|
26
|
-
message: str = "Object not found",
|
27
|
-
) -> None:
|
28
|
-
"""Throws an FastAPI HTTPException if the value is False. https://docs.python.org/3/library/stdtypes.html#truth-value-testing"""
|
29
|
-
if not value:
|
30
|
-
raise HTTPException(content=ExceptionContent(error=ApiError.OBJECT_NOT_FOUND, message=message))
|
6
|
+
from typing import Any
|
31
7
|
|
32
8
|
|
33
9
|
def gen_random_id(length: int = 20) -> str:
|
@@ -37,33 +13,6 @@ def gen_random_id(length: int = 20) -> str:
|
|
37
13
|
return "".join(random.choice(charset) for _ in range(length))
|
38
14
|
|
39
15
|
|
40
|
-
@typing_extensions.deprecated(
|
41
|
-
"The `is_equal` method is deprecated; use `math.is_close` instead.", category=None
|
42
|
-
)
|
43
|
-
def is_equal(
|
44
|
-
a: Union[float, Decimal],
|
45
|
-
b: Union[float, Decimal],
|
46
|
-
rel_tol: float = 1e-9,
|
47
|
-
abs_tol: float = 0.0,
|
48
|
-
) -> bool:
|
49
|
-
"""
|
50
|
-
Compare two Decimal numbers for approximate equality.
|
51
|
-
"""
|
52
|
-
warnings.warn(
|
53
|
-
"The `is_equal` method is deprecated; use `math.is_close` instead.",
|
54
|
-
category=CrypticornDeprecatedSince25,
|
55
|
-
)
|
56
|
-
if not isinstance(a, Decimal):
|
57
|
-
a = Decimal(str(a))
|
58
|
-
if not isinstance(b, Decimal):
|
59
|
-
b = Decimal(str(b))
|
60
|
-
|
61
|
-
# Convert tolerances to Decimal
|
62
|
-
return Decimal(abs(a - b)) <= max(
|
63
|
-
Decimal(str(rel_tol)) * max(abs(a), abs(b)), Decimal(str(abs_tol))
|
64
|
-
)
|
65
|
-
|
66
|
-
|
67
16
|
def optional_import(module_name: str, extra_name: str) -> Any:
|
68
17
|
"""
|
69
18
|
Tries to import a module. Raises `ImportError` if not found with a message to install the extra dependency.
|
crypticorn_utils/warnings.py
CHANGED
@@ -43,34 +43,6 @@ class CrypticornDeprecationWarning(DeprecationWarning):
|
|
43
43
|
return message
|
44
44
|
|
45
45
|
|
46
|
-
class CrypticornDeprecatedSince25(CrypticornDeprecationWarning):
|
47
|
-
"""A specific `CrypticornDeprecationWarning` subclass defining functionality deprecated since Crypticorn 2.5."""
|
48
|
-
|
49
|
-
def __init__(self, message: str, *args: object) -> None:
|
50
|
-
super().__init__(message, *args, since=(2, 5), expected_removal=(3, 0))
|
51
|
-
|
52
|
-
|
53
|
-
class CrypticornDeprecatedSince28(CrypticornDeprecationWarning):
|
54
|
-
"""A specific `CrypticornDeprecationWarning` subclass defining functionality deprecated since Crypticorn 2.8."""
|
55
|
-
|
56
|
-
def __init__(self, message: str, *args: object) -> None:
|
57
|
-
super().__init__(message, *args, since=(2, 8), expected_removal=(3, 0))
|
58
|
-
|
59
|
-
|
60
|
-
class CrypticornDeprecatedSince215(CrypticornDeprecationWarning):
|
61
|
-
"""A specific `CrypticornDeprecationWarning` subclass defining functionality deprecated since Crypticorn 2.15."""
|
62
|
-
|
63
|
-
def __init__(self, message: str, *args: object) -> None:
|
64
|
-
super().__init__(message, *args, since=(2, 15), expected_removal=(3, 0))
|
65
|
-
|
66
|
-
|
67
|
-
class CrypticornDeprecatedSince217(CrypticornDeprecationWarning):
|
68
|
-
"""A specific `CrypticornDeprecationWarning` subclass defining functionality deprecated since Crypticorn 2.17."""
|
69
|
-
|
70
|
-
def __init__(self, message: str, *args: object) -> None:
|
71
|
-
super().__init__(message, *args, since=(2, 17), expected_removal=(3, 0))
|
72
|
-
|
73
|
-
|
74
46
|
class CrypticornExperimentalWarning(Warning):
|
75
47
|
"""A Crypticorn specific experimental functionality warning.
|
76
48
|
|