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.
- flpit-0.1.1.dev1/PKG-INFO +57 -0
- flpit-0.1.1.dev1/README.md +41 -0
- flpit-0.1.1.dev1/pyproject.toml +44 -0
- flpit-0.1.1.dev1/pyproject.toml.orig +39 -0
- flpit-0.1.1.dev1/src/flp/__init__.py +44 -0
- flpit-0.1.1.dev1/src/flp/core/__init__.py +0 -0
- flpit-0.1.1.dev1/src/flp/core/linq.py +691 -0
- flpit-0.1.1.dev1/src/flp/py.typed +0 -0
|
@@ -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
|