ddutils 0.1.6__tar.gz → 0.2.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.1.6 → ddutils-0.2.1}/PKG-INFO +81 -19
- {ddutils-0.1.6 → ddutils-0.2.1}/README.md +76 -11
- {ddutils-0.1.6 → ddutils-0.2.1}/ddutils/annotation_helpers.py +3 -10
- {ddutils-0.1.6 → ddutils-0.2.1}/ddutils/class_helpers.py +5 -4
- ddutils-0.2.1/ddutils/datetime_helpers.py +5 -0
- {ddutils-0.1.6 → ddutils-0.2.1}/ddutils/function_exceptions_extractor.py +41 -34
- ddutils-0.2.1/ddutils/function_helpers.py +33 -0
- {ddutils-0.1.6 → ddutils-0.2.1}/ddutils/module_getter.py +1 -2
- {ddutils-0.1.6 → ddutils-0.2.1}/ddutils/object_getter.py +2 -2
- {ddutils-0.1.6 → ddutils-0.2.1}/ddutils/safe_decorators.py +6 -5
- ddutils-0.2.1/ddutils/scoped_registry.py +127 -0
- ddutils-0.2.1/pyproject.toml +50 -0
- ddutils-0.2.1/pyproject.toml.orig +46 -0
- ddutils-0.1.6/ddutils/datetime_helpers.py +0 -13
- ddutils-0.1.6/ddutils/function_helpers.py +0 -57
- ddutils-0.1.6/ddutils/scoped_registry.py +0 -91
- ddutils-0.1.6/pyproject.toml +0 -31
- {ddutils-0.1.6 → ddutils-0.2.1}/LICENSE +0 -0
- {ddutils-0.1.6 → ddutils-0.2.1}/ddutils/__init__.py +0 -0
- {ddutils-0.1.6 → ddutils-0.2.1}/ddutils/convertors.py +0 -0
- {ddutils-0.1.6 → ddutils-0.2.1}/ddutils/py.typed +0 -0
- {ddutils-0.1.6 → ddutils-0.2.1}/ddutils/sequence_helpers.py +0 -0
|
@@ -1,20 +1,18 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ddutils
|
|
3
|
-
Version: 0.1
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Domain Driven Utils Library
|
|
5
|
-
License: MIT
|
|
6
|
-
License-File: LICENSE
|
|
7
5
|
Keywords: python,ddutils
|
|
8
6
|
Author: davyddd
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
11
9
|
Classifier: Programming Language :: Python :: 3
|
|
12
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
14
10
|
Classifier: Programming Language :: Python :: 3.11
|
|
15
11
|
Classifier: Programming Language :: Python :: 3.12
|
|
16
12
|
Classifier: Programming Language :: Python :: 3.13
|
|
17
13
|
Classifier: Programming Language :: Python :: 3.14
|
|
14
|
+
Classifier: Typing :: Typed
|
|
15
|
+
Requires-Python: >=3.11, <3.15
|
|
18
16
|
Project-URL: Repository, https://github.com/davyddd/ddutils
|
|
19
17
|
Description-Content-Type: text/markdown
|
|
20
18
|
|
|
@@ -36,6 +34,21 @@ Install the library using pip:
|
|
|
36
34
|
pip install ddutils
|
|
37
35
|
```
|
|
38
36
|
|
|
37
|
+
## Development
|
|
38
|
+
|
|
39
|
+
The project runs entirely in Docker. Requires Docker and [Fabric](https://www.fabfile.org/) on the host:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
fab build # build the dev image
|
|
43
|
+
fab tests # run pytest
|
|
44
|
+
fab linters # run ruff (with --fix), ty and complexipy
|
|
45
|
+
fab shell # IPython inside the container
|
|
46
|
+
fab bash # bash inside the container
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
This project was generated from [dd-lib-stub](https://github.com/davyddd/dd-lib-stub);
|
|
50
|
+
run `uvx copier update` to pull in template updates.
|
|
51
|
+
|
|
39
52
|
## Type Annotation Helpers
|
|
40
53
|
|
|
41
54
|
Utilities for inspecting and working with Python type hints and generic types.
|
|
@@ -322,33 +335,83 @@ json_loads = get_object_by_path('json.loads')
|
|
|
322
335
|
|
|
323
336
|
## Scoped Registry
|
|
324
337
|
|
|
325
|
-
A registry that
|
|
338
|
+
A registry that keeps one object instance per scope and manages its lifetime.
|
|
326
339
|
|
|
327
340
|
### ScopedRegistry
|
|
328
341
|
|
|
329
|
-
|
|
342
|
+
Keeps one object per scope. `scope_func` decides what the current scope is and returns its key; `None` means
|
|
343
|
+
"no current scope" and makes every method raise `RuntimeError`.
|
|
344
|
+
|
|
345
|
+
`scope()` is the entry point into a scope: it creates the object on enter and removes it on exit, calling
|
|
346
|
+
the optional destructor exactly once. Nested `scope()` blocks join the existing object and leave it alone.
|
|
347
|
+
Concurrent first accesses with the same key call `create_func` only once.
|
|
330
348
|
|
|
331
349
|
```python
|
|
350
|
+
import asyncio
|
|
351
|
+
|
|
332
352
|
from ddutils.scoped_registry import ScopedRegistry
|
|
333
353
|
|
|
334
354
|
|
|
335
|
-
def
|
|
355
|
+
async def create_session(): ...
|
|
336
356
|
|
|
337
357
|
|
|
338
|
-
|
|
358
|
+
# One session per task: a child task spawned with gather/create_task gets its own session
|
|
359
|
+
session_registry = ScopedRegistry(create_func=create_session, scope_func=asyncio.current_task, destructor_method_name='close')
|
|
339
360
|
|
|
340
361
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
)
|
|
362
|
+
async def handle_request():
|
|
363
|
+
async with session_registry.scope() as session: # created here, closed and removed on exit
|
|
364
|
+
await session.execute('select ...')
|
|
365
|
+
async with session_registry.scope() as same_session: # nested block joins
|
|
366
|
+
...
|
|
367
|
+
```
|
|
345
368
|
|
|
369
|
+
Lower-level methods are available when a context manager does not fit:
|
|
346
370
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
371
|
+
- `get()` returns the current object or `None`;
|
|
372
|
+
- `set(**kwargs)` returns the existing object or creates it. Whoever calls it takes over the cleanup that
|
|
373
|
+
`scope()` does on exit and must call `clear()` when the object is no longer needed, if it ever is: a
|
|
374
|
+
per-process client may live until the process ends;
|
|
375
|
+
- `clear()` removes the object of the current scope and calls the destructor.
|
|
376
|
+
|
|
377
|
+
#### Scopes kept in a ContextVar
|
|
378
|
+
|
|
379
|
+
A `scope_func` that declares a `token` parameter receives a freshly generated token when `scope()` is entered
|
|
380
|
+
and `None` on every other access. This is for a scope of one execution (an HTTP request, an actor run) rather
|
|
381
|
+
than one task: the function stores the token in a `ContextVar`, so coroutines spawned inside the block
|
|
382
|
+
(`gather`, `TaskGroup`, `create_task`) inherit it and share the object with the parent, while outside of
|
|
383
|
+
`scope()` there is no key and every method raises.
|
|
384
|
+
|
|
385
|
+
```python
|
|
386
|
+
from contextvars import ContextVar
|
|
387
|
+
|
|
388
|
+
execution_key: ContextVar[str | None] = ContextVar('execution_key', default=None)
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def execution_scope(token: str | None) -> str | None:
|
|
392
|
+
key = execution_key.get()
|
|
393
|
+
if key is None and token is not None: # entering scope() outside of an execution: start one
|
|
394
|
+
execution_key.set(token)
|
|
395
|
+
key = token
|
|
396
|
+
return key
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
async def create_log_properties(**kwargs): ...
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
log_registry = ScopedRegistry(create_func=create_log_properties, scope_func=execution_scope)
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
async def middleware(headers):
|
|
406
|
+
async with log_registry.scope(headers=headers) as log_properties:
|
|
407
|
+
await asyncio.gather(work(), work()) # both see the same log_properties via log_registry.get()
|
|
350
408
|
```
|
|
351
409
|
|
|
410
|
+
Reads never start a scope: a `get()` from a long-living context (a startup log in the server's main task)
|
|
411
|
+
that planted a key would be inherited by every request task, making all requests share one object. For the
|
|
412
|
+
same reason the key is not reset when the block ends, so `scope()` must be entered in a task that does not
|
|
413
|
+
outlive the block (a request handler, an actor run).
|
|
414
|
+
|
|
352
415
|
## Function Exception Extraction
|
|
353
416
|
|
|
354
417
|
AST-based introspection for extracting exceptions from function source code.
|
|
@@ -376,4 +439,3 @@ get_safe_element(items, 0) # 1
|
|
|
376
439
|
get_safe_element(items, 10) # None (no IndexError)
|
|
377
440
|
get_safe_element(items, -1) # 3
|
|
378
441
|
```
|
|
379
|
-
|
|
@@ -16,6 +16,21 @@ Install the library using pip:
|
|
|
16
16
|
pip install ddutils
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
+
## Development
|
|
20
|
+
|
|
21
|
+
The project runs entirely in Docker. Requires Docker and [Fabric](https://www.fabfile.org/) on the host:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
fab build # build the dev image
|
|
25
|
+
fab tests # run pytest
|
|
26
|
+
fab linters # run ruff (with --fix), ty and complexipy
|
|
27
|
+
fab shell # IPython inside the container
|
|
28
|
+
fab bash # bash inside the container
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
This project was generated from [dd-lib-stub](https://github.com/davyddd/dd-lib-stub);
|
|
32
|
+
run `uvx copier update` to pull in template updates.
|
|
33
|
+
|
|
19
34
|
## Type Annotation Helpers
|
|
20
35
|
|
|
21
36
|
Utilities for inspecting and working with Python type hints and generic types.
|
|
@@ -302,33 +317,83 @@ json_loads = get_object_by_path('json.loads')
|
|
|
302
317
|
|
|
303
318
|
## Scoped Registry
|
|
304
319
|
|
|
305
|
-
A registry that
|
|
320
|
+
A registry that keeps one object instance per scope and manages its lifetime.
|
|
306
321
|
|
|
307
322
|
### ScopedRegistry
|
|
308
323
|
|
|
309
|
-
|
|
324
|
+
Keeps one object per scope. `scope_func` decides what the current scope is and returns its key; `None` means
|
|
325
|
+
"no current scope" and makes every method raise `RuntimeError`.
|
|
326
|
+
|
|
327
|
+
`scope()` is the entry point into a scope: it creates the object on enter and removes it on exit, calling
|
|
328
|
+
the optional destructor exactly once. Nested `scope()` blocks join the existing object and leave it alone.
|
|
329
|
+
Concurrent first accesses with the same key call `create_func` only once.
|
|
310
330
|
|
|
311
331
|
```python
|
|
332
|
+
import asyncio
|
|
333
|
+
|
|
312
334
|
from ddutils.scoped_registry import ScopedRegistry
|
|
313
335
|
|
|
314
336
|
|
|
315
|
-
def
|
|
337
|
+
async def create_session(): ...
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
# One session per task: a child task spawned with gather/create_task gets its own session
|
|
341
|
+
session_registry = ScopedRegistry(create_func=create_session, scope_func=asyncio.current_task, destructor_method_name='close')
|
|
316
342
|
|
|
317
343
|
|
|
318
|
-
def
|
|
344
|
+
async def handle_request():
|
|
345
|
+
async with session_registry.scope() as session: # created here, closed and removed on exit
|
|
346
|
+
await session.execute('select ...')
|
|
347
|
+
async with session_registry.scope() as same_session: # nested block joins
|
|
348
|
+
...
|
|
349
|
+
```
|
|
319
350
|
|
|
351
|
+
Lower-level methods are available when a context manager does not fit:
|
|
320
352
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
353
|
+
- `get()` returns the current object or `None`;
|
|
354
|
+
- `set(**kwargs)` returns the existing object or creates it. Whoever calls it takes over the cleanup that
|
|
355
|
+
`scope()` does on exit and must call `clear()` when the object is no longer needed, if it ever is: a
|
|
356
|
+
per-process client may live until the process ends;
|
|
357
|
+
- `clear()` removes the object of the current scope and calls the destructor.
|
|
325
358
|
|
|
359
|
+
#### Scopes kept in a ContextVar
|
|
326
360
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
361
|
+
A `scope_func` that declares a `token` parameter receives a freshly generated token when `scope()` is entered
|
|
362
|
+
and `None` on every other access. This is for a scope of one execution (an HTTP request, an actor run) rather
|
|
363
|
+
than one task: the function stores the token in a `ContextVar`, so coroutines spawned inside the block
|
|
364
|
+
(`gather`, `TaskGroup`, `create_task`) inherit it and share the object with the parent, while outside of
|
|
365
|
+
`scope()` there is no key and every method raises.
|
|
366
|
+
|
|
367
|
+
```python
|
|
368
|
+
from contextvars import ContextVar
|
|
369
|
+
|
|
370
|
+
execution_key: ContextVar[str | None] = ContextVar('execution_key', default=None)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def execution_scope(token: str | None) -> str | None:
|
|
374
|
+
key = execution_key.get()
|
|
375
|
+
if key is None and token is not None: # entering scope() outside of an execution: start one
|
|
376
|
+
execution_key.set(token)
|
|
377
|
+
key = token
|
|
378
|
+
return key
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
async def create_log_properties(**kwargs): ...
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
log_registry = ScopedRegistry(create_func=create_log_properties, scope_func=execution_scope)
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
async def middleware(headers):
|
|
388
|
+
async with log_registry.scope(headers=headers) as log_properties:
|
|
389
|
+
await asyncio.gather(work(), work()) # both see the same log_properties via log_registry.get()
|
|
330
390
|
```
|
|
331
391
|
|
|
392
|
+
Reads never start a scope: a `get()` from a long-living context (a startup log in the server's main task)
|
|
393
|
+
that planted a key would be inherited by every request task, making all requests share one object. For the
|
|
394
|
+
same reason the key is not reset when the block ends, so `scope()` must be entered in a task that does not
|
|
395
|
+
outlive the block (a request handler, an actor run).
|
|
396
|
+
|
|
332
397
|
## Function Exception Extraction
|
|
333
398
|
|
|
334
399
|
AST-based introspection for extracting exceptions from function source code.
|
|
@@ -1,14 +1,7 @@
|
|
|
1
1
|
import inspect
|
|
2
|
-
import sys
|
|
3
2
|
from collections.abc import Sequence
|
|
4
|
-
from
|
|
5
|
-
|
|
6
|
-
if sys.version_info >= (3, 10):
|
|
7
|
-
from types import UnionType
|
|
8
|
-
else:
|
|
9
|
-
|
|
10
|
-
class UnionType: ...
|
|
11
|
-
|
|
3
|
+
from types import UnionType
|
|
4
|
+
from typing import Any, Union, get_args
|
|
12
5
|
|
|
13
6
|
NON_COMPLEX_SEQUENCE_TYPES = (str, bytes, bytearray)
|
|
14
7
|
|
|
@@ -57,7 +50,7 @@ def get_complex_sequence_element_annotation(annotation: Any) -> Any:
|
|
|
57
50
|
return element_annotation
|
|
58
51
|
|
|
59
52
|
|
|
60
|
-
def get_dict_items_annotation(annotation: Any) ->
|
|
53
|
+
def get_dict_items_annotation(annotation: Any) -> tuple[Any, Any]:
|
|
61
54
|
if hasattr(annotation, '__supertype__'):
|
|
62
55
|
return get_dict_items_annotation(annotation.__supertype__)
|
|
63
56
|
elif not is_subclass(annotation, dict):
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import inspect
|
|
2
|
-
from
|
|
2
|
+
from collections.abc import Callable
|
|
3
|
+
from typing import Any, TypeVar
|
|
3
4
|
|
|
4
5
|
_T = TypeVar('_T', bound=Any)
|
|
5
6
|
|
|
6
7
|
|
|
7
|
-
def get_origin_class_of_method(cls: Any, method_name: str) ->
|
|
8
|
+
def get_origin_class_of_method(cls: Any, method_name: str) -> type | None:
|
|
8
9
|
"""
|
|
9
10
|
Find the class in the inheritance hierarchy where a method is originally defined.
|
|
10
11
|
|
|
@@ -50,9 +51,9 @@ class classproperty: # noqa: N801
|
|
|
50
51
|
"""
|
|
51
52
|
self.fget = fget
|
|
52
53
|
self.__doc__ = fget.__doc__
|
|
53
|
-
self.__name__ = fget.__name__
|
|
54
|
+
self.__name__ = getattr(fget, '__name__', type(fget).__name__)
|
|
54
55
|
|
|
55
|
-
def __get__(self, instance: Any, owner:
|
|
56
|
+
def __get__(self, instance: Any, owner: type[Any]) -> Any:
|
|
56
57
|
"""
|
|
57
58
|
Retrieve the property value using the descriptor protocol.
|
|
58
59
|
|
|
@@ -2,9 +2,9 @@ import ast
|
|
|
2
2
|
import builtins
|
|
3
3
|
import inspect
|
|
4
4
|
import textwrap
|
|
5
|
-
from collections.abc import Generator
|
|
5
|
+
from collections.abc import Callable, Generator
|
|
6
6
|
from importlib import import_module
|
|
7
|
-
from typing import Any,
|
|
7
|
+
from typing import Any, NamedTuple
|
|
8
8
|
|
|
9
9
|
from ddutils.module_getter import get_module
|
|
10
10
|
|
|
@@ -20,16 +20,16 @@ class Annotation(NamedTuple):
|
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
class ExceptionInfo:
|
|
23
|
-
exception_class:
|
|
24
|
-
args:
|
|
25
|
-
kwargs:
|
|
23
|
+
exception_class: type[Exception]
|
|
24
|
+
args: tuple[Any, ...]
|
|
25
|
+
kwargs: dict[str, Any]
|
|
26
26
|
|
|
27
|
-
def __init__(self, exception_class:
|
|
27
|
+
def __init__(self, exception_class: type[Exception], args: tuple[Any, ...], kwargs: dict[str, Any]):
|
|
28
28
|
self.exception_class = exception_class
|
|
29
29
|
self.args = tuple(_v.value if isinstance(_v, ast.Constant) else UNDEFINED_VALUE for _v in args)
|
|
30
30
|
self.kwargs = {_k: _v.value if isinstance(_v, ast.Constant) else UNDEFINED_VALUE for _k, _v in kwargs.items()}
|
|
31
31
|
|
|
32
|
-
def get_kwargs(self) ->
|
|
32
|
+
def get_kwargs(self) -> dict[str, Any]:
|
|
33
33
|
annotations = tuple(
|
|
34
34
|
Annotation(argument, argument_info.default)
|
|
35
35
|
for argument, argument_info in inspect.signature(self.exception_class.__init__).parameters.items()
|
|
@@ -52,7 +52,7 @@ class ExceptionInfo:
|
|
|
52
52
|
|
|
53
53
|
return kwargs
|
|
54
54
|
|
|
55
|
-
def get_exception_instance(self, dry_run: bool = True) ->
|
|
55
|
+
def get_exception_instance(self, dry_run: bool = True) -> Exception | None:
|
|
56
56
|
kwargs = {k: f'<{k}>' if v is UNDEFINED_VALUE else v for k, v in self.get_kwargs().items()}
|
|
57
57
|
try:
|
|
58
58
|
# There might be issues with strict typing because UNDEFINED_VALUE is always replaced with a string
|
|
@@ -75,37 +75,44 @@ def _get_node_name(node: ast.AST) -> str:
|
|
|
75
75
|
raise TypeError(f'Unsupported node type: {type(node)}')
|
|
76
76
|
|
|
77
77
|
|
|
78
|
-
def
|
|
79
|
-
|
|
80
|
-
|
|
78
|
+
def _resolve_exception_class(func: Callable, exception_name: str) -> type[Exception] | None:
|
|
79
|
+
*sub_modules, class_name = exception_name.split('.')
|
|
80
|
+
exception_module = get_module(import_module(func.__module__), sub_modules)
|
|
81
|
+
|
|
82
|
+
exception_class: type[Exception] | None = getattr(exception_module, class_name, None)
|
|
83
|
+
if exception_class is None and hasattr(builtins, exception_name):
|
|
84
|
+
exception_class = getattr(builtins, exception_name)
|
|
85
|
+
|
|
86
|
+
return exception_class
|
|
81
87
|
|
|
82
|
-
for node in ast.walk(tree):
|
|
83
|
-
if isinstance(node, ast.Raise):
|
|
84
|
-
if node.exc is None:
|
|
85
|
-
continue
|
|
86
88
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
exception_kwargs: Dict[str, Any] = {}
|
|
91
|
-
except TypeError:
|
|
92
|
-
continue
|
|
89
|
+
def _extract_raise_exception(func: Callable, node: ast.Raise) -> ExceptionInfo | None:
|
|
90
|
+
if node.exc is None:
|
|
91
|
+
return None
|
|
93
92
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
93
|
+
try:
|
|
94
|
+
exception_name = _get_node_name(node.exc)
|
|
95
|
+
except TypeError:
|
|
96
|
+
return None
|
|
97
97
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
98
|
+
exception_args: tuple[Any, ...] = ()
|
|
99
|
+
exception_kwargs: dict[str, Any] = {}
|
|
100
|
+
if isinstance(node.exc, ast.Call):
|
|
101
|
+
exception_args = tuple(node.exc.args)
|
|
102
|
+
exception_kwargs = {kw.arg: kw.value for kw in node.exc.keywords if isinstance(kw.arg, str)}
|
|
101
103
|
|
|
102
|
-
|
|
104
|
+
exception_class = _resolve_exception_class(func, exception_name)
|
|
105
|
+
if exception_class is None:
|
|
106
|
+
return None
|
|
103
107
|
|
|
104
|
-
|
|
105
|
-
if exception_class is None and hasattr(builtins, exception_name):
|
|
106
|
-
exception_class = getattr(builtins, exception_name)
|
|
108
|
+
return ExceptionInfo(exception_class=exception_class, args=exception_args, kwargs=exception_kwargs)
|
|
107
109
|
|
|
108
|
-
if exception_class is None:
|
|
109
|
-
continue
|
|
110
110
|
|
|
111
|
-
|
|
111
|
+
def extract_function_exceptions(func: Callable) -> Generator[ExceptionInfo, None, None]:
|
|
112
|
+
source = textwrap.dedent(inspect.getsource(func))
|
|
113
|
+
|
|
114
|
+
for node in ast.walk(ast.parse(source)):
|
|
115
|
+
if isinstance(node, ast.Raise):
|
|
116
|
+
exception_info = _extract_raise_exception(func, node)
|
|
117
|
+
if exception_info is not None:
|
|
118
|
+
yield exception_info
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from types import CellType, CodeType, FunctionType
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def create_new_function(base_func: FunctionType, new_name: str | None = None) -> FunctionType:
|
|
5
|
+
base_code = base_func.__code__
|
|
6
|
+
func_name = new_name or base_code.co_name
|
|
7
|
+
|
|
8
|
+
new_code = CodeType(
|
|
9
|
+
base_code.co_argcount,
|
|
10
|
+
base_code.co_posonlyargcount,
|
|
11
|
+
base_code.co_kwonlyargcount,
|
|
12
|
+
base_code.co_nlocals,
|
|
13
|
+
base_code.co_stacksize,
|
|
14
|
+
base_code.co_flags,
|
|
15
|
+
base_code.co_code,
|
|
16
|
+
base_code.co_consts,
|
|
17
|
+
base_code.co_names,
|
|
18
|
+
base_code.co_varnames,
|
|
19
|
+
base_code.co_filename,
|
|
20
|
+
func_name,
|
|
21
|
+
func_name,
|
|
22
|
+
base_code.co_firstlineno,
|
|
23
|
+
base_code.co_linetable,
|
|
24
|
+
base_code.co_exceptiontable,
|
|
25
|
+
base_code.co_freevars,
|
|
26
|
+
base_code.co_cellvars,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
closure = base_func.__closure__
|
|
30
|
+
if closure:
|
|
31
|
+
closure = tuple(CellType(cell.cell_contents) for cell in closure)
|
|
32
|
+
|
|
33
|
+
return FunctionType(new_code, base_func.__globals__, func_name, base_func.__defaults__, closure)
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
from types import ModuleType
|
|
2
|
-
from typing import List
|
|
3
2
|
|
|
4
3
|
|
|
5
|
-
def get_module(module: ModuleType, sub_modules:
|
|
4
|
+
def get_module(module: ModuleType, sub_modules: list[str]) -> ModuleType:
|
|
6
5
|
if not module:
|
|
7
6
|
raise ValueError('Argument `module` is required')
|
|
8
7
|
elif not isinstance(sub_modules, list):
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
from collections.abc import Callable
|
|
2
3
|
from functools import wraps
|
|
3
|
-
from typing import Any
|
|
4
|
+
from typing import Any
|
|
4
5
|
|
|
5
6
|
logger = logging.getLogger(__name__)
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
def safe_call(
|
|
9
|
-
func:
|
|
10
|
+
func: Callable | None = None,
|
|
10
11
|
capture_exception: bool = True,
|
|
11
|
-
default_result:
|
|
12
|
-
exceptions:
|
|
12
|
+
default_result: Any | None = None,
|
|
13
|
+
exceptions: tuple[type[Exception], ...] = (Exception,),
|
|
13
14
|
) -> Callable:
|
|
14
15
|
if func is None:
|
|
15
16
|
return lambda _func: safe_call(
|
|
@@ -32,7 +33,7 @@ def safe_call(
|
|
|
32
33
|
|
|
33
34
|
|
|
34
35
|
def retry_once_after_exception(
|
|
35
|
-
func:
|
|
36
|
+
func: Callable | None = None, capture_exception: bool = True, exceptions: tuple[type[Exception], ...] = (Exception,)
|
|
36
37
|
) -> Callable:
|
|
37
38
|
if func is None:
|
|
38
39
|
return lambda _func: retry_once_after_exception(func=_func, capture_exception=capture_exception, exceptions=exceptions)
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import inspect
|
|
3
|
+
from collections.abc import AsyncIterator, Awaitable, Callable, Hashable
|
|
4
|
+
from contextlib import asynccontextmanager
|
|
5
|
+
from typing import Any, Generic, TypeVar, get_args
|
|
6
|
+
from uuid import uuid4
|
|
7
|
+
|
|
8
|
+
_T = TypeVar('_T')
|
|
9
|
+
_CreateFuncType = Callable[..., _T] | Callable[..., Awaitable[_T]]
|
|
10
|
+
_ScopeKey = Hashable
|
|
11
|
+
_ScopeFuncType = Callable[..., _ScopeKey | None]
|
|
12
|
+
# A `scope_func` that declares this parameter receives a freshly generated key when `scope()` is entered
|
|
13
|
+
# and `None` on every other access; functions without it are called with no arguments.
|
|
14
|
+
SCOPE_TOKEN_PARAMETER = 'token'
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ScopedRegistry(Generic[_T]):
|
|
18
|
+
"""
|
|
19
|
+
One value per scope.
|
|
20
|
+
|
|
21
|
+
`scope_func` decides what the current scope is and returns its key; `None` means "no current scope" and
|
|
22
|
+
makes the registry raise `RuntimeError`. Plain key functions take no arguments:
|
|
23
|
+
|
|
24
|
+
asyncio.current_task # one value per task, a child task does not see the parent's value
|
|
25
|
+
os.getpid # one value per process
|
|
26
|
+
|
|
27
|
+
A `scope_func` that declares a `token` parameter is meant for scopes kept in a `ContextVar`: on entering
|
|
28
|
+
`scope()` (or on `set()`) it receives a freshly generated token that it may store as the key of a new scope,
|
|
29
|
+
and on every other access (`get`, `clear`, a nested `scope()`) it receives `None` and must only read:
|
|
30
|
+
|
|
31
|
+
def execution_scope(token): # one value per execution (HTTP request, actor run),
|
|
32
|
+
key = execution_key.get() # inherited by the tasks spawned inside the block
|
|
33
|
+
if key is None and token is not None:
|
|
34
|
+
execution_key.set(token)
|
|
35
|
+
key = token
|
|
36
|
+
return key
|
|
37
|
+
|
|
38
|
+
A scope started this way is not reset when the block ends, so `scope()` must be entered in a task that does
|
|
39
|
+
not outlive the block (a request handler, an actor run); a key left in a long-living task would be
|
|
40
|
+
inherited by every task spawned from it afterwards.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
__slots__ = ('create_func', 'scope_func', 'registry', 'destructor_method_name', '_lock', '__orig_class__')
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
create_func: _CreateFuncType,
|
|
48
|
+
scope_func: _ScopeFuncType,
|
|
49
|
+
destructor_method_name: str | None = None,
|
|
50
|
+
) -> None:
|
|
51
|
+
self.create_func = create_func
|
|
52
|
+
self.scope_func = scope_func
|
|
53
|
+
self.registry: dict[_ScopeKey, _T] = {}
|
|
54
|
+
self.destructor_method_name = destructor_method_name
|
|
55
|
+
self._lock = asyncio.Lock()
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def generic_type(self) -> type[_T]:
|
|
59
|
+
orig_class = getattr(self, '__orig_class__', None)
|
|
60
|
+
try:
|
|
61
|
+
return get_args(orig_class)[0]
|
|
62
|
+
except IndexError as e:
|
|
63
|
+
raise TypeError('Cannot determine generic type parameter') from e
|
|
64
|
+
|
|
65
|
+
def _get_scope_func_kwargs(self, token: str | None) -> dict[str, Any]:
|
|
66
|
+
kwargs = {}
|
|
67
|
+
if SCOPE_TOKEN_PARAMETER in inspect.signature(self.scope_func).parameters:
|
|
68
|
+
kwargs['token'] = token
|
|
69
|
+
return kwargs
|
|
70
|
+
|
|
71
|
+
def _get_scope_key(self, token: str | None = None) -> _ScopeKey:
|
|
72
|
+
key = self.scope_func(**self._get_scope_func_kwargs(token))
|
|
73
|
+
if key is None:
|
|
74
|
+
raise RuntimeError('There is no current scope')
|
|
75
|
+
if not isinstance(key, Hashable):
|
|
76
|
+
raise RuntimeError(f'scope_func must return a hashable scope key, got {key!r}')
|
|
77
|
+
return key
|
|
78
|
+
|
|
79
|
+
def get(self) -> _T | None:
|
|
80
|
+
return self.registry.get(self._get_scope_key())
|
|
81
|
+
|
|
82
|
+
async def _set(self, **kwargs: Any) -> tuple[_T, bool]:
|
|
83
|
+
"""Returns the value of the current scope and whether this call created it; may start a new scope."""
|
|
84
|
+
key = self._get_scope_key(uuid4().hex)
|
|
85
|
+
if key in self.registry:
|
|
86
|
+
return self.registry[key], False
|
|
87
|
+
|
|
88
|
+
async with self._lock:
|
|
89
|
+
if key in self.registry:
|
|
90
|
+
return self.registry[key], False
|
|
91
|
+
result = self.create_func(**kwargs)
|
|
92
|
+
self.registry[key] = await result if inspect.isawaitable(result) else result
|
|
93
|
+
return self.registry[key], True
|
|
94
|
+
|
|
95
|
+
async def set(self, **kwargs: Any) -> _T:
|
|
96
|
+
"""
|
|
97
|
+
Returns the value of the current scope, creating it with `create_func(**kwargs)` if needed.
|
|
98
|
+
|
|
99
|
+
Prefer `scope()`: it removes what it created on exit. Whoever calls `set()` directly takes over that
|
|
100
|
+
responsibility and must call `clear()` when the value is no longer needed (if it ever is: a per-process
|
|
101
|
+
client may live until the process ends). A `scope_func` with a `token` parameter may start a new scope here.
|
|
102
|
+
"""
|
|
103
|
+
value, _ = await self._set(**kwargs)
|
|
104
|
+
return value
|
|
105
|
+
|
|
106
|
+
async def clear(self) -> None:
|
|
107
|
+
"""Removes the value of the current scope and calls its destructor."""
|
|
108
|
+
instance = self.registry.pop(self._get_scope_key(), None)
|
|
109
|
+
destructor = getattr(instance, self.destructor_method_name or '', None)
|
|
110
|
+
if destructor is not None:
|
|
111
|
+
result = destructor()
|
|
112
|
+
if inspect.isawaitable(result):
|
|
113
|
+
await result
|
|
114
|
+
|
|
115
|
+
@asynccontextmanager
|
|
116
|
+
async def scope(self, **kwargs: Any) -> AsyncIterator[_T]:
|
|
117
|
+
"""
|
|
118
|
+
Enters the current scope: joins the existing value or creates one. Only the block that created the
|
|
119
|
+
value removes it (and calls the destructor) on exit; nested blocks join and leave it alone.
|
|
120
|
+
A `scope_func` with a `token` parameter is offered a fresh token here and may start a new scope with it.
|
|
121
|
+
"""
|
|
122
|
+
value, created = await self._set(**kwargs)
|
|
123
|
+
try:
|
|
124
|
+
yield value
|
|
125
|
+
finally:
|
|
126
|
+
if created:
|
|
127
|
+
await self.clear()
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "ddutils"
|
|
3
|
+
description = "Domain Driven Utils Library"
|
|
4
|
+
version = "0.2.1"
|
|
5
|
+
license = "MIT"
|
|
6
|
+
license-files = ["LICENSE"]
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
requires-python = ">=3.11,<3.15"
|
|
9
|
+
keywords = [
|
|
10
|
+
"python",
|
|
11
|
+
"ddutils",
|
|
12
|
+
]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"Programming Language :: Python :: 3.11",
|
|
16
|
+
"Programming Language :: Python :: 3.12",
|
|
17
|
+
"Programming Language :: Python :: 3.13",
|
|
18
|
+
"Programming Language :: Python :: 3.14",
|
|
19
|
+
"Typing :: Typed",
|
|
20
|
+
]
|
|
21
|
+
dependencies = []
|
|
22
|
+
|
|
23
|
+
[[project.authors]]
|
|
24
|
+
name = "davyddd"
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Repository = "https://github.com/davyddd/ddutils"
|
|
28
|
+
|
|
29
|
+
[dependency-groups]
|
|
30
|
+
dev = [
|
|
31
|
+
"ipdb==0.13.13",
|
|
32
|
+
"ipython==9.16.1",
|
|
33
|
+
]
|
|
34
|
+
test = [
|
|
35
|
+
"pytest==9.0.3",
|
|
36
|
+
"pytest-cov==5.0.0",
|
|
37
|
+
"parameterized==0.9.0",
|
|
38
|
+
]
|
|
39
|
+
linter = [
|
|
40
|
+
"ruff==0.15.10",
|
|
41
|
+
"ty==0.0.15",
|
|
42
|
+
"complexipy==8.0.1",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
[tool.uv.build-backend]
|
|
46
|
+
module-root = ""
|
|
47
|
+
|
|
48
|
+
[build-system]
|
|
49
|
+
requires = ["uv_build>=0.12.0,<0.13.0"]
|
|
50
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "ddutils"
|
|
3
|
+
description = "Domain Driven Utils Library"
|
|
4
|
+
version = "0.2.1"
|
|
5
|
+
authors = [{ name = "davyddd" }]
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
requires-python = ">=3.11,<3.15"
|
|
10
|
+
keywords = ["python", "ddutils"]
|
|
11
|
+
# uv_build does not derive these from requires-python (poetry did); PyPI and the pyversions badge read them
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Programming Language :: Python :: 3",
|
|
14
|
+
"Programming Language :: Python :: 3.11",
|
|
15
|
+
"Programming Language :: Python :: 3.12",
|
|
16
|
+
"Programming Language :: Python :: 3.13",
|
|
17
|
+
"Programming Language :: Python :: 3.14",
|
|
18
|
+
"Typing :: Typed",
|
|
19
|
+
]
|
|
20
|
+
dependencies = []
|
|
21
|
+
|
|
22
|
+
[project.urls]
|
|
23
|
+
Repository = "https://github.com/davyddd/ddutils"
|
|
24
|
+
|
|
25
|
+
[dependency-groups]
|
|
26
|
+
dev = [
|
|
27
|
+
"ipdb==0.13.13",
|
|
28
|
+
"ipython==9.16.1",
|
|
29
|
+
]
|
|
30
|
+
test = [
|
|
31
|
+
"pytest==9.0.3",
|
|
32
|
+
"pytest-cov==5.0.0",
|
|
33
|
+
"parameterized==0.9.0",
|
|
34
|
+
]
|
|
35
|
+
linter = [
|
|
36
|
+
"ruff==0.15.10",
|
|
37
|
+
"ty==0.0.15",
|
|
38
|
+
"complexipy==8.0.1",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
[tool.uv.build-backend]
|
|
42
|
+
module-root = ""
|
|
43
|
+
|
|
44
|
+
[build-system]
|
|
45
|
+
requires = ["uv_build>=0.12.0,<0.13.0"]
|
|
46
|
+
build-backend = "uv_build"
|
|
@@ -1,57 +0,0 @@
|
|
|
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)
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
import inspect
|
|
2
|
-
from collections.abc import Awaitable, Hashable
|
|
3
|
-
from typing import Any, Callable, Dict, Generic, Optional, Type, TypeVar, Union, get_args
|
|
4
|
-
|
|
5
|
-
_T = TypeVar('_T', bound=Any)
|
|
6
|
-
_CreateFuncType = Union[Callable[..., _T], Callable[..., Awaitable[_T]]]
|
|
7
|
-
_ScopeType = Hashable
|
|
8
|
-
_ScopeFuncType = Callable[[], _ScopeType]
|
|
9
|
-
_RegistryType = Dict[_ScopeType, _T]
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class ScopedRegistry(Generic[_T]):
|
|
13
|
-
__slots__ = 'create_func', 'scope_func', 'registry', 'destructor_method_name', '__orig_class__'
|
|
14
|
-
|
|
15
|
-
create_func: _CreateFuncType
|
|
16
|
-
scope_func: _ScopeFuncType
|
|
17
|
-
registry: _RegistryType
|
|
18
|
-
destructor_method_name: Optional[str]
|
|
19
|
-
|
|
20
|
-
def __init__(self, create_func: _CreateFuncType, scope_func: _ScopeFuncType, destructor_method_name: Optional[str] = None):
|
|
21
|
-
self.create_func = create_func
|
|
22
|
-
self.scope_func = scope_func
|
|
23
|
-
self.registry = {}
|
|
24
|
-
self.destructor_method_name = destructor_method_name
|
|
25
|
-
|
|
26
|
-
@property
|
|
27
|
-
def generic_type(self) -> Type[_T]:
|
|
28
|
-
orig_class = getattr(self, '__orig_class__', None)
|
|
29
|
-
try:
|
|
30
|
-
return get_args(orig_class)[0]
|
|
31
|
-
except IndexError as e:
|
|
32
|
-
raise TypeError('Cannot determine generic type parameter') from e
|
|
33
|
-
|
|
34
|
-
def get(self) -> Optional[_T]:
|
|
35
|
-
try:
|
|
36
|
-
key = self.scope_func()
|
|
37
|
-
return self.registry[key]
|
|
38
|
-
except Exception: # noqa: BLE001
|
|
39
|
-
return None
|
|
40
|
-
|
|
41
|
-
def _set(self, **kwargs: Any) -> tuple[_ScopeType, Union[_T, Awaitable[_T]]]:
|
|
42
|
-
key = self.scope_func()
|
|
43
|
-
if key in self.registry:
|
|
44
|
-
return key, self.registry[key]
|
|
45
|
-
return key, self.create_func(**kwargs)
|
|
46
|
-
|
|
47
|
-
def sync_set(self, **kwargs: Any) -> _T:
|
|
48
|
-
key, result = self._set(**kwargs)
|
|
49
|
-
if inspect.isawaitable(result):
|
|
50
|
-
raise TypeError('create_func returned an awaitable; use async_set instead')
|
|
51
|
-
self.registry[key] = result
|
|
52
|
-
return result
|
|
53
|
-
|
|
54
|
-
async def async_set(self, **kwargs: Any) -> _T:
|
|
55
|
-
key, result = self._set(**kwargs)
|
|
56
|
-
self.registry[key] = await result if inspect.isawaitable(result) else result
|
|
57
|
-
return self.registry[key]
|
|
58
|
-
|
|
59
|
-
def _clear(self, *scopes: _ScopeType):
|
|
60
|
-
if not scopes and (scope := self.scope_func()):
|
|
61
|
-
scopes = (scope,)
|
|
62
|
-
|
|
63
|
-
if not (self.registry and scopes):
|
|
64
|
-
return
|
|
65
|
-
|
|
66
|
-
for scope in scopes:
|
|
67
|
-
if scope in self.registry:
|
|
68
|
-
instance = self.registry.pop(scope)
|
|
69
|
-
|
|
70
|
-
if isinstance(self.destructor_method_name, str):
|
|
71
|
-
destructor_method = getattr(instance, self.destructor_method_name, None)
|
|
72
|
-
if destructor_method:
|
|
73
|
-
yield destructor_method()
|
|
74
|
-
|
|
75
|
-
def sync_clear(self, *scopes: _ScopeType) -> None:
|
|
76
|
-
for result in self._clear(*scopes):
|
|
77
|
-
if inspect.isawaitable(result):
|
|
78
|
-
raise TypeError('destructor returned an awaitable; use async clear instead')
|
|
79
|
-
|
|
80
|
-
async def async_clear(self, *scopes: _ScopeType) -> None:
|
|
81
|
-
for result in self._clear(*scopes):
|
|
82
|
-
if inspect.isawaitable(result):
|
|
83
|
-
await result
|
|
84
|
-
|
|
85
|
-
# deprecated: use async_set / async_clear instead
|
|
86
|
-
|
|
87
|
-
async def __call__(self, **kwargs: Any) -> _T:
|
|
88
|
-
return await self.async_set(**kwargs)
|
|
89
|
-
|
|
90
|
-
async def clear(self, *scopes: _ScopeType) -> None:
|
|
91
|
-
await self.async_clear(*scopes)
|
ddutils-0.1.6/pyproject.toml
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
[tool.poetry]
|
|
2
|
-
name = "ddutils"
|
|
3
|
-
description = "Domain Driven Utils Library"
|
|
4
|
-
version = "0.1.6"
|
|
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.9,<3.15"
|
|
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.18.2"
|
|
27
|
-
ruff = "0.14.4"
|
|
28
|
-
|
|
29
|
-
[build-system]
|
|
30
|
-
requires = ["poetry-core>=1.0.0"]
|
|
31
|
-
build-backend = "poetry.core.masonry.api"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|