ddutils 0.2.0__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.2.0/README.md → ddutils-0.2.1/PKG-INFO +57 -20
- ddutils-0.2.0/PKG-INFO → ddutils-0.2.1/README.md +39 -32
- {ddutils-0.2.0 → ddutils-0.2.1}/ddutils/scoped_registry.py +50 -8
- {ddutils-0.2.0 → ddutils-0.2.1}/pyproject.toml +9 -1
- {ddutils-0.2.0 → ddutils-0.2.1}/pyproject.toml.orig +10 -1
- {ddutils-0.2.0 → ddutils-0.2.1}/LICENSE +0 -0
- {ddutils-0.2.0 → ddutils-0.2.1}/ddutils/__init__.py +0 -0
- {ddutils-0.2.0 → ddutils-0.2.1}/ddutils/annotation_helpers.py +0 -0
- {ddutils-0.2.0 → ddutils-0.2.1}/ddutils/class_helpers.py +0 -0
- {ddutils-0.2.0 → ddutils-0.2.1}/ddutils/convertors.py +0 -0
- {ddutils-0.2.0 → ddutils-0.2.1}/ddutils/datetime_helpers.py +0 -0
- {ddutils-0.2.0 → ddutils-0.2.1}/ddutils/function_exceptions_extractor.py +0 -0
- {ddutils-0.2.0 → ddutils-0.2.1}/ddutils/function_helpers.py +0 -0
- {ddutils-0.2.0 → ddutils-0.2.1}/ddutils/module_getter.py +0 -0
- {ddutils-0.2.0 → ddutils-0.2.1}/ddutils/object_getter.py +0 -0
- {ddutils-0.2.0 → ddutils-0.2.1}/ddutils/py.typed +0 -0
- {ddutils-0.2.0 → ddutils-0.2.1}/ddutils/safe_decorators.py +0 -0
- {ddutils-0.2.0 → ddutils-0.2.1}/ddutils/sequence_helpers.py +0 -0
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ddutils
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: Domain Driven Utils Library
|
|
5
|
+
Keywords: python,ddutils
|
|
6
|
+
Author: davyddd
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
14
|
+
Classifier: Typing :: Typed
|
|
15
|
+
Requires-Python: >=3.11, <3.15
|
|
16
|
+
Project-URL: Repository, https://github.com/davyddd/ddutils
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
1
19
|
# DDUtils
|
|
2
20
|
|
|
3
21
|
[](https://pypi.python.org/pypi/ddutils)
|
|
@@ -321,9 +339,8 @@ A registry that keeps one object instance per scope and manages its lifetime.
|
|
|
321
339
|
|
|
322
340
|
### ScopedRegistry
|
|
323
341
|
|
|
324
|
-
Keeps one object per scope.
|
|
325
|
-
|
|
326
|
-
by the tasks spawned inside it).
|
|
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`.
|
|
327
344
|
|
|
328
345
|
`scope()` is the entry point into a scope: it creates the object on enter and removes it on exit, calling
|
|
329
346
|
the optional destructor exactly once. Nested `scope()` blocks join the existing object and leave it alone.
|
|
@@ -331,7 +348,6 @@ Concurrent first accesses with the same key call `create_func` only once.
|
|
|
331
348
|
|
|
332
349
|
```python
|
|
333
350
|
import asyncio
|
|
334
|
-
from contextvars import ContextVar
|
|
335
351
|
|
|
336
352
|
from ddutils.scoped_registry import ScopedRegistry
|
|
337
353
|
|
|
@@ -348,32 +364,53 @@ async def handle_request():
|
|
|
348
364
|
await session.execute('select ...')
|
|
349
365
|
async with session_registry.scope() as same_session: # nested block joins
|
|
350
366
|
...
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
Lower-level methods are available when a context manager does not fit:
|
|
370
|
+
|
|
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
|
|
351
397
|
|
|
352
398
|
|
|
353
399
|
async def create_log_properties(**kwargs): ...
|
|
354
400
|
|
|
355
401
|
|
|
356
|
-
|
|
357
|
-
request_key: ContextVar[object | None] = ContextVar('request_key', default=None)
|
|
358
|
-
log_registry = ScopedRegistry(create_func=create_log_properties, scope_func=request_key.get)
|
|
402
|
+
log_registry = ScopedRegistry(create_func=create_log_properties, scope_func=execution_scope)
|
|
359
403
|
|
|
360
404
|
|
|
361
405
|
async def middleware(headers):
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
async with log_registry.scope(headers=headers) as log_properties:
|
|
365
|
-
await asyncio.gather(work(), work()) # both see the same log_properties via log_registry.get()
|
|
366
|
-
finally:
|
|
367
|
-
request_key.reset(token)
|
|
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()
|
|
368
408
|
```
|
|
369
409
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
- `clear()` removes the object of the current scope and calls the destructor.
|
|
375
|
-
|
|
376
|
-
`scope_func` must return a hashable key; if it returns `None` or an unhashable value, every method raises `RuntimeError`.
|
|
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).
|
|
377
414
|
|
|
378
415
|
## Function Exception Extraction
|
|
379
416
|
|
|
@@ -1,15 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: ddutils
|
|
3
|
-
Version: 0.2.0
|
|
4
|
-
Summary: Domain Driven Utils Library
|
|
5
|
-
Keywords: python,ddutils
|
|
6
|
-
Author: davyddd
|
|
7
|
-
License-Expression: MIT
|
|
8
|
-
License-File: LICENSE
|
|
9
|
-
Requires-Python: >=3.11, <3.15
|
|
10
|
-
Project-URL: Repository, https://github.com/davyddd/ddutils
|
|
11
|
-
Description-Content-Type: text/markdown
|
|
12
|
-
|
|
13
1
|
# DDUtils
|
|
14
2
|
|
|
15
3
|
[](https://pypi.python.org/pypi/ddutils)
|
|
@@ -333,9 +321,8 @@ A registry that keeps one object instance per scope and manages its lifetime.
|
|
|
333
321
|
|
|
334
322
|
### ScopedRegistry
|
|
335
323
|
|
|
336
|
-
Keeps one object per scope.
|
|
337
|
-
|
|
338
|
-
by the tasks spawned inside it).
|
|
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`.
|
|
339
326
|
|
|
340
327
|
`scope()` is the entry point into a scope: it creates the object on enter and removes it on exit, calling
|
|
341
328
|
the optional destructor exactly once. Nested `scope()` blocks join the existing object and leave it alone.
|
|
@@ -343,7 +330,6 @@ Concurrent first accesses with the same key call `create_func` only once.
|
|
|
343
330
|
|
|
344
331
|
```python
|
|
345
332
|
import asyncio
|
|
346
|
-
from contextvars import ContextVar
|
|
347
333
|
|
|
348
334
|
from ddutils.scoped_registry import ScopedRegistry
|
|
349
335
|
|
|
@@ -360,32 +346,53 @@ async def handle_request():
|
|
|
360
346
|
await session.execute('select ...')
|
|
361
347
|
async with session_registry.scope() as same_session: # nested block joins
|
|
362
348
|
...
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
Lower-level methods are available when a context manager does not fit:
|
|
352
|
+
|
|
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.
|
|
358
|
+
|
|
359
|
+
#### Scopes kept in a ContextVar
|
|
360
|
+
|
|
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
|
|
363
379
|
|
|
364
380
|
|
|
365
381
|
async def create_log_properties(**kwargs): ...
|
|
366
382
|
|
|
367
383
|
|
|
368
|
-
|
|
369
|
-
request_key: ContextVar[object | None] = ContextVar('request_key', default=None)
|
|
370
|
-
log_registry = ScopedRegistry(create_func=create_log_properties, scope_func=request_key.get)
|
|
384
|
+
log_registry = ScopedRegistry(create_func=create_log_properties, scope_func=execution_scope)
|
|
371
385
|
|
|
372
386
|
|
|
373
387
|
async def middleware(headers):
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
async with log_registry.scope(headers=headers) as log_properties:
|
|
377
|
-
await asyncio.gather(work(), work()) # both see the same log_properties via log_registry.get()
|
|
378
|
-
finally:
|
|
379
|
-
request_key.reset(token)
|
|
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()
|
|
380
390
|
```
|
|
381
391
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
- `clear()` removes the object of the current scope and calls the destructor.
|
|
387
|
-
|
|
388
|
-
`scope_func` must return a hashable key; if it returns `None` or an unhashable value, every method raises `RuntimeError`.
|
|
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).
|
|
389
396
|
|
|
390
397
|
## Function Exception Extraction
|
|
391
398
|
|
|
@@ -3,15 +3,42 @@ import inspect
|
|
|
3
3
|
from collections.abc import AsyncIterator, Awaitable, Callable, Hashable
|
|
4
4
|
from contextlib import asynccontextmanager
|
|
5
5
|
from typing import Any, Generic, TypeVar, get_args
|
|
6
|
+
from uuid import uuid4
|
|
6
7
|
|
|
7
8
|
_T = TypeVar('_T')
|
|
8
9
|
_CreateFuncType = Callable[..., _T] | Callable[..., Awaitable[_T]]
|
|
9
10
|
_ScopeKey = Hashable
|
|
10
|
-
_ScopeFuncType = Callable[
|
|
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'
|
|
11
15
|
|
|
12
16
|
|
|
13
17
|
class ScopedRegistry(Generic[_T]):
|
|
14
|
-
"""
|
|
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
|
+
"""
|
|
15
42
|
|
|
16
43
|
__slots__ = ('create_func', 'scope_func', 'registry', 'destructor_method_name', '_lock', '__orig_class__')
|
|
17
44
|
|
|
@@ -35,9 +62,17 @@ class ScopedRegistry(Generic[_T]):
|
|
|
35
62
|
except IndexError as e:
|
|
36
63
|
raise TypeError('Cannot determine generic type parameter') from e
|
|
37
64
|
|
|
38
|
-
def
|
|
39
|
-
|
|
40
|
-
if
|
|
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):
|
|
41
76
|
raise RuntimeError(f'scope_func must return a hashable scope key, got {key!r}')
|
|
42
77
|
return key
|
|
43
78
|
|
|
@@ -45,8 +80,8 @@ class ScopedRegistry(Generic[_T]):
|
|
|
45
80
|
return self.registry.get(self._get_scope_key())
|
|
46
81
|
|
|
47
82
|
async def _set(self, **kwargs: Any) -> tuple[_T, bool]:
|
|
48
|
-
"""Returns the value of the current scope and whether this call created it."""
|
|
49
|
-
key = self._get_scope_key()
|
|
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)
|
|
50
85
|
if key in self.registry:
|
|
51
86
|
return self.registry[key], False
|
|
52
87
|
|
|
@@ -58,7 +93,13 @@ class ScopedRegistry(Generic[_T]):
|
|
|
58
93
|
return self.registry[key], True
|
|
59
94
|
|
|
60
95
|
async def set(self, **kwargs: Any) -> _T:
|
|
61
|
-
"""
|
|
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
|
+
"""
|
|
62
103
|
value, _ = await self._set(**kwargs)
|
|
63
104
|
return value
|
|
64
105
|
|
|
@@ -76,6 +117,7 @@ class ScopedRegistry(Generic[_T]):
|
|
|
76
117
|
"""
|
|
77
118
|
Enters the current scope: joins the existing value or creates one. Only the block that created the
|
|
78
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.
|
|
79
121
|
"""
|
|
80
122
|
value, created = await self._set(**kwargs)
|
|
81
123
|
try:
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "ddutils"
|
|
3
3
|
description = "Domain Driven Utils Library"
|
|
4
|
-
version = "0.2.
|
|
4
|
+
version = "0.2.1"
|
|
5
5
|
license = "MIT"
|
|
6
6
|
license-files = ["LICENSE"]
|
|
7
7
|
readme = "README.md"
|
|
@@ -10,6 +10,14 @@ keywords = [
|
|
|
10
10
|
"python",
|
|
11
11
|
"ddutils",
|
|
12
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
|
+
]
|
|
13
21
|
dependencies = []
|
|
14
22
|
|
|
15
23
|
[[project.authors]]
|
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "ddutils"
|
|
3
3
|
description = "Domain Driven Utils Library"
|
|
4
|
-
version = "0.2.
|
|
4
|
+
version = "0.2.1"
|
|
5
5
|
authors = [{ name = "davyddd" }]
|
|
6
6
|
license = "MIT"
|
|
7
7
|
license-files = ["LICENSE"]
|
|
8
8
|
readme = "README.md"
|
|
9
9
|
requires-python = ">=3.11,<3.15"
|
|
10
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
|
+
]
|
|
11
20
|
dependencies = []
|
|
12
21
|
|
|
13
22
|
[project.urls]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|