flpit 0.2.3__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: flpit
3
- Version: 0.2.3
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
- > **⚠️ Breaking Change — `FlpList` — v0.2.0**
36
- >
37
- > `FlpList` no longer derives from `collections.UserList`.
38
- >
39
- > **Retained:** Python collection behavior such as iteration, `len()`, `in`, indexing, and slicing.
40
- >
41
- > **LINQ:** `append()`, `concat()`, and other LINQ operations remain available with lazy semantics.
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. It is deliberately not an `asyncio` compatibility layer; cancellation of running synchronous functions is cooperative.
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 | Test Cases |
432
- ----------------------------------------------------------------
433
- | tests/nettests/ | |
434
- | ├── test_all.py | 80 |
435
- | ├── test_any.py | 90 |
436
- | ├── test_average.py | 572 |
437
- | ├── test_concat.py | 64 |
438
- | ├── test_count.py | 37 |
439
- | ├── test_generic_list.py | 95 |
440
- | ├── test_last.py | 29 |
441
- | ├── test_order_by.py | 100 |
442
- | ├── test_order_descending_by.py | 45 |
443
- | ├── test_skip.py | 75 |
444
- | ├── test_sum.py | 134 |
445
- | ├── test_take.py | 39 |
446
- | ├── test_then_by.py | 40 |
447
- | └── test_then_descending_by.py | 34 |
448
- ----------------------------------------------------------------
449
- | GRAND TOTALS | 1434 |
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
- > **⚠️ Breaking Change — `FlpList` — v0.2.0**
12
- >
13
- > `FlpList` no longer derives from `collections.UserList`.
14
- >
15
- > **Retained:** Python collection behavior such as iteration, `len()`, `in`, indexing, and slicing.
16
- >
17
- > **LINQ:** `append()`, `concat()`, and other LINQ operations remain available with lazy semantics.
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. It is deliberately not an `asyncio` compatibility layer; cancellation of running synchronous functions is cooperative.
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 | Test Cases |
408
- ----------------------------------------------------------------
409
- | tests/nettests/ | |
410
- | ├── test_all.py | 80 |
411
- | ├── test_any.py | 90 |
412
- | ├── test_average.py | 572 |
413
- | ├── test_concat.py | 64 |
414
- | ├── test_count.py | 37 |
415
- | ├── test_generic_list.py | 95 |
416
- | ├── test_last.py | 29 |
417
- | ├── test_order_by.py | 100 |
418
- | ├── test_order_descending_by.py | 45 |
419
- | ├── test_skip.py | 75 |
420
- | ├── test_sum.py | 134 |
421
- | ├── test_take.py | 39 |
422
- | ├── test_then_by.py | 40 |
423
- | └── test_then_descending_by.py | 34 |
424
- ----------------------------------------------------------------
425
- | GRAND TOTALS | 1434 |
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,6 +1,6 @@
1
1
  [project]
2
2
  name = "flpit"
3
- version = "0.2.3"
3
+ version = "0.3.0"
4
4
  description = "Fluent LINQ for Python — flip your iterables"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -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], seed: TAccumulate
464
+ self, seed: TAccumulate, func: Callable[[TAccumulate, TItem], TAccumulate], /
442
465
  ) -> TAccumulate: ...
443
466
 
444
- def aggregate(
467
+ @overload
468
+ def aggregate[TAcc, TResult](
445
469
  self,
446
- func: Callable[[Any, Any], Any],
447
- seed: Any = _SENTINEL,
448
- ) -> TItem | TAccumulate:
449
- """Applies an accumulator function over a sequence."""
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 seed is _SENTINEL:
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
- for item in it:
457
- accumulator = func(accumulator, item)
458
- else:
459
- accumulator = seed
460
- for item in it:
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], seed: TAccumulate
1196
+ self, seed: TAccumulate, func: Callable[[TAccumulate, TItem], TAccumulate], /
1118
1197
  ) -> TAccumulate: ...
1119
1198
 
1120
- def aggregate(
1199
+ @overload
1200
+ def aggregate[TAcc, TResult](
1121
1201
  self,
1122
- func: Callable[[Any, Any], Any],
1123
- seed: Any = _SENTINEL,
1124
- ) -> TItem | TAccumulate:
1125
- return FlpIt(self.__list.data).aggregate(func, seed=seed)
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