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,29 +1,34 @@
1
- import json
2
1
  import logging
3
- from typing import Any, Optional
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
- ApiError,
7
- ApiErrorIdentifier,
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 _ExceptionType(StrEnum):
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: ApiErrorIdentifier = Field(..., description="The unique error code")
38
- type: ApiErrorType = Field(..., description="The type of error")
39
- level: ApiErrorLevel = Field(..., description="The level of the error")
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 ExceptionContent(BaseModel):
45
- """Exception content used when raising an exception."""
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
- error: ApiError = Field(..., description="The unique error code")
48
- message: Optional[str] = Field(None, description="An additional error message")
49
- details: Any = Field(None, description="Additional details about the error")
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
- def enrich(
52
- self, _type: Optional[_ExceptionType] = _ExceptionType.HTTP
53
- ) -> ExceptionDetail:
54
- return ExceptionDetail(
55
- message=self.message,
56
- code=self.error.identifier,
57
- type=self.error.type,
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 HTTPException(FastAPIHTTPException):
69
- """A custom HTTP exception wrapper around FastAPI's HTTPException.
70
- It allows for a more structured way to handle errors, with a message and an error code. The status code is being derived from the detail's error.
71
- The ApiError class is the source of truth. If the error is not yet implemented, there are fallbacks in place.
68
+ class ApiErrorIdentifier(StrEnum):
69
+ ...
70
+
71
+ class ApiError(BaseError):
72
+ ...
73
+ ```
72
74
  """
73
75
 
74
76
  def __init__(
75
77
  self,
76
- content: ExceptionContent,
77
- headers: Optional[dict[str, str]] = None,
78
- _type: Optional[_ExceptionType] = _ExceptionType.HTTP,
78
+ callback: Callable[[str], "BaseError"],
79
+ type: Optional[ExceptionType] = ExceptionType.HTTP,
79
80
  ):
80
- self.content = content
81
- self.headers = headers
82
- assert isinstance(content, ExceptionContent)
83
- body = content.enrich(_type)
84
- super().__init__(
85
- status_code=body.status_code,
86
- detail=body.model_dump(mode="json"),
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
- class WebSocketException(HTTPException):
92
- """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.
93
- To be used in the same way as the HTTPException.
94
- """
95
-
96
- def __init__(
97
- self, content: ExceptionContent, headers: Optional[dict[str, str]] = None
98
- ):
99
- super().__init__(content, headers, _type=_ExceptionType.WEBSOCKET)
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
- @classmethod
102
- def from_http_exception(cls, http_exception: HTTPException):
103
- """Helper method to convert an HTTPException to a WebSocketException."""
104
- return WebSocketException(
105
- content=http_exception.content,
106
- headers=http_exception.headers,
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
- async def general_handler(request: Request, exc: Exception) -> JSONResponse:
111
- """Default exception handler for all exceptions."""
112
- body = ExceptionContent(message=str(exc), error=ApiError.UNKNOWN_ERROR)
113
- http_exc = HTTPException(content=body)
114
- res = JSONResponse(
115
- status_code=http_exc.status_code,
116
- content=http_exc.detail,
117
- headers=http_exc.headers,
118
- )
119
- _logger.error(f"General error: {json.loads(res.__dict__.get('body'))}")
120
- return res
121
-
122
-
123
- async def request_validation_handler(
124
- request: Request, exc: RequestValidationError
125
- ) -> JSONResponse:
126
- """Exception handler for all request validation errors."""
127
- body = ExceptionContent(message=str(exc), error=ApiError.INVALID_DATA_REQUEST)
128
- http_exc = HTTPException(content=body)
129
- res = JSONResponse(
130
- status_code=http_exc.status_code,
131
- content=http_exc.detail,
132
- headers=http_exc.headers,
133
- )
134
- _logger.error(f"Request validation error: {json.loads(res.__dict__.get('body'))}")
135
- return res
136
-
137
-
138
- async def response_validation_handler(
139
- request: Request, exc: ResponseValidationError
140
- ) -> JSONResponse:
141
- """Exception handler for all response validation errors."""
142
- body = ExceptionContent(message=str(exc), error=ApiError.INVALID_DATA_RESPONSE)
143
- http_exc = HTTPException(content=body)
144
- res = JSONResponse(
145
- status_code=http_exc.status_code,
146
- content=http_exc.detail,
147
- headers=http_exc.headers,
148
- )
149
- _logger.error(f"Response validation error: {json.loads(res.__dict__.get('body'))}")
150
- return res
151
-
152
-
153
- async def http_handler(request: Request, exc: HTTPException) -> JSONResponse:
154
- """Exception handler for HTTPExceptions. It unwraps the HTTPException and returns the detail in a flat JSON response."""
155
- res = JSONResponse(
156
- status_code=exc.status_code, content=exc.detail, headers=exc.headers
157
- )
158
- _logger.error(f"HTTP error: {json.loads(res.__dict__.get('body'))}")
159
- return res
160
-
161
-
162
- def register_exception_handlers(app: FastAPI):
163
- """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."""
164
- app.add_exception_handler(Exception, general_handler)
165
- app.add_exception_handler(FastAPIHTTPException, http_handler)
166
- app.add_exception_handler(RequestValidationError, request_validation_handler)
167
- app.add_exception_handler(ResponseValidationError, response_validation_handler)
168
-
169
-
170
- exception_response = {
171
- "default": {"model": ExceptionDetail, "description": "Error response"}
172
- }
173
-
174
-
175
- class CrypticornException(Exception):
176
- """A custom exception class for Crypticorn."""
177
-
178
- def __init__(self, error: ApiError, message: str = None):
179
- self.message = message
180
- self.error = error
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
+ # )
@@ -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.
@@ -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",
@@ -1,22 +1,18 @@
1
1
  import time
2
- import warnings
3
- from contextlib import asynccontextmanager
4
2
 
5
- from crypticorn_utils.logging import configure_logging
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
- HTTP_REQUEST_DURATION,
16
- HTTP_REQUESTS_COUNT,
17
- REQUEST_SIZE,
18
- RESPONSE_SIZE,
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 decimal import Decimal
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.
@@ -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