lionherd-core 1.0.0a3__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.
- lionherd_core/__init__.py +84 -0
- lionherd_core/base/__init__.py +30 -0
- lionherd_core/base/_utils.py +295 -0
- lionherd_core/base/broadcaster.py +128 -0
- lionherd_core/base/element.py +300 -0
- lionherd_core/base/event.py +322 -0
- lionherd_core/base/eventbus.py +112 -0
- lionherd_core/base/flow.py +236 -0
- lionherd_core/base/graph.py +616 -0
- lionherd_core/base/node.py +212 -0
- lionherd_core/base/pile.py +811 -0
- lionherd_core/base/progression.py +261 -0
- lionherd_core/errors.py +104 -0
- lionherd_core/libs/__init__.py +2 -0
- lionherd_core/libs/concurrency/__init__.py +60 -0
- lionherd_core/libs/concurrency/_cancel.py +85 -0
- lionherd_core/libs/concurrency/_errors.py +80 -0
- lionherd_core/libs/concurrency/_patterns.py +238 -0
- lionherd_core/libs/concurrency/_primitives.py +253 -0
- lionherd_core/libs/concurrency/_priority_queue.py +135 -0
- lionherd_core/libs/concurrency/_resource_tracker.py +66 -0
- lionherd_core/libs/concurrency/_task.py +58 -0
- lionherd_core/libs/concurrency/_utils.py +61 -0
- lionherd_core/libs/schema_handlers/__init__.py +35 -0
- lionherd_core/libs/schema_handlers/_function_call_parser.py +122 -0
- lionherd_core/libs/schema_handlers/_minimal_yaml.py +88 -0
- lionherd_core/libs/schema_handlers/_schema_to_model.py +251 -0
- lionherd_core/libs/schema_handlers/_typescript.py +153 -0
- lionherd_core/libs/string_handlers/__init__.py +15 -0
- lionherd_core/libs/string_handlers/_extract_json.py +65 -0
- lionherd_core/libs/string_handlers/_fuzzy_json.py +103 -0
- lionherd_core/libs/string_handlers/_string_similarity.py +347 -0
- lionherd_core/libs/string_handlers/_to_num.py +63 -0
- lionherd_core/ln/__init__.py +45 -0
- lionherd_core/ln/_async_call.py +314 -0
- lionherd_core/ln/_fuzzy_match.py +166 -0
- lionherd_core/ln/_fuzzy_validate.py +151 -0
- lionherd_core/ln/_hash.py +141 -0
- lionherd_core/ln/_json_dump.py +347 -0
- lionherd_core/ln/_list_call.py +110 -0
- lionherd_core/ln/_to_dict.py +373 -0
- lionherd_core/ln/_to_list.py +190 -0
- lionherd_core/ln/_utils.py +156 -0
- lionherd_core/lndl/__init__.py +62 -0
- lionherd_core/lndl/errors.py +30 -0
- lionherd_core/lndl/fuzzy.py +321 -0
- lionherd_core/lndl/parser.py +427 -0
- lionherd_core/lndl/prompt.py +137 -0
- lionherd_core/lndl/resolver.py +323 -0
- lionherd_core/lndl/types.py +287 -0
- lionherd_core/protocols.py +181 -0
- lionherd_core/py.typed +0 -0
- lionherd_core/types/__init__.py +46 -0
- lionherd_core/types/_sentinel.py +131 -0
- lionherd_core/types/base.py +341 -0
- lionherd_core/types/operable.py +133 -0
- lionherd_core/types/spec.py +313 -0
- lionherd_core/types/spec_adapters/__init__.py +10 -0
- lionherd_core/types/spec_adapters/_protocol.py +125 -0
- lionherd_core/types/spec_adapters/pydantic_field.py +177 -0
- lionherd_core-1.0.0a3.dist-info/METADATA +502 -0
- lionherd_core-1.0.0a3.dist-info/RECORD +64 -0
- lionherd_core-1.0.0a3.dist-info/WHEEL +4 -0
- lionherd_core-1.0.0a3.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Copyright (c) 2025, HaiyangLi <quantocean.li at gmail dot com>
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
from collections.abc import Callable, Iterable
|
|
5
|
+
from typing import Any, ParamSpec, TypeVar
|
|
6
|
+
|
|
7
|
+
from ._to_list import to_list
|
|
8
|
+
|
|
9
|
+
R = TypeVar("R")
|
|
10
|
+
T = TypeVar("T")
|
|
11
|
+
P = ParamSpec("P")
|
|
12
|
+
|
|
13
|
+
__all__ = ("lcall",)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def lcall(
|
|
17
|
+
input_: Iterable[T] | T,
|
|
18
|
+
func: Callable[[T], R] | Iterable[Callable[[T], R]],
|
|
19
|
+
/,
|
|
20
|
+
*args: Any,
|
|
21
|
+
input_flatten: bool = False,
|
|
22
|
+
input_dropna: bool = False,
|
|
23
|
+
input_unique: bool = False,
|
|
24
|
+
input_use_values: bool = False,
|
|
25
|
+
input_flatten_tuple_set: bool = False,
|
|
26
|
+
output_flatten: bool = False,
|
|
27
|
+
output_dropna: bool = False,
|
|
28
|
+
output_unique: bool = False,
|
|
29
|
+
output_flatten_tuple_set: bool = False,
|
|
30
|
+
**kwargs: Any,
|
|
31
|
+
) -> list[R]:
|
|
32
|
+
"""Apply function to each element synchronously with optional input/output processing.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
input_: Items to process
|
|
36
|
+
func: Callable to apply to each element
|
|
37
|
+
*args: Positional arguments passed to func
|
|
38
|
+
input_flatten: Flatten input structures
|
|
39
|
+
input_dropna: Remove None/undefined from input
|
|
40
|
+
input_unique: Remove duplicate inputs
|
|
41
|
+
input_use_values: Extract values from enums/mappings
|
|
42
|
+
input_flatten_tuple_set: Include tuples/sets in input flattening
|
|
43
|
+
output_flatten: Flatten output structures
|
|
44
|
+
output_dropna: Remove None/undefined from output
|
|
45
|
+
output_unique: Remove duplicate outputs
|
|
46
|
+
output_flatten_tuple_set: Include tuples/sets in output flattening
|
|
47
|
+
**kwargs: Keyword arguments passed to func
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
List of results
|
|
51
|
+
|
|
52
|
+
Raises:
|
|
53
|
+
ValueError: If func is not callable or output_unique without flatten/dropna
|
|
54
|
+
TypeError: If func or input processing fails
|
|
55
|
+
"""
|
|
56
|
+
# Validate and extract callable function
|
|
57
|
+
if not callable(func):
|
|
58
|
+
try:
|
|
59
|
+
func_list = list(func)
|
|
60
|
+
if len(func_list) != 1 or not callable(func_list[0]):
|
|
61
|
+
raise ValueError("func must contain exactly one callable function.")
|
|
62
|
+
func = func_list[0]
|
|
63
|
+
except TypeError as e:
|
|
64
|
+
raise ValueError("func must be callable or iterable with one callable.") from e
|
|
65
|
+
|
|
66
|
+
# Validate output processing options
|
|
67
|
+
if output_unique and not (output_flatten or output_dropna):
|
|
68
|
+
raise ValueError("output_unique requires output_flatten or output_dropna.")
|
|
69
|
+
|
|
70
|
+
# Process input based on sanitization flag
|
|
71
|
+
if input_flatten or input_dropna:
|
|
72
|
+
input_ = to_list(
|
|
73
|
+
input_,
|
|
74
|
+
flatten=input_flatten,
|
|
75
|
+
dropna=input_dropna,
|
|
76
|
+
unique=input_unique,
|
|
77
|
+
flatten_tuple_set=input_flatten_tuple_set,
|
|
78
|
+
use_values=input_use_values,
|
|
79
|
+
)
|
|
80
|
+
else:
|
|
81
|
+
if not isinstance(input_, list):
|
|
82
|
+
try:
|
|
83
|
+
input_ = list(input_)
|
|
84
|
+
except TypeError:
|
|
85
|
+
input_ = [input_]
|
|
86
|
+
|
|
87
|
+
# Process elements and collect results
|
|
88
|
+
out = []
|
|
89
|
+
append = out.append
|
|
90
|
+
|
|
91
|
+
for item in input_:
|
|
92
|
+
try:
|
|
93
|
+
result = func(item, *args, **kwargs)
|
|
94
|
+
append(result)
|
|
95
|
+
except InterruptedError:
|
|
96
|
+
return out
|
|
97
|
+
except Exception:
|
|
98
|
+
raise
|
|
99
|
+
|
|
100
|
+
# Apply output processing if requested
|
|
101
|
+
if output_flatten or output_dropna:
|
|
102
|
+
out = to_list(
|
|
103
|
+
out,
|
|
104
|
+
flatten=output_flatten,
|
|
105
|
+
dropna=output_dropna,
|
|
106
|
+
unique=output_unique,
|
|
107
|
+
flatten_tuple_set=output_flatten_tuple_set,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
return out
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
# Copyright (c) 2025, HaiyangLi <quantocean.li at gmail dot com>
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import contextlib
|
|
7
|
+
import dataclasses
|
|
8
|
+
from collections.abc import Callable, Iterable, Mapping, Sequence
|
|
9
|
+
from enum import Enum as _Enum
|
|
10
|
+
from typing import Any, cast
|
|
11
|
+
|
|
12
|
+
import orjson
|
|
13
|
+
|
|
14
|
+
from ..libs.string_handlers._fuzzy_json import fuzzy_json
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _is_na(obj: Any) -> bool:
|
|
18
|
+
"""None / Pydantic undefined sentinels -> treat as NA."""
|
|
19
|
+
if obj is None:
|
|
20
|
+
return True
|
|
21
|
+
# Avoid importing pydantic types; match by typename to stay lightweight
|
|
22
|
+
tname = type(obj).__name__
|
|
23
|
+
return tname in {
|
|
24
|
+
"Undefined",
|
|
25
|
+
"UndefinedType",
|
|
26
|
+
"PydanticUndefined",
|
|
27
|
+
"PydanticUndefinedType",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _enum_class_to_dict(enum_cls: type[_Enum], use_enum_values: bool) -> dict[str, Any]:
|
|
32
|
+
members = dict(enum_cls.__members__) # cheap, stable
|
|
33
|
+
if use_enum_values:
|
|
34
|
+
return {k: v.value for k, v in members.items()}
|
|
35
|
+
return {k: v for k, v in members.items()}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _parse_str(
|
|
39
|
+
s: str,
|
|
40
|
+
*,
|
|
41
|
+
fuzzy_parse: bool,
|
|
42
|
+
parser: Callable[[str], Any] | None,
|
|
43
|
+
**kwargs: Any,
|
|
44
|
+
) -> Any:
|
|
45
|
+
"""Parse str -> Python object (JSON only).
|
|
46
|
+
|
|
47
|
+
Keep imports local to avoid cold start overhead.
|
|
48
|
+
"""
|
|
49
|
+
if parser is not None:
|
|
50
|
+
return parser(s, **kwargs)
|
|
51
|
+
|
|
52
|
+
# JSON path
|
|
53
|
+
if fuzzy_parse:
|
|
54
|
+
# If the caller supplied a fuzzy parser in scope, use it; otherwise fallback.
|
|
55
|
+
# We intentionally do not import anything heavy here.
|
|
56
|
+
with contextlib.suppress(NameError):
|
|
57
|
+
return fuzzy_json(s, **kwargs) # type: ignore[name-defined]
|
|
58
|
+
# orjson.loads() doesn't accept kwargs like parse_float, object_hook, etc.
|
|
59
|
+
return orjson.loads(s)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _object_to_mapping_like(
|
|
63
|
+
obj: Any,
|
|
64
|
+
*,
|
|
65
|
+
prioritize_model_dump: bool = False,
|
|
66
|
+
**kwargs: Any,
|
|
67
|
+
) -> Mapping | dict | Any:
|
|
68
|
+
"""
|
|
69
|
+
Convert 'custom' objects to mapping-like, if possible.
|
|
70
|
+
Order:
|
|
71
|
+
1) Pydantic v2 'model_dump' (duck-typed)
|
|
72
|
+
2) Common methods: to_dict, dict, to_json/json (parsed if string)
|
|
73
|
+
3) Dataclass
|
|
74
|
+
4) __dict__
|
|
75
|
+
5) dict(obj)
|
|
76
|
+
"""
|
|
77
|
+
# 1) Pydantic v2
|
|
78
|
+
if prioritize_model_dump and hasattr(obj, "model_dump"):
|
|
79
|
+
return obj.model_dump(**kwargs)
|
|
80
|
+
|
|
81
|
+
# 2) Common methods
|
|
82
|
+
for name in ("to_dict", "dict", "to_json", "json", "model_dump"):
|
|
83
|
+
if hasattr(obj, name):
|
|
84
|
+
res = getattr(obj, name)(**kwargs)
|
|
85
|
+
return orjson.loads(res) if isinstance(res, str) else res
|
|
86
|
+
|
|
87
|
+
# 3) Dataclass
|
|
88
|
+
if dataclasses.is_dataclass(obj) and not isinstance(obj, type):
|
|
89
|
+
# asdict is already recursive; keep it (fast enough & simple)
|
|
90
|
+
return dataclasses.asdict(obj) # type: ignore[arg-type]
|
|
91
|
+
|
|
92
|
+
# 4) __dict__
|
|
93
|
+
if hasattr(obj, "__dict__"):
|
|
94
|
+
return obj.__dict__
|
|
95
|
+
|
|
96
|
+
# 5) Try dict() fallback
|
|
97
|
+
return dict(obj) # may raise -> handled by caller
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _enumerate_iterable(it: Iterable) -> dict[int, Any]:
|
|
101
|
+
return {i: v for i, v in enumerate(it)}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# ---------------------------------------
|
|
105
|
+
# Recursive pre-processing (single pass)
|
|
106
|
+
# ---------------------------------------
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _preprocess_recursive(
|
|
110
|
+
obj: Any,
|
|
111
|
+
*,
|
|
112
|
+
depth: int,
|
|
113
|
+
max_depth: int,
|
|
114
|
+
recursive_custom_types: bool,
|
|
115
|
+
str_parse_opts: dict[str, Any],
|
|
116
|
+
prioritize_model_dump: bool,
|
|
117
|
+
) -> Any:
|
|
118
|
+
"""
|
|
119
|
+
Recursively process nested structures:
|
|
120
|
+
- Parse strings (JSON only, or custom parser)
|
|
121
|
+
- Recurse into dict/list/tuple/set/etc.
|
|
122
|
+
- If recursive_custom_types=True, convert custom objects to mapping-like then continue
|
|
123
|
+
Containers retain their original types (dict stays dict, list stays list, set stays set, etc.)
|
|
124
|
+
"""
|
|
125
|
+
if depth >= max_depth:
|
|
126
|
+
return obj
|
|
127
|
+
|
|
128
|
+
# Fast paths by exact type where possible
|
|
129
|
+
t = type(obj)
|
|
130
|
+
|
|
131
|
+
# Strings: try to parse; on failure, keep as-is
|
|
132
|
+
if t is str:
|
|
133
|
+
with contextlib.suppress(Exception):
|
|
134
|
+
return _preprocess_recursive(
|
|
135
|
+
_parse_str(obj, **str_parse_opts),
|
|
136
|
+
depth=depth + 1,
|
|
137
|
+
max_depth=max_depth,
|
|
138
|
+
recursive_custom_types=recursive_custom_types,
|
|
139
|
+
str_parse_opts=str_parse_opts,
|
|
140
|
+
prioritize_model_dump=prioritize_model_dump,
|
|
141
|
+
)
|
|
142
|
+
return obj
|
|
143
|
+
|
|
144
|
+
# Dict-like
|
|
145
|
+
if isinstance(obj, Mapping):
|
|
146
|
+
# Recurse only into values (keys kept as-is)
|
|
147
|
+
return {
|
|
148
|
+
k: _preprocess_recursive(
|
|
149
|
+
v,
|
|
150
|
+
depth=depth + 1,
|
|
151
|
+
max_depth=max_depth,
|
|
152
|
+
recursive_custom_types=recursive_custom_types,
|
|
153
|
+
str_parse_opts=str_parse_opts,
|
|
154
|
+
prioritize_model_dump=prioritize_model_dump,
|
|
155
|
+
)
|
|
156
|
+
for k, v in obj.items()
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
# Sequence/Set-like (but not str)
|
|
160
|
+
if isinstance(obj, list | tuple | set | frozenset):
|
|
161
|
+
items = [
|
|
162
|
+
_preprocess_recursive(
|
|
163
|
+
v,
|
|
164
|
+
depth=depth + 1,
|
|
165
|
+
max_depth=max_depth,
|
|
166
|
+
recursive_custom_types=recursive_custom_types,
|
|
167
|
+
str_parse_opts=str_parse_opts,
|
|
168
|
+
prioritize_model_dump=prioritize_model_dump,
|
|
169
|
+
)
|
|
170
|
+
for v in obj
|
|
171
|
+
]
|
|
172
|
+
if t is list:
|
|
173
|
+
return items
|
|
174
|
+
if t is tuple:
|
|
175
|
+
return tuple(items)
|
|
176
|
+
if t is set:
|
|
177
|
+
return set(items)
|
|
178
|
+
if t is frozenset:
|
|
179
|
+
return frozenset(items)
|
|
180
|
+
|
|
181
|
+
# Enum *class* (rare in values, but preserve your original attempt)
|
|
182
|
+
if isinstance(obj, type) and issubclass(obj, _Enum):
|
|
183
|
+
with contextlib.suppress(Exception):
|
|
184
|
+
enum_map = _enum_class_to_dict(
|
|
185
|
+
obj,
|
|
186
|
+
use_enum_values=str_parse_opts.get("use_enum_values", True),
|
|
187
|
+
)
|
|
188
|
+
return _preprocess_recursive(
|
|
189
|
+
enum_map,
|
|
190
|
+
depth=depth + 1,
|
|
191
|
+
max_depth=max_depth,
|
|
192
|
+
recursive_custom_types=recursive_custom_types,
|
|
193
|
+
str_parse_opts=str_parse_opts,
|
|
194
|
+
prioritize_model_dump=prioritize_model_dump,
|
|
195
|
+
)
|
|
196
|
+
return obj
|
|
197
|
+
|
|
198
|
+
# Custom objects
|
|
199
|
+
if recursive_custom_types:
|
|
200
|
+
with contextlib.suppress(Exception):
|
|
201
|
+
mapped = _object_to_mapping_like(obj, prioritize_model_dump=prioritize_model_dump)
|
|
202
|
+
return _preprocess_recursive(
|
|
203
|
+
mapped,
|
|
204
|
+
depth=depth + 1,
|
|
205
|
+
max_depth=max_depth,
|
|
206
|
+
recursive_custom_types=recursive_custom_types,
|
|
207
|
+
str_parse_opts=str_parse_opts,
|
|
208
|
+
prioritize_model_dump=prioritize_model_dump,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
return obj
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
# ---------------------------------------
|
|
215
|
+
# Top-level conversion (non-recursive)
|
|
216
|
+
# ---------------------------------------
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _convert_top_level_to_dict(
|
|
220
|
+
obj: Any,
|
|
221
|
+
*,
|
|
222
|
+
fuzzy_parse: bool,
|
|
223
|
+
parser: Callable[[str], Any] | None,
|
|
224
|
+
prioritize_model_dump: bool,
|
|
225
|
+
use_enum_values: bool,
|
|
226
|
+
**kwargs: Any,
|
|
227
|
+
) -> dict[str | int, Any]:
|
|
228
|
+
"""
|
|
229
|
+
Convert a *single* object to dict using the 'brute force' rules.
|
|
230
|
+
Mirrors your original order, with fixes & optimizations.
|
|
231
|
+
"""
|
|
232
|
+
# Set -> {v: v}
|
|
233
|
+
if isinstance(obj, set):
|
|
234
|
+
return cast(dict[str | int, Any], {v: v for v in obj})
|
|
235
|
+
|
|
236
|
+
# Enum class -> members mapping
|
|
237
|
+
if isinstance(obj, type) and issubclass(obj, _Enum):
|
|
238
|
+
return cast(dict[str | int, Any], _enum_class_to_dict(obj, use_enum_values))
|
|
239
|
+
|
|
240
|
+
# Mapping -> copy to plain dict (preserve your copy semantics)
|
|
241
|
+
if isinstance(obj, Mapping):
|
|
242
|
+
return cast(dict[str | int, Any], dict(obj))
|
|
243
|
+
|
|
244
|
+
# None / pydantic undefined -> {}
|
|
245
|
+
if _is_na(obj):
|
|
246
|
+
return cast(dict[str | int, Any], {})
|
|
247
|
+
|
|
248
|
+
# str -> parse (and return *as parsed*, which may be list, dict, etc.)
|
|
249
|
+
if isinstance(obj, str):
|
|
250
|
+
return _parse_str(
|
|
251
|
+
obj,
|
|
252
|
+
fuzzy_parse=fuzzy_parse,
|
|
253
|
+
parser=parser,
|
|
254
|
+
**kwargs,
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
# Try "custom" object conversions
|
|
258
|
+
# (Covers BaseModel via model_dump, dataclasses, __dict__, json-strings, etc.)
|
|
259
|
+
with contextlib.suppress(Exception):
|
|
260
|
+
# If it's *not* a Sequence (e.g., numbers, objects) we try object conversion first,
|
|
261
|
+
# faithfully following your previous "non-Sequence -> model path" behavior.
|
|
262
|
+
if not isinstance(obj, Sequence):
|
|
263
|
+
converted = _object_to_mapping_like(
|
|
264
|
+
obj, prioritize_model_dump=prioritize_model_dump, **kwargs
|
|
265
|
+
)
|
|
266
|
+
# If conversion returned a string, try to parse JSON to mapping; else pass-through
|
|
267
|
+
if isinstance(converted, str):
|
|
268
|
+
return _parse_str(
|
|
269
|
+
converted,
|
|
270
|
+
fuzzy_parse=fuzzy_parse,
|
|
271
|
+
parser=None,
|
|
272
|
+
)
|
|
273
|
+
if isinstance(converted, Mapping):
|
|
274
|
+
return dict(converted)
|
|
275
|
+
# If it's a list/tuple/etc., enumerate (your original did that after the fact)
|
|
276
|
+
if isinstance(converted, Iterable) and not isinstance(
|
|
277
|
+
converted, str | bytes | bytearray
|
|
278
|
+
):
|
|
279
|
+
return cast(dict[str | int, Any], _enumerate_iterable(converted))
|
|
280
|
+
# Best effort final cast
|
|
281
|
+
return dict(converted)
|
|
282
|
+
|
|
283
|
+
# Iterable (list/tuple/namedtuple/frozenset/…): enumerate
|
|
284
|
+
if isinstance(obj, Iterable) and not isinstance(obj, str | bytes | bytearray):
|
|
285
|
+
return cast(dict[str | int, Any], _enumerate_iterable(obj))
|
|
286
|
+
|
|
287
|
+
# Dataclass fallback (reachable only if it wasn't caught above)
|
|
288
|
+
with contextlib.suppress(Exception):
|
|
289
|
+
if dataclasses.is_dataclass(obj) and not isinstance(obj, type):
|
|
290
|
+
return cast(dict[str | int, Any], dataclasses.asdict(obj)) # type: ignore[arg-type]
|
|
291
|
+
|
|
292
|
+
# Last-ditch attempt
|
|
293
|
+
return dict(obj) # may raise, handled by top-level try/except
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def to_dict(
|
|
297
|
+
input_: Any,
|
|
298
|
+
/,
|
|
299
|
+
*,
|
|
300
|
+
prioritize_model_dump: bool = False,
|
|
301
|
+
fuzzy_parse: bool = False,
|
|
302
|
+
suppress: bool = False,
|
|
303
|
+
parser: Callable[[str], Any] | None = None,
|
|
304
|
+
recursive: bool = False,
|
|
305
|
+
max_recursive_depth: int | None = None,
|
|
306
|
+
recursive_python_only: bool = True,
|
|
307
|
+
use_enum_values: bool = False,
|
|
308
|
+
**kwargs: Any,
|
|
309
|
+
) -> dict[str | int, Any]:
|
|
310
|
+
"""
|
|
311
|
+
Convert various input types to a dictionary, with optional recursive processing.
|
|
312
|
+
|
|
313
|
+
Supports JSON string parsing via orjson (or custom parser).
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
input_: Object to convert to dict
|
|
317
|
+
prioritize_model_dump: If True, prefer .model_dump() for Pydantic models
|
|
318
|
+
fuzzy_parse: If True, use fuzzy JSON parsing for strings
|
|
319
|
+
suppress: If True, return {} on errors instead of raising
|
|
320
|
+
parser: Custom parser callable for string inputs
|
|
321
|
+
recursive: If True, recursively process nested structures
|
|
322
|
+
max_recursive_depth: Maximum recursion depth (default 5, max 10)
|
|
323
|
+
recursive_python_only: If True, only recurse into Python builtins
|
|
324
|
+
use_enum_values: If True, use .value for Enum members
|
|
325
|
+
**kwargs: Additional kwargs passed to parser
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
Dictionary representation of input
|
|
329
|
+
"""
|
|
330
|
+
try:
|
|
331
|
+
# Clamp recursion depth (match your constraints)
|
|
332
|
+
if not isinstance(max_recursive_depth, int):
|
|
333
|
+
max_depth = 5
|
|
334
|
+
else:
|
|
335
|
+
if max_recursive_depth < 0:
|
|
336
|
+
raise ValueError("max_recursive_depth must be a non-negative integer")
|
|
337
|
+
if max_recursive_depth > 10:
|
|
338
|
+
raise ValueError("max_recursive_depth must be less than or equal to 10")
|
|
339
|
+
max_depth = max_recursive_depth
|
|
340
|
+
|
|
341
|
+
# Prepare one small dict to avoid repeated arg passing and lookups
|
|
342
|
+
str_parse_opts = {
|
|
343
|
+
"fuzzy_parse": fuzzy_parse,
|
|
344
|
+
"parser": parser,
|
|
345
|
+
"use_enum_values": use_enum_values, # threaded for enum class in recursion
|
|
346
|
+
**kwargs,
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
obj = input_
|
|
350
|
+
if recursive:
|
|
351
|
+
obj = _preprocess_recursive(
|
|
352
|
+
obj,
|
|
353
|
+
depth=0,
|
|
354
|
+
max_depth=max_depth,
|
|
355
|
+
recursive_custom_types=not recursive_python_only,
|
|
356
|
+
str_parse_opts=str_parse_opts,
|
|
357
|
+
prioritize_model_dump=prioritize_model_dump,
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
# Final top-level conversion
|
|
361
|
+
return _convert_top_level_to_dict(
|
|
362
|
+
obj,
|
|
363
|
+
fuzzy_parse=fuzzy_parse,
|
|
364
|
+
parser=parser,
|
|
365
|
+
prioritize_model_dump=prioritize_model_dump,
|
|
366
|
+
use_enum_values=use_enum_values,
|
|
367
|
+
**kwargs,
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
except Exception as e:
|
|
371
|
+
if suppress or input_ == "":
|
|
372
|
+
return {}
|
|
373
|
+
raise e
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# Copyright (c) 2025, HaiyangLi <quantocean.li at gmail dot com>
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from collections.abc import Iterable, Mapping
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from enum import Enum as _Enum
|
|
9
|
+
from typing import Any, ClassVar
|
|
10
|
+
|
|
11
|
+
from lionherd_core.types import Params
|
|
12
|
+
|
|
13
|
+
from ._hash import hash_dict
|
|
14
|
+
|
|
15
|
+
__all__ = ("ToListParams", "to_list")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
_INITIALIZED = False
|
|
19
|
+
_MODEL_LIKE = None
|
|
20
|
+
_MAP_LIKE = None
|
|
21
|
+
_SINGLETONE_TYPES = None
|
|
22
|
+
_SKIP_TYPE = None
|
|
23
|
+
_SKIP_TUPLE_SET = None
|
|
24
|
+
_BYTE_LIKE = (str, bytes, bytearray)
|
|
25
|
+
_TUPLE_SET = (tuple, set, frozenset)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def to_list(
|
|
29
|
+
input_: Any,
|
|
30
|
+
/,
|
|
31
|
+
*,
|
|
32
|
+
flatten: bool = False,
|
|
33
|
+
dropna: bool = False,
|
|
34
|
+
unique: bool = False,
|
|
35
|
+
use_values: bool = False,
|
|
36
|
+
flatten_tuple_set: bool = False,
|
|
37
|
+
) -> list:
|
|
38
|
+
"""Convert input to list with optional transformations.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
input_: Value to convert
|
|
42
|
+
flatten: Recursively flatten nested iterables
|
|
43
|
+
dropna: Remove None and undefined values
|
|
44
|
+
unique: Remove duplicates (requires flatten=True)
|
|
45
|
+
use_values: Extract values from enums/mappings
|
|
46
|
+
flatten_tuple_set: Include tuples/sets in flattening
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Processed list
|
|
50
|
+
|
|
51
|
+
Raises:
|
|
52
|
+
ValueError: If unique=True without flatten=True
|
|
53
|
+
"""
|
|
54
|
+
global _INITIALIZED
|
|
55
|
+
if _INITIALIZED is False:
|
|
56
|
+
from pydantic import BaseModel
|
|
57
|
+
from pydantic_core import PydanticUndefinedType
|
|
58
|
+
|
|
59
|
+
from lionherd_core.types import UndefinedType, UnsetType
|
|
60
|
+
|
|
61
|
+
global _MODEL_LIKE, _MAP_LIKE, _SINGLETONE_TYPES, _SKIP_TYPE, _SKIP_TUPLE_SET
|
|
62
|
+
_MODEL_LIKE = (BaseModel,)
|
|
63
|
+
_MAP_LIKE = (Mapping, *_MODEL_LIKE)
|
|
64
|
+
_SINGLETONE_TYPES = (UndefinedType, UnsetType, PydanticUndefinedType)
|
|
65
|
+
_SKIP_TYPE = (*_BYTE_LIKE, *_MAP_LIKE, _Enum)
|
|
66
|
+
_SKIP_TUPLE_SET = (*_SKIP_TYPE, *_TUPLE_SET)
|
|
67
|
+
_INITIALIZED = True
|
|
68
|
+
|
|
69
|
+
def _process_list(
|
|
70
|
+
lst: list[Any],
|
|
71
|
+
flatten: bool,
|
|
72
|
+
dropna: bool,
|
|
73
|
+
skip_types: tuple[type, ...],
|
|
74
|
+
) -> list[Any]:
|
|
75
|
+
"""Process list according to flatten and dropna options."""
|
|
76
|
+
assert _SINGLETONE_TYPES is not None
|
|
77
|
+
|
|
78
|
+
result: list[Any] = []
|
|
79
|
+
|
|
80
|
+
for item in lst:
|
|
81
|
+
if dropna and (item is None or isinstance(item, _SINGLETONE_TYPES)):
|
|
82
|
+
continue
|
|
83
|
+
|
|
84
|
+
is_iterable = isinstance(item, Iterable)
|
|
85
|
+
should_skip = isinstance(item, skip_types)
|
|
86
|
+
|
|
87
|
+
if is_iterable and not should_skip:
|
|
88
|
+
item_list = list(item)
|
|
89
|
+
if flatten:
|
|
90
|
+
result.extend(_process_list(item_list, flatten, dropna, skip_types))
|
|
91
|
+
else:
|
|
92
|
+
result.append(_process_list(item_list, flatten, dropna, skip_types))
|
|
93
|
+
else:
|
|
94
|
+
result.append(item)
|
|
95
|
+
|
|
96
|
+
return result
|
|
97
|
+
|
|
98
|
+
def _to_list_type(input_: Any, use_values: bool) -> list[Any]:
|
|
99
|
+
"""Convert input to initial list based on type."""
|
|
100
|
+
assert _SINGLETONE_TYPES is not None
|
|
101
|
+
assert _MODEL_LIKE is not None
|
|
102
|
+
assert _MAP_LIKE is not None
|
|
103
|
+
|
|
104
|
+
if input_ is None or isinstance(input_, _SINGLETONE_TYPES):
|
|
105
|
+
return []
|
|
106
|
+
|
|
107
|
+
if isinstance(input_, list):
|
|
108
|
+
return input_
|
|
109
|
+
|
|
110
|
+
if isinstance(input_, type) and issubclass(input_, _Enum):
|
|
111
|
+
members = input_.__members__.values()
|
|
112
|
+
return [member.value for member in members] if use_values else list(members)
|
|
113
|
+
|
|
114
|
+
if isinstance(input_, _BYTE_LIKE):
|
|
115
|
+
return list(input_) if use_values else [input_]
|
|
116
|
+
|
|
117
|
+
if isinstance(input_, Mapping):
|
|
118
|
+
return list(input_.values()) if use_values and hasattr(input_, "values") else [input_]
|
|
119
|
+
|
|
120
|
+
if isinstance(input_, _MODEL_LIKE):
|
|
121
|
+
return [input_]
|
|
122
|
+
|
|
123
|
+
if isinstance(input_, Iterable) and not isinstance(input_, _BYTE_LIKE):
|
|
124
|
+
return list(input_)
|
|
125
|
+
|
|
126
|
+
return [input_]
|
|
127
|
+
|
|
128
|
+
if unique and not flatten:
|
|
129
|
+
raise ValueError("unique=True requires flatten=True")
|
|
130
|
+
|
|
131
|
+
initial_list = _to_list_type(input_, use_values=use_values)
|
|
132
|
+
# Decide skip set once (micro-optimization)
|
|
133
|
+
skip_types = _SKIP_TYPE if flatten_tuple_set else _SKIP_TUPLE_SET
|
|
134
|
+
processed = _process_list(initial_list, flatten=flatten, dropna=dropna, skip_types=skip_types)
|
|
135
|
+
|
|
136
|
+
if unique:
|
|
137
|
+
seen = set()
|
|
138
|
+
out = []
|
|
139
|
+
use_hash_fallback = False
|
|
140
|
+
for i in processed:
|
|
141
|
+
try:
|
|
142
|
+
if not use_hash_fallback and i not in seen:
|
|
143
|
+
# Direct approach - try to use the item as hash key
|
|
144
|
+
seen.add(i)
|
|
145
|
+
out.append(i)
|
|
146
|
+
except TypeError:
|
|
147
|
+
# Switch to hash-based approach and restart
|
|
148
|
+
if not use_hash_fallback:
|
|
149
|
+
use_hash_fallback = True
|
|
150
|
+
seen = set()
|
|
151
|
+
out = []
|
|
152
|
+
# Restart from beginning with hash-based approach
|
|
153
|
+
for j in processed:
|
|
154
|
+
try:
|
|
155
|
+
hash_value = hash(j)
|
|
156
|
+
except TypeError:
|
|
157
|
+
if isinstance(j, _MAP_LIKE):
|
|
158
|
+
hash_value = hash_dict(j)
|
|
159
|
+
else:
|
|
160
|
+
raise ValueError(
|
|
161
|
+
"Unhashable type encountered in list unique value processing."
|
|
162
|
+
)
|
|
163
|
+
if hash_value not in seen:
|
|
164
|
+
seen.add(hash_value)
|
|
165
|
+
out.append(j)
|
|
166
|
+
break
|
|
167
|
+
return out
|
|
168
|
+
|
|
169
|
+
return processed
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@dataclass(slots=True, frozen=True, init=False)
|
|
173
|
+
class ToListParams(Params):
|
|
174
|
+
_func: ClassVar[Any] = to_list
|
|
175
|
+
|
|
176
|
+
flatten: bool
|
|
177
|
+
"""If True, recursively flatten nested iterables."""
|
|
178
|
+
dropna: bool
|
|
179
|
+
"""If True, remove None and undefined values."""
|
|
180
|
+
unique: bool
|
|
181
|
+
"""If True, remove duplicates (requires flatten=True)."""
|
|
182
|
+
use_values: bool
|
|
183
|
+
"""If True, extract values from enums/mappings."""
|
|
184
|
+
flatten_tuple_set: bool
|
|
185
|
+
"""If True, include tuples and sets in flattening."""
|
|
186
|
+
|
|
187
|
+
def __call__(self, input_: Any, **kw) -> list:
|
|
188
|
+
"""Apply to_list with stored parameters."""
|
|
189
|
+
kwargs = {**self.default_kw(), **kw}
|
|
190
|
+
return to_list(input_, **kwargs)
|