flpit 0.1.1__tar.gz

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.
flpit-0.1.1/PKG-INFO ADDED
@@ -0,0 +1,61 @@
1
+ Metadata-Version: 2.3
2
+ Name: flpit
3
+ Version: 0.1.1
4
+ Summary: Fluent LINQ for Python — flip your iterables
5
+ License: MIT
6
+ Classifier: Development Status :: 3 - Alpha
7
+ Classifier: Intended Audience :: Developers
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Programming Language :: Python :: 3.13
13
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
14
+ Requires-Python: >=3.11
15
+ Description-Content-Type: text/markdown
16
+
17
+ # flp
18
+
19
+ > **Fluent LINQ for Python – flip your iterables**
20
+
21
+ `flp` brings a LINQ-inspired fluent API to standard Python iterables with full static typing.
22
+
23
+ ## ⚡ Key Features
24
+
25
+ * **⚡ Lazy Evaluation (`FlpIt`):** Deferred execution using Python iterators and generators.
26
+ * **📦 Materialized Container (`FlpList`):** Eager, mutable container backed by `collections.UserList`.
27
+ * **🔹 Static-First Typing:** Built for full `mypy` and `pyright` inference without plugins.
28
+ * **🔄 No Implicit Caching:** Query results are not cached unless explicitly documented, such as `order_by`.
29
+
30
+ ## ⚙️ Installation
31
+
32
+ ```bash
33
+ uv add flpit
34
+ ```
35
+
36
+ ## 💡 Quick Start
37
+
38
+ ```python
39
+ import flp
40
+ from flp import FlpIt, FlpList
41
+
42
+ # Deferred iterable via shorthand
43
+ data: FlpIt[int] = flp.it([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
44
+
45
+ # Deferred / lazy pipeline
46
+ query: FlpIt[int] = (
47
+ data
48
+ .where(lambda x: x % 2 == 0)
49
+ .select(lambda x: x * 10)
50
+ )
51
+
52
+ # Materialize query results explicitly
53
+ result: FlpList[int] = query.to_list() # [20, 40, 60, 80, 100]
54
+
55
+ # Or start directly with an eager container
56
+ eager_list: FlpList[int] = flp.lst([1, 2, 3, 4])
57
+ ```
58
+
59
+ ## 📜 License
60
+
61
+ Distributed under the MIT License. See `LICENSE` for more information.
flpit-0.1.1/README.md ADDED
@@ -0,0 +1,45 @@
1
+ # flp
2
+
3
+ > **Fluent LINQ for Python – flip your iterables**
4
+
5
+ `flp` brings a LINQ-inspired fluent API to standard Python iterables with full static typing.
6
+
7
+ ## ⚡ Key Features
8
+
9
+ * **⚡ Lazy Evaluation (`FlpIt`):** Deferred execution using Python iterators and generators.
10
+ * **📦 Materialized Container (`FlpList`):** Eager, mutable container backed by `collections.UserList`.
11
+ * **🔹 Static-First Typing:** Built for full `mypy` and `pyright` inference without plugins.
12
+ * **🔄 No Implicit Caching:** Query results are not cached unless explicitly documented, such as `order_by`.
13
+
14
+ ## ⚙️ Installation
15
+
16
+ ```bash
17
+ uv add flpit
18
+ ```
19
+
20
+ ## 💡 Quick Start
21
+
22
+ ```python
23
+ import flp
24
+ from flp import FlpIt, FlpList
25
+
26
+ # Deferred iterable via shorthand
27
+ data: FlpIt[int] = flp.it([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
28
+
29
+ # Deferred / lazy pipeline
30
+ query: FlpIt[int] = (
31
+ data
32
+ .where(lambda x: x % 2 == 0)
33
+ .select(lambda x: x * 10)
34
+ )
35
+
36
+ # Materialize query results explicitly
37
+ result: FlpList[int] = query.to_list() # [20, 40, 60, 80, 100]
38
+
39
+ # Or start directly with an eager container
40
+ eager_list: FlpList[int] = flp.lst([1, 2, 3, 4])
41
+ ```
42
+
43
+ ## 📜 License
44
+
45
+ Distributed under the MIT License. See `LICENSE` for more information.
@@ -0,0 +1,44 @@
1
+ [project]
2
+ name = "flpit"
3
+ version = "0.1.1"
4
+ description = "Fluent LINQ for Python — flip your iterables"
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ classifiers = [
8
+ "Development Status :: 3 - Alpha",
9
+ "Intended Audience :: Developers",
10
+ "License :: OSI Approved :: MIT License",
11
+ "Programming Language :: Python :: 3",
12
+ "Programming Language :: Python :: 3.11",
13
+ "Programming Language :: Python :: 3.12",
14
+ "Programming Language :: Python :: 3.13",
15
+ "Topic :: Software Development :: Libraries :: Python Modules",
16
+ ]
17
+ dependencies = []
18
+
19
+ [project.license]
20
+ text = "MIT"
21
+
22
+ [dependency-groups]
23
+ dev = [
24
+ "pytest>=8.0",
25
+ "hypothesis>=6.168.0",
26
+ "pyrefly>=1.3.1",
27
+ "pyright>=1.1.414",
28
+ ]
29
+
30
+ [build-system]
31
+ requires = ["uv_build>=0.12.10,<0.13.0"]
32
+ build-backend = "uv_build"
33
+
34
+ [tool.uv.build-backend]
35
+ module-name = "flp"
36
+ module-root = "src"
37
+
38
+ [tool.pyright]
39
+ include = ["src"]
40
+ exclude = [
41
+ "**/__pycache__",
42
+ "tests",
43
+ ]
44
+ typeCheckingMode = "basic"
@@ -0,0 +1,39 @@
1
+ [project]
2
+ name = "flpit"
3
+ version = "0.1.1"
4
+ description = "Fluent LINQ for Python — flip your iterables"
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ license = { text = "MIT" }
8
+ classifiers = [
9
+ "Development Status :: 3 - Alpha",
10
+ "Intended Audience :: Developers",
11
+ "License :: OSI Approved :: MIT License",
12
+ "Programming Language :: Python :: 3",
13
+ "Programming Language :: Python :: 3.11",
14
+ "Programming Language :: Python :: 3.12",
15
+ "Programming Language :: Python :: 3.13",
16
+ "Topic :: Software Development :: Libraries :: Python Modules",
17
+ ]
18
+ dependencies = []
19
+
20
+ [dependency-groups]
21
+ dev = [
22
+ "pytest>=8.0",
23
+ "hypothesis>=6.168.0",
24
+ "pyrefly>=1.3.1",
25
+ "pyright>=1.1.414",
26
+ ]
27
+
28
+ [build-system]
29
+ requires = ["uv_build>=0.12.10,<0.13.0"]
30
+ build-backend = "uv_build"
31
+
32
+ [tool.uv.build-backend]
33
+ module-name = "flp"
34
+ module-root = "src"
35
+
36
+ [tool.pyright]
37
+ include = ["src"]
38
+ exclude = ["**/__pycache__", "tests"]
39
+ typeCheckingMode = "basic"
@@ -0,0 +1,41 @@
1
+ """
2
+ Fluent LINQ for Python — flip your iterables.
3
+ """
4
+ from __future__ import annotations
5
+ from importlib.metadata import version
6
+ __version__ = version("flpit")
7
+
8
+ import builtins
9
+ from typing import Iterable as _Iterable, TypeVar
10
+
11
+ from flp.core.linq import Grouping, FlpList, FlpIt, OrderedIt
12
+
13
+ TItem = TypeVar("TItem")
14
+
15
+ def it(iterable: _Iterable[TItem]) -> FlpIt[TItem]:
16
+ """shorthand to create a linq query object"""
17
+ return FlpIt(iterable)
18
+
19
+ def lst(iterable: _Iterable[TItem]) -> FlpList[TItem]:
20
+ """shorthand to create a linq list object"""
21
+ return FlpList(iterable)
22
+
23
+ # noinspection shadowing-builtins
24
+ def range(start: int, count: int) -> FlpIt[int]:
25
+ """Generates a lazy sequence of integral numbers within a specified range."""
26
+ return FlpIt(builtins.range(start, start + count))
27
+
28
+ def repeat(element: TItem, count: int) -> FlpIt[TItem]:
29
+ """Generates a lazy sequence that contains one repeated value."""
30
+ return FlpIt(element for _ in builtins.range(count))
31
+
32
+ __all__ = [
33
+ "FlpIt",
34
+ "OrderedIt",
35
+ "Grouping",
36
+ "FlpList",
37
+ "it",
38
+ "lst",
39
+ "range",
40
+ "repeat",
41
+ ]
File without changes
@@ -0,0 +1,790 @@
1
+ from __future__ import annotations
2
+
3
+ import builtins
4
+ from collections import UserList
5
+ from functools import wraps
6
+ from itertools import islice
7
+ from typing import (
8
+ Any,
9
+ Callable,
10
+ Generic,
11
+ Iterable,
12
+ Iterator,
13
+ List,
14
+ Optional,
15
+ Sequence,
16
+ Set,
17
+ Type,
18
+ TypeVar,
19
+ Union,
20
+ overload,
21
+ cast as typing_cast,
22
+ )
23
+
24
+ TItem = TypeVar("TItem")
25
+ TResult = TypeVar("TResult")
26
+ TKey = TypeVar("TKey")
27
+ TOther = TypeVar("TOther")
28
+ TAccumulate = TypeVar("TAccumulate")
29
+
30
+ _SENTINEL = object()
31
+
32
+
33
+ class EmptySequenceError(ValueError):
34
+ def __init__(self, message="Sequence contains no elements"):
35
+ super().__init__(message)
36
+
37
+
38
+ def _guard_empty(func: Callable[..., Any]) -> Callable[..., Any]:
39
+ """Catches Python's native empty-sequence ValueError and re-raises LINQ-compliant error."""
40
+ @wraps(func)
41
+ def wrapper(self, *args: Any, **kwargs: Any) -> Any:
42
+ try:
43
+ return func(self, *args, **kwargs)
44
+ except ValueError as e:
45
+ if not self.data:
46
+ raise EmptySequenceError() from e
47
+ raise
48
+ return wrapper
49
+
50
+
51
+ class _FactoryIterable(Iterable[TItem], Generic[TItem]):
52
+ """
53
+ Wraps a generator factory function into a clean Iterable[T].
54
+ Ensures __iter__ produces a brand-new iterator instance on every call
55
+ without type-checker warnings or runtime callable-check overhead.
56
+ """
57
+ __slots__ = ("_factory",)
58
+
59
+ def __init__(self, factory: Callable[[], Iterator[TItem]]) -> None:
60
+ self._factory = factory
61
+
62
+ def __iter__(self) -> Iterator[TItem]:
63
+ return self._factory()
64
+
65
+
66
+ class FlpIt(Iterable[TItem], Generic[TItem]):
67
+ """
68
+ | Fluent Iterable
69
+ Lazy evaluation wrapper around an iterable, inspired by .NET LINQ's
70
+ IEnumerable<T> semantics. Operations are deferred and do not cache results
71
+ unless explicitly documented otherwise.
72
+
73
+ Multiple enumeration is supported when the underlying source is re-iterable;
74
+ one-shot iterators and generators remain one-shot.
75
+ """
76
+ __slots__ = ("_iterable",)
77
+
78
+ def __init__(self, iterable: Iterable[TItem]) -> None:
79
+ self._iterable: Iterable[TItem] = iterable
80
+
81
+ def __iter__(self) -> Iterator[TItem]:
82
+ return iter(self._iterable)
83
+
84
+ # noinspection unused-parameter
85
+ def as_type(self, target_type: Type[TResult]) -> FlpIt[TResult]:
86
+ """Zero-cost static type hint update returning self directly without checks or allocations."""
87
+ return self # type: ignore[return-value]
88
+
89
+ # --- Deferred Execution (Lazy Operations) ---
90
+
91
+ def append(self, element: TItem) -> FlpIt[TItem]:
92
+ """Appends an element to the end of the sequence (deferred)."""
93
+ def _generator() -> Iterator[TItem]:
94
+ yield from self
95
+ yield element
96
+
97
+ return FlpIt(_FactoryIterable(_generator))
98
+
99
+ def concat(self, second: Iterable[TItem]) -> "FlpIt[TItem]":
100
+ def _generator() -> Iterator[TItem]:
101
+ yield from self
102
+ yield from second
103
+
104
+ return FlpIt(_FactoryIterable(_generator))
105
+
106
+ def prepend(self, element: TItem) -> FlpIt[TItem]:
107
+ """Prepends an element to the beginning of the sequence (deferred)."""
108
+ def _generator() -> Iterator[TItem]:
109
+ yield element
110
+ yield from self
111
+
112
+ return FlpIt(_FactoryIterable(_generator))
113
+
114
+ def where(self, predicate: Callable[[TItem], bool]) -> FlpIt[TItem]:
115
+ """Filters elements based on a predicate."""
116
+ def _generator() -> Iterator[TItem]:
117
+ for item in self:
118
+ if predicate(item):
119
+ yield item
120
+
121
+ return typing_cast(FlpIt[TItem], FlpIt(_FactoryIterable(_generator)))
122
+
123
+ def select(self, selector: Callable[[TItem], TResult]) -> FlpIt[TResult]:
124
+ """Projects each element into a new form."""
125
+ def _generator() -> Iterator[TResult]:
126
+ for item in self:
127
+ yield selector(item)
128
+
129
+ return FlpIt(_FactoryIterable(_generator))
130
+
131
+ def select_many(
132
+ self, selector: Callable[[TItem], Iterable[TResult]]
133
+ ) -> FlpIt[TResult]:
134
+ """Flattens sequence projections."""
135
+ def _generator() -> Iterator[TResult]:
136
+ for item in self:
137
+ yield from selector(item)
138
+
139
+ return FlpIt(_FactoryIterable(_generator))
140
+
141
+ def take(self, count: int) -> "FlpIt[TItem]":
142
+ """
143
+ |Returns a specified number of contiguous elements from the start.
144
+
145
+ Execution is deferred until the returned sequence is enumerated.
146
+ The operator consumes no more than `count` elements from upstream.
147
+
148
+ `take()` does not own the upstream iterator and therefore does not
149
+ close it when the limit is reached. Upstream resource lifetime is the
150
+ responsibility of the code that owns/acquires the source; use an
151
+ explicit context manager or close the source explicitly when required.
152
+
153
+ This is intentional: reaching the `take()` limit is normal completion,
154
+ not cancellation or disposal of the upstream sequence.
155
+ """
156
+ if count <= 0:
157
+ return FlpIt(())
158
+
159
+ def _generator() -> Iterator[TItem]:
160
+ yield from islice(iter(self), count)
161
+
162
+ return FlpIt(_FactoryIterable(_generator))
163
+
164
+ def cast(self, target_type: Type[TResult]) -> FlpIt[TResult]:
165
+ """Casts elements to a specified type or raises TypeError if cast fails."""
166
+ def _generator() -> Iterator[TResult]:
167
+ for item in self:
168
+ if not isinstance(item, target_type):
169
+ raise TypeError(
170
+ f"Cannot cast element {item!r} of type {type(item).__name__} to {target_type.__name__}"
171
+ )
172
+ yield item # type: ignore[misc]
173
+
174
+ return FlpIt(_FactoryIterable(_generator))
175
+
176
+ def of_type(self, target_type: Type[TResult]) -> FlpIt[TResult]:
177
+ """Filters the elements of an Iterable based on a specified type."""
178
+ def _generator() -> Iterator[TResult]:
179
+ for item in self:
180
+ if isinstance(item, target_type):
181
+ yield item # type: ignore[misc]
182
+
183
+ return FlpIt(_FactoryIterable(_generator))
184
+
185
+ def distinct(self) -> FlpIt[TItem]:
186
+ """Returns distinct elements from a sequence by using O(1) set lookups."""
187
+ def _generator() -> Iterator[TItem]:
188
+ seen: Set[TItem] = set()
189
+ for item in self:
190
+ if item not in seen:
191
+ seen.add(item)
192
+ yield item
193
+
194
+ return FlpIt(_FactoryIterable(_generator))
195
+
196
+ def distinct_by(
197
+ self, key_selector: Callable[[TItem], TKey]
198
+ ) -> FlpIt[TItem]:
199
+ """Returns distinct elements from a sequence according to a key selector function."""
200
+ def _generator() -> Iterator[TItem]:
201
+ seen: Set[TKey] = set()
202
+ for item in self:
203
+ key = key_selector(item)
204
+ if key not in seen:
205
+ seen.add(key)
206
+ yield item
207
+
208
+ return FlpIt(_FactoryIterable(_generator))
209
+
210
+ @overload
211
+ def zip(self, second: Iterable[TOther]) -> FlpIt[tuple[TItem, TOther]]: ...
212
+
213
+ @overload
214
+ def zip(
215
+ self,
216
+ second: Iterable[TOther],
217
+ result_selector: Callable[[TItem, TOther], TResult],
218
+ ) -> FlpIt[TResult]: ...
219
+
220
+ def zip(
221
+ self,
222
+ second: Iterable[TOther],
223
+ result_selector: Optional[Callable[[TItem, TOther], Any]] = None,
224
+ ) -> FlpIt[Any]:
225
+ """Applies a specified function to corresponding elements of two sequences."""
226
+ def _generator() -> Iterator[Any]:
227
+ for first_item, second_item in zip(self, second):
228
+ if result_selector is not None:
229
+ yield result_selector(first_item, second_item)
230
+ else:
231
+ yield first_item, second_item
232
+
233
+ return FlpIt(_FactoryIterable(_generator))
234
+
235
+ def chunk(self, size: int) -> FlpIt[FlpList[TItem]]:
236
+ """Splits the elements of a sequence into chunks of size at most size."""
237
+ if size <= 0:
238
+ raise ValueError("Chunk size must be greater than 0.")
239
+
240
+ def _generator() -> Iterator[FlpList[TItem]]:
241
+ current_chunk: List[TItem] = []
242
+ for item in self:
243
+ current_chunk.append(item)
244
+ if len(current_chunk) == size:
245
+ yield FlpList(current_chunk)
246
+ current_chunk = []
247
+ if current_chunk:
248
+ yield FlpList(current_chunk)
249
+
250
+ return FlpIt(_FactoryIterable(_generator))
251
+
252
+ def order_by(
253
+ self, key_selector: Callable[[TItem], Any]
254
+ ) -> OrderedIt[TItem]:
255
+ """Sorts elements in ascending order according to a key."""
256
+ return OrderedIt(self, key_selector, descending=False)
257
+
258
+ def order_by_descending(
259
+ self, key_selector: Callable[[TItem], Any]
260
+ ) -> OrderedIt[TItem]:
261
+ """Sorts elements in descending order according to a key."""
262
+ return OrderedIt(self, key_selector, descending=True)
263
+
264
+ def group_by(
265
+ self, key_selector: Callable[[TItem], TKey]
266
+ ) -> FlpIt[Grouping[TKey, TItem]]:
267
+ """Groups elements according to a specified key selector function."""
268
+ def _generator() -> Iterator[Grouping[TKey, TItem]]:
269
+ groups: dict[TKey, List[TItem]] = {}
270
+ for item in self:
271
+ key = key_selector(item)
272
+ groups.setdefault(key, []).append(item)
273
+ for k, v in groups.items():
274
+ yield Grouping(k, v)
275
+
276
+ return FlpIt(_FactoryIterable(_generator))
277
+
278
+ # --- Immediate Execution (Materialization & Aggregation) ---
279
+
280
+ @overload
281
+ def aggregate(self, func: Callable[[TItem, TItem], TItem]) -> TItem: ...
282
+
283
+ @overload
284
+ def aggregate(
285
+ self, func: Callable[[TAccumulate, TItem], TAccumulate], seed: TAccumulate
286
+ ) -> TAccumulate: ...
287
+
288
+ def aggregate(
289
+ self,
290
+ func: Callable[[Any, TItem], Any],
291
+ seed: Any = _SENTINEL,
292
+ ) -> Any:
293
+ """Applies an accumulator function over a sequence."""
294
+ it = iter(self)
295
+ if seed is _SENTINEL:
296
+ try:
297
+ accumulator = next(it)
298
+ except StopIteration:
299
+ raise EmptySequenceError()
300
+ for item in it:
301
+ accumulator = func(accumulator, item)
302
+ else:
303
+ accumulator = seed
304
+ for item in it:
305
+ accumulator = func(accumulator, item)
306
+ return accumulator
307
+
308
+ def min(self) -> TItem:
309
+ """Returns the minimum value in a sequence."""
310
+ try:
311
+ return builtins.min(self) # type: ignore[type-var]
312
+ except ValueError:
313
+ raise EmptySequenceError()
314
+
315
+ def min_by(self, key_selector: Callable[[TItem], Any]) -> TItem:
316
+ """Returns the value in a sequence that has the minimum key value."""
317
+ try:
318
+ return builtins.min(self, key=key_selector)
319
+ except ValueError:
320
+ raise EmptySequenceError()
321
+
322
+ def max(self) -> TItem:
323
+ """Returns the maximum value in a sequence."""
324
+ try:
325
+ return builtins.max(self) # type: ignore[type-var]
326
+ except ValueError:
327
+ raise EmptySequenceError()
328
+
329
+ def max_by(self, key_selector: Callable[[TItem], Any]) -> TItem:
330
+ """Returns the value in a sequence that has the maximum key value."""
331
+ try:
332
+ return builtins.max(self, key=key_selector)
333
+ except ValueError:
334
+ raise EmptySequenceError()
335
+
336
+ def average(
337
+ self, selector: Optional[Callable[[TItem], Union[int, float]]] = None
338
+ ) -> float:
339
+ """Computes the arithmetic mean of the sequence, optionally applying a selector."""
340
+ total = 0.0
341
+ count = 0
342
+ query = (selector(x) for x in self) if selector is not None else self
343
+ for item in query:
344
+ total += float(item) # type: ignore[arg-type]
345
+ count += 1
346
+ if count == 0:
347
+ raise EmptySequenceError()
348
+ return total / count
349
+
350
+ avg = average
351
+
352
+ def average_by(self, key_selector: Callable[[TItem], Union[int, float]]) -> float:
353
+ """Computes the average of a sequence of numeric values projected by a key selector."""
354
+ total = 0.0
355
+ count = 0
356
+ for item in self:
357
+ total += float(key_selector(item))
358
+ count += 1
359
+ if count == 0:
360
+ raise EmptySequenceError()
361
+ return total / count
362
+
363
+ avg_by = average_by
364
+
365
+ def sum(
366
+ self, selector: Optional[Callable[[TItem], Union[int, float]]] = None
367
+ ) -> Union[int, float]:
368
+ """Calculates the sum of the sequence, optionally applying a selector."""
369
+ if selector is not None:
370
+ return builtins.sum(selector(x) for x in self)
371
+ return builtins.sum(self) # type: ignore[arg-type]
372
+
373
+ def count(self, predicate: Optional[Callable[[TItem], bool]] = None) -> int:
374
+ """Counts elements in the sequence matching an optional predicate."""
375
+ query = self.where(predicate) if predicate else self
376
+ return builtins.sum(1 for _ in query)
377
+
378
+ def element_at(self, index: int) -> TItem:
379
+ """Returns the element at a specified index in a sequence."""
380
+ if index < 0:
381
+ raise IndexError("Index out of range")
382
+ for i, item in enumerate(self):
383
+ if i == index:
384
+ return item
385
+ raise IndexError("Index out of range")
386
+
387
+ def first(self, predicate: Optional[Callable[[TItem], bool]] = None) -> TItem:
388
+ """Returns the first element matching a predicate, or raises ValueError."""
389
+ query = self.where(predicate) if predicate else self
390
+ for item in query:
391
+ return item
392
+ raise ValueError("Sequence contains no matching elements")
393
+
394
+ def first_or_default(
395
+ self, default: TResult, predicate: Optional[Callable[[TItem], bool]] = None
396
+ ) -> Union[TItem, TResult]:
397
+ """Returns the first element matching a predicate, or a default value."""
398
+ try:
399
+ return self.first(predicate)
400
+ except ValueError:
401
+ return default
402
+
403
+ def single(self, predicate: Optional[Callable[[TItem], bool]] = None) -> TItem:
404
+ """Returns the single, unique element matching a predicate."""
405
+ query = self.where(predicate) if predicate else self
406
+ it = iter(query)
407
+ try:
408
+ first_val = next(it)
409
+ except StopIteration:
410
+ raise ValueError("Sequence contains no matching elements")
411
+
412
+ try:
413
+ next(it)
414
+ except StopIteration:
415
+ return first_val
416
+
417
+ raise ValueError("Sequence contains more than one matching element")
418
+
419
+ def to_list(self) -> FlpList[TItem]:
420
+ """Explicitly materializes the query into a FlpList."""
421
+ return FlpList(self)
422
+
423
+
424
+ from threading import Lock
425
+
426
+ class OrderedIt(FlpIt[TItem]):
427
+ """
428
+ Ordered Iterable.
429
+
430
+ Sorts the source lazily on first iteration and caches the resulting order.
431
+ The source is consumed at most once; subsequent iterations reuse the cached
432
+ result.
433
+ """
434
+
435
+ __slots__ = (
436
+ "_source",
437
+ "_key_selector",
438
+ "_descending",
439
+ "_parent",
440
+ "_cached_result",
441
+ "_lock",
442
+ )
443
+
444
+ def __init__(
445
+ self,
446
+ source: Iterable[TItem],
447
+ key_selector: Callable[[TItem], Any],
448
+ descending: bool = False,
449
+ parent: Optional["OrderedIt[TItem]"] = None,
450
+ ) -> None:
451
+ super().__init__(source)
452
+
453
+ self._source = source
454
+ self._key_selector = key_selector
455
+ self._descending = descending
456
+ self._parent = parent
457
+
458
+ self._cached_result: Optional[list[TItem]] = None
459
+ self._lock = Lock()
460
+
461
+ def then_by(
462
+ self,
463
+ key_selector: Callable[[TItem], Any],
464
+ ) -> "OrderedIt[TItem]":
465
+ return OrderedIt(
466
+ self._source,
467
+ key_selector,
468
+ descending=False,
469
+ parent=self,
470
+ )
471
+
472
+ def then_by_descending(
473
+ self,
474
+ key_selector: Callable[[TItem], Any],
475
+ ) -> "OrderedIt[TItem]":
476
+ return OrderedIt(
477
+ self._source,
478
+ key_selector,
479
+ descending=True,
480
+ parent=self,
481
+ )
482
+
483
+ def __iter__(self) -> Iterator[TItem]:
484
+ cached = self._cached_result
485
+
486
+ if cached is None:
487
+ with self._lock:
488
+ cached = self._cached_result
489
+
490
+ if cached is None:
491
+ # Collect the complete ordering chain.
492
+ comparers: list[
493
+ tuple[Callable[[TItem], Any], bool]
494
+ ] = []
495
+
496
+ node: Optional["OrderedIt[TItem]"] = self
497
+
498
+ while node is not None:
499
+ comparers.append(
500
+ (node._key_selector, node._descending)
501
+ )
502
+ node = node._parent
503
+
504
+ comparers.reverse()
505
+
506
+ # Consume the source exactly once.
507
+ items = list(self._source)
508
+
509
+ class SortWrapper:
510
+ __slots__ = ("obj", "keys")
511
+
512
+ def __init__(self, obj: Any) -> None:
513
+ self.obj = obj
514
+ self.keys = [
515
+ selector(obj)
516
+ for selector, _ in comparers
517
+ ]
518
+
519
+ def __lt__(self, other: "SortWrapper") -> bool:
520
+ for index, (_, descending) in enumerate(comparers):
521
+ left = self.keys[index]
522
+ right = other.keys[index]
523
+
524
+ if left == right:
525
+ continue
526
+
527
+ return right < left if descending else left < right
528
+
529
+ return False
530
+
531
+ wrapped_items = [
532
+ SortWrapper(item)
533
+ for item in items
534
+ ]
535
+
536
+ wrapped_items.sort()
537
+
538
+ # Store ONLY the actual result objects.
539
+ cached = [
540
+ wrapper.obj
541
+ for wrapper in wrapped_items
542
+ ]
543
+
544
+ self._cached_result = cached
545
+
546
+ # Important: __iter__ is a generator function.
547
+ # Release potentially large temporary structures
548
+ # before yielding anything.
549
+ del wrapped_items
550
+ del items
551
+ del comparers
552
+
553
+ yield from cached
554
+
555
+
556
+ class Grouping(FlpIt[TItem], Generic[TKey, TItem]):
557
+ """Represents a collection of elements sharing a common key (.NET IGrouping<TKey, TElement>)."""
558
+ __slots__ = ("_key")
559
+
560
+ def __init__(self, key: TKey, elements: Iterable[TItem]) -> None:
561
+ self._key: TKey = key
562
+ super().__init__(elements)
563
+
564
+ @property
565
+ def key(self) -> TKey:
566
+ return self._key
567
+
568
+ def __repr__(self) -> str:
569
+ return f"Grouping(key={self.key!r}, elements={self.to_list()!r})"
570
+
571
+ def __eq__(self, other: Any) -> bool:
572
+ # Check if the other object is a Grouping (or subclass)
573
+ if not isinstance(other, Grouping):
574
+ return False
575
+
576
+ # Compare the keys, then compare the elements inside FlpIt
577
+ return self.key == other.key and self.to_list() == other.to_list()
578
+
579
+
580
+ class FlpList(UserList[TItem], Sequence[TItem], Generic[TItem]):
581
+ """
582
+ | Fluent List
583
+ A materialized list extending UserList that yields lazy FlpIt instances for query operations.
584
+ """
585
+
586
+ def add(self, item: TItem) -> None:
587
+ """
588
+ |Adds an item and performs O(1) type consistency check against the first element.
589
+
590
+ |Type safety via
591
+ - type checks
592
+ - manual of_type(...) filter if you don't trust your checks
593
+ """
594
+ self.data.append(item)
595
+
596
+ def add_range(self, items: Iterable[TItem]) -> None:
597
+ """
598
+ | Adds an Iterable sequence or stream.
599
+ Optimizes paths based on input type without destroying volatile generators.
600
+
601
+ |Type safety via
602
+ - type checks
603
+ - manual of_type(...) filter if you don't trust your checks
604
+ """
605
+ # 1. Optimized Path: Fast memory extensions for pre-materialized sequences
606
+ if isinstance(items, (Sequence, list, tuple, UserList)):
607
+ self.data.extend(items)
608
+ return
609
+
610
+ # 2. Stream Path: Volatile one-shot generator handling
611
+ it = iter(items)
612
+ try:
613
+ first_item = next(it)
614
+ except StopIteration:
615
+ return
616
+
617
+ # Append the tracked peek-element and stream the remainder safely
618
+ self.data.append(first_item)
619
+ self.data.extend(it)
620
+
621
+ def to_list(self) -> "FlpList[TItem]":
622
+ """Explicitly returns a new shallow copy instance to isolate mutations matching .NET."""
623
+ return FlpList(self.data.copy())
624
+
625
+ def append_linq(self, element: TItem) -> FlpIt[TItem]:
626
+ """Appends an element to the sequence lazily, returning a FlpIt without mutating this list."""
627
+ return FlpIt(self.data).append(element)
628
+
629
+ def prepend(self, element: TItem) -> FlpIt[TItem]:
630
+ """Prepends an element to the sequence lazily, returning a FlpIt without mutating this list."""
631
+ return FlpIt(self.data).prepend(element)
632
+
633
+ # noinspection unused-parameter
634
+ def as_type(self, target_type: Type[TResult]) -> FlpList[TResult]:
635
+ return self # type: ignore[return-value]
636
+
637
+ def where(self, predicate: Callable[[TItem], bool]) -> FlpIt[TItem]:
638
+ return FlpIt(self.data).where(predicate)
639
+
640
+ def select(self, selector: Callable[[TItem], TResult]) -> FlpIt[TResult]:
641
+ return FlpIt(self.data).select(selector)
642
+
643
+ def select_many(
644
+ self, selector: Callable[[TItem], Iterable[TResult]]
645
+ ) -> FlpIt[TResult]:
646
+ return FlpIt(self.data).select_many(selector)
647
+
648
+ def take(self, count: int) -> FlpIt[TItem]:
649
+ return FlpIt(self.data).take(count)
650
+
651
+ def cast(self, target_type: Type[TResult]) -> FlpIt[TResult]:
652
+ return FlpIt(self.data).cast(target_type)
653
+
654
+ def of_type(self, target_type: Type[TResult]) -> FlpIt[TResult]:
655
+ return FlpIt(self.data).of_type(target_type)
656
+
657
+ def distinct(self) -> FlpIt[TItem]:
658
+ return FlpIt(self.data).distinct()
659
+
660
+ def distinct_by(
661
+ self, key_selector: Callable[[TItem], TKey]
662
+ ) -> FlpIt[TItem]:
663
+ return FlpIt(self.data).distinct_by(key_selector)
664
+
665
+ @overload
666
+ def zip(self, second: Iterable[TOther]) -> FlpIt[tuple[TItem, TOther]]: ...
667
+
668
+ @overload
669
+ def zip(
670
+ self,
671
+ second: Iterable[TOther],
672
+ result_selector: Callable[[TItem, TOther], TResult],
673
+ ) -> FlpIt[TResult]: ...
674
+
675
+ def zip(
676
+ self,
677
+ second: Iterable[TOther],
678
+ result_selector: Optional[Callable[[TItem, TOther], Any]] = None,
679
+ ) -> FlpIt[Any]:
680
+ return FlpIt(self.data).zip(second, result_selector)
681
+
682
+ def chunk(self, size: int) -> FlpIt[FlpList[TItem]]:
683
+ return FlpIt(self.data).chunk(size)
684
+
685
+ def order_by(
686
+ self, key_selector: Callable[[TItem], Any]
687
+ ) -> OrderedIt[TItem]:
688
+ return FlpIt(self.data).order_by(key_selector)
689
+
690
+ def order_by_descending(
691
+ self, key_selector: Callable[[TItem], Any]
692
+ ) -> OrderedIt[TItem]:
693
+ return FlpIt(self.data).order_by_descending(key_selector)
694
+
695
+ def group_by(
696
+ self, key_selector: Callable[[TItem], TKey]
697
+ ) -> FlpIt[Grouping[TKey, TItem]]:
698
+ return FlpIt(self.data).group_by(key_selector)
699
+
700
+ @overload
701
+ def aggregate(self, func: Callable[[TItem, TItem], TItem]) -> TItem: ...
702
+
703
+ @overload
704
+ def aggregate(
705
+ self, func: Callable[[TAccumulate, TItem], TAccumulate], seed: TAccumulate
706
+ ) -> TAccumulate: ...
707
+
708
+ def aggregate(
709
+ self,
710
+ func: Callable[[Any, Any], Any],
711
+ seed: Any = _SENTINEL,
712
+ ) -> Any:
713
+ return FlpIt(self.data).aggregate(func, seed=seed)
714
+
715
+ @_guard_empty
716
+ def min(self) -> TItem:
717
+ return builtins.min(self.data)
718
+
719
+ @_guard_empty
720
+ def min_by(self, key_selector: Callable[[TItem], Any]) -> TItem:
721
+ return builtins.min(self.data, key=key_selector)
722
+
723
+ @_guard_empty
724
+ def max(self) -> TItem:
725
+ return builtins.max(self.data)
726
+
727
+ @_guard_empty
728
+ def max_by(self, key_selector: Callable[[TItem], Any]) -> TItem:
729
+ return builtins.max(self.data, key=key_selector)
730
+
731
+ def sum(
732
+ self, selector: Optional[Callable[[TItem], Union[int, float]]] = None
733
+ ) -> Union[int, float]:
734
+ """Calculates the sum of elements, optionally applying a selector."""
735
+ return FlpIt(self.data).sum(selector)
736
+
737
+ def average(
738
+ self, selector: Optional[Callable[[TItem], Union[int, float]]] = None
739
+ ) -> float:
740
+ """Calculates the arithmetic mean, optionally applying a selector."""
741
+ return FlpIt(self.data).average(selector)
742
+
743
+ # --- Overloads for the Type System ---
744
+ # 1. Native compatibility path (MUST be first): exact value match
745
+ @overload
746
+ def count(self, item: TItem) -> int: ...
747
+
748
+ # 2. LINQ style path: matching via predicate function
749
+ @overload
750
+ def count(self, item: Callable[[TItem], bool]) -> int: ...
751
+
752
+ # 3. LINQ style path: no arguments (counts everything)
753
+ @overload
754
+ def count(self) -> int: ...
755
+
756
+ # --- The Clean Implementation ---
757
+ # We name the parameter 'item' to perfectly match UserList, but default it to None
758
+ def count(self, item: Any = _SENTINEL) -> int:
759
+ """Counts elements in the list matching an optional predicate or exact value."""
760
+ # Scenario C: No argument passed (.count()) -> Return total length
761
+ if item is _SENTINEL:
762
+ return len(self.data)
763
+
764
+ # Scenario A: A LINQ predicate function was passed
765
+ if callable(item):
766
+ return FlpIt(self.data).count(item)
767
+
768
+ # Scenario B: An exact raw value was passed (.count(4))
769
+ # Invokes the original parent implementation of UserList to remain 100% compliant
770
+ return super().count(item)
771
+
772
+ def element_at(self, index: int) -> TItem:
773
+ if index < 0 or index >= len(self.data):
774
+ raise IndexError("Index out of range")
775
+ return self.data[index]
776
+
777
+ def first(self, predicate: Optional[Callable[[TItem], bool]] = None) -> TItem:
778
+ return FlpIt(self.data).first(predicate)
779
+
780
+ def first_or_default(
781
+ self, default: TResult, predicate: Optional[Callable[[TItem], bool]] = None
782
+ ) -> Union[TItem, TResult]:
783
+ return FlpIt(self.data).first_or_default(default, predicate)
784
+
785
+ def single(self, predicate: Optional[Callable[[TItem], bool]] = None) -> TItem:
786
+ return FlpIt(self.data).single(predicate)
787
+
788
+ def to_list(self) -> FlpList[TItem]:
789
+ """Explicitly returns a shallow copy instance to isolate mutations."""
790
+ return FlpList(self.data.copy())
File without changes