flpit 0.1.1.dev1__tar.gz → 0.1.1.dev3__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 → flpit-0.1.1.dev3}/PKG-INFO +14 -10
- flpit-0.1.1.dev3/README.md +45 -0
- {flpit-0.1.1.dev1 → flpit-0.1.1.dev3}/pyproject.toml +1 -1
- {flpit-0.1.1.dev1 → flpit-0.1.1.dev3}/pyproject.toml.orig +1 -1
- {flpit-0.1.1.dev1 → flpit-0.1.1.dev3}/src/flp/__init__.py +2 -5
- {flpit-0.1.1.dev1 → flpit-0.1.1.dev3}/src/flp/core/linq.py +182 -83
- flpit-0.1.1.dev1/README.md +0 -41
- {flpit-0.1.1.dev1 → flpit-0.1.1.dev3}/src/flp/core/__init__.py +0 -0
- {flpit-0.1.1.dev1 → flpit-0.1.1.dev3}/src/flp/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: flpit
|
|
3
|
-
Version: 0.1.1.
|
|
3
|
+
Version: 0.1.1.dev3
|
|
4
4
|
Summary: Fluent LINQ for Python — flip your iterables
|
|
5
5
|
License: MIT
|
|
6
6
|
Classifier: Development Status :: 3 - Alpha
|
|
@@ -18,28 +18,29 @@ Description-Content-Type: text/markdown
|
|
|
18
18
|
|
|
19
19
|
> **Fluent LINQ for Python – flip your iterables**
|
|
20
20
|
|
|
21
|
-
`flp` brings
|
|
21
|
+
`flp` brings a LINQ-inspired fluent API to standard Python iterables with full static typing.
|
|
22
22
|
|
|
23
23
|
## ⚡ Key Features
|
|
24
24
|
|
|
25
|
-
* **⚡ Lazy Evaluation (`FlpIt`):** Deferred
|
|
26
|
-
* **📦
|
|
25
|
+
* **⚡ Lazy Evaluation (`FlpIt`):** Deferred execution using Python iterators and generators.
|
|
26
|
+
* **📦 Materialized Container (`FlpList`):** Eager, mutable container backed by `collections.UserList`.
|
|
27
27
|
* **🔹 Static-First Typing:** Built for full `mypy` and `pyright` inference without plugins.
|
|
28
|
-
*
|
|
28
|
+
* **🔄 No Implicit Caching:** Query results are not cached unless explicitly documented, such as `order_by`.
|
|
29
29
|
|
|
30
30
|
## ⚙️ Installation
|
|
31
31
|
|
|
32
32
|
```bash
|
|
33
|
-
uv add
|
|
33
|
+
uv add flpit
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
## 💡 Quick Start
|
|
37
37
|
|
|
38
38
|
```python
|
|
39
|
-
|
|
39
|
+
import flp
|
|
40
|
+
from flp import FlpIt, FlpList
|
|
40
41
|
|
|
41
|
-
#
|
|
42
|
-
data =
|
|
42
|
+
# Deferred iterable via shorthand
|
|
43
|
+
data: FlpIt[int] = flp.it([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
|
|
43
44
|
|
|
44
45
|
# Deferred / lazy pipeline
|
|
45
46
|
query: FlpIt[int] = (
|
|
@@ -48,8 +49,11 @@ query: FlpIt[int] = (
|
|
|
48
49
|
.select(lambda x: x * 10)
|
|
49
50
|
)
|
|
50
51
|
|
|
51
|
-
#
|
|
52
|
+
# Materialize query results explicitly
|
|
52
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])
|
|
53
57
|
```
|
|
54
58
|
|
|
55
59
|
## 📜 License
|
|
@@ -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.
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
Fluent LINQ for Python — flip your iterables.
|
|
3
3
|
"""
|
|
4
4
|
from __future__ import annotations
|
|
5
|
+
from importlib.metadata import version
|
|
6
|
+
__version__ = version("flpit")
|
|
5
7
|
|
|
6
8
|
import builtins
|
|
7
9
|
from typing import Iterable as _Iterable, TypeVar
|
|
@@ -27,9 +29,6 @@ def repeat(element: TItem, count: int) -> FlpIt[TItem]:
|
|
|
27
29
|
"""Generates a lazy sequence that contains one repeated value."""
|
|
28
30
|
return FlpIt(element for _ in builtins.range(count))
|
|
29
31
|
|
|
30
|
-
Iterable = it
|
|
31
|
-
List = lst
|
|
32
|
-
|
|
33
32
|
__all__ = [
|
|
34
33
|
"FlpIt",
|
|
35
34
|
"OrderedIt",
|
|
@@ -37,8 +36,6 @@ __all__ = [
|
|
|
37
36
|
"FlpList",
|
|
38
37
|
"it",
|
|
39
38
|
"lst",
|
|
40
|
-
"Iterable",
|
|
41
|
-
"List",
|
|
42
39
|
"range",
|
|
43
40
|
"repeat",
|
|
44
41
|
]
|
|
@@ -66,8 +66,12 @@ class _FactoryIterable(Iterable[TItem], Generic[TItem]):
|
|
|
66
66
|
class FlpIt(Iterable[TItem], Generic[TItem]):
|
|
67
67
|
"""
|
|
68
68
|
| Fluent Iterable
|
|
69
|
-
Lazy evaluation wrapper around an iterable
|
|
70
|
-
|
|
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.
|
|
71
75
|
"""
|
|
72
76
|
__slots__ = ("_iterable",)
|
|
73
77
|
|
|
@@ -92,6 +96,13 @@ class FlpIt(Iterable[TItem], Generic[TItem]):
|
|
|
92
96
|
|
|
93
97
|
return FlpIt(_FactoryIterable(_generator))
|
|
94
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
|
+
|
|
95
106
|
def prepend(self, element: TItem) -> FlpIt[TItem]:
|
|
96
107
|
"""Prepends an element to the beginning of the sequence (deferred)."""
|
|
97
108
|
def _generator() -> Iterator[TItem]:
|
|
@@ -128,20 +139,25 @@ class FlpIt(Iterable[TItem], Generic[TItem]):
|
|
|
128
139
|
return FlpIt(_FactoryIterable(_generator))
|
|
129
140
|
|
|
130
141
|
def take(self, count: int) -> "FlpIt[TItem]":
|
|
131
|
-
"""
|
|
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
|
+
"""
|
|
132
156
|
if count <= 0:
|
|
133
157
|
return FlpIt(())
|
|
134
158
|
|
|
135
159
|
def _generator() -> Iterator[TItem]:
|
|
136
|
-
|
|
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()
|
|
160
|
+
yield from islice(iter(self), count)
|
|
145
161
|
|
|
146
162
|
return FlpIt(_FactoryIterable(_generator))
|
|
147
163
|
|
|
@@ -259,29 +275,7 @@ class FlpIt(Iterable[TItem], Generic[TItem]):
|
|
|
259
275
|
|
|
260
276
|
return FlpIt(_FactoryIterable(_generator))
|
|
261
277
|
|
|
262
|
-
|
|
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) ---
|
|
278
|
+
# --- Immediate Execution (Materialization & Aggregation) ---
|
|
285
279
|
|
|
286
280
|
@overload
|
|
287
281
|
def aggregate(self, func: Callable[[TItem, TItem], TItem]) -> TItem: ...
|
|
@@ -427,42 +421,136 @@ class FlpIt(Iterable[TItem], Generic[TItem]):
|
|
|
427
421
|
return FlpList(self)
|
|
428
422
|
|
|
429
423
|
|
|
424
|
+
from threading import Lock
|
|
425
|
+
|
|
430
426
|
class OrderedIt(FlpIt[TItem]):
|
|
431
427
|
"""
|
|
432
|
-
|
|
433
|
-
|
|
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.
|
|
434
433
|
"""
|
|
435
|
-
|
|
434
|
+
|
|
435
|
+
__slots__ = (
|
|
436
|
+
"_source",
|
|
437
|
+
"_key_selector",
|
|
438
|
+
"_descending",
|
|
439
|
+
"_parent",
|
|
440
|
+
"_cached_result",
|
|
441
|
+
"_lock",
|
|
442
|
+
)
|
|
436
443
|
|
|
437
444
|
def __init__(
|
|
438
445
|
self,
|
|
439
446
|
source: Iterable[TItem],
|
|
440
447
|
key_selector: Callable[[TItem], Any],
|
|
441
448
|
descending: bool = False,
|
|
449
|
+
parent: Optional["OrderedIt[TItem]"] = None,
|
|
442
450
|
) -> None:
|
|
443
451
|
super().__init__(source)
|
|
444
|
-
|
|
445
|
-
self.
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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
|
+
)
|
|
460
482
|
|
|
461
483
|
def __iter__(self) -> Iterator[TItem]:
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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
|
|
466
554
|
|
|
467
555
|
|
|
468
556
|
class Grouping(FlpIt[TItem], Generic[TKey, TItem]):
|
|
@@ -480,6 +568,14 @@ class Grouping(FlpIt[TItem], Generic[TKey, TItem]):
|
|
|
480
568
|
def __repr__(self) -> str:
|
|
481
569
|
return f"Grouping(key={self.key!r}, elements={self.to_list()!r})"
|
|
482
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
|
+
|
|
483
579
|
|
|
484
580
|
class FlpList(UserList[TItem], Sequence[TItem], Generic[TItem]):
|
|
485
581
|
"""
|
|
@@ -488,31 +584,44 @@ class FlpList(UserList[TItem], Sequence[TItem], Generic[TItem]):
|
|
|
488
584
|
"""
|
|
489
585
|
|
|
490
586
|
def add(self, item: TItem) -> None:
|
|
491
|
-
"""
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
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
|
+
"""
|
|
497
594
|
self.data.append(item)
|
|
498
595
|
|
|
499
596
|
def add_range(self, items: Iterable[TItem]) -> None:
|
|
500
|
-
"""
|
|
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
|
|
501
611
|
it = iter(items)
|
|
502
612
|
try:
|
|
503
613
|
first_item = next(it)
|
|
504
614
|
except StopIteration:
|
|
505
615
|
return
|
|
506
616
|
|
|
507
|
-
|
|
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
|
-
|
|
617
|
+
# Append the tracked peek-element and stream the remainder safely
|
|
513
618
|
self.data.append(first_item)
|
|
514
619
|
self.data.extend(it)
|
|
515
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
|
+
|
|
516
625
|
def append_linq(self, element: TItem) -> FlpIt[TItem]:
|
|
517
626
|
"""Appends an element to the sequence lazily, returning a FlpIt without mutating this list."""
|
|
518
627
|
return FlpIt(self.data).append(element)
|
|
@@ -588,17 +697,6 @@ class FlpList(UserList[TItem], Sequence[TItem], Generic[TItem]):
|
|
|
588
697
|
) -> FlpIt[Grouping[TKey, TItem]]:
|
|
589
698
|
return FlpIt(self.data).group_by(key_selector)
|
|
590
699
|
|
|
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
700
|
@overload
|
|
603
701
|
def aggregate(self, func: Callable[[TItem, TItem], TItem]) -> TItem: ...
|
|
604
702
|
|
|
@@ -688,4 +786,5 @@ class FlpList(UserList[TItem], Sequence[TItem], Generic[TItem]):
|
|
|
688
786
|
return FlpIt(self.data).single(predicate)
|
|
689
787
|
|
|
690
788
|
def to_list(self) -> FlpList[TItem]:
|
|
691
|
-
|
|
789
|
+
"""Explicitly returns a shallow copy instance to isolate mutations."""
|
|
790
|
+
return FlpList(self.data.copy())
|
flpit-0.1.1.dev1/README.md
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
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.
|
|
File without changes
|
|
File without changes
|