python-corekit 0.4.1__py3-none-any.whl → 0.6.0__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.
- corekit/connections/sql/connection.py +6 -2
- corekit/docker/__init__.py +4 -2
- corekit/docker/client.py +48 -0
- corekit/docker/constants.py +19 -0
- corekit/docker/disk/__init__.py +8 -0
- corekit/docker/disk/constants.py +14 -0
- corekit/docker/disk/inspector.py +217 -0
- corekit/docker/disk/models.py +44 -0
- corekit/docker/watchdog.py +10 -8
- corekit/exceptions/__init__.py +2 -0
- corekit/exceptions/base.py +19 -0
- corekit/files/__init__.py +3 -0
- corekit/files/disk.py +123 -0
- corekit/html/__init__.py +22 -0
- corekit/html/client.py +22 -0
- corekit/html/models.py +20 -0
- corekit/html/parser.py +272 -0
- corekit/html/scraper.py +127 -0
- corekit/html/settings.py +197 -0
- corekit/html/state.py +56 -0
- corekit/http/__init__.py +2 -1
- corekit/http/api.py +71 -3
- corekit/http/client.py +24 -54
- corekit/http/response.py +38 -5
- corekit/llm/client.py +3 -2
- corekit/llm/tools/loop.py +5 -2
- corekit/llm/wire.py +11 -5
- corekit/process/__init__.py +7 -0
- corekit/process/runner.py +47 -0
- corekit/schemas/__init__.py +10 -1
- corekit/schemas/models/arbitrary.py +49 -5
- corekit/schemas/size.py +85 -0
- corekit/utils/__init__.py +62 -7
- corekit/utils/coercion.py +59 -73
- corekit/utils/collections.py +242 -4
- corekit/utils/parsers.py +205 -0
- {python_corekit-0.4.1.dist-info → python_corekit-0.6.0.dist-info}/METADATA +10 -8
- {python_corekit-0.4.1.dist-info → python_corekit-0.6.0.dist-info}/RECORD +41 -23
- {python_corekit-0.4.1.dist-info → python_corekit-0.6.0.dist-info}/WHEEL +1 -1
- {python_corekit-0.4.1.dist-info → python_corekit-0.6.0.dist-info}/licenses/LICENSE +0 -0
- {python_corekit-0.4.1.dist-info → python_corekit-0.6.0.dist-info}/top_level.txt +0 -0
corekit/utils/__init__.py
CHANGED
|
@@ -1,15 +1,41 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Small, dependency-free helpers.
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
Five groups: flexible parsers with caller-controlled fallbacks (``parse_*``),
|
|
5
|
+
total conversions that never raise (``safe_*`` wrappers around those parsers),
|
|
6
|
+
string assembly for human-readable output, identifier generation, and
|
|
7
|
+
nested-dictionary lookup. Plus timezone-aware time helpers and the placeholder
|
|
8
|
+
callbacks (``void``, ``raise_exc``, ``true_validator``, ``false_validator``)
|
|
9
|
+
that callbacks-by-configuration need.
|
|
9
10
|
"""
|
|
10
11
|
|
|
11
|
-
from corekit.utils.coercion import
|
|
12
|
-
|
|
12
|
+
from corekit.utils.coercion import (
|
|
13
|
+
safe_dict,
|
|
14
|
+
safe_float,
|
|
15
|
+
safe_int,
|
|
16
|
+
safe_list,
|
|
17
|
+
safe_set,
|
|
18
|
+
safe_string,
|
|
19
|
+
safe_tuple,
|
|
20
|
+
)
|
|
21
|
+
from corekit.utils.collections import (
|
|
22
|
+
UNSET,
|
|
23
|
+
MultiMatch,
|
|
24
|
+
aiterator,
|
|
25
|
+
attr_or_key,
|
|
26
|
+
first,
|
|
27
|
+
first_float,
|
|
28
|
+
first_int,
|
|
29
|
+
first_string,
|
|
30
|
+
getval,
|
|
31
|
+
iterator,
|
|
32
|
+
keygetter,
|
|
33
|
+
kv_string,
|
|
34
|
+
list_map,
|
|
35
|
+
repeated_get,
|
|
36
|
+
set_map,
|
|
37
|
+
split_list,
|
|
38
|
+
)
|
|
13
39
|
from corekit.utils.ids import (
|
|
14
40
|
SHORTCODE_CHARS,
|
|
15
41
|
generate_session_token,
|
|
@@ -17,6 +43,16 @@ from corekit.utils.ids import (
|
|
|
17
43
|
generate_shortcodes,
|
|
18
44
|
generate_uuid,
|
|
19
45
|
)
|
|
46
|
+
from corekit.utils.parsers import (
|
|
47
|
+
ParsedValue,
|
|
48
|
+
parse_dict,
|
|
49
|
+
parse_float,
|
|
50
|
+
parse_int,
|
|
51
|
+
parse_list,
|
|
52
|
+
parse_set,
|
|
53
|
+
parse_string,
|
|
54
|
+
parse_tuple,
|
|
55
|
+
)
|
|
20
56
|
from corekit.utils.payload import Payload, decode_payload, encode_payload
|
|
21
57
|
from corekit.utils.raise_exc import raise_exc
|
|
22
58
|
from corekit.utils.text import join_lines, list_to_english, sanitize_filename, truncate
|
|
@@ -28,28 +64,47 @@ __all__ = [
|
|
|
28
64
|
"SHORTCODE_CHARS",
|
|
29
65
|
"UNSET",
|
|
30
66
|
"MultiMatch",
|
|
67
|
+
"ParsedValue",
|
|
31
68
|
"Payload",
|
|
69
|
+
"aiterator",
|
|
32
70
|
"attr_or_key",
|
|
33
71
|
"decode_payload",
|
|
34
72
|
"encode_payload",
|
|
35
73
|
"false_validator",
|
|
74
|
+
"first",
|
|
75
|
+
"first_string",
|
|
76
|
+
"first_int",
|
|
77
|
+
"first_float",
|
|
36
78
|
"generate_session_token",
|
|
37
79
|
"generate_shortcode",
|
|
38
80
|
"generate_shortcodes",
|
|
39
81
|
"generate_uuid",
|
|
82
|
+
"getval",
|
|
40
83
|
"isoformat_now",
|
|
84
|
+
"iterator",
|
|
41
85
|
"join_lines",
|
|
42
86
|
"keygetter",
|
|
87
|
+
"kv_string",
|
|
88
|
+
"list_map",
|
|
43
89
|
"list_to_english",
|
|
90
|
+
"parse_dict",
|
|
44
91
|
"parse_docker_timestamp",
|
|
92
|
+
"parse_float",
|
|
93
|
+
"parse_int",
|
|
94
|
+
"parse_list",
|
|
95
|
+
"parse_set",
|
|
96
|
+
"parse_string",
|
|
97
|
+
"parse_tuple",
|
|
45
98
|
"raise_exc",
|
|
46
99
|
"repeated_get",
|
|
47
100
|
"safe_dict",
|
|
48
101
|
"safe_float",
|
|
49
102
|
"safe_int",
|
|
50
103
|
"safe_list",
|
|
104
|
+
"safe_set",
|
|
51
105
|
"safe_string",
|
|
52
106
|
"safe_tuple",
|
|
107
|
+
"set_map",
|
|
53
108
|
"sanitize_filename",
|
|
54
109
|
"split_list",
|
|
55
110
|
"time_now",
|
corekit/utils/coercion.py
CHANGED
|
@@ -6,20 +6,39 @@ empty value rather than raising. Use them at boundaries where the input is
|
|
|
6
6
|
genuinely unknown -- an API payload, a config value, a scraped field -- and a
|
|
7
7
|
crash would be worse than a default.
|
|
8
8
|
|
|
9
|
+
These are thin wrappers around ``corekit.utils.parsers``. Extraction logic lives
|
|
10
|
+
there once; here we only fix the fallback to the type's empty value so the
|
|
11
|
+
return type is guaranteed.
|
|
12
|
+
|
|
9
13
|
The tradeoff is real: ``safe_int("banana")`` is ``0``, indistinguishable from a
|
|
10
|
-
genuine zero. Where telling those apart matters,
|
|
14
|
+
genuine zero. Where telling those apart matters, call the parser directly and
|
|
15
|
+
inspect ``ParsedValue.success``, or pass a sentinel ``default``.
|
|
11
16
|
"""
|
|
12
17
|
|
|
13
|
-
import json
|
|
14
|
-
from datetime import date, datetime
|
|
15
18
|
from typing import Any
|
|
16
19
|
|
|
17
|
-
from corekit.utils.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
from corekit.utils.parsers import (
|
|
21
|
+
parse_dict,
|
|
22
|
+
parse_float,
|
|
23
|
+
parse_int,
|
|
24
|
+
parse_list,
|
|
25
|
+
parse_set,
|
|
26
|
+
parse_string,
|
|
27
|
+
parse_tuple,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"safe_dict",
|
|
32
|
+
"safe_float",
|
|
33
|
+
"safe_int",
|
|
34
|
+
"safe_list",
|
|
35
|
+
"safe_set",
|
|
36
|
+
"safe_string",
|
|
37
|
+
"safe_tuple",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def safe_string(item: Any, pretty: bool = False, default: str = "") -> str:
|
|
23
42
|
"""
|
|
24
43
|
Convert anything to a string, or ``""`` if there is no sensible rendering.
|
|
25
44
|
|
|
@@ -27,92 +46,59 @@ def safe_string(item: Any, pretty: bool = False) -> str:
|
|
|
27
46
|
list instead -- ``"a, b, c"``, or ``"key: value, ..."`` for a mapping --
|
|
28
47
|
which reads better in a notification than raw JSON.
|
|
29
48
|
"""
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
# bool is a subclass of int, so both land here and render as "True"/"1".
|
|
35
|
-
if isinstance(item, (int, float)):
|
|
36
|
-
return str(item)
|
|
37
|
-
if isinstance(item, (list, tuple, set, dict)):
|
|
38
|
-
if pretty:
|
|
39
|
-
if isinstance(item, dict):
|
|
40
|
-
return list_to_english([f"{key}: {value}" for key, value in item.items()])
|
|
41
|
-
return list_to_english([safe_string(element) for element in item])
|
|
42
|
-
return json.dumps(item)
|
|
43
|
-
if isinstance(item, (date, datetime)):
|
|
44
|
-
return item.isoformat()
|
|
45
|
-
return ""
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def safe_int(item: Any) -> int:
|
|
49
|
+
return parse_string(item, pretty=pretty, default=default).value
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def safe_int(item: Any, default: int = 0) -> int:
|
|
49
53
|
"""
|
|
50
54
|
Convert anything to an int, or ``0`` if it will not convert.
|
|
51
55
|
"""
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if isinstance(item, (float, str)):
|
|
57
|
-
try:
|
|
58
|
-
return int(item)
|
|
59
|
-
except ValueError:
|
|
60
|
-
return 0
|
|
61
|
-
return 0
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def safe_float(item: Any) -> float:
|
|
56
|
+
return parse_int(item, default=default).value
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def safe_float(item: Any, default: float = 0.0) -> float:
|
|
65
60
|
"""
|
|
66
61
|
Convert anything to a float, or ``0.0`` if it will not convert.
|
|
67
62
|
"""
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if isinstance(item, (int, str)):
|
|
73
|
-
try:
|
|
74
|
-
return float(item)
|
|
75
|
-
except ValueError:
|
|
76
|
-
return 0.0
|
|
77
|
-
return 0.0
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
def safe_list(item: Any) -> list[Any]:
|
|
63
|
+
return parse_float(item, default=default).value
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def safe_list(item: Any, default: list[Any] | None = None) -> list[Any]:
|
|
81
67
|
"""
|
|
82
68
|
Convert a collection to a list, or ``[]`` for anything else.
|
|
83
69
|
|
|
84
70
|
A mapping yields its values. A string is *not* treated as a collection --
|
|
85
71
|
exploding it into characters is almost never what the caller meant.
|
|
86
72
|
"""
|
|
87
|
-
|
|
88
|
-
return item
|
|
89
|
-
if isinstance(item, (tuple, set)):
|
|
90
|
-
return list(item)
|
|
91
|
-
if isinstance(item, dict):
|
|
92
|
-
return list(item.values())
|
|
93
|
-
return []
|
|
73
|
+
return parse_list(item, default=default if default is not None else []).value
|
|
94
74
|
|
|
95
75
|
|
|
96
|
-
def safe_tuple(item: Any) -> tuple[Any, ...]:
|
|
76
|
+
def safe_tuple(item: Any, default: tuple[Any, ...] | None = None) -> tuple[Any, ...]:
|
|
97
77
|
"""
|
|
98
78
|
Convert a collection to a tuple, or ``()`` for anything else.
|
|
99
79
|
|
|
100
80
|
A mapping yields its values. A string is *not* treated as a collection --
|
|
101
81
|
exploding it into characters is almost never what the caller meant.
|
|
102
82
|
"""
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
if isinstance(item, (list, set)):
|
|
106
|
-
return tuple(item)
|
|
107
|
-
if isinstance(item, dict):
|
|
108
|
-
return tuple(item.values())
|
|
109
|
-
return ()
|
|
83
|
+
return parse_tuple(item, default=default if default is not None else ()).value
|
|
84
|
+
|
|
110
85
|
|
|
86
|
+
def safe_set(item: Any, default: set[Any] | None = None) -> set[Any]:
|
|
87
|
+
"""
|
|
88
|
+
Convert a collection to a set, or ``set()`` for anything else.
|
|
111
89
|
|
|
112
|
-
|
|
90
|
+
A mapping yields its values. A string is *not* treated as a collection --
|
|
91
|
+
exploding it into characters is almost never what the caller meant.
|
|
92
|
+
Unhashable elements yield ``set()``.
|
|
93
|
+
"""
|
|
94
|
+
return parse_set(item, default=default if default is not None else set()).value
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def safe_dict(item: Any, default: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
113
98
|
"""
|
|
114
99
|
Return the mapping unchanged, or ``{}`` for anything else.
|
|
100
|
+
|
|
101
|
+
Also accepts objects that expose ``to_dict``, ``model_dump``, or ``dict``,
|
|
102
|
+
and falls back to ``__dict__`` when present.
|
|
115
103
|
"""
|
|
116
|
-
|
|
117
|
-
return item
|
|
118
|
-
return {}
|
|
104
|
+
return parse_dict(item, default=default if default is not None else {}).value
|
corekit/utils/collections.py
CHANGED
|
@@ -4,13 +4,42 @@ Nested lookup and small collection helpers.
|
|
|
4
4
|
``repeated_get`` walks a path into nested dictionaries without a pile of
|
|
5
5
|
``.get()`` calls, and ``keygetter`` lets a step in that path match many keys at
|
|
6
6
|
once. Built for JSON from somewhere you do not control, where any level may be
|
|
7
|
-
missing or the wrong type.
|
|
7
|
+
missing or the wrong type. ``getval`` is the mixed-source counterpart: it walks
|
|
8
|
+
dicts and attribute-bearing objects alike, with ordered fallback keys.
|
|
8
9
|
"""
|
|
9
10
|
|
|
10
|
-
from
|
|
11
|
-
from typing import Any, Callable
|
|
11
|
+
from __future__ import annotations
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
import inspect
|
|
14
|
+
from collections.abc import AsyncIterable, AsyncIterator, Iterable, Mapping
|
|
15
|
+
from copy import deepcopy
|
|
16
|
+
from typing import Any, Awaitable, Callable, Iterator, Literal, Sequence
|
|
17
|
+
|
|
18
|
+
from corekit.utils.coercion import safe_float, safe_int, safe_string
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"MultiMatch",
|
|
22
|
+
"aiterator",
|
|
23
|
+
"attr_or_key",
|
|
24
|
+
"first",
|
|
25
|
+
"first_float",
|
|
26
|
+
"first_int",
|
|
27
|
+
"first_string",
|
|
28
|
+
"getval",
|
|
29
|
+
"iterator",
|
|
30
|
+
"keygetter",
|
|
31
|
+
"kv_string",
|
|
32
|
+
"list_map",
|
|
33
|
+
"repeated_get",
|
|
34
|
+
"set_map",
|
|
35
|
+
"split_list",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
WhereSeq = Callable[[Any], bool]
|
|
39
|
+
WhereMap = Callable[[Any, Any], bool]
|
|
40
|
+
AsyncWhereSeq = Callable[[Any], bool | Awaitable[bool]]
|
|
41
|
+
AsyncWhereMap = Callable[[Any, Any], bool | Awaitable[bool]]
|
|
42
|
+
IteratorMode = Literal["keys", "values", "items"]
|
|
14
43
|
|
|
15
44
|
KEY_SEPARATOR = "."
|
|
16
45
|
|
|
@@ -40,6 +69,215 @@ def attr_or_key(obj: Any, name: str) -> Any:
|
|
|
40
69
|
return getattr(obj, name, None)
|
|
41
70
|
|
|
42
71
|
|
|
72
|
+
def _mapping_item(key: Any, value: Any, mode: IteratorMode) -> Any:
|
|
73
|
+
if mode == "values":
|
|
74
|
+
return value
|
|
75
|
+
if mode == "items":
|
|
76
|
+
return key, value
|
|
77
|
+
return key
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def iterator(
|
|
81
|
+
obj: Iterable[Any] | Mapping[Any, Any] | None = None,
|
|
82
|
+
*,
|
|
83
|
+
mode: IteratorMode = "keys",
|
|
84
|
+
where: WhereSeq | WhereMap | None = None,
|
|
85
|
+
compact: bool = False,
|
|
86
|
+
) -> Iterator[Any]:
|
|
87
|
+
"""
|
|
88
|
+
Iterate an iterable or mapping, optionally filtered.
|
|
89
|
+
|
|
90
|
+
Non-mappings ignore ``mode`` and yield elements. Mappings yield keys,
|
|
91
|
+
values, or items according to ``mode`` (default ``keys``).
|
|
92
|
+
|
|
93
|
+
``compact=True`` skips ``None`` items, or mapping entries whose **value**
|
|
94
|
+
is ``None``. ``where`` is a sync predicate: one argument for iterables,
|
|
95
|
+
``(key, value)`` for mappings — the entry is filtered, then ``mode``
|
|
96
|
+
selects what is yielded.
|
|
97
|
+
|
|
98
|
+
``None`` yields nothing.
|
|
99
|
+
"""
|
|
100
|
+
if obj is None:
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
if isinstance(obj, Mapping):
|
|
104
|
+
for key, value in obj.items():
|
|
105
|
+
if compact and value is None:
|
|
106
|
+
continue
|
|
107
|
+
if where is not None and not bool(where(key, value)): # type: ignore[call-arg, arg-type]
|
|
108
|
+
continue
|
|
109
|
+
yield _mapping_item(key, value, mode)
|
|
110
|
+
return
|
|
111
|
+
|
|
112
|
+
for item in obj:
|
|
113
|
+
if compact and item is None:
|
|
114
|
+
continue
|
|
115
|
+
if where is not None and not bool(where(item)): # type: ignore[call-arg, arg-type]
|
|
116
|
+
continue
|
|
117
|
+
yield item
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
async def aiterator(
|
|
121
|
+
obj: AsyncIterable[Any] | Iterable[Any] | Mapping[Any, Any] | None = None,
|
|
122
|
+
*,
|
|
123
|
+
mode: IteratorMode = "keys",
|
|
124
|
+
where: AsyncWhereSeq | AsyncWhereMap | None = None,
|
|
125
|
+
compact: bool = False,
|
|
126
|
+
) -> AsyncIterator[Any]:
|
|
127
|
+
"""
|
|
128
|
+
Async counterpart to ``iterator``.
|
|
129
|
+
|
|
130
|
+
Accepts async iterables, sync iterables, and mappings. ``where`` may be
|
|
131
|
+
sync or async; mapping predicates still receive ``(key, value)``. Use with
|
|
132
|
+
``async for``.
|
|
133
|
+
"""
|
|
134
|
+
if obj is None:
|
|
135
|
+
return
|
|
136
|
+
|
|
137
|
+
async def _allow_mapping(_key: Any, _value: Any) -> bool:
|
|
138
|
+
if where is None:
|
|
139
|
+
return True
|
|
140
|
+
result = where(_key, _value) # type: ignore[call-arg, arg-type]
|
|
141
|
+
if inspect.isawaitable(result):
|
|
142
|
+
result = await result
|
|
143
|
+
return bool(result)
|
|
144
|
+
|
|
145
|
+
async def _allow_item(_item: Any) -> bool:
|
|
146
|
+
if where is None:
|
|
147
|
+
return True
|
|
148
|
+
result = where(_item) # type: ignore[call-arg, arg-type]
|
|
149
|
+
if inspect.isawaitable(result):
|
|
150
|
+
result = await result
|
|
151
|
+
return bool(result)
|
|
152
|
+
|
|
153
|
+
if isinstance(obj, Mapping):
|
|
154
|
+
for key, value in obj.items():
|
|
155
|
+
if compact and value is None:
|
|
156
|
+
continue
|
|
157
|
+
if not await _allow_mapping(key, value):
|
|
158
|
+
continue
|
|
159
|
+
yield _mapping_item(key, value, mode)
|
|
160
|
+
return
|
|
161
|
+
|
|
162
|
+
if isinstance(obj, AsyncIterable):
|
|
163
|
+
async for item in obj:
|
|
164
|
+
if compact and item is None:
|
|
165
|
+
continue
|
|
166
|
+
if not await _allow_item(item):
|
|
167
|
+
continue
|
|
168
|
+
yield item
|
|
169
|
+
return
|
|
170
|
+
|
|
171
|
+
for item in obj:
|
|
172
|
+
if compact and item is None:
|
|
173
|
+
continue
|
|
174
|
+
if not await _allow_item(item):
|
|
175
|
+
continue
|
|
176
|
+
yield item
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def first(seq: Sequence[Any], default: Any = None) -> Any:
|
|
180
|
+
"""
|
|
181
|
+
Get the first item from a sequence, or ``default`` if empty.
|
|
182
|
+
"""
|
|
183
|
+
if not seq:
|
|
184
|
+
return default
|
|
185
|
+
return next(iter(seq), default)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def first_string(seq: Sequence[Any], default: str = "") -> str:
|
|
189
|
+
"""
|
|
190
|
+
First item of ``seq`` as a string, or ``default`` if empty/unconvertible.
|
|
191
|
+
"""
|
|
192
|
+
return safe_string(first(seq, default), default=default)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def first_int(seq: Sequence[Any], default: int = 0) -> int:
|
|
196
|
+
"""
|
|
197
|
+
First item of ``seq`` as an int, or ``default`` if empty/unconvertible.
|
|
198
|
+
"""
|
|
199
|
+
return safe_int(first(seq, default), default=default)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def first_float(seq: Sequence[Any], default: float = 0.0) -> float:
|
|
203
|
+
"""
|
|
204
|
+
First item of ``seq`` as a float, or ``default`` if empty/unconvertible.
|
|
205
|
+
"""
|
|
206
|
+
return safe_float(first(seq, default), default=default)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def list_map(
|
|
210
|
+
obj: Iterable[Any] | Mapping[Any, Any] | None,
|
|
211
|
+
func: Callable[[Any], Any],
|
|
212
|
+
*,
|
|
213
|
+
mode: IteratorMode = "keys",
|
|
214
|
+
where: WhereSeq | WhereMap | None = None,
|
|
215
|
+
compact: bool = False,
|
|
216
|
+
) -> list[Any]:
|
|
217
|
+
return [func(item) for item in iterator(obj, mode=mode, where=where, compact=compact)]
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def set_map(
|
|
221
|
+
obj: Iterable[Any] | Mapping[Any, Any] | None,
|
|
222
|
+
func: Callable[[Any], Any],
|
|
223
|
+
*,
|
|
224
|
+
mode: IteratorMode = "keys",
|
|
225
|
+
where: WhereSeq | WhereMap | None = None,
|
|
226
|
+
compact: bool = False,
|
|
227
|
+
) -> set[Any]:
|
|
228
|
+
return {func(item) for item in iterator(obj, mode=mode, where=where, compact=compact)}
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def getval(
|
|
232
|
+
obj: Any,
|
|
233
|
+
key: str,
|
|
234
|
+
default: Any = None,
|
|
235
|
+
default_factory: Callable[[], Any] | None = None,
|
|
236
|
+
fallback_keys: list[str] | None = None,
|
|
237
|
+
) -> Any:
|
|
238
|
+
"""
|
|
239
|
+
Read `key` (dot-paths supported) from a dict, pydantic model, dataclass, or any other
|
|
240
|
+
arbitrary object. Returns `default` on miss, or `default_factory`, if provided. Use
|
|
241
|
+
`default_factory` for mutable defaults so callers never share an instance. Before the
|
|
242
|
+
default is applied, we will try each item in `fallback_keys` (in order).
|
|
243
|
+
|
|
244
|
+
A key that is present with value ``None`` is a hit, not a miss: ``None``
|
|
245
|
+
is returned and neither fallbacks nor defaults apply. For dict-only walks
|
|
246
|
+
with predicate steps, use ``repeated_get`` instead.
|
|
247
|
+
"""
|
|
248
|
+
|
|
249
|
+
def _get(_obj: Any, _key: str) -> Any:
|
|
250
|
+
if _obj is UNSET or _obj is None:
|
|
251
|
+
return UNSET
|
|
252
|
+
if isinstance(_obj, dict):
|
|
253
|
+
return _obj.get(_key, UNSET)
|
|
254
|
+
if hasattr(_obj, _key):
|
|
255
|
+
return getattr(_obj, _key, UNSET)
|
|
256
|
+
return UNSET
|
|
257
|
+
|
|
258
|
+
value = obj
|
|
259
|
+
for next_key in key.split("."):
|
|
260
|
+
value = _get(value, next_key)
|
|
261
|
+
|
|
262
|
+
if value is not UNSET:
|
|
263
|
+
return value
|
|
264
|
+
|
|
265
|
+
if fallback_keys:
|
|
266
|
+
next_key, *remaining = fallback_keys
|
|
267
|
+
return getval(obj, next_key, default, default_factory, remaining)
|
|
268
|
+
|
|
269
|
+
if default_factory is not None and callable(default_factory):
|
|
270
|
+
return default_factory()
|
|
271
|
+
return default
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def kv_string(key: str, value: Any, sep: str = ": ", prefix: str = "") -> str:
|
|
275
|
+
"""
|
|
276
|
+
Format a key-value pair into a string.
|
|
277
|
+
"""
|
|
278
|
+
return f"{prefix}{key}{sep}{value}"
|
|
279
|
+
|
|
280
|
+
|
|
43
281
|
class MultiMatch(dict[str, Any]):
|
|
44
282
|
"""
|
|
45
283
|
The result of a path step that matched more than one key.
|