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.
Files changed (41) hide show
  1. corekit/connections/sql/connection.py +6 -2
  2. corekit/docker/__init__.py +4 -2
  3. corekit/docker/client.py +48 -0
  4. corekit/docker/constants.py +19 -0
  5. corekit/docker/disk/__init__.py +8 -0
  6. corekit/docker/disk/constants.py +14 -0
  7. corekit/docker/disk/inspector.py +217 -0
  8. corekit/docker/disk/models.py +44 -0
  9. corekit/docker/watchdog.py +10 -8
  10. corekit/exceptions/__init__.py +2 -0
  11. corekit/exceptions/base.py +19 -0
  12. corekit/files/__init__.py +3 -0
  13. corekit/files/disk.py +123 -0
  14. corekit/html/__init__.py +22 -0
  15. corekit/html/client.py +22 -0
  16. corekit/html/models.py +20 -0
  17. corekit/html/parser.py +272 -0
  18. corekit/html/scraper.py +127 -0
  19. corekit/html/settings.py +197 -0
  20. corekit/html/state.py +56 -0
  21. corekit/http/__init__.py +2 -1
  22. corekit/http/api.py +71 -3
  23. corekit/http/client.py +24 -54
  24. corekit/http/response.py +38 -5
  25. corekit/llm/client.py +3 -2
  26. corekit/llm/tools/loop.py +5 -2
  27. corekit/llm/wire.py +11 -5
  28. corekit/process/__init__.py +7 -0
  29. corekit/process/runner.py +47 -0
  30. corekit/schemas/__init__.py +10 -1
  31. corekit/schemas/models/arbitrary.py +49 -5
  32. corekit/schemas/size.py +85 -0
  33. corekit/utils/__init__.py +62 -7
  34. corekit/utils/coercion.py +59 -73
  35. corekit/utils/collections.py +242 -4
  36. corekit/utils/parsers.py +205 -0
  37. {python_corekit-0.4.1.dist-info → python_corekit-0.6.0.dist-info}/METADATA +10 -8
  38. {python_corekit-0.4.1.dist-info → python_corekit-0.6.0.dist-info}/RECORD +41 -23
  39. {python_corekit-0.4.1.dist-info → python_corekit-0.6.0.dist-info}/WHEEL +1 -1
  40. {python_corekit-0.4.1.dist-info → python_corekit-0.6.0.dist-info}/licenses/LICENSE +0 -0
  41. {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
- Four groups: total conversions that never raise (``safe_*``), string assembly
5
- for human-readable output, identifier generation, and nested-dictionary lookup.
6
- Plus timezone-aware time helpers and the placeholder callbacks (``void``,
7
- ``raise_exc``, ``true_validator``, ``false_validator``) that callbacks-by-
8
- configuration need.
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 safe_dict, safe_float, safe_int, safe_list, safe_string, safe_tuple
12
- from corekit.utils.collections import UNSET, MultiMatch, attr_or_key, keygetter, repeated_get, split_list
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, parse explicitly instead.
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.text import list_to_english
18
-
19
- __all__ = ["safe_dict", "safe_float", "safe_int", "safe_list", "safe_string", "safe_tuple"]
20
-
21
-
22
- def safe_string(item: Any, pretty: bool = False) -> str:
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
- if isinstance(item, bytes):
31
- item = item.decode("utf-8", errors="ignore")
32
- if isinstance(item, str):
33
- return item.strip()
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
- if isinstance(item, int):
53
- return item
54
- if isinstance(item, bytes):
55
- item = item.decode("utf-8", errors="ignore")
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
- if isinstance(item, float):
69
- return item
70
- if isinstance(item, bytes):
71
- item = item.decode("utf-8", errors="ignore")
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
- if isinstance(item, list):
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
- if isinstance(item, tuple):
104
- return item
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
- def safe_dict(item: Any) -> dict[str, Any]:
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
- if isinstance(item, dict):
117
- return item
118
- return {}
104
+ return parse_dict(item, default=default if default is not None else {}).value
@@ -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 copy import deepcopy
11
- from typing import Any, Callable
11
+ from __future__ import annotations
12
12
 
13
- __all__ = ["MultiMatch", "attr_or_key", "keygetter", "repeated_get", "split_list"]
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.