ddutils 0.3.0__tar.gz → 0.3.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.3.0 → ddutils-0.3.1}/PKG-INFO +53 -1
- {ddutils-0.3.0 → ddutils-0.3.1}/README.md +52 -0
- ddutils-0.3.1/ddutils/connections.py +81 -0
- {ddutils-0.3.0 → ddutils-0.3.1}/pyproject.toml +1 -1
- {ddutils-0.3.0 → ddutils-0.3.1}/pyproject.toml.orig +1 -1
- {ddutils-0.3.0 → ddutils-0.3.1}/LICENSE +0 -0
- {ddutils-0.3.0 → ddutils-0.3.1}/ddutils/__init__.py +0 -0
- {ddutils-0.3.0 → ddutils-0.3.1}/ddutils/annotation_helpers.py +0 -0
- {ddutils-0.3.0 → ddutils-0.3.1}/ddutils/class_helpers.py +0 -0
- {ddutils-0.3.0 → ddutils-0.3.1}/ddutils/convertors.py +0 -0
- {ddutils-0.3.0 → ddutils-0.3.1}/ddutils/datetime_helpers.py +0 -0
- {ddutils-0.3.0 → ddutils-0.3.1}/ddutils/function_exceptions_extractor.py +0 -0
- {ddutils-0.3.0 → ddutils-0.3.1}/ddutils/function_helpers.py +0 -0
- {ddutils-0.3.0 → ddutils-0.3.1}/ddutils/module_getter.py +0 -0
- {ddutils-0.3.0 → ddutils-0.3.1}/ddutils/object_getter.py +0 -0
- {ddutils-0.3.0 → ddutils-0.3.1}/ddutils/py.typed +0 -0
- {ddutils-0.3.0 → ddutils-0.3.1}/ddutils/safe_decorators.py +0 -0
- {ddutils-0.3.0 → ddutils-0.3.1}/ddutils/scoped_registry.py +0 -0
- {ddutils-0.3.0 → ddutils-0.3.1}/ddutils/sequence_helpers.py +0 -0
- {ddutils-0.3.0 → ddutils-0.3.1}/ddutils/value_helpers.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ddutils
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Domain Driven Utils Library
|
|
5
5
|
Keywords: python,ddutils
|
|
6
6
|
Author: davyddd
|
|
@@ -519,6 +519,58 @@ that planted a key would be inherited by every request task, making all requests
|
|
|
519
519
|
same reason the key is not reset when the block ends, so `scope()` must be entered in a task that does not
|
|
520
520
|
outlive the block (a request handler, an actor run).
|
|
521
521
|
|
|
522
|
+
## Connections
|
|
523
|
+
|
|
524
|
+
`ddutils.connections` sits between a `ScopedRegistry` and the code that uses a connection: a query, a cache
|
|
525
|
+
lookup, a Kafka send. A registry holds one connection per scope: a pooled client per process, a session
|
|
526
|
+
per task. The connections module adds two things on top:
|
|
527
|
+
|
|
528
|
+
- `ConnectionManagerFactory(registries, default, connection_manager_class=ConnectionManager)` resolves a
|
|
529
|
+
connection alias to its registry, like Django's `using`. Calling it opens a connection manager on the
|
|
530
|
+
chosen registry; `registry(alias)` returns the registry itself; `close()` clears every registry for the
|
|
531
|
+
current scope, calling the connection's destructor, which is the shutdown hook of process-scoped clients.
|
|
532
|
+
- `ConnectionManager(registry)` is the default connection manager: an async context manager that hands out
|
|
533
|
+
the connection of the registry's current scope and releases nothing on exit. Enough for clients that are
|
|
534
|
+
pools themselves, such as ClickHouse, Redis or a Kafka producer.
|
|
535
|
+
|
|
536
|
+
Databases with transactions pass their own `connection_manager_class`: a unit of work that begins, commits
|
|
537
|
+
or rolls back and closes the session on exit. It depends on the driver, so it lives in the project.
|
|
538
|
+
|
|
539
|
+
```python
|
|
540
|
+
import os
|
|
541
|
+
from enum import Enum
|
|
542
|
+
|
|
543
|
+
from ddutils.connections import ConnectionManagerFactory
|
|
544
|
+
from ddutils.scoped_registry import ScopedRegistry
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
class ClickhouseDB(str, Enum):
|
|
548
|
+
PRIMARY = 'primary'
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
client_registries = {
|
|
552
|
+
db: ScopedRegistry(create_func=make_client_for(db), scope_func=os.getpid, destructor_method_name='close')
|
|
553
|
+
for db in ClickhouseDB
|
|
554
|
+
}
|
|
555
|
+
clickhouse = ConnectionManagerFactory(client_registries, default=ClickhouseDB.PRIMARY)
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
async with clickhouse() as client:
|
|
559
|
+
result = await client.query(sql)
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
# a transactional database: `Atomic` is your unit of work over `registry.scope()`
|
|
563
|
+
atomic = ConnectionManagerFactory(session_registries, default=PostgresDB.PRIMARY, connection_manager_class=Atomic)
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
async with atomic(alias=PostgresDB.REPLICA) as session:
|
|
567
|
+
...
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
# on shutdown: closes the clients of every alias that was used in this process
|
|
571
|
+
await clickhouse.close()
|
|
572
|
+
```
|
|
573
|
+
|
|
522
574
|
## Function Exception Extraction
|
|
523
575
|
|
|
524
576
|
AST-based introspection for extracting exceptions from function source code.
|
|
@@ -501,6 +501,58 @@ that planted a key would be inherited by every request task, making all requests
|
|
|
501
501
|
same reason the key is not reset when the block ends, so `scope()` must be entered in a task that does not
|
|
502
502
|
outlive the block (a request handler, an actor run).
|
|
503
503
|
|
|
504
|
+
## Connections
|
|
505
|
+
|
|
506
|
+
`ddutils.connections` sits between a `ScopedRegistry` and the code that uses a connection: a query, a cache
|
|
507
|
+
lookup, a Kafka send. A registry holds one connection per scope: a pooled client per process, a session
|
|
508
|
+
per task. The connections module adds two things on top:
|
|
509
|
+
|
|
510
|
+
- `ConnectionManagerFactory(registries, default, connection_manager_class=ConnectionManager)` resolves a
|
|
511
|
+
connection alias to its registry, like Django's `using`. Calling it opens a connection manager on the
|
|
512
|
+
chosen registry; `registry(alias)` returns the registry itself; `close()` clears every registry for the
|
|
513
|
+
current scope, calling the connection's destructor, which is the shutdown hook of process-scoped clients.
|
|
514
|
+
- `ConnectionManager(registry)` is the default connection manager: an async context manager that hands out
|
|
515
|
+
the connection of the registry's current scope and releases nothing on exit. Enough for clients that are
|
|
516
|
+
pools themselves, such as ClickHouse, Redis or a Kafka producer.
|
|
517
|
+
|
|
518
|
+
Databases with transactions pass their own `connection_manager_class`: a unit of work that begins, commits
|
|
519
|
+
or rolls back and closes the session on exit. It depends on the driver, so it lives in the project.
|
|
520
|
+
|
|
521
|
+
```python
|
|
522
|
+
import os
|
|
523
|
+
from enum import Enum
|
|
524
|
+
|
|
525
|
+
from ddutils.connections import ConnectionManagerFactory
|
|
526
|
+
from ddutils.scoped_registry import ScopedRegistry
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
class ClickhouseDB(str, Enum):
|
|
530
|
+
PRIMARY = 'primary'
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
client_registries = {
|
|
534
|
+
db: ScopedRegistry(create_func=make_client_for(db), scope_func=os.getpid, destructor_method_name='close')
|
|
535
|
+
for db in ClickhouseDB
|
|
536
|
+
}
|
|
537
|
+
clickhouse = ConnectionManagerFactory(client_registries, default=ClickhouseDB.PRIMARY)
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
async with clickhouse() as client:
|
|
541
|
+
result = await client.query(sql)
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
# a transactional database: `Atomic` is your unit of work over `registry.scope()`
|
|
545
|
+
atomic = ConnectionManagerFactory(session_registries, default=PostgresDB.PRIMARY, connection_manager_class=Atomic)
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
async with atomic(alias=PostgresDB.REPLICA) as session:
|
|
549
|
+
...
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
# on shutdown: closes the clients of every alias that was used in this process
|
|
553
|
+
await clickhouse.close()
|
|
554
|
+
```
|
|
555
|
+
|
|
504
556
|
## Function Exception Extraction
|
|
505
557
|
|
|
506
558
|
AST-based introspection for extracting exceptions from function source code.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from collections.abc import Callable, Hashable, Mapping
|
|
2
|
+
from typing import Any, Generic, TypeVar
|
|
3
|
+
|
|
4
|
+
from ddutils.scoped_registry import ScopedRegistry
|
|
5
|
+
|
|
6
|
+
# The name a connection is registered under and selected by, like Django's `using` alias
|
|
7
|
+
ConnectionAliasT = TypeVar('ConnectionAliasT', bound=Hashable)
|
|
8
|
+
# The object a registry holds for a connection: a pooled client, a session
|
|
9
|
+
ConnectionT = TypeVar('ConnectionT')
|
|
10
|
+
# The context manager the factory opens on the chosen registry
|
|
11
|
+
ConnectionManagerT = TypeVar('ConnectionManagerT')
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ConnectionManager(Generic[ConnectionT]):
|
|
15
|
+
"""
|
|
16
|
+
Default connection manager: hands out the connection of the registry's current scope and releases nothing on exit.
|
|
17
|
+
|
|
18
|
+
The connection is created on first use via `registry.set()` and kept for the life of its scope,
|
|
19
|
+
typically a process, since a client is itself a connection pool (ClickHouse, Redis, a Kafka producer). Databases
|
|
20
|
+
with transactions need their own connection manager on top of the registry, e.g. a unit of work that begins,
|
|
21
|
+
commits and closes a session on exit.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, registry: ScopedRegistry[ConnectionT]) -> None:
|
|
25
|
+
self._registry = registry
|
|
26
|
+
|
|
27
|
+
async def __aenter__(self) -> ConnectionT:
|
|
28
|
+
return await self._registry.set()
|
|
29
|
+
|
|
30
|
+
async def __aexit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: Any) -> None:
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ConnectionManagerFactory(Generic[ConnectionAliasT, ConnectionT, ConnectionManagerT]):
|
|
35
|
+
"""
|
|
36
|
+
Resolves a connection alias to its registry and opens a connection manager on it, like Django's `using`.
|
|
37
|
+
|
|
38
|
+
`connection_manager_class` is the context manager opened on the chosen registry: `ConnectionManager` by
|
|
39
|
+
default, a project class for anything that needs setup and teardown around the connection (transactions):
|
|
40
|
+
|
|
41
|
+
clickhouse = ConnectionManagerFactory(client_registries, default=ClickhouseAlias.PRIMARY)
|
|
42
|
+
atomic = ConnectionManagerFactory(session_registries, default=PostgresAlias.PRIMARY, connection_manager_class=Atomic)
|
|
43
|
+
|
|
44
|
+
async with clickhouse() as client: ...
|
|
45
|
+
async with atomic(alias=PostgresAlias.REPLICA) as session: ...
|
|
46
|
+
await clickhouse.close() # on shutdown
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
registries: Mapping[ConnectionAliasT, ScopedRegistry[ConnectionT]],
|
|
52
|
+
default: ConnectionAliasT,
|
|
53
|
+
connection_manager_class: Callable[[ScopedRegistry[ConnectionT]], ConnectionManagerT] = ConnectionManager, # type: ignore[assignment]
|
|
54
|
+
) -> None:
|
|
55
|
+
if default not in registries:
|
|
56
|
+
raise ValueError(f'Default connection {default!r} is not among the registries')
|
|
57
|
+
self.registries = registries
|
|
58
|
+
self.default = default
|
|
59
|
+
self.connection_manager_class = connection_manager_class
|
|
60
|
+
|
|
61
|
+
def registry(self, alias: Hashable | None = None) -> ScopedRegistry[ConnectionT]:
|
|
62
|
+
"""The registry of `alias`, or of the default connection; `Hashable` because `Adapter.alias` is untyped."""
|
|
63
|
+
alias = self.default if alias is None else alias
|
|
64
|
+
if alias not in self.registries:
|
|
65
|
+
raise KeyError(f'Unknown connection {alias!r}; expected one of {list(self.registries)!r}')
|
|
66
|
+
return self.registries[alias] # type: ignore[index]
|
|
67
|
+
|
|
68
|
+
def __call__(self, alias: Hashable | None = None) -> ConnectionManagerT:
|
|
69
|
+
return self.connection_manager_class(self.registry(alias))
|
|
70
|
+
|
|
71
|
+
async def close(self) -> None:
|
|
72
|
+
"""
|
|
73
|
+
Clears every registry for the current scope, calling the connection's destructor: the shutdown hook of
|
|
74
|
+
a process-scoped client (a pool, a producer with a buffer to flush). A registry without a connection in
|
|
75
|
+
the current scope is left alone, so the hook is safe to call for aliases that were never used.
|
|
76
|
+
"""
|
|
77
|
+
for registry in self.registries.values():
|
|
78
|
+
await registry.clear()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
__all__ = ('ConnectionManager', 'ConnectionManagerFactory')
|
|
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
|
|
File without changes
|
|
File without changes
|