flpit 0.1.1.dev2__tar.gz → 0.1.2__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.3
2
2
  Name: flpit
3
- Version: 0.1.1.dev2
3
+ Version: 0.1.2
4
4
  Summary: Fluent LINQ for Python — flip your iterables
5
5
  License: MIT
6
6
  Classifier: Development Status :: 3 - Alpha
@@ -30,16 +30,17 @@ Description-Content-Type: text/markdown
30
30
  ## ⚙️ Installation
31
31
 
32
32
  ```bash
33
- uv add flp
33
+ uv add flpit
34
34
  ```
35
35
 
36
36
  ## 💡 Quick Start
37
37
 
38
38
  ```python
39
- from flp import FlpList, FlpIt
39
+ import flp
40
+ from flp import FlpIt, FlpList
40
41
 
41
- # Eager collection
42
- data = FlpList([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
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
- # Materialization happens explicitly
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
@@ -14,16 +14,17 @@
14
14
  ## ⚙️ Installation
15
15
 
16
16
  ```bash
17
- uv add flp
17
+ uv add flpit
18
18
  ```
19
19
 
20
20
  ## 💡 Quick Start
21
21
 
22
22
  ```python
23
- from flp import FlpList, FlpIt
23
+ import flp
24
+ from flp import FlpIt, FlpList
24
25
 
25
- # Eager collection
26
- data = FlpList([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
26
+ # Deferred iterable via shorthand
27
+ data: FlpIt[int] = flp.it([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
27
28
 
28
29
  # Deferred / lazy pipeline
29
30
  query: FlpIt[int] = (
@@ -32,8 +33,11 @@ query: FlpIt[int] = (
32
33
  .select(lambda x: x * 10)
33
34
  )
34
35
 
35
- # Materialization happens explicitly
36
+ # Materialize query results explicitly
36
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])
37
41
  ```
38
42
 
39
43
  ## 📜 License
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "flpit"
3
- version = "0.1.1.dev2"
3
+ version = "0.1.2"
4
4
  description = "Fluent LINQ for Python — flip your iterables"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "flpit"
3
- version = "0.1.1.dev2"
3
+ version = "0.1.2"
4
4
  description = "Fluent LINQ for Python — flip your iterables"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -37,3 +37,7 @@ module-root = "src"
37
37
  include = ["src"]
38
38
  exclude = ["**/__pycache__", "tests"]
39
39
  typeCheckingMode = "basic"
40
+
41
+ #[tool.pyrefly]
42
+ #extra-paths = ["src", "tests"]
43
+ #python-interpreter-path = ".venv/bin/python" # or .venv/Scripts/python.exe on Windows
@@ -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
  ]
@@ -1,5 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from typing import cast
4
+
3
5
  import builtins
4
6
  from collections import UserList
5
7
  from functools import wraps
@@ -28,12 +30,25 @@ TOther = TypeVar("TOther")
28
30
  TAccumulate = TypeVar("TAccumulate")
29
31
 
30
32
  _SENTINEL = object()
33
+ _MISSING = object()
31
34
 
32
35
 
33
36
  class EmptySequenceError(ValueError):
34
37
  def __init__(self, message="Sequence contains no elements"):
35
38
  super().__init__(message)
36
39
 
40
+ class NoMatchError(ValueError):
41
+ def __init__(self, message="Sequence contains no matching elements"):
42
+ super().__init__(message)
43
+
44
+ class MultipleMatchesError(ValueError):
45
+ def __init__(self, message="Sequence contains more than one matching element"):
46
+ super().__init__(message)
47
+
48
+ class MultipleElementsError(ValueError):
49
+ def __init__(self, message="Sequence contains more than one element"):
50
+ super().__init__(message)
51
+
37
52
 
38
53
  def _guard_empty(func: Callable[..., Any]) -> Callable[..., Any]:
39
54
  """Catches Python's native empty-sequence ValueError and re-raises LINQ-compliant error."""
@@ -96,6 +111,49 @@ class FlpIt(Iterable[TItem], Generic[TItem]):
96
111
 
97
112
  return FlpIt(_FactoryIterable(_generator))
98
113
 
114
+ @overload
115
+ def any(self) -> bool: ...
116
+
117
+ @overload
118
+ def any(self, predicate: Callable[[TItem], bool]) -> bool: ...
119
+
120
+ def any(
121
+ self,
122
+ predicate: Optional[Callable[[TItem], bool]] = None,
123
+ ) -> bool:
124
+ """
125
+ Determines whether the sequence contains any elements,
126
+ or whether any element satisfies the predicate.
127
+
128
+ Mirrors Enumerable.Any:
129
+ Any()
130
+ Any(predicate)
131
+
132
+ Evaluation stops as soon as the result is known.
133
+ """
134
+ if predicate is not None:
135
+ return builtins.any(map(predicate, self))
136
+
137
+ iterator = iter(self)
138
+ try:
139
+ next(iterator)
140
+ except StopIteration:
141
+ return False
142
+
143
+ return True
144
+
145
+
146
+ def all(self, predicate: Callable[[TItem], bool]) -> bool:
147
+ """
148
+ Determines whether all elements satisfy the predicate.
149
+
150
+ Mirrors Enumerable.All.
151
+
152
+ Returns True for an empty sequence.
153
+ Evaluation stops at the first element that fails the predicate.
154
+ """
155
+ return builtins.all(map(predicate, self))
156
+
99
157
  def concat(self, second: Iterable[TItem]) -> "FlpIt[TItem]":
100
158
  def _generator() -> Iterator[TItem]:
101
159
  yield from self
@@ -275,29 +333,7 @@ class FlpIt(Iterable[TItem], Generic[TItem]):
275
333
 
276
334
  return FlpIt(_FactoryIterable(_generator))
277
335
 
278
- # def join(
279
- # self,
280
- # inner: Iterable[TOther],
281
- # outer_key_selector: Callable[[TItem], TKey],
282
- # inner_key_selector: Callable[[TOther], TKey],
283
- # result_selector: Callable[[TItem, TOther], TResult],
284
- # ) -> FlpIt[TResult]:
285
- # """Correlates elements of two sequences based on matching keys (Hash Join)."""
286
- # def _generator() -> Iterator[TResult]:
287
- # lookup: dict[TKey, List[TOther]] = {}
288
- # for inner_item in inner:
289
- # key = inner_key_selector(inner_item)
290
- # lookup.setdefault(key, []).append(inner_item)
291
- #
292
- # for outer_item in self:
293
- # key = outer_key_selector(outer_item)
294
- # if key in lookup:
295
- # for inner_item in lookup[key]:
296
- # yield result_selector(outer_item, inner_item)
297
- #
298
- # return FlpIt(_FactoryIterable(_generator))
299
-
300
- # --- Immediate Execution (Materialization & Aggregation) ---
336
+ # --- Immediate Execution (Materialization & Aggregation) ---
301
337
 
302
338
  @overload
303
339
  def aggregate(self, func: Callable[[TItem, TItem], TItem]) -> TItem: ...
@@ -327,33 +363,61 @@ class FlpIt(Iterable[TItem], Generic[TItem]):
327
363
  accumulator = func(accumulator, item)
328
364
  return accumulator
329
365
 
366
+ @overload
367
+ def last(self) -> TItem: ...
368
+
369
+ @overload
370
+ def last(self, predicate: Callable[[TItem], bool]) -> TItem: ...
371
+
372
+ def last(
373
+ self,
374
+ predicate: Optional[Callable[[TItem], bool]] = None,
375
+ ) -> TItem:
376
+ """
377
+ Returns the last element of the sequence, or the last element
378
+ satisfying the predicate.
379
+
380
+ Mirrors Enumerable.Last:
381
+ Last()
382
+ Last(predicate)
383
+
384
+ Raises EmptySequenceError if the sequence is empty or if no
385
+ element satisfies the predicate.
386
+ """
387
+ last_item: TItem | object = _SENTINEL
388
+
389
+ for item in self:
390
+ if predicate is None or predicate(item):
391
+ last_item = item
392
+
393
+ if last_item is _SENTINEL:
394
+ raise EmptySequenceError() if predicate is None else NoMatchError()
395
+
396
+ return cast(TItem, last_item)
397
+
398
+ def _extreme(self, func, **kwargs) -> TItem:
399
+ result = func(self, default=_MISSING, **kwargs)
400
+
401
+ if result is _MISSING:
402
+ raise EmptySequenceError()
403
+
404
+ return cast(TItem, result)
405
+
330
406
  def min(self) -> TItem:
331
407
  """Returns the minimum value in a sequence."""
332
- try:
333
- return builtins.min(self) # type: ignore[type-var]
334
- except ValueError:
335
- raise EmptySequenceError()
408
+ return self._extreme(builtins.min)
336
409
 
337
410
  def min_by(self, key_selector: Callable[[TItem], Any]) -> TItem:
338
411
  """Returns the value in a sequence that has the minimum key value."""
339
- try:
340
- return builtins.min(self, key=key_selector)
341
- except ValueError:
342
- raise EmptySequenceError()
412
+ return self._extreme(builtins.min, key=key_selector)
343
413
 
344
414
  def max(self) -> TItem:
345
415
  """Returns the maximum value in a sequence."""
346
- try:
347
- return builtins.max(self) # type: ignore[type-var]
348
- except ValueError:
349
- raise EmptySequenceError()
416
+ return self._extreme(builtins.max)
350
417
 
351
418
  def max_by(self, key_selector: Callable[[TItem], Any]) -> TItem:
352
419
  """Returns the value in a sequence that has the maximum key value."""
353
- try:
354
- return builtins.max(self, key=key_selector)
355
- except ValueError:
356
- raise EmptySequenceError()
420
+ return self._extreme(builtins.max, key=key_selector)
357
421
 
358
422
  def average(
359
423
  self, selector: Optional[Callable[[TItem], Union[int, float]]] = None
@@ -394,7 +458,7 @@ class FlpIt(Iterable[TItem], Generic[TItem]):
394
458
 
395
459
  def count(self, predicate: Optional[Callable[[TItem], bool]] = None) -> int:
396
460
  """Counts elements in the sequence matching an optional predicate."""
397
- query = self.where(predicate) if predicate else self
461
+ query = self.where(predicate) if predicate is not None else self
398
462
  return builtins.sum(1 for _ in query)
399
463
 
400
464
  def element_at(self, index: int) -> TItem:
@@ -408,10 +472,10 @@ class FlpIt(Iterable[TItem], Generic[TItem]):
408
472
 
409
473
  def first(self, predicate: Optional[Callable[[TItem], bool]] = None) -> TItem:
410
474
  """Returns the first element matching a predicate, or raises ValueError."""
411
- query = self.where(predicate) if predicate else self
475
+ query = self.where(predicate) if predicate is not None else self
412
476
  for item in query:
413
477
  return item
414
- raise ValueError("Sequence contains no matching elements")
478
+ raise EmptySequenceError() if predicate is None else NoMatchError()
415
479
 
416
480
  def first_or_default(
417
481
  self, default: TResult, predicate: Optional[Callable[[TItem], bool]] = None
@@ -419,24 +483,30 @@ class FlpIt(Iterable[TItem], Generic[TItem]):
419
483
  """Returns the first element matching a predicate, or a default value."""
420
484
  try:
421
485
  return self.first(predicate)
422
- except ValueError:
486
+ except (EmptySequenceError, NoMatchError):
423
487
  return default
424
488
 
425
- def single(self, predicate: Optional[Callable[[TItem], bool]] = None) -> TItem:
489
+ def single(self, predicate: Optional[Callable[[TItem], bool]] = _SENTINEL) -> TItem:
426
490
  """Returns the single, unique element matching a predicate."""
427
- query = self.where(predicate) if predicate else self
491
+ if predicate is None:
492
+ raise TypeError("Predicate cannot be None")
493
+
494
+ has_predicate = predicate is not _SENTINEL
495
+ query = self.where(predicate) if has_predicate else self
428
496
  it = iter(query)
497
+
429
498
  try:
430
499
  first_val = next(it)
431
500
  except StopIteration:
432
- raise ValueError("Sequence contains no matching elements")
501
+ raise NoMatchError() if has_predicate else EmptySequenceError()
433
502
 
434
503
  try:
435
504
  next(it)
436
505
  except StopIteration:
437
506
  return first_val
438
507
 
439
- raise ValueError("Sequence contains more than one matching element")
508
+ raise MultipleMatchesError() if has_predicate else MultipleElementsError()
509
+
440
510
 
441
511
  def to_list(self) -> FlpList[TItem]:
442
512
  """Explicitly materializes the query into a FlpList."""
@@ -640,9 +710,26 @@ class FlpList(UserList[TItem], Sequence[TItem], Generic[TItem]):
640
710
  self.data.append(first_item)
641
711
  self.data.extend(it)
642
712
 
643
- def to_list(self) -> "FlpList[TItem]":
644
- """Explicitly returns a new shallow copy instance to isolate mutations matching .NET."""
645
- return FlpList(self.data.copy())
713
+ @overload
714
+ def any(self) -> bool: ...
715
+
716
+ @overload
717
+ def any(self, predicate: Callable[[TItem], bool]) -> bool: ...
718
+
719
+ def any(
720
+ self,
721
+ predicate: Optional[Callable[[TItem], bool]] = None,
722
+ ) -> bool:
723
+ if predicate is None:
724
+ return len(self.data) > 0
725
+
726
+ return any(predicate(item) for item in self.data)
727
+
728
+ def all(
729
+ self,
730
+ predicate: Callable[[TItem], bool],
731
+ ) -> bool:
732
+ return all(predicate(item) for item in self.data)
646
733
 
647
734
  def append_linq(self, element: TItem) -> FlpIt[TItem]:
648
735
  """Appends an element to the sequence lazily, returning a FlpIt without mutating this list."""
@@ -719,17 +806,6 @@ class FlpList(UserList[TItem], Sequence[TItem], Generic[TItem]):
719
806
  ) -> FlpIt[Grouping[TKey, TItem]]:
720
807
  return FlpIt(self.data).group_by(key_selector)
721
808
 
722
- # def join(
723
- # self,
724
- # inner: Iterable[TOther],
725
- # outer_key_selector: Callable[[TItem], TKey],
726
- # inner_key_selector: Callable[[TOther], TKey],
727
- # result_selector: Callable[[TItem, TOther], TResult],
728
- # ) -> FlpIt[TResult]:
729
- # return FlpIt(self.data).join(
730
- # inner, outer_key_selector, inner_key_selector, result_selector
731
- # )
732
-
733
809
  @overload
734
810
  def aggregate(self, func: Callable[[TItem, TItem], TItem]) -> TItem: ...
735
811
 
@@ -810,14 +886,46 @@ class FlpList(UserList[TItem], Sequence[TItem], Generic[TItem]):
810
886
  def first(self, predicate: Optional[Callable[[TItem], bool]] = None) -> TItem:
811
887
  return FlpIt(self.data).first(predicate)
812
888
 
889
+ @overload
890
+ def last(self) -> TItem: ...
891
+
892
+ @overload
893
+ def last(self, predicate: Callable[[TItem], bool]) -> TItem: ...
894
+
895
+ def last(
896
+ self,
897
+ predicate: Optional[Callable[[TItem], bool]] = None,
898
+ ) -> TItem:
899
+ """
900
+ Returns the last element of the sequence, or the last element
901
+ satisfying the predicate.
902
+
903
+ Mirrors Enumerable.Last:
904
+ Last()
905
+ Last(predicate)
906
+
907
+ Raises EmptySequenceError if the sequence is empty or if no
908
+ element satisfies the predicate.
909
+ """
910
+ if predicate is None:
911
+ if not self.data:
912
+ raise EmptySequenceError()
913
+ return self.data[-1]
914
+
915
+ for item in reversed(self.data):
916
+ if predicate(item):
917
+ return item
918
+
919
+ raise NoMatchError()
920
+
813
921
  def first_or_default(
814
922
  self, default: TResult, predicate: Optional[Callable[[TItem], bool]] = None
815
923
  ) -> Union[TItem, TResult]:
816
924
  return FlpIt(self.data).first_or_default(default, predicate)
817
925
 
818
- def single(self, predicate: Optional[Callable[[TItem], bool]] = None) -> TItem:
926
+ def single(self, predicate: Optional[Callable[[TItem], bool]] = _SENTINEL) -> TItem:
819
927
  return FlpIt(self.data).single(predicate)
820
928
 
821
929
  def to_list(self) -> FlpList[TItem]:
822
930
  """Explicitly returns a shallow copy instance to isolate mutations."""
823
- return FlpList(self.data.copy())
931
+ return FlpList(self.data.copy())
File without changes