ddutils 0.2.4__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.2.4 → ddutils-0.3.1}/PKG-INFO +102 -20
- {ddutils-0.2.4 → ddutils-0.3.1}/README.md +101 -19
- {ddutils-0.2.4 → ddutils-0.3.1}/ddutils/__init__.py +2 -0
- ddutils-0.3.1/ddutils/connections.py +81 -0
- ddutils-0.3.1/ddutils/convertors.py +86 -0
- ddutils-0.3.1/ddutils/value_helpers.py +37 -0
- {ddutils-0.2.4 → ddutils-0.3.1}/pyproject.toml +1 -1
- {ddutils-0.2.4 → ddutils-0.3.1}/pyproject.toml.orig +1 -1
- ddutils-0.2.4/ddutils/convertors.py +0 -34
- ddutils-0.2.4/ddutils/value_helpers.py +0 -30
- {ddutils-0.2.4 → ddutils-0.3.1}/LICENSE +0 -0
- {ddutils-0.2.4 → ddutils-0.3.1}/ddutils/annotation_helpers.py +0 -0
- {ddutils-0.2.4 → ddutils-0.3.1}/ddutils/class_helpers.py +0 -0
- {ddutils-0.2.4 → ddutils-0.3.1}/ddutils/datetime_helpers.py +0 -0
- {ddutils-0.2.4 → ddutils-0.3.1}/ddutils/function_exceptions_extractor.py +0 -0
- {ddutils-0.2.4 → ddutils-0.3.1}/ddutils/function_helpers.py +0 -0
- {ddutils-0.2.4 → ddutils-0.3.1}/ddutils/module_getter.py +0 -0
- {ddutils-0.2.4 → ddutils-0.3.1}/ddutils/object_getter.py +0 -0
- {ddutils-0.2.4 → ddutils-0.3.1}/ddutils/py.typed +0 -0
- {ddutils-0.2.4 → ddutils-0.3.1}/ddutils/safe_decorators.py +0 -0
- {ddutils-0.2.4 → ddutils-0.3.1}/ddutils/scoped_registry.py +0 -0
- {ddutils-0.2.4 → ddutils-0.3.1}/ddutils/sequence_helpers.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ddutils
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Domain Driven Utils Library
|
|
5
5
|
Keywords: python,ddutils
|
|
6
6
|
Author: davyddd
|
|
@@ -337,28 +337,34 @@ delta = timedelta(seconds=5, milliseconds=500)
|
|
|
337
337
|
convert_timedelta_to_milliseconds(delta) # 5500
|
|
338
338
|
```
|
|
339
339
|
|
|
340
|
-
###
|
|
340
|
+
### convert_to_base_value
|
|
341
341
|
|
|
342
|
-
Casts a value to its
|
|
343
|
-
a `str` subclass becomes a plain `str`,
|
|
344
|
-
|
|
342
|
+
Casts a value to its base value type (see [get_base_value_type](#get_base_value_type)):
|
|
343
|
+
a `str` subclass becomes a plain `str`, a `UUID` subclass a plain `UUID`, a `datetime` subclass a plain `datetime`
|
|
344
|
+
with the same `tzinfo` and `fold`, an `Enum` member becomes its `.value` cast the same way, `None` stays `None`.
|
|
345
|
+
Raises `TypeError` for a value without a base type.
|
|
346
|
+
|
|
347
|
+
Normalize a value once, then dispatch on the result with plain `isinstance` checks: subclasses, `Enum` members
|
|
348
|
+
and third-party types such as a `pendulum.DateTime` all land on the stdlib type.
|
|
345
349
|
|
|
346
350
|
```python
|
|
347
351
|
from enum import Enum
|
|
348
|
-
from
|
|
352
|
+
from uuid import UUID
|
|
353
|
+
from ddutils.convertors import convert_to_base_value
|
|
349
354
|
|
|
350
355
|
|
|
351
356
|
class Kind(str, Enum):
|
|
352
357
|
A = 'a'
|
|
353
358
|
|
|
354
359
|
|
|
355
|
-
class
|
|
360
|
+
class ProfileId(UUID):
|
|
356
361
|
pass
|
|
357
362
|
|
|
358
363
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
364
|
+
convert_to_base_value(None) # None
|
|
365
|
+
convert_to_base_value(Kind.A) # 'a' (not 'Kind.A', which `str(Kind.A)` would give)
|
|
366
|
+
convert_to_base_value(ProfileId('12345678-1234-5678-1234-567812345678')) # UUID('1234...'), exact type UUID
|
|
367
|
+
convert_to_base_value(object()) # TypeError
|
|
362
368
|
```
|
|
363
369
|
|
|
364
370
|
## Datetime Helpers
|
|
@@ -513,6 +519,58 @@ that planted a key would be inherited by every request task, making all requests
|
|
|
513
519
|
same reason the key is not reset when the block ends, so `scope()` must be entered in a task that does not
|
|
514
520
|
outlive the block (a request handler, an actor run).
|
|
515
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
|
+
|
|
516
574
|
## Function Exception Extraction
|
|
517
575
|
|
|
518
576
|
AST-based introspection for extracting exceptions from function source code.
|
|
@@ -526,29 +584,53 @@ Returns an iterator of `ExceptionInfo` objects describing each raised exception.
|
|
|
526
584
|
|
|
527
585
|
Runtime counterparts of the annotation helpers: they inspect values, not type hints.
|
|
528
586
|
|
|
529
|
-
###
|
|
587
|
+
### get_base_value_type
|
|
530
588
|
|
|
531
|
-
Returns the
|
|
532
|
-
|
|
533
|
-
|
|
589
|
+
Returns the base value type a value should be treated as, looking past subclasses. The candidates, in order,
|
|
590
|
+
are `BASE_VALUE_TYPES`: `NoneType`, `bool`, `int`, `float`, `str`, `bytes`, `bytearray`, `datetime`, `date`, `time`,
|
|
591
|
+
`timedelta`, `UUID`, `Decimal` (`bool` is checked before `int`, `datetime` before `date`). An `Enum` member is looked at
|
|
592
|
+
through its `.value`. Raises `TypeError` for any other value, a collection or a mapping included. Useful for APIs that dispatch on the exact type
|
|
593
|
+
(serializers, query builders).
|
|
534
594
|
|
|
535
595
|
```python
|
|
596
|
+
from datetime import datetime
|
|
536
597
|
from enum import Enum
|
|
537
|
-
from
|
|
598
|
+
from uuid import UUID
|
|
599
|
+
from ddutils.value_helpers import get_base_value_type
|
|
538
600
|
|
|
539
601
|
|
|
540
602
|
class Kind(str, Enum):
|
|
541
603
|
A = 'a'
|
|
542
604
|
|
|
543
605
|
|
|
544
|
-
class
|
|
606
|
+
class ProfileId(UUID):
|
|
545
607
|
pass
|
|
546
608
|
|
|
547
609
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
610
|
+
get_base_value_type(None) # <class 'NoneType'>
|
|
611
|
+
get_base_value_type(True) # <class 'bool'>
|
|
612
|
+
get_base_value_type(Kind.A) # <class 'str'>
|
|
613
|
+
get_base_value_type(ProfileId('12345678-1234-5678-1234-567812345678')) # <class 'uuid.UUID'>
|
|
614
|
+
get_base_value_type(datetime.now()) # <class 'datetime.datetime'>
|
|
615
|
+
get_base_value_type(object()) # TypeError
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
### unwrap_enum
|
|
619
|
+
|
|
620
|
+
Returns the `.value` of an `Enum` member and any other value as is. `str(Kind.A)` of a `class Kind(str, Enum)`
|
|
621
|
+
is `'Kind.A'`, so a member has to be unwrapped before it is formatted or cast.
|
|
622
|
+
|
|
623
|
+
```python
|
|
624
|
+
from enum import Enum
|
|
625
|
+
from ddutils.value_helpers import unwrap_enum
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
class Kind(str, Enum):
|
|
629
|
+
A = 'a'
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
unwrap_enum(Kind.A) # 'a'
|
|
633
|
+
unwrap_enum('a') # 'a'
|
|
552
634
|
```
|
|
553
635
|
|
|
554
636
|
### is_complex_sequence_value
|
|
@@ -319,28 +319,34 @@ delta = timedelta(seconds=5, milliseconds=500)
|
|
|
319
319
|
convert_timedelta_to_milliseconds(delta) # 5500
|
|
320
320
|
```
|
|
321
321
|
|
|
322
|
-
###
|
|
322
|
+
### convert_to_base_value
|
|
323
323
|
|
|
324
|
-
Casts a value to its
|
|
325
|
-
a `str` subclass becomes a plain `str`,
|
|
326
|
-
|
|
324
|
+
Casts a value to its base value type (see [get_base_value_type](#get_base_value_type)):
|
|
325
|
+
a `str` subclass becomes a plain `str`, a `UUID` subclass a plain `UUID`, a `datetime` subclass a plain `datetime`
|
|
326
|
+
with the same `tzinfo` and `fold`, an `Enum` member becomes its `.value` cast the same way, `None` stays `None`.
|
|
327
|
+
Raises `TypeError` for a value without a base type.
|
|
328
|
+
|
|
329
|
+
Normalize a value once, then dispatch on the result with plain `isinstance` checks: subclasses, `Enum` members
|
|
330
|
+
and third-party types such as a `pendulum.DateTime` all land on the stdlib type.
|
|
327
331
|
|
|
328
332
|
```python
|
|
329
333
|
from enum import Enum
|
|
330
|
-
from
|
|
334
|
+
from uuid import UUID
|
|
335
|
+
from ddutils.convertors import convert_to_base_value
|
|
331
336
|
|
|
332
337
|
|
|
333
338
|
class Kind(str, Enum):
|
|
334
339
|
A = 'a'
|
|
335
340
|
|
|
336
341
|
|
|
337
|
-
class
|
|
342
|
+
class ProfileId(UUID):
|
|
338
343
|
pass
|
|
339
344
|
|
|
340
345
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
346
|
+
convert_to_base_value(None) # None
|
|
347
|
+
convert_to_base_value(Kind.A) # 'a' (not 'Kind.A', which `str(Kind.A)` would give)
|
|
348
|
+
convert_to_base_value(ProfileId('12345678-1234-5678-1234-567812345678')) # UUID('1234...'), exact type UUID
|
|
349
|
+
convert_to_base_value(object()) # TypeError
|
|
344
350
|
```
|
|
345
351
|
|
|
346
352
|
## Datetime Helpers
|
|
@@ -495,6 +501,58 @@ that planted a key would be inherited by every request task, making all requests
|
|
|
495
501
|
same reason the key is not reset when the block ends, so `scope()` must be entered in a task that does not
|
|
496
502
|
outlive the block (a request handler, an actor run).
|
|
497
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
|
+
|
|
498
556
|
## Function Exception Extraction
|
|
499
557
|
|
|
500
558
|
AST-based introspection for extracting exceptions from function source code.
|
|
@@ -508,29 +566,53 @@ Returns an iterator of `ExceptionInfo` objects describing each raised exception.
|
|
|
508
566
|
|
|
509
567
|
Runtime counterparts of the annotation helpers: they inspect values, not type hints.
|
|
510
568
|
|
|
511
|
-
###
|
|
569
|
+
### get_base_value_type
|
|
512
570
|
|
|
513
|
-
Returns the
|
|
514
|
-
|
|
515
|
-
|
|
571
|
+
Returns the base value type a value should be treated as, looking past subclasses. The candidates, in order,
|
|
572
|
+
are `BASE_VALUE_TYPES`: `NoneType`, `bool`, `int`, `float`, `str`, `bytes`, `bytearray`, `datetime`, `date`, `time`,
|
|
573
|
+
`timedelta`, `UUID`, `Decimal` (`bool` is checked before `int`, `datetime` before `date`). An `Enum` member is looked at
|
|
574
|
+
through its `.value`. Raises `TypeError` for any other value, a collection or a mapping included. Useful for APIs that dispatch on the exact type
|
|
575
|
+
(serializers, query builders).
|
|
516
576
|
|
|
517
577
|
```python
|
|
578
|
+
from datetime import datetime
|
|
518
579
|
from enum import Enum
|
|
519
|
-
from
|
|
580
|
+
from uuid import UUID
|
|
581
|
+
from ddutils.value_helpers import get_base_value_type
|
|
520
582
|
|
|
521
583
|
|
|
522
584
|
class Kind(str, Enum):
|
|
523
585
|
A = 'a'
|
|
524
586
|
|
|
525
587
|
|
|
526
|
-
class
|
|
588
|
+
class ProfileId(UUID):
|
|
527
589
|
pass
|
|
528
590
|
|
|
529
591
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
592
|
+
get_base_value_type(None) # <class 'NoneType'>
|
|
593
|
+
get_base_value_type(True) # <class 'bool'>
|
|
594
|
+
get_base_value_type(Kind.A) # <class 'str'>
|
|
595
|
+
get_base_value_type(ProfileId('12345678-1234-5678-1234-567812345678')) # <class 'uuid.UUID'>
|
|
596
|
+
get_base_value_type(datetime.now()) # <class 'datetime.datetime'>
|
|
597
|
+
get_base_value_type(object()) # TypeError
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
### unwrap_enum
|
|
601
|
+
|
|
602
|
+
Returns the `.value` of an `Enum` member and any other value as is. `str(Kind.A)` of a `class Kind(str, Enum)`
|
|
603
|
+
is `'Kind.A'`, so a member has to be unwrapped before it is formatted or cast.
|
|
604
|
+
|
|
605
|
+
```python
|
|
606
|
+
from enum import Enum
|
|
607
|
+
from ddutils.value_helpers import unwrap_enum
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
class Kind(str, Enum):
|
|
611
|
+
A = 'a'
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
unwrap_enum(Kind.A) # 'a'
|
|
615
|
+
unwrap_enum('a') # 'a'
|
|
534
616
|
```
|
|
535
617
|
|
|
536
618
|
### is_complex_sequence_value
|
|
@@ -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')
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from datetime import date, datetime, time, timedelta
|
|
3
|
+
from decimal import Decimal
|
|
4
|
+
from types import NoneType
|
|
5
|
+
from typing import Any, Protocol
|
|
6
|
+
from uuid import UUID
|
|
7
|
+
|
|
8
|
+
from ddutils.value_helpers import BASE_VALUE_TYPES, get_base_value_type, unwrap_enum
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ClassInstanceWithDict(Protocol):
|
|
12
|
+
__dict__: dict
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def convert_camel_case_to_snake_case(string: str) -> str:
|
|
16
|
+
return ''.join(f'_{char.lower()}' if char.isupper() else char for char in string).lstrip('_')
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def convert_to_repr(obj: ClassInstanceWithDict) -> str:
|
|
20
|
+
class_name = obj.__class__.__name__
|
|
21
|
+
if not hasattr(obj, '__dict__'):
|
|
22
|
+
raise TypeError(f'Instance of {class_name} class does not have a `__dict__` attribute.')
|
|
23
|
+
|
|
24
|
+
attrs = ', '.join(f'{key}={value!r}' for key, value in obj.__dict__.items() if not key.startswith('_'))
|
|
25
|
+
return f'{class_name}({attrs})'
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def convert_timedelta_to_milliseconds(delta: timedelta) -> int:
|
|
29
|
+
return int(delta.total_seconds() * 1000)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _convert_to_datetime(value: datetime) -> datetime:
|
|
33
|
+
return datetime(
|
|
34
|
+
value.year, value.month, value.day, value.hour, value.minute, value.second, value.microsecond,
|
|
35
|
+
value.tzinfo, fold=value.fold,
|
|
36
|
+
) # fmt: skip
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _convert_to_date(value: date) -> date:
|
|
40
|
+
return date(value.year, value.month, value.day)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _convert_to_time(value: time) -> time:
|
|
44
|
+
return time(value.hour, value.minute, value.second, value.microsecond, value.tzinfo, fold=value.fold)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _convert_to_timedelta(value: timedelta) -> timedelta:
|
|
48
|
+
# not `total_seconds()`: it goes through a float and loses microseconds on large values
|
|
49
|
+
return timedelta(days=value.days, seconds=value.seconds, microseconds=value.microseconds)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _convert_to_uuid(value: UUID) -> UUID:
|
|
53
|
+
return UUID(int=value.int)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# A cast for every type of `BASE_VALUE_TYPES`: the constructor where it accepts an instance of a subclass,
|
|
57
|
+
# a function building the value from its fields otherwise
|
|
58
|
+
BASE_VALUE_CONVERTORS: dict[type, Callable[[Any], Any]] = {
|
|
59
|
+
NoneType: lambda _: None,
|
|
60
|
+
bool: bool,
|
|
61
|
+
int: int,
|
|
62
|
+
float: float,
|
|
63
|
+
str: str,
|
|
64
|
+
bytes: bytes,
|
|
65
|
+
bytearray: bytearray,
|
|
66
|
+
datetime: _convert_to_datetime,
|
|
67
|
+
date: _convert_to_date,
|
|
68
|
+
time: _convert_to_time,
|
|
69
|
+
timedelta: _convert_to_timedelta,
|
|
70
|
+
UUID: _convert_to_uuid,
|
|
71
|
+
Decimal: Decimal,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if set(BASE_VALUE_CONVERTORS) != set(BASE_VALUE_TYPES):
|
|
75
|
+
raise RuntimeError('`BASE_VALUE_CONVERTORS` must define a convertor for exactly the types of `BASE_VALUE_TYPES`')
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def convert_to_base_value(value: Any) -> Any:
|
|
79
|
+
"""
|
|
80
|
+
Cast a value to its base value type (see `get_base_value_type`): a `str` subclass becomes a plain `str`,
|
|
81
|
+
a `UUID` subclass a plain `UUID`, a `datetime` subclass a plain `datetime` with the same `tzinfo` and `fold`,
|
|
82
|
+
an `Enum` member becomes its `.value` cast the same way, `None` stays `None`.
|
|
83
|
+
Raises `TypeError` for a value without a base type.
|
|
84
|
+
"""
|
|
85
|
+
value = unwrap_enum(value)
|
|
86
|
+
return BASE_VALUE_CONVERTORS[get_base_value_type(value)](value)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from collections.abc import Sequence
|
|
2
|
+
from datetime import date, datetime, time, timedelta
|
|
3
|
+
from decimal import Decimal
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from types import NoneType
|
|
6
|
+
from typing import Any
|
|
7
|
+
from uuid import UUID
|
|
8
|
+
|
|
9
|
+
from ddutils.annotation_helpers import NON_COMPLEX_SEQUENCE_TYPES
|
|
10
|
+
|
|
11
|
+
# The order matters, a value is matched against the first type it is an instance of:
|
|
12
|
+
# `bool` is a subclass of `int`, `datetime` is a subclass of `date`
|
|
13
|
+
BASE_VALUE_TYPES = (NoneType, bool, int, float, str, bytes, bytearray, datetime, date, time, timedelta, UUID, Decimal)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def unwrap_enum(value: Any) -> Any:
|
|
17
|
+
"""Return the `.value` of an `Enum` member, any other value as is."""
|
|
18
|
+
return value.value if isinstance(value, Enum) else value
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_base_value_type(value: Any) -> type:
|
|
22
|
+
"""
|
|
23
|
+
Return the base value type a value should be treated as, looking past subclasses: one of `BASE_VALUE_TYPES`
|
|
24
|
+
(`NoneType`, `bool`, `int`, `float`, `str`, `bytes`, `bytearray`, `datetime`, `date`, `time`, `timedelta`, `UUID`,
|
|
25
|
+
`Decimal`). An `Enum` member is looked at through its `.value`. Raises `TypeError` for any other value.
|
|
26
|
+
"""
|
|
27
|
+
value = unwrap_enum(value)
|
|
28
|
+
for base_type in BASE_VALUE_TYPES:
|
|
29
|
+
if isinstance(value, base_type):
|
|
30
|
+
return base_type
|
|
31
|
+
|
|
32
|
+
raise TypeError(f'Value of type `{type(value).__name__}` has no base value type')
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def is_complex_sequence_value(value: Any) -> bool:
|
|
36
|
+
"""Return whether a value is a sequence other than `str`, `bytes` or `bytearray`; subclasses are handled."""
|
|
37
|
+
return isinstance(value, Sequence) and not isinstance(value, NON_COMPLEX_SEQUENCE_TYPES)
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
from datetime import timedelta
|
|
2
|
-
from typing import Any, Protocol
|
|
3
|
-
|
|
4
|
-
from ddutils.value_helpers import get_builtin_base_type, unwrap_enum
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class ClassInstanceWithDict(Protocol):
|
|
8
|
-
__dict__: dict
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def convert_camel_case_to_snake_case(string: str) -> str:
|
|
12
|
-
return ''.join(f'_{char.lower()}' if char.isupper() else char for char in string).lstrip('_')
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def convert_to_repr(obj: ClassInstanceWithDict) -> str:
|
|
16
|
-
class_name = obj.__class__.__name__
|
|
17
|
-
if not hasattr(obj, '__dict__'):
|
|
18
|
-
raise TypeError(f'Instance of {class_name} class does not have a `__dict__` attribute.')
|
|
19
|
-
|
|
20
|
-
attrs = ', '.join(f'{key}={value!r}' for key, value in obj.__dict__.items() if not key.startswith('_'))
|
|
21
|
-
return f'{class_name}({attrs})'
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def convert_timedelta_to_milliseconds(delta: timedelta) -> int:
|
|
25
|
-
return int(delta.total_seconds() * 1000)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def convert_to_builtin(value: Any) -> bool | int | float | str | bytes:
|
|
29
|
-
"""
|
|
30
|
-
Cast a value to its builtin base type (see `get_builtin_base_type`): a `str` subclass becomes a plain `str`,
|
|
31
|
-
an `Enum` member becomes its `.value` cast the same way. Raises `TypeError` for a value without a builtin base.
|
|
32
|
-
"""
|
|
33
|
-
value = unwrap_enum(value)
|
|
34
|
-
return get_builtin_base_type(value)(value)
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
from collections.abc import Sequence
|
|
2
|
-
from enum import Enum
|
|
3
|
-
from typing import Any
|
|
4
|
-
|
|
5
|
-
from ddutils.annotation_helpers import NON_COMPLEX_SEQUENCE_TYPES
|
|
6
|
-
|
|
7
|
-
# `bool` goes before `int` because `bool` is a subclass of `int`
|
|
8
|
-
BUILTIN_BASE_TYPES = (bool, int, float, str, bytes)
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def unwrap_enum(value: Any) -> Any:
|
|
12
|
-
return value.value if isinstance(value, Enum) else value
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def get_builtin_base_type(value: Any) -> type:
|
|
16
|
-
"""
|
|
17
|
-
Return the builtin type a value should be treated as, looking past subclasses: `bool`, `int`, `float`, `str`
|
|
18
|
-
or `bytes`. An `Enum` member is looked at through its `.value`. Raises `TypeError` for any other value.
|
|
19
|
-
"""
|
|
20
|
-
value = unwrap_enum(value)
|
|
21
|
-
for base_type in BUILTIN_BASE_TYPES:
|
|
22
|
-
if isinstance(value, base_type):
|
|
23
|
-
return base_type
|
|
24
|
-
|
|
25
|
-
raise TypeError(f'Value of type `{type(value).__name__}` has no builtin base type')
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def is_complex_sequence_value(value: Any) -> bool:
|
|
29
|
-
"""Return whether a value is a sequence other than `str`, `bytes` or `bytearray`; subclasses are handled."""
|
|
30
|
-
return isinstance(value, Sequence) and not isinstance(value, NON_COMPLEX_SEQUENCE_TYPES)
|
|
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
|