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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ddutils
3
- Version: 0.2.4
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
- ### convert_to_builtin
340
+ ### convert_to_base_value
341
341
 
342
- Casts a value to its builtin base type (see [get_builtin_base_type](#get_builtin_base_type)):
343
- a `str` subclass becomes a plain `str`, an `Enum` member becomes its `.value` cast the same way.
344
- Raises `TypeError` for a value without a builtin base.
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 ddutils.convertors import convert_to_builtin
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 UserId(int):
360
+ class ProfileId(UUID):
356
361
  pass
357
362
 
358
363
 
359
- convert_to_builtin(Kind.A) # 'a' (not 'Kind.A', which `str(Kind.A)` would give)
360
- convert_to_builtin(UserId(2)) # 2
361
- convert_to_builtin(object()) # TypeError
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
- ### get_builtin_base_type
587
+ ### get_base_value_type
530
588
 
531
- Returns the builtin type a value should be treated as, looking past subclasses: `bool`, `int`, `float`, `str` or `bytes`
532
- (`bool` is checked before `int`). An `Enum` member is looked at through its `.value`.
533
- Raises `TypeError` for any other value. Useful for APIs that dispatch on the exact type (serializers, query builders).
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 ddutils.value_helpers import get_builtin_base_type
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 UserId(int):
606
+ class ProfileId(UUID):
545
607
  pass
546
608
 
547
609
 
548
- get_builtin_base_type(UserId(2)) # <class 'int'>
549
- get_builtin_base_type(True) # <class 'bool'>
550
- get_builtin_base_type(Kind.A) # <class 'str'>
551
- get_builtin_base_type(object()) # TypeError
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
- ### convert_to_builtin
322
+ ### convert_to_base_value
323
323
 
324
- Casts a value to its builtin base type (see [get_builtin_base_type](#get_builtin_base_type)):
325
- a `str` subclass becomes a plain `str`, an `Enum` member becomes its `.value` cast the same way.
326
- Raises `TypeError` for a value without a builtin base.
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 ddutils.convertors import convert_to_builtin
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 UserId(int):
342
+ class ProfileId(UUID):
338
343
  pass
339
344
 
340
345
 
341
- convert_to_builtin(Kind.A) # 'a' (not 'Kind.A', which `str(Kind.A)` would give)
342
- convert_to_builtin(UserId(2)) # 2
343
- convert_to_builtin(object()) # TypeError
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
- ### get_builtin_base_type
569
+ ### get_base_value_type
512
570
 
513
- Returns the builtin type a value should be treated as, looking past subclasses: `bool`, `int`, `float`, `str` or `bytes`
514
- (`bool` is checked before `int`). An `Enum` member is looked at through its `.value`.
515
- Raises `TypeError` for any other value. Useful for APIs that dispatch on the exact type (serializers, query builders).
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 ddutils.value_helpers import get_builtin_base_type
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 UserId(int):
588
+ class ProfileId(UUID):
527
589
  pass
528
590
 
529
591
 
530
- get_builtin_base_type(UserId(2)) # <class 'int'>
531
- get_builtin_base_type(True) # <class 'bool'>
532
- get_builtin_base_type(Kind.A) # <class 'str'>
533
- get_builtin_base_type(object()) # TypeError
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
@@ -10,6 +10,7 @@ from . import (
10
10
  safe_decorators,
11
11
  scoped_registry,
12
12
  sequence_helpers,
13
+ value_helpers,
13
14
  )
14
15
 
15
16
  __all__ = (
@@ -24,4 +25,5 @@ __all__ = (
24
25
  'safe_decorators',
25
26
  'scoped_registry',
26
27
  'sequence_helpers',
28
+ 'value_helpers',
27
29
  )
@@ -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,7 +1,7 @@
1
1
  [project]
2
2
  name = "ddutils"
3
3
  description = "Domain Driven Utils Library"
4
- version = "0.2.4"
4
+ version = "0.3.1"
5
5
  license = "MIT"
6
6
  license-files = ["LICENSE"]
7
7
  readme = "README.md"
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "ddutils"
3
3
  description = "Domain Driven Utils Library"
4
- version = "0.2.4"
4
+ version = "0.3.1"
5
5
  authors = [{ name = "davyddd" }]
6
6
  license = "MIT"
7
7
  license-files = ["LICENSE"]
@@ -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