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.
Files changed (64) hide show
  1. lionherd_core/__init__.py +84 -0
  2. lionherd_core/base/__init__.py +30 -0
  3. lionherd_core/base/_utils.py +295 -0
  4. lionherd_core/base/broadcaster.py +128 -0
  5. lionherd_core/base/element.py +300 -0
  6. lionherd_core/base/event.py +322 -0
  7. lionherd_core/base/eventbus.py +112 -0
  8. lionherd_core/base/flow.py +236 -0
  9. lionherd_core/base/graph.py +616 -0
  10. lionherd_core/base/node.py +212 -0
  11. lionherd_core/base/pile.py +811 -0
  12. lionherd_core/base/progression.py +261 -0
  13. lionherd_core/errors.py +104 -0
  14. lionherd_core/libs/__init__.py +2 -0
  15. lionherd_core/libs/concurrency/__init__.py +60 -0
  16. lionherd_core/libs/concurrency/_cancel.py +85 -0
  17. lionherd_core/libs/concurrency/_errors.py +80 -0
  18. lionherd_core/libs/concurrency/_patterns.py +238 -0
  19. lionherd_core/libs/concurrency/_primitives.py +253 -0
  20. lionherd_core/libs/concurrency/_priority_queue.py +135 -0
  21. lionherd_core/libs/concurrency/_resource_tracker.py +66 -0
  22. lionherd_core/libs/concurrency/_task.py +58 -0
  23. lionherd_core/libs/concurrency/_utils.py +61 -0
  24. lionherd_core/libs/schema_handlers/__init__.py +35 -0
  25. lionherd_core/libs/schema_handlers/_function_call_parser.py +122 -0
  26. lionherd_core/libs/schema_handlers/_minimal_yaml.py +88 -0
  27. lionherd_core/libs/schema_handlers/_schema_to_model.py +251 -0
  28. lionherd_core/libs/schema_handlers/_typescript.py +153 -0
  29. lionherd_core/libs/string_handlers/__init__.py +15 -0
  30. lionherd_core/libs/string_handlers/_extract_json.py +65 -0
  31. lionherd_core/libs/string_handlers/_fuzzy_json.py +103 -0
  32. lionherd_core/libs/string_handlers/_string_similarity.py +347 -0
  33. lionherd_core/libs/string_handlers/_to_num.py +63 -0
  34. lionherd_core/ln/__init__.py +45 -0
  35. lionherd_core/ln/_async_call.py +314 -0
  36. lionherd_core/ln/_fuzzy_match.py +166 -0
  37. lionherd_core/ln/_fuzzy_validate.py +151 -0
  38. lionherd_core/ln/_hash.py +141 -0
  39. lionherd_core/ln/_json_dump.py +347 -0
  40. lionherd_core/ln/_list_call.py +110 -0
  41. lionherd_core/ln/_to_dict.py +373 -0
  42. lionherd_core/ln/_to_list.py +190 -0
  43. lionherd_core/ln/_utils.py +156 -0
  44. lionherd_core/lndl/__init__.py +62 -0
  45. lionherd_core/lndl/errors.py +30 -0
  46. lionherd_core/lndl/fuzzy.py +321 -0
  47. lionherd_core/lndl/parser.py +427 -0
  48. lionherd_core/lndl/prompt.py +137 -0
  49. lionherd_core/lndl/resolver.py +323 -0
  50. lionherd_core/lndl/types.py +287 -0
  51. lionherd_core/protocols.py +181 -0
  52. lionherd_core/py.typed +0 -0
  53. lionherd_core/types/__init__.py +46 -0
  54. lionherd_core/types/_sentinel.py +131 -0
  55. lionherd_core/types/base.py +341 -0
  56. lionherd_core/types/operable.py +133 -0
  57. lionherd_core/types/spec.py +313 -0
  58. lionherd_core/types/spec_adapters/__init__.py +10 -0
  59. lionherd_core/types/spec_adapters/_protocol.py +125 -0
  60. lionherd_core/types/spec_adapters/pydantic_field.py +177 -0
  61. lionherd_core-1.0.0a3.dist-info/METADATA +502 -0
  62. lionherd_core-1.0.0a3.dist-info/RECORD +64 -0
  63. lionherd_core-1.0.0a3.dist-info/WHEEL +4 -0
  64. 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)