dycw-utilities 0.108.2__py3-none-any.whl → 0.108.4__py3-none-any.whl
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.
- {dycw_utilities-0.108.2.dist-info → dycw_utilities-0.108.4.dist-info}/METADATA +5 -5
- {dycw_utilities-0.108.2.dist-info → dycw_utilities-0.108.4.dist-info}/RECORD +8 -8
- utilities/__init__.py +1 -1
- utilities/dataclasses.py +166 -18
- utilities/functions.py +28 -0
- utilities/python_dotenv.py +16 -159
- {dycw_utilities-0.108.2.dist-info → dycw_utilities-0.108.4.dist-info}/WHEEL +0 -0
- {dycw_utilities-0.108.2.dist-info → dycw_utilities-0.108.4.dist-info}/licenses/LICENSE +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: dycw-utilities
|
3
|
-
Version: 0.108.
|
3
|
+
Version: 0.108.4
|
4
4
|
Author-email: Derek Wan <d.wan@icloud.com>
|
5
5
|
License-File: LICENSE
|
6
6
|
Requires-Python: >=3.12
|
@@ -82,7 +82,7 @@ Requires-Dist: asyncpg<0.31,>=0.30.0; extra == 'zzz-test-hypothesis'
|
|
82
82
|
Requires-Dist: greenlet<3.3,>=3.2.0; extra == 'zzz-test-hypothesis'
|
83
83
|
Requires-Dist: hypothesis<6.132,>=6.131.6; extra == 'zzz-test-hypothesis'
|
84
84
|
Requires-Dist: luigi<3.7,>=3.6.0; extra == 'zzz-test-hypothesis'
|
85
|
-
Requires-Dist: numpy<2.3,>=2.2.
|
85
|
+
Requires-Dist: numpy<2.3,>=2.2.5; extra == 'zzz-test-hypothesis'
|
86
86
|
Requires-Dist: pathvalidate<3.3,>=3.2.3; extra == 'zzz-test-hypothesis'
|
87
87
|
Requires-Dist: redis<5.3,>=5.2.1; extra == 'zzz-test-hypothesis'
|
88
88
|
Requires-Dist: sqlalchemy<2.1,>=2.0.40; extra == 'zzz-test-hypothesis'
|
@@ -112,14 +112,14 @@ Provides-Extra: zzz-test-luigi
|
|
112
112
|
Requires-Dist: luigi<3.7,>=3.6.0; extra == 'zzz-test-luigi'
|
113
113
|
Requires-Dist: whenever<0.8,>=0.7.3; extra == 'zzz-test-luigi'
|
114
114
|
Provides-Extra: zzz-test-math
|
115
|
-
Requires-Dist: numpy<2.3,>=2.2.
|
115
|
+
Requires-Dist: numpy<2.3,>=2.2.5; extra == 'zzz-test-math'
|
116
116
|
Provides-Extra: zzz-test-memory-profiler
|
117
117
|
Requires-Dist: memory-profiler<0.62,>=0.61.0; extra == 'zzz-test-memory-profiler'
|
118
118
|
Provides-Extra: zzz-test-modules
|
119
119
|
Provides-Extra: zzz-test-more-itertools
|
120
120
|
Requires-Dist: more-itertools<10.7,>=10.6.0; extra == 'zzz-test-more-itertools'
|
121
121
|
Provides-Extra: zzz-test-numpy
|
122
|
-
Requires-Dist: numpy<2.3,>=2.2.
|
122
|
+
Requires-Dist: numpy<2.3,>=2.2.5; extra == 'zzz-test-numpy'
|
123
123
|
Provides-Extra: zzz-test-operator
|
124
124
|
Requires-Dist: polars-lts-cpu<1.28,>=1.27.1; extra == 'zzz-test-operator'
|
125
125
|
Requires-Dist: whenever<0.8,>=0.7.3; extra == 'zzz-test-operator'
|
@@ -177,7 +177,7 @@ Requires-Dist: scipy<1.16,>=1.15.2; extra == 'zzz-test-scipy'
|
|
177
177
|
Provides-Extra: zzz-test-sentinel
|
178
178
|
Provides-Extra: zzz-test-shelve
|
179
179
|
Provides-Extra: zzz-test-slack-sdk
|
180
|
-
Requires-Dist: aiohttp<3.12,>=3.11.
|
180
|
+
Requires-Dist: aiohttp<3.12,>=3.11.17; extra == 'zzz-test-slack-sdk'
|
181
181
|
Requires-Dist: slack-sdk<3.36,>=3.35.0; extra == 'zzz-test-slack-sdk'
|
182
182
|
Provides-Extra: zzz-test-socket
|
183
183
|
Provides-Extra: zzz-test-sqlalchemy
|
@@ -1,4 +1,4 @@
|
|
1
|
-
utilities/__init__.py,sha256
|
1
|
+
utilities/__init__.py,sha256=-2lmFv1UZnPQpN8IvJ-ViPH2DA03IC4wyaSgQYQI0zc,60
|
2
2
|
utilities/altair.py,sha256=NSyDsm8QlkAGmsGdxVwCkHnPxt_35yJBa9Lg7bz9Ays,9054
|
3
3
|
utilities/astor.py,sha256=xuDUkjq0-b6fhtwjhbnebzbqQZAjMSHR1IIS5uOodVg,777
|
4
4
|
utilities/asyncio.py,sha256=41oQUurWMvadFK5gFnaG21hMM0Vmfn2WS6OpC0R9mas,14757
|
@@ -11,14 +11,14 @@ utilities/contextlib.py,sha256=OOIIEa5lXKGzFAnauaul40nlQnQko6Na4ryiMJcHkIg,478
|
|
11
11
|
utilities/contextvars.py,sha256=RsSGGrbQqqZ67rOydnM7WWIsM2lIE31UHJLejnHJPWY,505
|
12
12
|
utilities/cryptography.py,sha256=HyOewI20cl3uRXsKivhIaeLVDInQdzgXZGaly7hS5dE,771
|
13
13
|
utilities/cvxpy.py,sha256=Rv1-fD-XYerosCavRF8Pohop2DBkU3AlFaGTfD8AEAA,13776
|
14
|
-
utilities/dataclasses.py,sha256=
|
14
|
+
utilities/dataclasses.py,sha256=SOWaLVMK6p_HTMPhiXtgCk5F1KGiY8wNxQkYRBz1dVg,18683
|
15
15
|
utilities/datetime.py,sha256=GOs-MIEW_A49kzqa1yhIoeNeSqqPVgGO-h2AThtgTDk,37326
|
16
16
|
utilities/enum.py,sha256=HoRwVCWzsnH0vpO9ZEcAAIZLMv0Sn2vJxxA4sYMQgDs,5793
|
17
17
|
utilities/errors.py,sha256=BtSNP0JC3ik536ddPyTerLomCRJV9f6kdMe6POz0QHM,361
|
18
18
|
utilities/eventkit.py,sha256=bG2rjgQqPaaOEW879Pc8vOCX6zRAl1frIhB1Y6fqQXg,13149
|
19
19
|
utilities/fastapi.py,sha256=uwqOGbGwzIbP-lfm-ApG1ZEN3BA_TDsaiuTghhLmxb8,2413
|
20
20
|
utilities/fpdf2.py,sha256=zM3gwOYcAfv7P4qhbyvzPmRY4PPAiAQ-ZnPC6I9SZ1M,1832
|
21
|
-
utilities/functions.py,sha256=
|
21
|
+
utilities/functions.py,sha256=BH4F_X34tqHuk-BzG9lzooYIP1OmVKm6GQw51qqYShM,27461
|
22
22
|
utilities/functools.py,sha256=WrpHt7NLNWSUn9A1Q_ZIWlNaYZOEI4IFKyBG9HO3BC4,1643
|
23
23
|
utilities/getpass.py,sha256=DfN5UgMAtFCqS3dSfFHUfqIMZX2shXvwphOz_6J6f6A,103
|
24
24
|
utilities/git.py,sha256=wpt5dZ5Oi5931pN24_VLZYaQOvmR0OcQuVtgHzFUN1k,2359
|
@@ -53,7 +53,7 @@ utilities/pyinstrument.py,sha256=ROq2txPwbe2ZUuYJ2IDNbfT97lu2ca0v5_C_yn6sSlM,800
|
|
53
53
|
utilities/pyrsistent.py,sha256=TLJfiiKO4cKNU_pCoM3zDqmSM421qpuoaeaBNnyC_Ac,2489
|
54
54
|
utilities/pytest.py,sha256=85QUax4g2VBBAqAHtM9wekcSLB7_9O8AKFTaCshztL8,7989
|
55
55
|
utilities/pytest_regressions.py,sha256=Kp1NS_cyXvBFqyiF_oSzYmSJzIOdAZ0SFcSGmbL_UtI,5001
|
56
|
-
utilities/python_dotenv.py,sha256=
|
56
|
+
utilities/python_dotenv.py,sha256=7N4ZbBxXpPNttOTfg-hpaLFFWA5iJwF7tREBzUnbPOM,3415
|
57
57
|
utilities/random.py,sha256=lYdjgxB7GCfU_fwFVl5U-BIM_HV3q6_urL9byjrwDM8,4157
|
58
58
|
utilities/re.py,sha256=5J4d8VwIPFVrX2Eb8zfoxImDv7IwiN_U7mJ07wR2Wvs,3958
|
59
59
|
utilities/redis.py,sha256=CsDQqc9V6ASLzLQwtbQXZQEndyG9pJiCOhPlPeszt7Y,21203
|
@@ -84,7 +84,7 @@ utilities/warnings.py,sha256=yUgjnmkCRf6QhdyAXzl7u0qQFejhQG3PrjoSwxpbHrs,1819
|
|
84
84
|
utilities/whenever.py,sha256=5x2t47VJmJRWcd_NLFy54NkB3uom-XQYxEbLtEfL1bs,17775
|
85
85
|
utilities/zipfile.py,sha256=24lQc9ATcJxHXBPc_tBDiJk48pWyRrlxO2fIsFxU0A8,699
|
86
86
|
utilities/zoneinfo.py,sha256=-DQz5a0Ikw9jfSZtL0BEQkXOMC9yGn_xiJYNCLMiqEc,1989
|
87
|
-
dycw_utilities-0.108.
|
88
|
-
dycw_utilities-0.108.
|
89
|
-
dycw_utilities-0.108.
|
90
|
-
dycw_utilities-0.108.
|
87
|
+
dycw_utilities-0.108.4.dist-info/METADATA,sha256=rAF0wyx5NT-CYkx5Meatz3jzjaRSMeBHx6mSCdfBcM8,13004
|
88
|
+
dycw_utilities-0.108.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
89
|
+
dycw_utilities-0.108.4.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
|
90
|
+
dycw_utilities-0.108.4.dist-info/RECORD,,
|
utilities/__init__.py
CHANGED
utilities/dataclasses.py
CHANGED
@@ -1,7 +1,17 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
+
from collections.abc import Mapping
|
3
4
|
from dataclasses import MISSING, dataclass, field, fields, replace
|
4
|
-
from typing import
|
5
|
+
from typing import (
|
6
|
+
TYPE_CHECKING,
|
7
|
+
Any,
|
8
|
+
Generic,
|
9
|
+
Literal,
|
10
|
+
TypeVar,
|
11
|
+
assert_never,
|
12
|
+
overload,
|
13
|
+
override,
|
14
|
+
)
|
5
15
|
|
6
16
|
from utilities.errors import ImpossibleCaseError
|
7
17
|
from utilities.functions import (
|
@@ -11,14 +21,16 @@ from utilities.functions import (
|
|
11
21
|
)
|
12
22
|
from utilities.iterables import OneStrEmptyError, OneStrNonUniqueError, one_str
|
13
23
|
from utilities.operator import is_equal
|
24
|
+
from utilities.parse import ParseTextError, parse_text
|
14
25
|
from utilities.reprlib import get_repr
|
15
26
|
from utilities.sentinel import Sentinel, sentinel
|
27
|
+
from utilities.types import TDataclass
|
16
28
|
from utilities.typing import get_type_hints
|
17
29
|
|
18
30
|
if TYPE_CHECKING:
|
19
|
-
from collections.abc import Callable, Iterable, Iterator
|
31
|
+
from collections.abc import Callable, Iterable, Iterator
|
20
32
|
|
21
|
-
from utilities.types import Dataclass, StrMapping
|
33
|
+
from utilities.types import Dataclass, StrMapping
|
22
34
|
|
23
35
|
|
24
36
|
_T = TypeVar("_T")
|
@@ -118,7 +130,7 @@ def dataclass_to_dict(
|
|
118
130
|
recursive: bool = False,
|
119
131
|
) -> StrMapping:
|
120
132
|
"""Convert a dataclass to a dictionary."""
|
121
|
-
out:
|
133
|
+
out: StrMapping = {}
|
122
134
|
for fld in yield_fields(obj, globalns=globalns, localns=localns):
|
123
135
|
if fld.keep(
|
124
136
|
include=include,
|
@@ -233,9 +245,7 @@ class _MappingToDataclassEmptyError(MappingToDataclassError):
|
|
233
245
|
@override
|
234
246
|
def __str__(self) -> str:
|
235
247
|
desc = f"Mapping {get_repr(self.mapping)} does not contain {self.field!r}"
|
236
|
-
if
|
237
|
-
desc += " (modulo case)"
|
238
|
-
return desc
|
248
|
+
return desc if self.case_sensitive else f"{desc} (modulo case)"
|
239
249
|
|
240
250
|
|
241
251
|
@dataclass(kw_only=True, slots=True)
|
@@ -280,6 +290,142 @@ def replace_non_sentinel(
|
|
280
290
|
##
|
281
291
|
|
282
292
|
|
293
|
+
def text_to_dataclass(
|
294
|
+
text_or_mapping: str | Mapping[str, str],
|
295
|
+
cls: type[TDataclass],
|
296
|
+
/,
|
297
|
+
*,
|
298
|
+
globalns: StrMapping | None = None,
|
299
|
+
localns: StrMapping | None = None,
|
300
|
+
case_sensitive: bool = False,
|
301
|
+
) -> TDataclass:
|
302
|
+
"""Construct a dataclass from a string or a mapping or strings."""
|
303
|
+
fields = list(yield_fields(cls, globalns=globalns, localns=localns))
|
304
|
+
match text_or_mapping:
|
305
|
+
case str() as text:
|
306
|
+
text_mapping = _text_to_dataclass_split_text(text, cls)
|
307
|
+
case Mapping() as text_mapping:
|
308
|
+
...
|
309
|
+
case _ as never:
|
310
|
+
assert_never(never)
|
311
|
+
value_mapping = dict(
|
312
|
+
_text_to_dataclass_get_and_parse(
|
313
|
+
fields, key, value, cls, case_sensitive=case_sensitive
|
314
|
+
)
|
315
|
+
for key, value in text_mapping.items()
|
316
|
+
)
|
317
|
+
return mapping_to_dataclass(
|
318
|
+
cls,
|
319
|
+
value_mapping,
|
320
|
+
globalns=globalns,
|
321
|
+
localns=localns,
|
322
|
+
case_sensitive=case_sensitive,
|
323
|
+
)
|
324
|
+
|
325
|
+
|
326
|
+
def _text_to_dataclass_split_text(
|
327
|
+
text: str, cls: type[TDataclass], /
|
328
|
+
) -> Mapping[str, str]:
|
329
|
+
pairs = (t for t in text.split(",") if t != "")
|
330
|
+
return dict(_text_to_dataclass_split_key_value_pair(pair, cls) for pair in pairs)
|
331
|
+
|
332
|
+
|
333
|
+
def _text_to_dataclass_split_key_value_pair(
|
334
|
+
text: str, cls: type[Dataclass], /
|
335
|
+
) -> tuple[str, str]:
|
336
|
+
try:
|
337
|
+
key, value = text.split("=")
|
338
|
+
except ValueError:
|
339
|
+
raise _TextToDataClassSplitKeyValuePairError(cls=cls, text=text) from None
|
340
|
+
return key, value
|
341
|
+
|
342
|
+
|
343
|
+
def _text_to_dataclass_get_and_parse(
|
344
|
+
fields: Iterable[_YieldFieldsClass[Any]],
|
345
|
+
key: str,
|
346
|
+
value: str,
|
347
|
+
cls: type[Dataclass],
|
348
|
+
/,
|
349
|
+
*,
|
350
|
+
case_sensitive: bool = False,
|
351
|
+
) -> tuple[str, Any]:
|
352
|
+
mapping = {f.name: f for f in fields}
|
353
|
+
try:
|
354
|
+
name = one_str(mapping, key, head=True, case_sensitive=case_sensitive)
|
355
|
+
except OneStrEmptyError:
|
356
|
+
raise _TextToDataClassGetFieldEmptyError(
|
357
|
+
cls=cls, key=key, case_sensitive=case_sensitive
|
358
|
+
) from None
|
359
|
+
except OneStrNonUniqueError as error:
|
360
|
+
raise _TextToDataClassGetFieldNonUniqueError(
|
361
|
+
cls=cls,
|
362
|
+
key=key,
|
363
|
+
case_sensitive=case_sensitive,
|
364
|
+
first=error.first,
|
365
|
+
second=error.second,
|
366
|
+
) from None
|
367
|
+
field = mapping[name]
|
368
|
+
try:
|
369
|
+
parsed = parse_text(field.type_, value, case_sensitive=case_sensitive)
|
370
|
+
except ParseTextError:
|
371
|
+
raise _TextToDataClassParseValueError(
|
372
|
+
cls=cls, field=field, text=value
|
373
|
+
) from None
|
374
|
+
return key, parsed
|
375
|
+
|
376
|
+
|
377
|
+
@dataclass(kw_only=True, slots=True)
|
378
|
+
class TextToDataClassError(Exception, Generic[TDataclass]):
|
379
|
+
cls: type[TDataclass]
|
380
|
+
|
381
|
+
|
382
|
+
@dataclass(kw_only=True, slots=True)
|
383
|
+
class _TextToDataClassSplitKeyValuePairError(TextToDataClassError):
|
384
|
+
text: str
|
385
|
+
|
386
|
+
@override
|
387
|
+
def __str__(self) -> str:
|
388
|
+
return f"Unable to construct {get_class_name(self.cls)!r}; failed to split key-value pair {self.text!r}"
|
389
|
+
|
390
|
+
|
391
|
+
@dataclass(kw_only=True, slots=True)
|
392
|
+
class _TextToDataClassGetFieldEmptyError(TextToDataClassError[TDataclass]):
|
393
|
+
key: str
|
394
|
+
case_sensitive: bool = False
|
395
|
+
|
396
|
+
@override
|
397
|
+
def __str__(self) -> str:
|
398
|
+
desc = f"Dataclass {get_class_name(self.cls)!r} does not contain any field starting with {self.key!r}"
|
399
|
+
return desc if self.case_sensitive else f"{desc} (modulo case)"
|
400
|
+
|
401
|
+
|
402
|
+
@dataclass(kw_only=True, slots=True)
|
403
|
+
class _TextToDataClassGetFieldNonUniqueError(TextToDataClassError[TDataclass]):
|
404
|
+
key: str
|
405
|
+
case_sensitive: bool = False
|
406
|
+
first: str
|
407
|
+
second: str
|
408
|
+
|
409
|
+
@override
|
410
|
+
def __str__(self) -> str:
|
411
|
+
head = f"Dataclass {get_class_name(self.cls)!r} must contain exactly one field starting with {self.key!r}"
|
412
|
+
mid = "" if self.case_sensitive else " (modulo case)"
|
413
|
+
return f"{head}{mid}; got {self.first!r}, {self.second!r} and perhaps more"
|
414
|
+
|
415
|
+
|
416
|
+
@dataclass(kw_only=True, slots=True)
|
417
|
+
class _TextToDataClassParseValueError(TextToDataClassError[TDataclass]):
|
418
|
+
field: _YieldFieldsClass[Any]
|
419
|
+
text: str
|
420
|
+
|
421
|
+
@override
|
422
|
+
def __str__(self) -> str:
|
423
|
+
return f"Unable to construct {get_class_name(self.cls)!r}; unable to parse field {self.field.name!r} of type {self.field.type_!r}; got {self.text!r}"
|
424
|
+
|
425
|
+
|
426
|
+
##
|
427
|
+
|
428
|
+
|
283
429
|
@overload
|
284
430
|
def yield_fields(
|
285
431
|
obj: Dataclass,
|
@@ -346,18 +492,18 @@ def yield_fields(
|
|
346
492
|
raise YieldFieldsError(obj=obj)
|
347
493
|
|
348
494
|
|
349
|
-
@dataclass(kw_only=True, slots=True)
|
495
|
+
@dataclass(order=True, unsafe_hash=True, kw_only=True, slots=True)
|
350
496
|
class _YieldFieldsInstance(Generic[_T]):
|
351
497
|
name: str
|
352
|
-
value: _T
|
353
|
-
type_: Any
|
354
|
-
default: _T | Sentinel = sentinel
|
355
|
-
default_factory: Callable[[], _T] | Sentinel = sentinel
|
498
|
+
value: _T = field(hash=False)
|
499
|
+
type_: Any = field(hash=False)
|
500
|
+
default: _T | Sentinel = field(default=sentinel, hash=False)
|
501
|
+
default_factory: Callable[[], _T] | Sentinel = field(default=sentinel, hash=False)
|
356
502
|
repr: bool = True
|
357
503
|
hash_: bool | None = None
|
358
504
|
init: bool = True
|
359
505
|
compare: bool = True
|
360
|
-
metadata: StrMapping = field(default_factory=dict)
|
506
|
+
metadata: StrMapping = field(default_factory=dict, hash=False)
|
361
507
|
kw_only: bool | Sentinel = sentinel
|
362
508
|
|
363
509
|
def equals_default(
|
@@ -407,17 +553,17 @@ class _YieldFieldsInstance(Generic[_T]):
|
|
407
553
|
return (defaults and equal) or not equal
|
408
554
|
|
409
555
|
|
410
|
-
@dataclass(kw_only=True, slots=True)
|
556
|
+
@dataclass(order=True, unsafe_hash=True, kw_only=True, slots=True)
|
411
557
|
class _YieldFieldsClass(Generic[_T]):
|
412
558
|
name: str
|
413
|
-
type_: Any
|
414
|
-
default: _T | Sentinel = sentinel
|
415
|
-
default_factory: Callable[[], _T] | Sentinel = sentinel
|
559
|
+
type_: Any = field(hash=False)
|
560
|
+
default: _T | Sentinel = field(default=sentinel, hash=False)
|
561
|
+
default_factory: Callable[[], _T] | Sentinel = field(default=sentinel, hash=False)
|
416
562
|
repr: bool = True
|
417
563
|
hash_: bool | None = None
|
418
564
|
init: bool = True
|
419
565
|
compare: bool = True
|
420
|
-
metadata: StrMapping = field(default_factory=dict)
|
566
|
+
metadata: StrMapping = field(default_factory=dict, hash=False)
|
421
567
|
kw_only: bool | Sentinel = sentinel
|
422
568
|
|
423
569
|
|
@@ -434,10 +580,12 @@ class YieldFieldsError(Exception):
|
|
434
580
|
|
435
581
|
__all__ = [
|
436
582
|
"MappingToDataclassError",
|
583
|
+
"TextToDataClassError",
|
437
584
|
"YieldFieldsError",
|
438
585
|
"dataclass_repr",
|
439
586
|
"dataclass_to_dict",
|
440
587
|
"mapping_to_dataclass",
|
441
588
|
"replace_non_sentinel",
|
589
|
+
"text_to_dataclass",
|
442
590
|
"yield_fields",
|
443
591
|
]
|
utilities/functions.py
CHANGED
@@ -5,6 +5,7 @@ from collections.abc import Callable, Iterable, Iterator, Sequence
|
|
5
5
|
from dataclasses import asdict, dataclass, is_dataclass
|
6
6
|
from functools import _lru_cache_wrapper, cached_property, partial, reduce, wraps
|
7
7
|
from inspect import getattr_static
|
8
|
+
from pathlib import Path
|
8
9
|
from re import findall
|
9
10
|
from types import (
|
10
11
|
BuiltinFunctionType,
|
@@ -405,6 +406,31 @@ class EnsureNumberError(Exception):
|
|
405
406
|
##
|
406
407
|
|
407
408
|
|
409
|
+
@overload
|
410
|
+
def ensure_path(obj: Any, /, *, nullable: bool) -> Path | None: ...
|
411
|
+
@overload
|
412
|
+
def ensure_path(obj: Any, /, *, nullable: Literal[False] = False) -> Path: ...
|
413
|
+
def ensure_path(obj: Any, /, *, nullable: bool = False) -> Path | None:
|
414
|
+
"""Ensure an object is a Path."""
|
415
|
+
try:
|
416
|
+
return ensure_class(obj, Path, nullable=nullable)
|
417
|
+
except EnsureClassError as error:
|
418
|
+
raise EnsurePathError(obj=error.obj, nullable=nullable) from None
|
419
|
+
|
420
|
+
|
421
|
+
@dataclass(kw_only=True, slots=True)
|
422
|
+
class EnsurePathError(Exception):
|
423
|
+
obj: Any
|
424
|
+
nullable: bool
|
425
|
+
|
426
|
+
@override
|
427
|
+
def __str__(self) -> str:
|
428
|
+
return _make_error_msg(self.obj, "a Path", nullable=self.nullable)
|
429
|
+
|
430
|
+
|
431
|
+
##
|
432
|
+
|
433
|
+
|
408
434
|
def ensure_sized(obj: Any, /) -> Sized:
|
409
435
|
"""Ensure an object is sized."""
|
410
436
|
if is_sized(obj):
|
@@ -985,6 +1011,7 @@ __all__ = [
|
|
985
1011
|
"EnsureMemberError",
|
986
1012
|
"EnsureNotNoneError",
|
987
1013
|
"EnsureNumberError",
|
1014
|
+
"EnsurePathError",
|
988
1015
|
"EnsureSizedError",
|
989
1016
|
"EnsureSizedNotStrError",
|
990
1017
|
"EnsureStrError",
|
@@ -1004,6 +1031,7 @@ __all__ = [
|
|
1004
1031
|
"ensure_member",
|
1005
1032
|
"ensure_not_none",
|
1006
1033
|
"ensure_number",
|
1034
|
+
"ensure_path",
|
1007
1035
|
"ensure_sized",
|
1008
1036
|
"ensure_sized_not_str",
|
1009
1037
|
"ensure_str",
|
utilities/python_dotenv.py
CHANGED
@@ -1,12 +1,8 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
-
import datetime as dt
|
4
3
|
from dataclasses import dataclass
|
5
|
-
from enum import Enum
|
6
4
|
from functools import partial
|
7
5
|
from os import environ
|
8
|
-
from pathlib import Path
|
9
|
-
from re import IGNORECASE, search
|
10
6
|
from typing import TYPE_CHECKING, Any, override
|
11
7
|
|
12
8
|
from dotenv import dotenv_values
|
@@ -16,16 +12,15 @@ from utilities.dataclasses import (
|
|
16
12
|
_YieldFieldsClass,
|
17
13
|
mapping_to_dataclass,
|
18
14
|
)
|
19
|
-
from utilities.enum import EnsureEnumError, ensure_enum
|
20
|
-
from utilities.functions import get_class_name
|
21
15
|
from utilities.git import get_repo_root
|
22
|
-
from utilities.iterables import MergeStrMappingsError, merge_str_mappings
|
16
|
+
from utilities.iterables import MergeStrMappingsError, merge_str_mappings
|
17
|
+
from utilities.parse import ParseTextError, parse_text
|
23
18
|
from utilities.pathlib import PWD
|
24
19
|
from utilities.reprlib import get_repr
|
25
|
-
from utilities.typing import get_args, is_literal_type, is_optional_type
|
26
20
|
|
27
21
|
if TYPE_CHECKING:
|
28
22
|
from collections.abc import Mapping
|
23
|
+
from pathlib import Path
|
29
24
|
|
30
25
|
from utilities.types import PathLike, StrMapping, TDataclass
|
31
26
|
|
@@ -65,72 +60,14 @@ def load_settings(
|
|
65
60
|
|
66
61
|
|
67
62
|
def _load_settings_post(
|
68
|
-
field: _YieldFieldsClass[Any],
|
63
|
+
field: _YieldFieldsClass[Any], text: str, /, *, path: Path, values: StrMapping
|
69
64
|
) -> Any:
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
if value == "1" or search("true", value, flags=IGNORECASE):
|
77
|
-
return True
|
78
|
-
raise _LoadSettingsInvalidBoolError(
|
79
|
-
path=path, values=values, field=field.name, value=value
|
80
|
-
)
|
81
|
-
if type_ is float:
|
82
|
-
try:
|
83
|
-
return float(value)
|
84
|
-
except ValueError:
|
85
|
-
raise _LoadSettingsInvalidFloatError(
|
86
|
-
path=path, values=values, field=field.name, value=value
|
87
|
-
) from None
|
88
|
-
if type_ is int:
|
89
|
-
try:
|
90
|
-
return int(value)
|
91
|
-
except ValueError:
|
92
|
-
raise _LoadSettingsInvalidIntError(
|
93
|
-
path=path, values=values, field=field.name, value=value
|
94
|
-
) from None
|
95
|
-
if type_ is Path:
|
96
|
-
return Path(value).expanduser()
|
97
|
-
if type_ is dt.date:
|
98
|
-
from utilities.whenever import ParseDateError, parse_date
|
99
|
-
|
100
|
-
try:
|
101
|
-
return parse_date(value)
|
102
|
-
except ParseDateError:
|
103
|
-
raise _LoadSettingsInvalidDateError(
|
104
|
-
path=path, values=values, field=field.name, value=value
|
105
|
-
) from None
|
106
|
-
if type_ is dt.timedelta:
|
107
|
-
from utilities.whenever import ParseTimedeltaError, parse_timedelta
|
108
|
-
|
109
|
-
try:
|
110
|
-
return parse_timedelta(value)
|
111
|
-
except ParseTimedeltaError:
|
112
|
-
raise _LoadSettingsInvalidTimeDeltaError(
|
113
|
-
path=path, values=values, field=field.name, value=value
|
114
|
-
) from None
|
115
|
-
if isinstance(type_, type) and issubclass(type_, Enum):
|
116
|
-
try:
|
117
|
-
return ensure_enum(value, type_)
|
118
|
-
except EnsureEnumError:
|
119
|
-
raise _LoadSettingsInvalidEnumError(
|
120
|
-
path=path, values=values, field=field.name, type_=type_, value=value
|
121
|
-
) from None
|
122
|
-
if is_literal_type(type_):
|
123
|
-
return one_str(get_args(type_), value)
|
124
|
-
if is_optional_type(type_) and (one(get_args(type_)) is int):
|
125
|
-
if (value is None) or (value == "") or search("none", value, flags=IGNORECASE):
|
126
|
-
return None
|
127
|
-
try:
|
128
|
-
return int(value)
|
129
|
-
except ValueError:
|
130
|
-
raise _LoadSettingsInvalidNullableIntError(
|
131
|
-
path=path, values=values, field=field.name, value=value
|
132
|
-
) from None
|
133
|
-
raise _LoadSettingsTypeError(path=path, field=field.name, type=type_)
|
65
|
+
try:
|
66
|
+
return parse_text(field.type_, text)
|
67
|
+
except ParseTextError:
|
68
|
+
raise _LoadSettingsParseTextError(
|
69
|
+
path=path, values=values, field=field, text=text
|
70
|
+
) from None
|
134
71
|
|
135
72
|
|
136
73
|
@dataclass(kw_only=True, slots=True)
|
@@ -138,13 +75,6 @@ class LoadSettingsError(Exception):
|
|
138
75
|
path: Path
|
139
76
|
|
140
77
|
|
141
|
-
@dataclass(kw_only=True, slots=True)
|
142
|
-
class _LoadSettingsFileNotFoundError(LoadSettingsError):
|
143
|
-
@override
|
144
|
-
def __str__(self) -> str:
|
145
|
-
return f"Path {str(self.path)!r} must exist"
|
146
|
-
|
147
|
-
|
148
78
|
@dataclass(kw_only=True, slots=True)
|
149
79
|
class _LoadSettingsDuplicateKeysError(LoadSettingsError):
|
150
80
|
values: StrMapping
|
@@ -166,94 +96,21 @@ class _LoadSettingsEmptyError(LoadSettingsError):
|
|
166
96
|
|
167
97
|
|
168
98
|
@dataclass(kw_only=True, slots=True)
|
169
|
-
class
|
170
|
-
values: StrMapping
|
171
|
-
field: str
|
172
|
-
value: str
|
173
|
-
|
174
|
-
@override
|
175
|
-
def __str__(self) -> str:
|
176
|
-
return f"Field {self.field!r} must contain a valid boolean; got {self.value!r}"
|
177
|
-
|
178
|
-
|
179
|
-
@dataclass(kw_only=True, slots=True)
|
180
|
-
class _LoadSettingsInvalidDateError(LoadSettingsError):
|
181
|
-
values: StrMapping
|
182
|
-
field: str
|
183
|
-
value: str
|
184
|
-
|
185
|
-
@override
|
186
|
-
def __str__(self) -> str:
|
187
|
-
return f"Field {self.field!r} must contain a valid date; got {self.value!r}"
|
188
|
-
|
189
|
-
|
190
|
-
@dataclass(kw_only=True, slots=True)
|
191
|
-
class _LoadSettingsInvalidEnumError(LoadSettingsError):
|
192
|
-
values: StrMapping
|
193
|
-
field: str
|
194
|
-
type_: type[Enum]
|
195
|
-
value: str
|
196
|
-
|
197
|
-
@override
|
198
|
-
def __str__(self) -> str:
|
199
|
-
type_ = get_class_name(self.type_)
|
200
|
-
return f"Field {self.field!r} must contain a valid member of {type_!r}; got {self.value!r}"
|
201
|
-
|
202
|
-
|
203
|
-
@dataclass(kw_only=True, slots=True)
|
204
|
-
class _LoadSettingsInvalidFloatError(LoadSettingsError):
|
205
|
-
values: StrMapping
|
206
|
-
field: str
|
207
|
-
value: str
|
208
|
-
|
209
|
-
@override
|
210
|
-
def __str__(self) -> str:
|
211
|
-
return f"Field {self.field!r} must contain a valid float; got {self.value!r}"
|
212
|
-
|
213
|
-
|
214
|
-
@dataclass(kw_only=True, slots=True)
|
215
|
-
class _LoadSettingsInvalidIntError(LoadSettingsError):
|
216
|
-
values: StrMapping
|
217
|
-
field: str
|
218
|
-
value: str
|
219
|
-
|
220
|
-
@override
|
221
|
-
def __str__(self) -> str:
|
222
|
-
return f"Field {self.field!r} must contain a valid integer; got {self.value!r}"
|
223
|
-
|
224
|
-
|
225
|
-
@dataclass(kw_only=True, slots=True)
|
226
|
-
class _LoadSettingsInvalidNullableIntError(LoadSettingsError):
|
227
|
-
values: StrMapping
|
228
|
-
field: str
|
229
|
-
value: str
|
230
|
-
|
99
|
+
class _LoadSettingsFileNotFoundError(LoadSettingsError):
|
231
100
|
@override
|
232
101
|
def __str__(self) -> str:
|
233
|
-
return f"
|
102
|
+
return f"Path {str(self.path)!r} must exist"
|
234
103
|
|
235
104
|
|
236
105
|
@dataclass(kw_only=True, slots=True)
|
237
|
-
class
|
106
|
+
class _LoadSettingsParseTextError(LoadSettingsError):
|
238
107
|
values: StrMapping
|
239
|
-
field:
|
240
|
-
|
241
|
-
|
242
|
-
@override
|
243
|
-
def __str__(self) -> str:
|
244
|
-
return (
|
245
|
-
f"Field {self.field!r} must contain a valid timedelta; got {self.value!r}"
|
246
|
-
)
|
247
|
-
|
248
|
-
|
249
|
-
@dataclass(kw_only=True, slots=True)
|
250
|
-
class _LoadSettingsTypeError(LoadSettingsError):
|
251
|
-
field: str
|
252
|
-
type: Any
|
108
|
+
field: _YieldFieldsClass[Any]
|
109
|
+
text: str
|
253
110
|
|
254
111
|
@override
|
255
112
|
def __str__(self) -> str:
|
256
|
-
return f"
|
113
|
+
return f"Unable to parse field {self.field.name!r} of type {self.field.type_!r}; got {self.text!r}"
|
257
114
|
|
258
115
|
|
259
116
|
__all__ = ["LoadSettingsError", "load_settings"]
|
File without changes
|
File without changes
|