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 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
+ [![pypi](https://img.shields.io/pypi/v/ddutils.svg)](https://pypi.python.org/pypi/ddutils)
23
+ [![downloads](https://static.pepy.tech/badge/ddutils/month)](https://pepy.tech/project/ddutils)
24
+ [![versions](https://img.shields.io/pypi/pyversions/ddutils.svg)](https://github.com/davyddd/ddutils)
25
+ [![codecov](https://codecov.io/gh/davyddd/ddutils/branch/main/graph/badge.svg)](https://app.codecov.io/github/davyddd/ddutils)
26
+ [![license](https://img.shields.io/github/license/davyddd/ddutils.svg)](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
+
@@ -0,0 +1,14 @@
1
+ # DDUtils
2
+
3
+ [![pypi](https://img.shields.io/pypi/v/ddutils.svg)](https://pypi.python.org/pypi/ddutils)
4
+ [![downloads](https://static.pepy.tech/badge/ddutils/month)](https://pepy.tech/project/ddutils)
5
+ [![versions](https://img.shields.io/pypi/pyversions/ddutils.svg)](https://github.com/davyddd/ddutils)
6
+ [![codecov](https://codecov.io/gh/davyddd/ddutils/branch/main/graph/badge.svg)](https://app.codecov.io/github/davyddd/ddutils)
7
+ [![license](https://img.shields.io/github/license/davyddd/ddutils.svg)](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,11 @@
1
+ __all__ = (
2
+ 'convertors',
3
+ 'class_helpers',
4
+ 'function_exceptions_extractor',
5
+ 'module_getter',
6
+ 'sequence_helpers',
7
+ 'annotation_helpers',
8
+ 'function_helpers',
9
+ 'safe_decorators',
10
+ 'sequence_helpers',
11
+ )
@@ -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,8 @@
1
+ import inspect
2
+ from typing import Any
3
+
4
+
5
+ def get_origin_class_of_method(cls: Any, method_name: str):
6
+ for base in inspect.getmro(cls):
7
+ if method_name in base.__dict__:
8
+ return base
@@ -0,0 +1,2 @@
1
+ def convert_camel_case_to_snake_case(string: str) -> str:
2
+ return ''.join(f'_{char.lower()}' if char.isupper() else char for char in string).lstrip('_')
@@ -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,8 @@
1
+ from typing import Any, Sequence
2
+
3
+
4
+ def get_safe_element(seq: Sequence[Any], index: int) -> Any:
5
+ try:
6
+ return seq[index]
7
+ except IndexError:
8
+ return None
@@ -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"