flpit 0.2.4__tar.gz → 0.3.0__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.2.4 → flpit-0.3.0}/PKG-INFO +57 -34
- {flpit-0.2.4 → flpit-0.3.0}/README.md +56 -33
- {flpit-0.2.4 → flpit-0.3.0}/pyproject.toml +1 -1
- {flpit-0.2.4 → flpit-0.3.0}/src/flpit/__init__.py +7 -2
- flpit-0.3.0/src/flpit/core/_grouping.py +29 -0
- {flpit-0.2.4 → flpit-0.3.0}/src/flpit/core/linq.py +163 -46
- {flpit-0.2.4 → flpit-0.3.0}/src/flpit/task.py +389 -59
- flpit-0.3.0/src/flpit/threading.py +171 -0
- {flpit-0.2.4 → flpit-0.3.0}/.gitignore +0 -0
- {flpit-0.2.4 → flpit-0.3.0}/LICENSE +0 -0
- {flpit-0.2.4 → flpit-0.3.0}/src/flp/__init__.py +0 -0
- {flpit-0.2.4 → flpit-0.3.0}/src/flpit/core/__init__.py +0 -0
- {flpit-0.2.4 → flpit-0.3.0}/src/flpit/csv.py +0 -0
- {flpit-0.2.4 → flpit-0.3.0}/src/flpit/flp.py +0 -0
- {flpit-0.2.4 → flpit-0.3.0}/src/flpit/json.py +0 -0
- {flpit-0.2.4 → flpit-0.3.0}/src/flpit/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: flpit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Fluent LINQ for Python — flip your iterables
|
|
5
5
|
Project-URL: Homepage, https://github.com/riemar/flpit
|
|
6
6
|
Project-URL: Repository, https://github.com/riemar/flpit
|
|
@@ -24,6 +24,9 @@ Description-Content-Type: text/markdown
|
|
|
24
24
|
|
|
25
25
|
# FlpIt
|
|
26
26
|
|
|
27
|
+
**[🌐 View on GitHub](https://github.com/riemar/flpit)** | [📦 View on PyPI](https://pypi.org/project/flpit/)
|
|
28
|
+
|
|
29
|
+
|
|
27
30
|
> **Fluent LINQ for Python — LINQ semantics, Python-native execution**
|
|
28
31
|
|
|
29
32
|
`FlpIt` brings a strongly typed, LINQ-style API to standard Python iterables. It combines the fluent query model and semantics of .NET LINQ with Python's native iterable and generator model.
|
|
@@ -32,17 +35,14 @@ Description-Content-Type: text/markdown
|
|
|
32
35
|
> Where Python's language or type system requires a different API contract, the contract is adapted accordingly.
|
|
33
36
|
> The implementation is actively developed, and semantic deviations may still exist.
|
|
34
37
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
> **`List<T>` semantics:** `add()`, `add_range()`, and other mutating operations follow the `List<T>` API where applicable.
|
|
44
|
-
>
|
|
45
|
-
> **Removed:** `copy()`, `+`, `*`, and other `UserList`-specific operations that are not part of the `FlpList` API.
|
|
38
|
+
## Familiar LINQ, Now in Python
|
|
39
|
+
|
|
40
|
+
FlpIt is a Python implementation of the popular .NET Language Integrated Query (LINQ), adapted to Python.
|
|
41
|
+
|
|
42
|
+
If you know C# LINQ, you already know most of FlpIt. Existing LINQ examples, query patterns, and documentation can generally be translated directly.
|
|
43
|
+
FlpIt aims to make its implemented LINQ operators behave like their corresponding .NET `System.Linq.Enumerable` counterparts.
|
|
44
|
+
|
|
45
|
+
This makes the extensive .NET LINQ ecosystem a valuable reference for developers and AI coding assistants alike.
|
|
46
46
|
|
|
47
47
|
## ⚡ Key Features
|
|
48
48
|
|
|
@@ -405,8 +405,13 @@ Key features:
|
|
|
405
405
|
- **Task coordination:** `Task.when_all()` waits for all tasks while preserving input order; `Task.when_any()` returns the first completed task; `Task.collect()` yields tasks as they complete.
|
|
406
406
|
- **Cancellation:** `CancellationToken` and `CancellationTokenSource` support cooperative cancellation, cancellation callbacks, timeouts, and linked cancellation tokens.
|
|
407
407
|
- **Resource management:** Configure worker capacity and pending background-work limits, handle queue saturation explicitly, and shut down the process-wide runtime with an observable shutdown report.
|
|
408
|
+
- **Asyncio interoperability:** FlpIt Tasks can be awaited directly inside asyncio coroutines when Task.asyncio_interop is enabled.
|
|
409
|
+
- This is disabled by default.
|
|
410
|
+
- Enabling it adds runtime-detection overhead to every pending await.
|
|
411
|
+
- await in flpit runtime: 200ish nano sec (just loop detection)
|
|
412
|
+
- await in asyncio loop:
|
|
408
413
|
|
|
409
|
-
The runtime starts lazily and manages its own lifecycle.
|
|
414
|
+
The runtime starts lazily and manages its own lifecycle. Cancellation of running synchronous functions is cooperative.
|
|
410
415
|
|
|
411
416
|
See the [Task Runtime examples](https://github.com/riemar/flpit/tree/main/examples/task) for usage examples.
|
|
412
417
|
|
|
@@ -427,25 +432,43 @@ Distributed under the MIT License. See `LICENSE` for more information.
|
|
|
427
432
|
> Note: The test cases exercise both core types, `FlpIt` and `FlpList`, with transitions between lazy and eager execution occurring as part of the individual test cases.
|
|
428
433
|
|
|
429
434
|
```
|
|
430
|
-
|
|
431
|
-
| Test Module File
|
|
432
|
-
|
|
433
|
-
| tests/nettests/
|
|
434
|
-
| ├──
|
|
435
|
-
| ├──
|
|
436
|
-
| ├──
|
|
437
|
-
| ├──
|
|
438
|
-
| ├──
|
|
439
|
-
| ├──
|
|
440
|
-
| ├──
|
|
441
|
-
| ├──
|
|
442
|
-
| ├──
|
|
443
|
-
| ├──
|
|
444
|
-
| ├──
|
|
445
|
-
| ├──
|
|
446
|
-
| ├──
|
|
447
|
-
|
|
|
448
|
-
|
|
449
|
-
|
|
|
450
|
-
|
|
435
|
+
================================================================================
|
|
436
|
+
| Test Module File | Test Cases |
|
|
437
|
+
--------------------------------------------------------------------------------
|
|
438
|
+
| tests/nettests/ | |
|
|
439
|
+
| ├── linq/ | |
|
|
440
|
+
| │ ├── test_aggregate.py | 42 |
|
|
441
|
+
| │ ├── test_all.py | 80 |
|
|
442
|
+
| │ ├── test_any.py | 90 |
|
|
443
|
+
| │ ├── test_average.py | 572 |
|
|
444
|
+
| │ ├── test_concat.py | 64 |
|
|
445
|
+
| │ ├── test_count.py | 37 |
|
|
446
|
+
| │ ├── test_distinct.py | 87 |
|
|
447
|
+
| │ ├── test_element_at.py | 43 |
|
|
448
|
+
| │ ├── test_first.py | 29 |
|
|
449
|
+
| │ ├── test_first_or_default.py | 50 |
|
|
450
|
+
| │ ├── test_generic_list.py | 95 |
|
|
451
|
+
| │ ├── test_group_by.py | 23 |
|
|
452
|
+
| │ ├── test_last.py | 29 |
|
|
453
|
+
| │ ├── test_order_by.py | 98 |
|
|
454
|
+
| │ ├── test_order_descending_by.py | 45 |
|
|
455
|
+
| │ ├── test_select.py | 140 |
|
|
456
|
+
| │ ├── test_select_many.py | 85 |
|
|
457
|
+
| │ ├── test_single.py | 34 |
|
|
458
|
+
| │ ├── test_single_or_default.py | 48 |
|
|
459
|
+
| │ ├── test_skip.py | 75 |
|
|
460
|
+
| │ ├── test_sum.py | 134 |
|
|
461
|
+
| │ ├── test_take.py | 39 |
|
|
462
|
+
| │ ├── test_then_by.py | 40 |
|
|
463
|
+
| │ ├── test_then_descending_by.py | 34 |
|
|
464
|
+
| │ └── test_where.py | 76 |
|
|
465
|
+
| └── task/ | |
|
|
466
|
+
| ├── test_cancellation.py | 69 |
|
|
467
|
+
| ├── test_task_completion_source.py | 26 |
|
|
468
|
+
| ├── test_task_factory.py | 3 |
|
|
469
|
+
| ├── test_task_rt.py | 12 |
|
|
470
|
+
| └── test_task_rt_core.py | 22 |
|
|
471
|
+
--------------------------------------------------------------------------------
|
|
472
|
+
| GRAND TOTALS | 2221 |
|
|
473
|
+
================================================================================
|
|
451
474
|
```
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# FlpIt
|
|
2
2
|
|
|
3
|
+
**[🌐 View on GitHub](https://github.com/riemar/flpit)** | [📦 View on PyPI](https://pypi.org/project/flpit/)
|
|
4
|
+
|
|
5
|
+
|
|
3
6
|
> **Fluent LINQ for Python — LINQ semantics, Python-native execution**
|
|
4
7
|
|
|
5
8
|
`FlpIt` brings a strongly typed, LINQ-style API to standard Python iterables. It combines the fluent query model and semantics of .NET LINQ with Python's native iterable and generator model.
|
|
@@ -8,17 +11,14 @@
|
|
|
8
11
|
> Where Python's language or type system requires a different API contract, the contract is adapted accordingly.
|
|
9
12
|
> The implementation is actively developed, and semantic deviations may still exist.
|
|
10
13
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
> **`List<T>` semantics:** `add()`, `add_range()`, and other mutating operations follow the `List<T>` API where applicable.
|
|
20
|
-
>
|
|
21
|
-
> **Removed:** `copy()`, `+`, `*`, and other `UserList`-specific operations that are not part of the `FlpList` API.
|
|
14
|
+
## Familiar LINQ, Now in Python
|
|
15
|
+
|
|
16
|
+
FlpIt is a Python implementation of the popular .NET Language Integrated Query (LINQ), adapted to Python.
|
|
17
|
+
|
|
18
|
+
If you know C# LINQ, you already know most of FlpIt. Existing LINQ examples, query patterns, and documentation can generally be translated directly.
|
|
19
|
+
FlpIt aims to make its implemented LINQ operators behave like their corresponding .NET `System.Linq.Enumerable` counterparts.
|
|
20
|
+
|
|
21
|
+
This makes the extensive .NET LINQ ecosystem a valuable reference for developers and AI coding assistants alike.
|
|
22
22
|
|
|
23
23
|
## ⚡ Key Features
|
|
24
24
|
|
|
@@ -381,8 +381,13 @@ Key features:
|
|
|
381
381
|
- **Task coordination:** `Task.when_all()` waits for all tasks while preserving input order; `Task.when_any()` returns the first completed task; `Task.collect()` yields tasks as they complete.
|
|
382
382
|
- **Cancellation:** `CancellationToken` and `CancellationTokenSource` support cooperative cancellation, cancellation callbacks, timeouts, and linked cancellation tokens.
|
|
383
383
|
- **Resource management:** Configure worker capacity and pending background-work limits, handle queue saturation explicitly, and shut down the process-wide runtime with an observable shutdown report.
|
|
384
|
+
- **Asyncio interoperability:** FlpIt Tasks can be awaited directly inside asyncio coroutines when Task.asyncio_interop is enabled.
|
|
385
|
+
- This is disabled by default.
|
|
386
|
+
- Enabling it adds runtime-detection overhead to every pending await.
|
|
387
|
+
- await in flpit runtime: 200ish nano sec (just loop detection)
|
|
388
|
+
- await in asyncio loop:
|
|
384
389
|
|
|
385
|
-
The runtime starts lazily and manages its own lifecycle.
|
|
390
|
+
The runtime starts lazily and manages its own lifecycle. Cancellation of running synchronous functions is cooperative.
|
|
386
391
|
|
|
387
392
|
See the [Task Runtime examples](https://github.com/riemar/flpit/tree/main/examples/task) for usage examples.
|
|
388
393
|
|
|
@@ -403,25 +408,43 @@ Distributed under the MIT License. See `LICENSE` for more information.
|
|
|
403
408
|
> Note: The test cases exercise both core types, `FlpIt` and `FlpList`, with transitions between lazy and eager execution occurring as part of the individual test cases.
|
|
404
409
|
|
|
405
410
|
```
|
|
406
|
-
|
|
407
|
-
| Test Module File
|
|
408
|
-
|
|
409
|
-
| tests/nettests/
|
|
410
|
-
| ├──
|
|
411
|
-
| ├──
|
|
412
|
-
| ├──
|
|
413
|
-
| ├──
|
|
414
|
-
| ├──
|
|
415
|
-
| ├──
|
|
416
|
-
| ├──
|
|
417
|
-
| ├──
|
|
418
|
-
| ├──
|
|
419
|
-
| ├──
|
|
420
|
-
| ├──
|
|
421
|
-
| ├──
|
|
422
|
-
| ├──
|
|
423
|
-
|
|
|
424
|
-
|
|
425
|
-
|
|
|
426
|
-
|
|
411
|
+
================================================================================
|
|
412
|
+
| Test Module File | Test Cases |
|
|
413
|
+
--------------------------------------------------------------------------------
|
|
414
|
+
| tests/nettests/ | |
|
|
415
|
+
| ├── linq/ | |
|
|
416
|
+
| │ ├── test_aggregate.py | 42 |
|
|
417
|
+
| │ ├── test_all.py | 80 |
|
|
418
|
+
| │ ├── test_any.py | 90 |
|
|
419
|
+
| │ ├── test_average.py | 572 |
|
|
420
|
+
| │ ├── test_concat.py | 64 |
|
|
421
|
+
| │ ├── test_count.py | 37 |
|
|
422
|
+
| │ ├── test_distinct.py | 87 |
|
|
423
|
+
| │ ├── test_element_at.py | 43 |
|
|
424
|
+
| │ ├── test_first.py | 29 |
|
|
425
|
+
| │ ├── test_first_or_default.py | 50 |
|
|
426
|
+
| │ ├── test_generic_list.py | 95 |
|
|
427
|
+
| │ ├── test_group_by.py | 23 |
|
|
428
|
+
| │ ├── test_last.py | 29 |
|
|
429
|
+
| │ ├── test_order_by.py | 98 |
|
|
430
|
+
| │ ├── test_order_descending_by.py | 45 |
|
|
431
|
+
| │ ├── test_select.py | 140 |
|
|
432
|
+
| │ ├── test_select_many.py | 85 |
|
|
433
|
+
| │ ├── test_single.py | 34 |
|
|
434
|
+
| │ ├── test_single_or_default.py | 48 |
|
|
435
|
+
| │ ├── test_skip.py | 75 |
|
|
436
|
+
| │ ├── test_sum.py | 134 |
|
|
437
|
+
| │ ├── test_take.py | 39 |
|
|
438
|
+
| │ ├── test_then_by.py | 40 |
|
|
439
|
+
| │ ├── test_then_descending_by.py | 34 |
|
|
440
|
+
| │ └── test_where.py | 76 |
|
|
441
|
+
| └── task/ | |
|
|
442
|
+
| ├── test_cancellation.py | 69 |
|
|
443
|
+
| ├── test_task_completion_source.py | 26 |
|
|
444
|
+
| ├── test_task_factory.py | 3 |
|
|
445
|
+
| ├── test_task_rt.py | 12 |
|
|
446
|
+
| └── test_task_rt_core.py | 22 |
|
|
447
|
+
--------------------------------------------------------------------------------
|
|
448
|
+
| GRAND TOTALS | 2221 |
|
|
449
|
+
================================================================================
|
|
427
450
|
```
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Fluent LINQ for Python — flip your iterables.
|
|
3
3
|
"""
|
|
4
|
-
from __future__ import annotations
|
|
5
4
|
from importlib.metadata import version
|
|
6
5
|
__version__ = version("flpit")
|
|
7
6
|
|
|
@@ -9,7 +8,7 @@ from flpit import flp
|
|
|
9
8
|
from flpit.core.linq import (
|
|
10
9
|
FlpIt,
|
|
11
10
|
FlpList,
|
|
12
|
-
Grouping,
|
|
11
|
+
# Grouping,
|
|
13
12
|
OrderedIt,
|
|
14
13
|
|
|
15
14
|
#====================
|
|
@@ -31,11 +30,15 @@ from flpit.core.linq import (
|
|
|
31
30
|
CollectionNoneError,
|
|
32
31
|
SelectorNoneError,
|
|
33
32
|
SecondNoneError,
|
|
33
|
+
FuncNoneError,
|
|
34
|
+
ResultSelectorNoneError,
|
|
34
35
|
ArgumentOutOfRangeError,
|
|
35
36
|
ArgumentNonNegError,
|
|
36
37
|
ArgumentOutOfBoundsError
|
|
37
38
|
)
|
|
38
39
|
|
|
40
|
+
from flpit.core._grouping import Grouping
|
|
41
|
+
|
|
39
42
|
__all__ = [
|
|
40
43
|
"flp",
|
|
41
44
|
"FlpIt",
|
|
@@ -56,6 +59,8 @@ __all__ = [
|
|
|
56
59
|
"CollectionNoneError",
|
|
57
60
|
"SelectorNoneError",
|
|
58
61
|
"SecondNoneError",
|
|
62
|
+
"FuncNoneError",
|
|
63
|
+
"ResultSelectorNoneError",
|
|
59
64
|
"ArgumentOutOfRangeError",
|
|
60
65
|
"ArgumentNonNegError",
|
|
61
66
|
"ArgumentOutOfBoundsError",
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
from flpit.core.linq import FlpList
|
|
3
|
+
from typing import override
|
|
4
|
+
from collections.abc import Iterable
|
|
5
|
+
|
|
6
|
+
class Grouping[TKey, TItem](FlpList[TItem]):
|
|
7
|
+
"""Represents a collection of elements sharing a common key (.NET IGrouping<TKey, TElement>)."""
|
|
8
|
+
__slots__ = ("_key", )
|
|
9
|
+
|
|
10
|
+
def __init__(self, key: TKey, elements: Iterable[TItem]) -> None:
|
|
11
|
+
self._key: TKey = key
|
|
12
|
+
super().__init__(elements)
|
|
13
|
+
|
|
14
|
+
@property
|
|
15
|
+
def key(self) -> TKey:
|
|
16
|
+
return self._key
|
|
17
|
+
|
|
18
|
+
@override
|
|
19
|
+
def __repr__(self) -> str:
|
|
20
|
+
return f"Grouping(key={self.key!r}, elements={self.to_list()!r})"
|
|
21
|
+
|
|
22
|
+
@override
|
|
23
|
+
def __eq__(self, other: Any) -> bool:
|
|
24
|
+
# Check if the other object is a Grouping (or subclass)
|
|
25
|
+
if not isinstance(other, Grouping):
|
|
26
|
+
return False
|
|
27
|
+
|
|
28
|
+
# Compare the keys, then compare the elements inside FlpIt
|
|
29
|
+
return self.key == other.key and self.to_list() == other.to_list()
|
|
@@ -100,6 +100,14 @@ class SecondNoneError(ArgumentNullError):
|
|
|
100
100
|
def __init__(self):
|
|
101
101
|
super().__init__("second")
|
|
102
102
|
|
|
103
|
+
class FuncNoneError(ArgumentNullError):
|
|
104
|
+
def __init__(self):
|
|
105
|
+
super().__init__("func")
|
|
106
|
+
|
|
107
|
+
class ResultSelectorNoneError(ArgumentNullError):
|
|
108
|
+
def __init__(self):
|
|
109
|
+
super().__init__("resultSelector")
|
|
110
|
+
|
|
103
111
|
class ArgumentOutOfRangeError(ArgumentError):
|
|
104
112
|
def __init__(self, argument: str, pattern:str="Index was out of range. Must be non-negative and less than the size of the collection."):
|
|
105
113
|
super().__init__(f"{pattern} (Parameter '{argument}')")
|
|
@@ -257,6 +265,9 @@ class FlpIt[TItem](Iterable[TItem]):
|
|
|
257
265
|
|
|
258
266
|
def where(self, predicate: Callable[[TItem], bool]) -> FlpIt[TItem]:
|
|
259
267
|
"""Filters elements based on a predicate."""
|
|
268
|
+
if predicate is None:
|
|
269
|
+
raise PredicateNoneError()
|
|
270
|
+
|
|
260
271
|
def _generator() -> Iterator[TItem]:
|
|
261
272
|
for item in self:
|
|
262
273
|
if predicate(item):
|
|
@@ -266,6 +277,9 @@ class FlpIt[TItem](Iterable[TItem]):
|
|
|
266
277
|
|
|
267
278
|
def select[TResult](self, selector: Callable[[TItem], TResult]) -> FlpIt[TResult]:
|
|
268
279
|
"""Projects each element into a new form."""
|
|
280
|
+
if selector is None:
|
|
281
|
+
raise SelectorNoneError()
|
|
282
|
+
|
|
269
283
|
def _generator() -> Iterator[TResult]:
|
|
270
284
|
for item in self:
|
|
271
285
|
yield selector(item)
|
|
@@ -276,6 +290,9 @@ class FlpIt[TItem](Iterable[TItem]):
|
|
|
276
290
|
self, selector: Callable[[TItem], Iterable[TResult]]
|
|
277
291
|
) -> FlpIt[TResult]:
|
|
278
292
|
"""Flattens sequence projections."""
|
|
293
|
+
if selector is None:
|
|
294
|
+
raise SelectorNoneError()
|
|
295
|
+
|
|
279
296
|
def _generator() -> Iterator[TResult]:
|
|
280
297
|
for item in self:
|
|
281
298
|
yield from selector(item)
|
|
@@ -353,6 +370,9 @@ class FlpIt[TItem](Iterable[TItem]):
|
|
|
353
370
|
self, key_selector: Callable[[TItem], TKey]
|
|
354
371
|
) -> FlpIt[TItem]:
|
|
355
372
|
"""Returns distinct elements from a sequence according to a key selector function."""
|
|
373
|
+
if key_selector is None:
|
|
374
|
+
raise SelectorNoneError()
|
|
375
|
+
|
|
356
376
|
def _generator() -> Iterator[TItem]:
|
|
357
377
|
seen: Set[TKey] = set()
|
|
358
378
|
for item in self:
|
|
@@ -421,6 +441,9 @@ class FlpIt[TItem](Iterable[TItem]):
|
|
|
421
441
|
self, key_selector: Callable[[TItem], TKey]
|
|
422
442
|
) -> FlpIt[Grouping[TKey, TItem]]:
|
|
423
443
|
"""Groups elements according to a specified key selector function."""
|
|
444
|
+
if key_selector is None:
|
|
445
|
+
raise SelectorNoneError()
|
|
446
|
+
|
|
424
447
|
def _generator() -> Iterator[Grouping[TKey, TItem]]:
|
|
425
448
|
groups: dict[TKey, List[TItem]] = {}
|
|
426
449
|
for item in self:
|
|
@@ -434,31 +457,66 @@ class FlpIt[TItem](Iterable[TItem]):
|
|
|
434
457
|
# --- Immediate Execution (Materialization & Aggregation) ---
|
|
435
458
|
|
|
436
459
|
@overload
|
|
437
|
-
def aggregate(self, func: Callable[[TItem, TItem], TItem]) -> TItem: ...
|
|
460
|
+
def aggregate(self, func: Callable[[TItem, TItem], TItem], /) -> TItem: ...
|
|
438
461
|
|
|
439
462
|
@overload
|
|
440
463
|
def aggregate(
|
|
441
|
-
self, func: Callable[[TAccumulate, TItem], TAccumulate],
|
|
464
|
+
self, seed: TAccumulate, func: Callable[[TAccumulate, TItem], TAccumulate], /
|
|
442
465
|
) -> TAccumulate: ...
|
|
443
466
|
|
|
444
|
-
|
|
467
|
+
@overload
|
|
468
|
+
def aggregate[TAcc, TResult](
|
|
445
469
|
self,
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
470
|
+
seed: TAcc,
|
|
471
|
+
func: Callable[[TAcc, TItem], TAcc],
|
|
472
|
+
result_selector: Callable[[TAcc], TResult],
|
|
473
|
+
/,
|
|
474
|
+
) -> TResult: ...
|
|
475
|
+
|
|
476
|
+
def aggregate(self, *args: Any) -> Any:
|
|
477
|
+
"""Applies an accumulator function over a sequence.
|
|
478
|
+
|
|
479
|
+
aggregate(func) first element is the seed; raises
|
|
480
|
+
EmptySequenceError for an empty source
|
|
481
|
+
aggregate(seed, func) seed is the initial accumulator
|
|
482
|
+
aggregate(seed, func, result_selector) result_selector is applied to the
|
|
483
|
+
final accumulator value
|
|
484
|
+
"""
|
|
485
|
+
if len(args) == 1:
|
|
486
|
+
(func,) = args
|
|
487
|
+
has_seed = False
|
|
488
|
+
seed = None
|
|
489
|
+
result_selector = _SENTINEL
|
|
490
|
+
elif len(args) == 2:
|
|
491
|
+
seed, func = args
|
|
492
|
+
has_seed = True
|
|
493
|
+
result_selector = _SENTINEL
|
|
494
|
+
elif len(args) == 3:
|
|
495
|
+
seed, func, result_selector = args
|
|
496
|
+
has_seed = True
|
|
497
|
+
else:
|
|
498
|
+
raise TypeError(
|
|
499
|
+
f"aggregate() takes from 1 to 3 positional arguments but {len(args)} were given"
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
if func is None:
|
|
503
|
+
raise FuncNoneError()
|
|
504
|
+
if result_selector is None:
|
|
505
|
+
raise ResultSelectorNoneError()
|
|
506
|
+
|
|
450
507
|
it = iter(self)
|
|
451
|
-
if
|
|
508
|
+
if has_seed:
|
|
509
|
+
accumulator = seed
|
|
510
|
+
else:
|
|
452
511
|
try:
|
|
453
512
|
accumulator = next(it)
|
|
454
513
|
except StopIteration:
|
|
455
514
|
raise EmptySequenceError()
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
accumulator = func(accumulator, item)
|
|
515
|
+
for item in it:
|
|
516
|
+
accumulator = func(accumulator, item)
|
|
517
|
+
|
|
518
|
+
if result_selector is not _SENTINEL:
|
|
519
|
+
return result_selector(accumulator)
|
|
462
520
|
return accumulator
|
|
463
521
|
|
|
464
522
|
@overload
|
|
@@ -632,6 +690,51 @@ class FlpIt[TItem](Iterable[TItem]):
|
|
|
632
690
|
|
|
633
691
|
raise MultipleMatchesError() if has_predicate else MultipleElementsError()
|
|
634
692
|
|
|
693
|
+
def single_or_default[TResult](
|
|
694
|
+
self,
|
|
695
|
+
default: TResult,
|
|
696
|
+
predicate: Callable[[TItem], bool] | _Sentinel = _SENTINEL,
|
|
697
|
+
) -> TItem | TResult:
|
|
698
|
+
"""Returns the single matching element, or the specified default."""
|
|
699
|
+
try:
|
|
700
|
+
return self.single(predicate)
|
|
701
|
+
except (EmptySequenceError, NoMatchError):
|
|
702
|
+
return default
|
|
703
|
+
|
|
704
|
+
@overload
|
|
705
|
+
def to_dictionary[TKey](
|
|
706
|
+
self, key_selector: Callable[[TItem], TKey], /
|
|
707
|
+
) -> dict[TKey, TItem]: ...
|
|
708
|
+
|
|
709
|
+
@overload
|
|
710
|
+
def to_dictionary[TKey, TValue](
|
|
711
|
+
self,
|
|
712
|
+
key_selector: Callable[[TItem], TKey],
|
|
713
|
+
element_selector: Callable[[TItem], TValue],
|
|
714
|
+
/,
|
|
715
|
+
) -> dict[TKey, TValue]: ...
|
|
716
|
+
|
|
717
|
+
def to_dictionary(
|
|
718
|
+
self,
|
|
719
|
+
key_selector: Callable[[TItem], Any],
|
|
720
|
+
element_selector: Callable[[TItem], Any] | _Sentinel = _SENTINEL,
|
|
721
|
+
) -> dict[Any, Any]:
|
|
722
|
+
"""Materializes the sequence as a dictionary using the selected key and optional value."""
|
|
723
|
+
if key_selector is None:
|
|
724
|
+
raise SelectorNoneError()
|
|
725
|
+
if element_selector is None:
|
|
726
|
+
raise SelectorNoneError()
|
|
727
|
+
|
|
728
|
+
result: dict[Any, Any] = {}
|
|
729
|
+
for item in self:
|
|
730
|
+
key = key_selector(item)
|
|
731
|
+
if key in result:
|
|
732
|
+
raise ValueError(f"An item with the same key has already been added. Key: {key!r}")
|
|
733
|
+
value = item if element_selector is _SENTINEL else element_selector(item)
|
|
734
|
+
result[key] = value
|
|
735
|
+
|
|
736
|
+
return result
|
|
737
|
+
|
|
635
738
|
def to_list(self) -> FlpList[TItem]:
|
|
636
739
|
"""Explicitly materializes the query into a FlpList."""
|
|
637
740
|
return FlpList[TItem](self)
|
|
@@ -744,30 +847,6 @@ class OrderedIt[TItem](FlpIt[TItem]):
|
|
|
744
847
|
)
|
|
745
848
|
|
|
746
849
|
|
|
747
|
-
class Grouping[TKey, TItem](FlpIt[TItem]):
|
|
748
|
-
"""Represents a collection of elements sharing a common key (.NET IGrouping<TKey, TElement>)."""
|
|
749
|
-
__slots__ = ("_key", )
|
|
750
|
-
|
|
751
|
-
def __init__(self, key: TKey, elements: Iterable[TItem]) -> None:
|
|
752
|
-
self._key: TKey = key
|
|
753
|
-
super().__init__(elements)
|
|
754
|
-
|
|
755
|
-
@property
|
|
756
|
-
def key(self) -> TKey:
|
|
757
|
-
return self._key
|
|
758
|
-
|
|
759
|
-
def __repr__(self) -> str:
|
|
760
|
-
return f"Grouping(key={self.key!r}, elements={self.to_list()!r})"
|
|
761
|
-
|
|
762
|
-
def __eq__(self, other: Any) -> bool:
|
|
763
|
-
# Check if the other object is a Grouping (or subclass)
|
|
764
|
-
if not isinstance(other, Grouping):
|
|
765
|
-
return False
|
|
766
|
-
|
|
767
|
-
# Compare the keys, then compare the elements inside FlpIt
|
|
768
|
-
return self.key == other.key and self.to_list() == other.to_list()
|
|
769
|
-
|
|
770
|
-
|
|
771
850
|
class FlpList[TItem](Collection[TItem]):
|
|
772
851
|
"""
|
|
773
852
|
| Fluent List
|
|
@@ -1110,19 +1189,24 @@ class FlpList[TItem](Collection[TItem]):
|
|
|
1110
1189
|
return FlpIt(self.__list.data).group_by(key_selector)
|
|
1111
1190
|
|
|
1112
1191
|
@overload
|
|
1113
|
-
def aggregate(self, func: Callable[[TItem, TItem], TItem]) -> TItem: ...
|
|
1192
|
+
def aggregate(self, func: Callable[[TItem, TItem], TItem], /) -> TItem: ...
|
|
1114
1193
|
|
|
1115
1194
|
@overload
|
|
1116
1195
|
def aggregate(
|
|
1117
|
-
self, func: Callable[[TAccumulate, TItem], TAccumulate],
|
|
1196
|
+
self, seed: TAccumulate, func: Callable[[TAccumulate, TItem], TAccumulate], /
|
|
1118
1197
|
) -> TAccumulate: ...
|
|
1119
1198
|
|
|
1120
|
-
|
|
1199
|
+
@overload
|
|
1200
|
+
def aggregate[TAcc, TResult](
|
|
1121
1201
|
self,
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1202
|
+
seed: TAcc,
|
|
1203
|
+
func: Callable[[TAcc, TItem], TAcc],
|
|
1204
|
+
result_selector: Callable[[TAcc], TResult],
|
|
1205
|
+
/,
|
|
1206
|
+
) -> TResult: ...
|
|
1207
|
+
|
|
1208
|
+
def aggregate(self, *args: Any) -> Any:
|
|
1209
|
+
return FlpIt(self.__list.data).aggregate(*args)
|
|
1126
1210
|
|
|
1127
1211
|
@_guard_empty
|
|
1128
1212
|
def min(self) -> TItem:
|
|
@@ -1219,6 +1303,36 @@ class FlpList[TItem](Collection[TItem]):
|
|
|
1219
1303
|
def single(self, predicate: Callable[[TItem], bool] | _Sentinel = _SENTINEL) -> TItem:
|
|
1220
1304
|
return FlpIt(self.__list.data).single(predicate)
|
|
1221
1305
|
|
|
1306
|
+
def single_or_default[TResult](
|
|
1307
|
+
self,
|
|
1308
|
+
default: TResult,
|
|
1309
|
+
predicate: Callable[[TItem], bool] | _Sentinel = _SENTINEL,
|
|
1310
|
+
) -> TItem | TResult:
|
|
1311
|
+
return FlpIt(self.__list.data).single_or_default(default, predicate)
|
|
1312
|
+
|
|
1313
|
+
@overload
|
|
1314
|
+
def to_dictionary[TKey](
|
|
1315
|
+
self, key_selector: Callable[[TItem], TKey], /
|
|
1316
|
+
) -> dict[TKey, TItem]: ...
|
|
1317
|
+
|
|
1318
|
+
@overload
|
|
1319
|
+
def to_dictionary[TKey, TValue](
|
|
1320
|
+
self,
|
|
1321
|
+
key_selector: Callable[[TItem], TKey],
|
|
1322
|
+
element_selector: Callable[[TItem], TValue],
|
|
1323
|
+
/,
|
|
1324
|
+
) -> dict[TKey, TValue]: ...
|
|
1325
|
+
|
|
1326
|
+
def to_dictionary(
|
|
1327
|
+
self,
|
|
1328
|
+
key_selector: Callable[[TItem], Any],
|
|
1329
|
+
element_selector: Callable[[TItem], Any] | _Sentinel = _SENTINEL,
|
|
1330
|
+
) -> dict[Any, Any]:
|
|
1331
|
+
return FlpIt(self.__list.data).to_dictionary(key_selector, element_selector)
|
|
1332
|
+
|
|
1333
|
+
def to_list(self) -> FlpList[TItem]:
|
|
1334
|
+
"""Explicitly returns a shallow copy instance to isolate mutations."""
|
|
1335
|
+
return FlpList(self.__list.data.copy())
|
|
1222
1336
|
def to_list(self) -> FlpList[TItem]:
|
|
1223
1337
|
"""Explicitly returns a shallow copy instance to isolate mutations."""
|
|
1224
1338
|
return FlpList(self.__list.data.copy())
|
|
@@ -1243,4 +1357,7 @@ class FlpList[TItem](Collection[TItem]):
|
|
|
1243
1357
|
output: BinaryIO,
|
|
1244
1358
|
jso: JsonSerializerOptions | None = None,
|
|
1245
1359
|
) -> None:
|
|
1246
|
-
JsonConvert.serialize_to(self, item_type, output, jso)
|
|
1360
|
+
JsonConvert.serialize_to(self, item_type, output, jso)
|
|
1361
|
+
|
|
1362
|
+
# I hate it, but Python being Python
|
|
1363
|
+
from ._grouping import Grouping
|