flpit 0.1.1.dev1__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.
@@ -0,0 +1,57 @@
1
+ Metadata-Version: 2.3
2
+ Name: flpit
3
+ Version: 0.1.1.dev1
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 .NET's LINQ fluent API to standard Python iterables with full static typing.
22
+
23
+ ## ⚡ Key Features
24
+
25
+ * **⚡ Lazy Evaluation (`FlpIt`):** Deferred evaluation via pure Python generator expressions.
26
+ * **📦 Eager Container (`FlpList`):** Persisted state backed by `collections.UserList`.
27
+ * **🔹 Static-First Typing:** Built for full `mypy` and `pyright` inference without plugins.
28
+ * **🟢 O(1) Boundary Validation:** Boundary checks rely on O(1) sampling instead of O(N) scans.
29
+
30
+ ## ⚙️ Installation
31
+
32
+ ```bash
33
+ uv add flp
34
+ ```
35
+
36
+ ## 💡 Quick Start
37
+
38
+ ```python
39
+ from flp import FlpList, FlpIt
40
+
41
+ # Eager collection
42
+ data = FlpList([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
43
+
44
+ # Deferred / lazy pipeline
45
+ query: FlpIt[int] = (
46
+ data
47
+ .where(lambda x: x % 2 == 0)
48
+ .select(lambda x: x * 10)
49
+ )
50
+
51
+ # Materialization happens explicitly
52
+ result: FlpList[int] = query.to_list() # [20, 40, 60, 80, 100]
53
+ ```
54
+
55
+ ## 📜 License
56
+
57
+ Distributed under the MIT License. See `LICENSE` for more information.
@@ -0,0 +1,41 @@
1
+ # flp
2
+
3
+ > **Fluent LINQ for Python – flip your iterables**
4
+
5
+ `flp` brings .NET's LINQ fluent API to standard Python iterables with full static typing.
6
+
7
+ ## ⚡ Key Features
8
+
9
+ * **⚡ Lazy Evaluation (`FlpIt`):** Deferred evaluation via pure Python generator expressions.
10
+ * **📦 Eager Container (`FlpList`):** Persisted state backed by `collections.UserList`.
11
+ * **🔹 Static-First Typing:** Built for full `mypy` and `pyright` inference without plugins.
12
+ * **🟢 O(1) Boundary Validation:** Boundary checks rely on O(1) sampling instead of O(N) scans.
13
+
14
+ ## ⚙️ Installation
15
+
16
+ ```bash
17
+ uv add flp
18
+ ```
19
+
20
+ ## 💡 Quick Start
21
+
22
+ ```python
23
+ from flp import FlpList, FlpIt
24
+
25
+ # Eager collection
26
+ data = FlpList([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
27
+
28
+ # Deferred / lazy pipeline
29
+ query: FlpIt[int] = (
30
+ data
31
+ .where(lambda x: x % 2 == 0)
32
+ .select(lambda x: x * 10)
33
+ )
34
+
35
+ # Materialization happens explicitly
36
+ result: FlpList[int] = query.to_list() # [20, 40, 60, 80, 100]
37
+ ```
38
+
39
+ ## 📜 License
40
+
41
+ Distributed under the MIT License. See `LICENSE` for more information.
@@ -0,0 +1,44 @@
1
+ [project]
2
+ name = "flpit"
3
+ version = "0.1.1.dev1"
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.dev1"
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,44 @@
1
+ """
2
+ Fluent LINQ for Python — flip your iterables.
3
+ """
4
+ from __future__ import annotations
5
+
6
+ import builtins
7
+ from typing import Iterable as _Iterable, TypeVar
8
+
9
+ from flp.core.linq import Grouping, FlpList, FlpIt, OrderedIt
10
+
11
+ TItem = TypeVar("TItem")
12
+
13
+ def it(iterable: _Iterable[TItem]) -> FlpIt[TItem]:
14
+ """shorthand to create a linq query object"""
15
+ return FlpIt(iterable)
16
+
17
+ def lst(iterable: _Iterable[TItem]) -> FlpList[TItem]:
18
+ """shorthand to create a linq list object"""
19
+ return FlpList(iterable)
20
+
21
+ # noinspection shadowing-builtins
22
+ def range(start: int, count: int) -> FlpIt[int]:
23
+ """Generates a lazy sequence of integral numbers within a specified range."""
24
+ return FlpIt(builtins.range(start, start + count))
25
+
26
+ def repeat(element: TItem, count: int) -> FlpIt[TItem]:
27
+ """Generates a lazy sequence that contains one repeated value."""
28
+ return FlpIt(element for _ in builtins.range(count))
29
+
30
+ Iterable = it
31
+ List = lst
32
+
33
+ __all__ = [
34
+ "FlpIt",
35
+ "OrderedIt",
36
+ "Grouping",
37
+ "FlpList",
38
+ "it",
39
+ "lst",
40
+ "Iterable",
41
+ "List",
42
+ "range",
43
+ "repeat",
44
+ ]
File without changes
@@ -0,0 +1,691 @@
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 (matching .NET IEnumerable<T>). No internal caching.
70
+ Supports multiple passes over queries if the underlying collection is re-iterable.
71
+ """
72
+ __slots__ = ("_iterable",)
73
+
74
+ def __init__(self, iterable: Iterable[TItem]) -> None:
75
+ self._iterable: Iterable[TItem] = iterable
76
+
77
+ def __iter__(self) -> Iterator[TItem]:
78
+ return iter(self._iterable)
79
+
80
+ # noinspection unused-parameter
81
+ def as_type(self, target_type: Type[TResult]) -> FlpIt[TResult]:
82
+ """Zero-cost static type hint update returning self directly without checks or allocations."""
83
+ return self # type: ignore[return-value]
84
+
85
+ # --- Deferred Execution (Lazy Operations) ---
86
+
87
+ def append(self, element: TItem) -> FlpIt[TItem]:
88
+ """Appends an element to the end of the sequence (deferred)."""
89
+ def _generator() -> Iterator[TItem]:
90
+ yield from self
91
+ yield element
92
+
93
+ return FlpIt(_FactoryIterable(_generator))
94
+
95
+ def prepend(self, element: TItem) -> FlpIt[TItem]:
96
+ """Prepends an element to the beginning of the sequence (deferred)."""
97
+ def _generator() -> Iterator[TItem]:
98
+ yield element
99
+ yield from self
100
+
101
+ return FlpIt(_FactoryIterable(_generator))
102
+
103
+ def where(self, predicate: Callable[[TItem], bool]) -> FlpIt[TItem]:
104
+ """Filters elements based on a predicate."""
105
+ def _generator() -> Iterator[TItem]:
106
+ for item in self:
107
+ if predicate(item):
108
+ yield item
109
+
110
+ return typing_cast(FlpIt[TItem], FlpIt(_FactoryIterable(_generator)))
111
+
112
+ def select(self, selector: Callable[[TItem], TResult]) -> FlpIt[TResult]:
113
+ """Projects each element into a new form."""
114
+ def _generator() -> Iterator[TResult]:
115
+ for item in self:
116
+ yield selector(item)
117
+
118
+ return FlpIt(_FactoryIterable(_generator))
119
+
120
+ def select_many(
121
+ self, selector: Callable[[TItem], Iterable[TResult]]
122
+ ) -> FlpIt[TResult]:
123
+ """Flattens sequence projections."""
124
+ def _generator() -> Iterator[TResult]:
125
+ for item in self:
126
+ yield from selector(item)
127
+
128
+ return FlpIt(_FactoryIterable(_generator))
129
+
130
+ def take(self, count: int) -> "FlpIt[TItem]":
131
+ """Returns a specified number of contiguous elements from the start."""
132
+ if count <= 0:
133
+ return FlpIt(())
134
+
135
+ def _generator() -> Iterator[TItem]:
136
+ # 1. We must actively grab the live iterator instance from self
137
+ upstream_iterator = iter(self)
138
+ try:
139
+ yield from islice(upstream_iterator, count)
140
+ finally:
141
+ # 2. once islice finishes or gets aborted,
142
+ # we FORCE the upstream chain to collapse and trigger its cleanup!
143
+ if hasattr(upstream_iterator, "close"):
144
+ upstream_iterator.close()
145
+
146
+ return FlpIt(_FactoryIterable(_generator))
147
+
148
+ def cast(self, target_type: Type[TResult]) -> FlpIt[TResult]:
149
+ """Casts elements to a specified type or raises TypeError if cast fails."""
150
+ def _generator() -> Iterator[TResult]:
151
+ for item in self:
152
+ if not isinstance(item, target_type):
153
+ raise TypeError(
154
+ f"Cannot cast element {item!r} of type {type(item).__name__} to {target_type.__name__}"
155
+ )
156
+ yield item # type: ignore[misc]
157
+
158
+ return FlpIt(_FactoryIterable(_generator))
159
+
160
+ def of_type(self, target_type: Type[TResult]) -> FlpIt[TResult]:
161
+ """Filters the elements of an Iterable based on a specified type."""
162
+ def _generator() -> Iterator[TResult]:
163
+ for item in self:
164
+ if isinstance(item, target_type):
165
+ yield item # type: ignore[misc]
166
+
167
+ return FlpIt(_FactoryIterable(_generator))
168
+
169
+ def distinct(self) -> FlpIt[TItem]:
170
+ """Returns distinct elements from a sequence by using O(1) set lookups."""
171
+ def _generator() -> Iterator[TItem]:
172
+ seen: Set[TItem] = set()
173
+ for item in self:
174
+ if item not in seen:
175
+ seen.add(item)
176
+ yield item
177
+
178
+ return FlpIt(_FactoryIterable(_generator))
179
+
180
+ def distinct_by(
181
+ self, key_selector: Callable[[TItem], TKey]
182
+ ) -> FlpIt[TItem]:
183
+ """Returns distinct elements from a sequence according to a key selector function."""
184
+ def _generator() -> Iterator[TItem]:
185
+ seen: Set[TKey] = set()
186
+ for item in self:
187
+ key = key_selector(item)
188
+ if key not in seen:
189
+ seen.add(key)
190
+ yield item
191
+
192
+ return FlpIt(_FactoryIterable(_generator))
193
+
194
+ @overload
195
+ def zip(self, second: Iterable[TOther]) -> FlpIt[tuple[TItem, TOther]]: ...
196
+
197
+ @overload
198
+ def zip(
199
+ self,
200
+ second: Iterable[TOther],
201
+ result_selector: Callable[[TItem, TOther], TResult],
202
+ ) -> FlpIt[TResult]: ...
203
+
204
+ def zip(
205
+ self,
206
+ second: Iterable[TOther],
207
+ result_selector: Optional[Callable[[TItem, TOther], Any]] = None,
208
+ ) -> FlpIt[Any]:
209
+ """Applies a specified function to corresponding elements of two sequences."""
210
+ def _generator() -> Iterator[Any]:
211
+ for first_item, second_item in zip(self, second):
212
+ if result_selector is not None:
213
+ yield result_selector(first_item, second_item)
214
+ else:
215
+ yield first_item, second_item
216
+
217
+ return FlpIt(_FactoryIterable(_generator))
218
+
219
+ def chunk(self, size: int) -> FlpIt[FlpList[TItem]]:
220
+ """Splits the elements of a sequence into chunks of size at most size."""
221
+ if size <= 0:
222
+ raise ValueError("Chunk size must be greater than 0.")
223
+
224
+ def _generator() -> Iterator[FlpList[TItem]]:
225
+ current_chunk: List[TItem] = []
226
+ for item in self:
227
+ current_chunk.append(item)
228
+ if len(current_chunk) == size:
229
+ yield FlpList(current_chunk)
230
+ current_chunk = []
231
+ if current_chunk:
232
+ yield FlpList(current_chunk)
233
+
234
+ return FlpIt(_FactoryIterable(_generator))
235
+
236
+ def order_by(
237
+ self, key_selector: Callable[[TItem], Any]
238
+ ) -> OrderedIt[TItem]:
239
+ """Sorts elements in ascending order according to a key."""
240
+ return OrderedIt(self, key_selector, descending=False)
241
+
242
+ def order_by_descending(
243
+ self, key_selector: Callable[[TItem], Any]
244
+ ) -> OrderedIt[TItem]:
245
+ """Sorts elements in descending order according to a key."""
246
+ return OrderedIt(self, key_selector, descending=True)
247
+
248
+ def group_by(
249
+ self, key_selector: Callable[[TItem], TKey]
250
+ ) -> FlpIt[Grouping[TKey, TItem]]:
251
+ """Groups elements according to a specified key selector function."""
252
+ def _generator() -> Iterator[Grouping[TKey, TItem]]:
253
+ groups: dict[TKey, List[TItem]] = {}
254
+ for item in self:
255
+ key = key_selector(item)
256
+ groups.setdefault(key, []).append(item)
257
+ for k, v in groups.items():
258
+ yield Grouping(k, v)
259
+
260
+ return FlpIt(_FactoryIterable(_generator))
261
+
262
+ def join(
263
+ self,
264
+ inner: Iterable[TOther],
265
+ outer_key_selector: Callable[[TItem], TKey],
266
+ inner_key_selector: Callable[[TOther], TKey],
267
+ result_selector: Callable[[TItem, TOther], TResult],
268
+ ) -> FlpIt[TResult]:
269
+ """Correlates elements of two sequences based on matching keys (Hash Join)."""
270
+ def _generator() -> Iterator[TResult]:
271
+ lookup: dict[TKey, List[TOther]] = {}
272
+ for inner_item in inner:
273
+ key = inner_key_selector(inner_item)
274
+ lookup.setdefault(key, []).append(inner_item)
275
+
276
+ for outer_item in self:
277
+ key = outer_key_selector(outer_item)
278
+ if key in lookup:
279
+ for inner_item in lookup[key]:
280
+ yield result_selector(outer_item, inner_item)
281
+
282
+ return FlpIt(_FactoryIterable(_generator))
283
+
284
+ # --- Immediate Execution (Materialization & Aggregation) ---
285
+
286
+ @overload
287
+ def aggregate(self, func: Callable[[TItem, TItem], TItem]) -> TItem: ...
288
+
289
+ @overload
290
+ def aggregate(
291
+ self, func: Callable[[TAccumulate, TItem], TAccumulate], seed: TAccumulate
292
+ ) -> TAccumulate: ...
293
+
294
+ def aggregate(
295
+ self,
296
+ func: Callable[[Any, TItem], Any],
297
+ seed: Any = _SENTINEL,
298
+ ) -> Any:
299
+ """Applies an accumulator function over a sequence."""
300
+ it = iter(self)
301
+ if seed is _SENTINEL:
302
+ try:
303
+ accumulator = next(it)
304
+ except StopIteration:
305
+ raise EmptySequenceError()
306
+ for item in it:
307
+ accumulator = func(accumulator, item)
308
+ else:
309
+ accumulator = seed
310
+ for item in it:
311
+ accumulator = func(accumulator, item)
312
+ return accumulator
313
+
314
+ def min(self) -> TItem:
315
+ """Returns the minimum value in a sequence."""
316
+ try:
317
+ return builtins.min(self) # type: ignore[type-var]
318
+ except ValueError:
319
+ raise EmptySequenceError()
320
+
321
+ def min_by(self, key_selector: Callable[[TItem], Any]) -> TItem:
322
+ """Returns the value in a sequence that has the minimum key value."""
323
+ try:
324
+ return builtins.min(self, key=key_selector)
325
+ except ValueError:
326
+ raise EmptySequenceError()
327
+
328
+ def max(self) -> TItem:
329
+ """Returns the maximum value in a sequence."""
330
+ try:
331
+ return builtins.max(self) # type: ignore[type-var]
332
+ except ValueError:
333
+ raise EmptySequenceError()
334
+
335
+ def max_by(self, key_selector: Callable[[TItem], Any]) -> TItem:
336
+ """Returns the value in a sequence that has the maximum key value."""
337
+ try:
338
+ return builtins.max(self, key=key_selector)
339
+ except ValueError:
340
+ raise EmptySequenceError()
341
+
342
+ def average(
343
+ self, selector: Optional[Callable[[TItem], Union[int, float]]] = None
344
+ ) -> float:
345
+ """Computes the arithmetic mean of the sequence, optionally applying a selector."""
346
+ total = 0.0
347
+ count = 0
348
+ query = (selector(x) for x in self) if selector is not None else self
349
+ for item in query:
350
+ total += float(item) # type: ignore[arg-type]
351
+ count += 1
352
+ if count == 0:
353
+ raise EmptySequenceError()
354
+ return total / count
355
+
356
+ avg = average
357
+
358
+ def average_by(self, key_selector: Callable[[TItem], Union[int, float]]) -> float:
359
+ """Computes the average of a sequence of numeric values projected by a key selector."""
360
+ total = 0.0
361
+ count = 0
362
+ for item in self:
363
+ total += float(key_selector(item))
364
+ count += 1
365
+ if count == 0:
366
+ raise EmptySequenceError()
367
+ return total / count
368
+
369
+ avg_by = average_by
370
+
371
+ def sum(
372
+ self, selector: Optional[Callable[[TItem], Union[int, float]]] = None
373
+ ) -> Union[int, float]:
374
+ """Calculates the sum of the sequence, optionally applying a selector."""
375
+ if selector is not None:
376
+ return builtins.sum(selector(x) for x in self)
377
+ return builtins.sum(self) # type: ignore[arg-type]
378
+
379
+ def count(self, predicate: Optional[Callable[[TItem], bool]] = None) -> int:
380
+ """Counts elements in the sequence matching an optional predicate."""
381
+ query = self.where(predicate) if predicate else self
382
+ return builtins.sum(1 for _ in query)
383
+
384
+ def element_at(self, index: int) -> TItem:
385
+ """Returns the element at a specified index in a sequence."""
386
+ if index < 0:
387
+ raise IndexError("Index out of range")
388
+ for i, item in enumerate(self):
389
+ if i == index:
390
+ return item
391
+ raise IndexError("Index out of range")
392
+
393
+ def first(self, predicate: Optional[Callable[[TItem], bool]] = None) -> TItem:
394
+ """Returns the first element matching a predicate, or raises ValueError."""
395
+ query = self.where(predicate) if predicate else self
396
+ for item in query:
397
+ return item
398
+ raise ValueError("Sequence contains no matching elements")
399
+
400
+ def first_or_default(
401
+ self, default: TResult, predicate: Optional[Callable[[TItem], bool]] = None
402
+ ) -> Union[TItem, TResult]:
403
+ """Returns the first element matching a predicate, or a default value."""
404
+ try:
405
+ return self.first(predicate)
406
+ except ValueError:
407
+ return default
408
+
409
+ def single(self, predicate: Optional[Callable[[TItem], bool]] = None) -> TItem:
410
+ """Returns the single, unique element matching a predicate."""
411
+ query = self.where(predicate) if predicate else self
412
+ it = iter(query)
413
+ try:
414
+ first_val = next(it)
415
+ except StopIteration:
416
+ raise ValueError("Sequence contains no matching elements")
417
+
418
+ try:
419
+ next(it)
420
+ except StopIteration:
421
+ return first_val
422
+
423
+ raise ValueError("Sequence contains more than one matching element")
424
+
425
+ def to_list(self) -> FlpList[TItem]:
426
+ """Explicitly materializes the query into a FlpList."""
427
+ return FlpList(self)
428
+
429
+
430
+ class OrderedIt(FlpIt[TItem]):
431
+ """
432
+ | Ordered Iterable
433
+ Represents a sorted sequence (matching .NET IOrderedEnumerable<T>). Supports then_by chaining.
434
+ """
435
+ __slots__ = ("_source", "_comparers")
436
+
437
+ def __init__(
438
+ self,
439
+ source: Iterable[TItem],
440
+ key_selector: Callable[[TItem], Any],
441
+ descending: bool = False,
442
+ ) -> None:
443
+ super().__init__(source)
444
+ self._source: Iterable[TItem] = source
445
+ self._comparers: list[tuple[Callable[[TItem], Any], bool]] = [
446
+ (key_selector, descending)
447
+ ]
448
+
449
+ def then_by(self, key_selector: Callable[[TItem], Any]) -> OrderedIt[TItem]:
450
+ """Performs a subsequent ordering in ascending order."""
451
+ new_ordered = OrderedIt(self._source, key_selector, descending=False)
452
+ new_ordered._comparers = self._comparers + [(key_selector, False)]
453
+ return new_ordered
454
+
455
+ def then_by_descending(self, key_selector: Callable[[TItem], Any]) -> OrderedIt[TItem]:
456
+ """Performs a subsequent ordering in descending order."""
457
+ new_ordered = OrderedIt(self._source, key_selector, descending=True)
458
+ new_ordered._comparers = self._comparers + [(key_selector, True)]
459
+ return new_ordered
460
+
461
+ def __iter__(self) -> Iterator[TItem]:
462
+ items = list(self._source)
463
+ for key_selector, descending in reversed(self._comparers):
464
+ items.sort(key=key_selector, reverse=descending)
465
+ return iter(items)
466
+
467
+
468
+ class Grouping(FlpIt[TItem], Generic[TKey, TItem]):
469
+ """Represents a collection of elements sharing a common key (.NET IGrouping<TKey, TElement>)."""
470
+ __slots__ = ("_key")
471
+
472
+ def __init__(self, key: TKey, elements: Iterable[TItem]) -> None:
473
+ self._key: TKey = key
474
+ super().__init__(elements)
475
+
476
+ @property
477
+ def key(self) -> TKey:
478
+ return self._key
479
+
480
+ def __repr__(self) -> str:
481
+ return f"Grouping(key={self.key!r}, elements={self.to_list()!r})"
482
+
483
+
484
+ class FlpList(UserList[TItem], Sequence[TItem], Generic[TItem]):
485
+ """
486
+ | Fluent List
487
+ A materialized list extending UserList that yields lazy FlpIt instances for query operations.
488
+ """
489
+
490
+ def add(self, item: TItem) -> None:
491
+ """Adds an item and performs O(1) type consistency check against the first element."""
492
+ if self.data and not isinstance(item, type(self.data[0])):
493
+ raise TypeError(
494
+ f"Element of type '{type(item).__name__}' does not match "
495
+ f"list item type '{type(self.data[0]).__name__}'."
496
+ )
497
+ self.data.append(item)
498
+
499
+ def add_range(self, items: Iterable[TItem]) -> None:
500
+ """Adds a sequence of items and performs O(1) type checking on the first incoming element."""
501
+ it = iter(items)
502
+ try:
503
+ first_item = next(it)
504
+ except StopIteration:
505
+ return
506
+
507
+ if self.data and not isinstance(first_item, type(self.data[0])):
508
+ raise TypeError(
509
+ f"Element of type '{type(first_item).__name__}' does not match "
510
+ f"list item type '{type(self.data[0]).__name__}'."
511
+ )
512
+
513
+ self.data.append(first_item)
514
+ self.data.extend(it)
515
+
516
+ def append_linq(self, element: TItem) -> FlpIt[TItem]:
517
+ """Appends an element to the sequence lazily, returning a FlpIt without mutating this list."""
518
+ return FlpIt(self.data).append(element)
519
+
520
+ def prepend(self, element: TItem) -> FlpIt[TItem]:
521
+ """Prepends an element to the sequence lazily, returning a FlpIt without mutating this list."""
522
+ return FlpIt(self.data).prepend(element)
523
+
524
+ # noinspection unused-parameter
525
+ def as_type(self, target_type: Type[TResult]) -> FlpList[TResult]:
526
+ return self # type: ignore[return-value]
527
+
528
+ def where(self, predicate: Callable[[TItem], bool]) -> FlpIt[TItem]:
529
+ return FlpIt(self.data).where(predicate)
530
+
531
+ def select(self, selector: Callable[[TItem], TResult]) -> FlpIt[TResult]:
532
+ return FlpIt(self.data).select(selector)
533
+
534
+ def select_many(
535
+ self, selector: Callable[[TItem], Iterable[TResult]]
536
+ ) -> FlpIt[TResult]:
537
+ return FlpIt(self.data).select_many(selector)
538
+
539
+ def take(self, count: int) -> FlpIt[TItem]:
540
+ return FlpIt(self.data).take(count)
541
+
542
+ def cast(self, target_type: Type[TResult]) -> FlpIt[TResult]:
543
+ return FlpIt(self.data).cast(target_type)
544
+
545
+ def of_type(self, target_type: Type[TResult]) -> FlpIt[TResult]:
546
+ return FlpIt(self.data).of_type(target_type)
547
+
548
+ def distinct(self) -> FlpIt[TItem]:
549
+ return FlpIt(self.data).distinct()
550
+
551
+ def distinct_by(
552
+ self, key_selector: Callable[[TItem], TKey]
553
+ ) -> FlpIt[TItem]:
554
+ return FlpIt(self.data).distinct_by(key_selector)
555
+
556
+ @overload
557
+ def zip(self, second: Iterable[TOther]) -> FlpIt[tuple[TItem, TOther]]: ...
558
+
559
+ @overload
560
+ def zip(
561
+ self,
562
+ second: Iterable[TOther],
563
+ result_selector: Callable[[TItem, TOther], TResult],
564
+ ) -> FlpIt[TResult]: ...
565
+
566
+ def zip(
567
+ self,
568
+ second: Iterable[TOther],
569
+ result_selector: Optional[Callable[[TItem, TOther], Any]] = None,
570
+ ) -> FlpIt[Any]:
571
+ return FlpIt(self.data).zip(second, result_selector)
572
+
573
+ def chunk(self, size: int) -> FlpIt[FlpList[TItem]]:
574
+ return FlpIt(self.data).chunk(size)
575
+
576
+ def order_by(
577
+ self, key_selector: Callable[[TItem], Any]
578
+ ) -> OrderedIt[TItem]:
579
+ return FlpIt(self.data).order_by(key_selector)
580
+
581
+ def order_by_descending(
582
+ self, key_selector: Callable[[TItem], Any]
583
+ ) -> OrderedIt[TItem]:
584
+ return FlpIt(self.data).order_by_descending(key_selector)
585
+
586
+ def group_by(
587
+ self, key_selector: Callable[[TItem], TKey]
588
+ ) -> FlpIt[Grouping[TKey, TItem]]:
589
+ return FlpIt(self.data).group_by(key_selector)
590
+
591
+ def join(
592
+ self,
593
+ inner: Iterable[TOther],
594
+ outer_key_selector: Callable[[TItem], TKey],
595
+ inner_key_selector: Callable[[TOther], TKey],
596
+ result_selector: Callable[[TItem, TOther], TResult],
597
+ ) -> FlpIt[TResult]:
598
+ return FlpIt(self.data).join(
599
+ inner, outer_key_selector, inner_key_selector, result_selector
600
+ )
601
+
602
+ @overload
603
+ def aggregate(self, func: Callable[[TItem, TItem], TItem]) -> TItem: ...
604
+
605
+ @overload
606
+ def aggregate(
607
+ self, func: Callable[[TAccumulate, TItem], TAccumulate], seed: TAccumulate
608
+ ) -> TAccumulate: ...
609
+
610
+ def aggregate(
611
+ self,
612
+ func: Callable[[Any, Any], Any],
613
+ seed: Any = _SENTINEL,
614
+ ) -> Any:
615
+ return FlpIt(self.data).aggregate(func, seed=seed)
616
+
617
+ @_guard_empty
618
+ def min(self) -> TItem:
619
+ return builtins.min(self.data)
620
+
621
+ @_guard_empty
622
+ def min_by(self, key_selector: Callable[[TItem], Any]) -> TItem:
623
+ return builtins.min(self.data, key=key_selector)
624
+
625
+ @_guard_empty
626
+ def max(self) -> TItem:
627
+ return builtins.max(self.data)
628
+
629
+ @_guard_empty
630
+ def max_by(self, key_selector: Callable[[TItem], Any]) -> TItem:
631
+ return builtins.max(self.data, key=key_selector)
632
+
633
+ def sum(
634
+ self, selector: Optional[Callable[[TItem], Union[int, float]]] = None
635
+ ) -> Union[int, float]:
636
+ """Calculates the sum of elements, optionally applying a selector."""
637
+ return FlpIt(self.data).sum(selector)
638
+
639
+ def average(
640
+ self, selector: Optional[Callable[[TItem], Union[int, float]]] = None
641
+ ) -> float:
642
+ """Calculates the arithmetic mean, optionally applying a selector."""
643
+ return FlpIt(self.data).average(selector)
644
+
645
+ # --- Overloads for the Type System ---
646
+ # 1. Native compatibility path (MUST be first): exact value match
647
+ @overload
648
+ def count(self, item: TItem) -> int: ...
649
+
650
+ # 2. LINQ style path: matching via predicate function
651
+ @overload
652
+ def count(self, item: Callable[[TItem], bool]) -> int: ...
653
+
654
+ # 3. LINQ style path: no arguments (counts everything)
655
+ @overload
656
+ def count(self) -> int: ...
657
+
658
+ # --- The Clean Implementation ---
659
+ # We name the parameter 'item' to perfectly match UserList, but default it to None
660
+ def count(self, item: Any = _SENTINEL) -> int:
661
+ """Counts elements in the list matching an optional predicate or exact value."""
662
+ # Scenario C: No argument passed (.count()) -> Return total length
663
+ if item is _SENTINEL:
664
+ return len(self.data)
665
+
666
+ # Scenario A: A LINQ predicate function was passed
667
+ if callable(item):
668
+ return FlpIt(self.data).count(item)
669
+
670
+ # Scenario B: An exact raw value was passed (.count(4))
671
+ # Invokes the original parent implementation of UserList to remain 100% compliant
672
+ return super().count(item)
673
+
674
+ def element_at(self, index: int) -> TItem:
675
+ if index < 0 or index >= len(self.data):
676
+ raise IndexError("Index out of range")
677
+ return self.data[index]
678
+
679
+ def first(self, predicate: Optional[Callable[[TItem], bool]] = None) -> TItem:
680
+ return FlpIt(self.data).first(predicate)
681
+
682
+ def first_or_default(
683
+ self, default: TResult, predicate: Optional[Callable[[TItem], bool]] = None
684
+ ) -> Union[TItem, TResult]:
685
+ return FlpIt(self.data).first_or_default(default, predicate)
686
+
687
+ def single(self, predicate: Optional[Callable[[TItem], bool]] = None) -> TItem:
688
+ return FlpIt(self.data).single(predicate)
689
+
690
+ def to_list(self) -> FlpList[TItem]:
691
+ return self
File without changes