ddutils 0.0.1__tar.gz
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.
- ddutils-0.0.1/LICENSE +21 -0
- ddutils-0.0.1/PKG-INFO +34 -0
- ddutils-0.0.1/README.md +14 -0
- ddutils-0.0.1/ddutils/__init__.py +11 -0
- ddutils-0.0.1/ddutils/annotation_helpers.py +85 -0
- ddutils-0.0.1/ddutils/class_helpers.py +8 -0
- ddutils-0.0.1/ddutils/convertors.py +2 -0
- ddutils-0.0.1/ddutils/function_exceptions_extractor.py +110 -0
- ddutils-0.0.1/ddutils/function_helpers.py +57 -0
- ddutils-0.0.1/ddutils/module_getter.py +20 -0
- ddutils-0.0.1/ddutils/py.typed +0 -0
- ddutils-0.0.1/ddutils/safe_decorators.py +50 -0
- ddutils-0.0.1/ddutils/sequence_helpers.py +8 -0
- ddutils-0.0.1/pyproject.toml +31 -0
ddutils-0.0.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Artem Davydov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
ddutils-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: ddutils
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Domain Driven Utils Library
|
|
5
|
+
Home-page: https://github.com/davyddd/ddutils
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: python,ddutils
|
|
8
|
+
Author: davyddd
|
|
9
|
+
Requires-Python: >=3.8,<3.13
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Project-URL: Repository, https://github.com/davyddd/ddutils
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# DDUtils
|
|
21
|
+
|
|
22
|
+
[](https://pypi.python.org/pypi/ddutils)
|
|
23
|
+
[](https://pepy.tech/project/ddutils)
|
|
24
|
+
[](https://github.com/davyddd/ddutils)
|
|
25
|
+
[](https://app.codecov.io/github/davyddd/ddutils)
|
|
26
|
+
[](https://github.com/davyddd/ddutils/blob/main/LICENSE)
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
Install the library using pip:
|
|
31
|
+
```bash
|
|
32
|
+
pip install ddutils
|
|
33
|
+
```
|
|
34
|
+
|
ddutils-0.0.1/README.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# DDUtils
|
|
2
|
+
|
|
3
|
+
[](https://pypi.python.org/pypi/ddutils)
|
|
4
|
+
[](https://pepy.tech/project/ddutils)
|
|
5
|
+
[](https://github.com/davyddd/ddutils)
|
|
6
|
+
[](https://app.codecov.io/github/davyddd/ddutils)
|
|
7
|
+
[](https://github.com/davyddd/ddutils/blob/main/LICENSE)
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
Install the library using pip:
|
|
12
|
+
```bash
|
|
13
|
+
pip install ddutils
|
|
14
|
+
```
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import sys
|
|
3
|
+
from typing import Any, Sequence, Tuple, Union, get_args
|
|
4
|
+
|
|
5
|
+
if sys.version_info >= (3, 10):
|
|
6
|
+
from types import UnionType
|
|
7
|
+
else:
|
|
8
|
+
|
|
9
|
+
class UnionType:
|
|
10
|
+
...
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
NON_COMPLEX_SEQUENCE_TYPES = (str, bytes, bytearray)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_annotation_origin(annotation: Any) -> Any:
|
|
17
|
+
if annotation is None:
|
|
18
|
+
return annotation
|
|
19
|
+
elif hasattr(annotation, '__origin__'):
|
|
20
|
+
# This is a generic type from the typing module
|
|
21
|
+
# Generic types like List[int], Dict[str, int] have an '__origin__' attribute
|
|
22
|
+
return get_annotation_origin(annotation.__origin__)
|
|
23
|
+
elif hasattr(annotation, '__supertype__'):
|
|
24
|
+
# This is a type created with NewType
|
|
25
|
+
return get_annotation_origin(annotation.__supertype__)
|
|
26
|
+
elif annotation is Union or isinstance(annotation, UnionType):
|
|
27
|
+
# This also includes Optional types, since Optional[T] is Union[T, None]
|
|
28
|
+
raise TypeError('Union types are not supported')
|
|
29
|
+
elif not isinstance(annotation, type):
|
|
30
|
+
raise TypeError('Annotation must be instance of type')
|
|
31
|
+
|
|
32
|
+
return annotation
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def is_subclass(annotation: Any, *base_types: Any) -> bool:
|
|
36
|
+
annotation_origin = get_annotation_origin(annotation)
|
|
37
|
+
return inspect.isclass(annotation_origin) and issubclass(
|
|
38
|
+
annotation_origin, tuple(get_annotation_origin(base_type) for base_type in base_types)
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def is_complex_sequence(annotation: Any) -> bool:
|
|
43
|
+
annotation_origin = get_annotation_origin(annotation)
|
|
44
|
+
return annotation_origin not in NON_COMPLEX_SEQUENCE_TYPES and is_subclass(annotation_origin, Sequence)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def get_complex_sequence_element_annotation(annotation: Any) -> Any:
|
|
48
|
+
if hasattr(annotation, '__supertype__'):
|
|
49
|
+
return get_complex_sequence_element_annotation(annotation.__supertype__)
|
|
50
|
+
elif not is_complex_sequence(annotation):
|
|
51
|
+
raise ValueError('Annotation must be a complex sequence')
|
|
52
|
+
|
|
53
|
+
element_annotation = next(iter(get_args(annotation)), None)
|
|
54
|
+
if element_annotation is None:
|
|
55
|
+
raise ValueError('Annotation must have subtype')
|
|
56
|
+
|
|
57
|
+
return element_annotation
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def get_dict_items_annotation(annotation: Any) -> Tuple[Any, Any]:
|
|
61
|
+
if hasattr(annotation, '__supertype__'):
|
|
62
|
+
return get_dict_items_annotation(annotation.__supertype__)
|
|
63
|
+
elif not is_subclass(annotation, dict):
|
|
64
|
+
raise TypeError('`annotation` must be a dict')
|
|
65
|
+
|
|
66
|
+
annotation_args = get_args(annotation)
|
|
67
|
+
if len(annotation_args) != 2: # noqa: PLR2004
|
|
68
|
+
raise ValueError('Dict annotation must have two arguments')
|
|
69
|
+
|
|
70
|
+
key_annotation, value_annotation = annotation_args
|
|
71
|
+
|
|
72
|
+
return key_annotation, value_annotation
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def get_annotation_without_optional(annotation: Any) -> Any:
|
|
76
|
+
try:
|
|
77
|
+
get_annotation_origin(annotation)
|
|
78
|
+
return annotation
|
|
79
|
+
except TypeError:
|
|
80
|
+
annotation_args = tuple(arg for arg in get_args(annotation) if arg is not type(None))
|
|
81
|
+
|
|
82
|
+
if len(annotation_args) != 1: # noqa: PLR2004
|
|
83
|
+
raise TypeError('Union types are not supported') from None
|
|
84
|
+
|
|
85
|
+
return annotation_args[0]
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
import builtins
|
|
3
|
+
import inspect
|
|
4
|
+
import textwrap
|
|
5
|
+
from importlib import import_module
|
|
6
|
+
from typing import Any, Callable, Dict, Generator, NamedTuple, Optional, Tuple, Type
|
|
7
|
+
|
|
8
|
+
from ddutils.module_getter import get_module
|
|
9
|
+
|
|
10
|
+
UNDEFINED_VALUE = object()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Annotation(NamedTuple):
|
|
14
|
+
argument: str
|
|
15
|
+
default_value: Any
|
|
16
|
+
|
|
17
|
+
def is_empty(self) -> bool:
|
|
18
|
+
return self.default_value is inspect._empty
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ExceptionInfo:
|
|
22
|
+
exception_class: Type[Exception]
|
|
23
|
+
args: Tuple[Any, ...]
|
|
24
|
+
kwargs: Dict[str, Any]
|
|
25
|
+
|
|
26
|
+
def __init__(self, exception_class: Type[Exception], args: Tuple[Any, ...], kwargs: Dict[str, Any]):
|
|
27
|
+
self.exception_class = exception_class
|
|
28
|
+
self.args = tuple(_v.value if isinstance(_v, ast.Constant) else UNDEFINED_VALUE for _v in args)
|
|
29
|
+
self.kwargs = {_k: _v.value if isinstance(_v, ast.Constant) else UNDEFINED_VALUE for _k, _v in kwargs.items()}
|
|
30
|
+
|
|
31
|
+
def get_kwargs(self) -> Dict[str, Any]:
|
|
32
|
+
annotations = tuple(
|
|
33
|
+
Annotation(argument, argument_info.default)
|
|
34
|
+
for argument, argument_info in inspect.signature(self.exception_class.__init__).parameters.items()
|
|
35
|
+
if argument not in {'self', 'args', 'kwargs'}
|
|
36
|
+
)
|
|
37
|
+
kwargs = {
|
|
38
|
+
**{annotation.argument: self.args[item] for item, annotation in enumerate(annotations[: len(self.args)])},
|
|
39
|
+
**self.kwargs,
|
|
40
|
+
}
|
|
41
|
+
for annotation in annotations:
|
|
42
|
+
if annotation.argument not in kwargs:
|
|
43
|
+
if not annotation.is_empty():
|
|
44
|
+
kwargs[annotation.argument] = annotation.default_value
|
|
45
|
+
elif hasattr(self.exception_class, annotation.argument):
|
|
46
|
+
kwargs[annotation.argument] = getattr(self.exception_class, annotation.argument)
|
|
47
|
+
else:
|
|
48
|
+
kwargs[annotation.argument] = UNDEFINED_VALUE
|
|
49
|
+
|
|
50
|
+
del annotations
|
|
51
|
+
|
|
52
|
+
return kwargs
|
|
53
|
+
|
|
54
|
+
def get_exception_instance(self, dry_run: bool = True) -> Optional[Exception]:
|
|
55
|
+
kwargs = {k: f'<{k}>' if v is UNDEFINED_VALUE else v for k, v in self.get_kwargs().items()}
|
|
56
|
+
try:
|
|
57
|
+
# There might be issues with strict typing because UNDEFINED_VALUE is always replaced with a string
|
|
58
|
+
return self.exception_class(**kwargs)
|
|
59
|
+
except Exception as err: # noqa: BLE001
|
|
60
|
+
if dry_run:
|
|
61
|
+
return None
|
|
62
|
+
else:
|
|
63
|
+
raise err
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _get_node_name(node: ast.AST) -> str:
|
|
67
|
+
if isinstance(node, ast.Name):
|
|
68
|
+
return node.id
|
|
69
|
+
elif isinstance(node, ast.Attribute):
|
|
70
|
+
return f'{_get_node_name(node.value)}.{node.attr}'
|
|
71
|
+
elif isinstance(node, ast.Call):
|
|
72
|
+
return _get_node_name(node.func)
|
|
73
|
+
else:
|
|
74
|
+
raise TypeError(f'Unsupported node type: {type(node)}')
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def extract_function_exceptions(func: Callable) -> Generator[ExceptionInfo, None, None]:
|
|
78
|
+
source = textwrap.dedent(inspect.getsource(func))
|
|
79
|
+
tree = ast.parse(source)
|
|
80
|
+
|
|
81
|
+
for node in ast.walk(tree):
|
|
82
|
+
if isinstance(node, ast.Raise):
|
|
83
|
+
if node.exc is None:
|
|
84
|
+
continue
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
exception_name: str = _get_node_name(node.exc)
|
|
88
|
+
exception_args: Tuple[Any, ...] = ()
|
|
89
|
+
exception_kwargs: Dict[str, Any] = {}
|
|
90
|
+
except TypeError:
|
|
91
|
+
continue
|
|
92
|
+
|
|
93
|
+
if isinstance(node.exc, ast.Call):
|
|
94
|
+
exception_args = tuple(node.exc.args)
|
|
95
|
+
exception_kwargs = {kw.arg: kw.value for kw in node.exc.keywords if isinstance(kw.arg, str)}
|
|
96
|
+
|
|
97
|
+
exception_name_slices = exception_name.split('.')
|
|
98
|
+
class_name = exception_name_slices[-1]
|
|
99
|
+
sub_modules = exception_name_slices[:-1]
|
|
100
|
+
|
|
101
|
+
exception_module = get_module(import_module(func.__module__), sub_modules)
|
|
102
|
+
|
|
103
|
+
exception_class: Optional[Type[Exception]] = getattr(exception_module, class_name, None)
|
|
104
|
+
if exception_class is None and hasattr(builtins, exception_name):
|
|
105
|
+
exception_class = getattr(builtins, exception_name)
|
|
106
|
+
|
|
107
|
+
if exception_class is None:
|
|
108
|
+
continue
|
|
109
|
+
|
|
110
|
+
yield ExceptionInfo(exception_class=exception_class, args=exception_args, kwargs=exception_kwargs)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from types import CellType, CodeType, FunctionType
|
|
3
|
+
from typing import Callable, Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def create_new_function(base_func: Callable, new_name: Optional[str] = None) -> Callable:
|
|
7
|
+
base_code = base_func.__code__
|
|
8
|
+
func_name = new_name or base_code.co_name
|
|
9
|
+
|
|
10
|
+
if sys.version_info >= (3, 11):
|
|
11
|
+
# for python 3.11 and newer
|
|
12
|
+
new_code = CodeType(
|
|
13
|
+
base_code.co_argcount,
|
|
14
|
+
base_code.co_posonlyargcount,
|
|
15
|
+
base_code.co_kwonlyargcount,
|
|
16
|
+
base_code.co_nlocals,
|
|
17
|
+
base_code.co_stacksize,
|
|
18
|
+
base_code.co_flags,
|
|
19
|
+
base_code.co_code,
|
|
20
|
+
base_code.co_consts,
|
|
21
|
+
base_code.co_names,
|
|
22
|
+
base_code.co_varnames,
|
|
23
|
+
base_code.co_filename,
|
|
24
|
+
func_name,
|
|
25
|
+
func_name,
|
|
26
|
+
base_code.co_firstlineno,
|
|
27
|
+
base_code.co_lnotab,
|
|
28
|
+
base_code.co_exceptiontable,
|
|
29
|
+
base_code.co_freevars,
|
|
30
|
+
base_code.co_cellvars,
|
|
31
|
+
)
|
|
32
|
+
else:
|
|
33
|
+
# for python 3.10 and older
|
|
34
|
+
new_code = CodeType(
|
|
35
|
+
base_code.co_argcount,
|
|
36
|
+
base_code.co_posonlyargcount,
|
|
37
|
+
base_code.co_kwonlyargcount,
|
|
38
|
+
base_code.co_nlocals,
|
|
39
|
+
base_code.co_stacksize,
|
|
40
|
+
base_code.co_flags,
|
|
41
|
+
base_code.co_code,
|
|
42
|
+
base_code.co_consts,
|
|
43
|
+
base_code.co_names,
|
|
44
|
+
base_code.co_varnames,
|
|
45
|
+
base_code.co_filename,
|
|
46
|
+
func_name,
|
|
47
|
+
base_code.co_firstlineno,
|
|
48
|
+
base_code.co_lnotab,
|
|
49
|
+
base_code.co_freevars,
|
|
50
|
+
base_code.co_cellvars,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
closure = base_func.__closure__
|
|
54
|
+
if closure:
|
|
55
|
+
closure = tuple(CellType(cell.cell_contents) for cell in closure)
|
|
56
|
+
|
|
57
|
+
return FunctionType(new_code, base_func.__globals__, func_name, base_func.__defaults__, closure)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from types import ModuleType
|
|
2
|
+
from typing import List
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def get_module(module: ModuleType, sub_modules: List[str]) -> ModuleType:
|
|
6
|
+
if not module:
|
|
7
|
+
raise ValueError('Argument `module` is required')
|
|
8
|
+
elif not isinstance(sub_modules, list):
|
|
9
|
+
raise ValueError('Argument `sub_modules` must be a list of strings')
|
|
10
|
+
|
|
11
|
+
if len(sub_modules) == 0:
|
|
12
|
+
return module
|
|
13
|
+
|
|
14
|
+
sub_module = sub_modules.pop(0)
|
|
15
|
+
|
|
16
|
+
if hasattr(module, sub_module):
|
|
17
|
+
module = getattr(module, sub_module)
|
|
18
|
+
return get_module(module, sub_modules)
|
|
19
|
+
else:
|
|
20
|
+
raise ValueError(f'Module {sub_module} not found in {module.__name__}')
|
|
File without changes
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from functools import wraps
|
|
3
|
+
from typing import Any, Callable, Optional, Tuple, Type
|
|
4
|
+
|
|
5
|
+
logger = logging.getLogger(__name__)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def safe_call(
|
|
9
|
+
func: Optional[Callable] = None,
|
|
10
|
+
capture_exception: bool = True,
|
|
11
|
+
default_result: Optional[Any] = None,
|
|
12
|
+
exceptions: Tuple[Type[Exception], ...] = (Exception,),
|
|
13
|
+
) -> Callable:
|
|
14
|
+
if func is None:
|
|
15
|
+
return lambda _func: safe_call(
|
|
16
|
+
func=_func, capture_exception=capture_exception, default_result=default_result, exceptions=exceptions
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
@wraps(func)
|
|
20
|
+
def wrapped_func(*args, **kwargs):
|
|
21
|
+
try:
|
|
22
|
+
result = func(*args, **kwargs)
|
|
23
|
+
except exceptions as error:
|
|
24
|
+
if capture_exception:
|
|
25
|
+
logger.exception(error)
|
|
26
|
+
|
|
27
|
+
result = default_result
|
|
28
|
+
|
|
29
|
+
return result
|
|
30
|
+
|
|
31
|
+
return wrapped_func
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def retry_once_after_exception(
|
|
35
|
+
func: Optional[Callable] = None, capture_exception: bool = True, exceptions: Tuple[Type[Exception], ...] = (Exception,)
|
|
36
|
+
) -> Callable:
|
|
37
|
+
if func is None:
|
|
38
|
+
return lambda _func: retry_once_after_exception(func=_func, capture_exception=capture_exception, exceptions=exceptions)
|
|
39
|
+
|
|
40
|
+
@wraps(func)
|
|
41
|
+
def wrapped_func(*args, **kwargs):
|
|
42
|
+
try:
|
|
43
|
+
return func(*args, **kwargs)
|
|
44
|
+
except exceptions as error:
|
|
45
|
+
if capture_exception:
|
|
46
|
+
logger.exception(error)
|
|
47
|
+
|
|
48
|
+
return func(*args, **kwargs)
|
|
49
|
+
|
|
50
|
+
return wrapped_func
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "ddutils"
|
|
3
|
+
description = "Domain Driven Utils Library"
|
|
4
|
+
version = "0.0.1"
|
|
5
|
+
authors = ["davyddd"]
|
|
6
|
+
license = "MIT"
|
|
7
|
+
repository = "https://github.com/davyddd/ddutils"
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
keywords = ["python", "ddutils"]
|
|
10
|
+
packages = [{include = "ddutils"}]
|
|
11
|
+
|
|
12
|
+
[tool.poetry.dependencies]
|
|
13
|
+
python = ">=3.8,<3.13"
|
|
14
|
+
|
|
15
|
+
[tool.poetry.group.dev.dependencies]
|
|
16
|
+
ipdb = "0.13.9"
|
|
17
|
+
ipython = "8.12.3"
|
|
18
|
+
|
|
19
|
+
[tool.poetry.group.test.dependencies]
|
|
20
|
+
pytest = "8.1.1"
|
|
21
|
+
pytest-cov = "5.0.0"
|
|
22
|
+
parameterized = "0.9.0"
|
|
23
|
+
typing-extensions = "^4.12.2"
|
|
24
|
+
|
|
25
|
+
[tool.poetry.group.linter.dependencies]
|
|
26
|
+
mypy = "1.1.1"
|
|
27
|
+
ruff = "0.1.7"
|
|
28
|
+
|
|
29
|
+
[build-system]
|
|
30
|
+
requires = ["poetry-core>=1.0.0"]
|
|
31
|
+
build-backend = "poetry.core.masonry.api"
|