flpit 0.1.4.dev7__tar.gz → 0.1.6__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.6/PKG-INFO ADDED
@@ -0,0 +1,331 @@
1
+ Metadata-Version: 2.5
2
+ Name: flpit
3
+ Version: 0.1.6
4
+ Summary: Fluent LINQ for Python — flip your iterables
5
+ License: MIT
6
+ License-File: LICENSE
7
+ Keywords: fluent,generators,iterable,lazy-evaluation,linq,pipeline,pyrefly,type-safe
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
+ Classifier: Typing :: Typed
18
+ Requires-Python: >=3.11
19
+ Description-Content-Type: text/markdown
20
+
21
+ # FlpIt
22
+
23
+ > **Fluent LINQ for Python — LINQ semantics, Python-native execution**
24
+
25
+ `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.
26
+
27
+ > **Semantic goal:** `flp` strives to faithfully reproduce .NET LINQ semantics.
28
+ > Where Python's language or type system requires a different API contract, the contract is adapted accordingly.
29
+ > The implementation is actively developed, and semantic deviations may still exist.
30
+
31
+ ## ⚡ Key Features
32
+
33
+ * **⚡ Lazy Evaluation (`FlpIt`):** Deferred, streaming execution where the underlying operation permits it. `FlpIt` wraps a standard Python `Iterable[T]`.
34
+ * **📦 Materialized Container (`FlpList`):** Eager, mutable container backed by `collections.UserList`.
35
+ * **🎯 LINQ Semantics:** Operators such as `.any()`, `.first()`, `.single()`, `.order_by()`, and `.then_by()` follow .NET LINQ semantics.
36
+ * **🔀 Independent Query Pipelines:** Derived queries are independent and never mutate or retroactively affect separately created queries.
37
+ * **🧠 Lazy Internal Materialization:** Some operators, such as ordering and grouping, require internal materialization during deferred execution, while the query itself remains lazy.
38
+ * **🔄 Source Enumeration Semantics** — FlpIt preserves the enumeration characteristics of its source. A query over a re-enumerable source can be enumerated repeatedly; a one-shot source such as a Python generator remains one-shot. FlpIt does not clone or rewind arbitrary Python iterators.
39
+ * **🔹 Static-First Typing:** Designed for precise `pyrefly` inference without plugins (mypy and pyright might give varying results).
40
+ * **🐍 Python-Native:** Built on standard Python iterables, iterators, generators, and callable semantics.
41
+
42
+ ## Scope & Intent
43
+
44
+ FlpIt focuses exclusively on replicating the **`IEnumerable` (`System.Linq.Enumerable`)** model.
45
+
46
+ * **Direct Execution:** It operates directly on standard Python callables (lambdas and functions) over Python iterables.
47
+ * **No Provider Abstraction:** It does not model or emit Expression Trees (`IQueryable`) for translation by external providers. There is no query-provider layer that rewrites FlpIt operations into database queries or other backend-specific operations.
48
+
49
+ FlpIt therefore follows the execution model of in-memory LINQ: query operators build a pipeline, execution is deferred where the operator permits it, and each operator's actual enumeration and buffering behavior is part of the query semantics.
50
+
51
+ ## 🤸 Flipping the Pipeline
52
+
53
+ Native Python functional operations tend to produce an inside-out, right-to-left reading pattern...
54
+
55
+ ```python
56
+ # 🔁 Native Built-ins
57
+ result = list(map(lambda x: x * 10, filter(lambda x: x % 2 == 0, range(1, 11))))
58
+
59
+ # 🤸 FlpIt Pipeline
60
+ result = (
61
+ flp.it(range(1, 11))
62
+ .where(lambda x: x % 2 == 0)
63
+ .select(lambda x: x * 10)
64
+ .to_list()
65
+ )
66
+ ```
67
+
68
+ ## 💡 Quick Start
69
+
70
+ ```python
71
+ from flpit import flp, FlpIt, FlpList
72
+
73
+ # Deferred iterable via shorthand
74
+ data: FlpIt[int] = flp.it(range(1, 11))
75
+
76
+ # Lazy, streaming pipeline
77
+ query: FlpIt[int] = (
78
+ data
79
+ .where(lambda x: x % 2 == 0)
80
+ .select(lambda x: x * 10)
81
+ )
82
+
83
+ # Explicit materialization
84
+ result: FlpList[int] = query.to_list()
85
+ # [20, 40, 60, 80, 100]
86
+
87
+ # Or start directly with an eager container
88
+ eager_list: FlpList[int] = flp.lst([1, 2, 3, 4])
89
+ ```
90
+
91
+ ## Typed Domain Objects
92
+
93
+ ```python
94
+ from dataclasses import dataclass
95
+ from flpit import flp
96
+
97
+
98
+ @dataclass
99
+ class Employee:
100
+ name: str
101
+ role: str
102
+ salary: int
103
+
104
+
105
+ @dataclass
106
+ class Department:
107
+ name: str
108
+ employees: list[Employee]
109
+
110
+
111
+ company = [
112
+ Department(
113
+ "Engineering",
114
+ [
115
+ Employee("Alice", "Backend Engineer", 95_000),
116
+ Employee("Bob", "Frontend Engineer", 90_000),
117
+ ],
118
+ ),
119
+ Department(
120
+ "Design",
121
+ [
122
+ Employee("Charlie", "UI Designer", 82_000),
123
+ ],
124
+ ),
125
+ ]
126
+
127
+ # Extract the names of backend engineers earning at least 90,000.
128
+ backend_engineers = (
129
+ flp.it(company)
130
+ .select_many(lambda dept: dept.employees)
131
+ .where(lambda emp: emp.role == "Backend Engineer")
132
+ .where(lambda emp: emp.salary >= 90_000)
133
+ .select(lambda emp: emp.name)
134
+ )
135
+
136
+ # The query is evaluated when it is enumerated.
137
+ print(list(backend_engineers))
138
+ # ['Alice']
139
+ ```
140
+
141
+ ## 🔬 LINQ Semantics, Python Runtime
142
+
143
+ `flpit` deliberately separates the two:
144
+
145
+ - **LINQ defines the query semantics.**
146
+ - **Python provides the underlying iterable and execution model.**
147
+
148
+ For example:
149
+
150
+ ```python
151
+ query = flp.range(1, 11).where(lambda x: x % 2 == 0)
152
+
153
+ query.any() # LINQ-style terminal operation
154
+ query.first() # LINQ-style terminal operation
155
+ query.single() # LINQ-style terminal operation
156
+ ```
157
+
158
+ The query can also be consumed through normal Python built-ins:
159
+
160
+ ```python
161
+ any(query)
162
+ list(query)
163
+ ```
164
+
165
+ The two forms serve different purposes: LINQ-style operators provide the query API and its associated semantics, while Python built-ins continue to operate through the normal iterable protocol.
166
+
167
+ ## Source Repeatability
168
+
169
+ FlpIt does not imply that every source is repeatable. Re-enumeration depends on the underlying source:
170
+
171
+ ```python
172
+ source = [1, 2, 3]
173
+ query = flp.it(source)
174
+
175
+ list(query) # [1, 2, 3]
176
+ list(query) # [1, 2, 3]
177
+ ```
178
+
179
+ A one-shot iterator remains one-shot:
180
+
181
+ ```python
182
+ source = (x for x in range(3))
183
+ query = flp.it(source)
184
+
185
+ list(query) # [0, 1, 2]
186
+ list(query) # []
187
+ ```
188
+
189
+ ## Intentional Deviation from Python List Contracts (`.count()`)
190
+
191
+ To faithfully provide standard .NET LINQ semantics, `FlpList` deliberately uses LINQ-style count() semantics instead of Python list.count(value) semantics of Python's standard `list.count(value)`.
192
+
193
+ * **Vanilla Python `list.count(value)`** counts occurrences of a specific *value*.
194
+ * **FlpIt `FlpList.count()`** counts all elements.
195
+ * **FlpIt `FlpList.count(predicate)`** counts elements matching the predicate.
196
+
197
+ ```python
198
+ # Pure LINQ semantics instead of standard list behavior
199
+ total_evens = eager_list.count(lambda x: x % 2 == 0)
200
+ ```
201
+
202
+ Because `FlpList` is an explicit, fluent wrapper designed to enforce LINQ behavior, this signature change is a deliberate design choice. If you need the native Python behavior to count occurrences of a specific item value, use the explicitly provided fallback method:
203
+
204
+ ```python
205
+ # Use the fallback for traditional Python value-counting
206
+ occurrences = eager_list.count_item(4)
207
+ ```
208
+
209
+ ## 🔗 Independent Query Pipelines
210
+
211
+ Query pipelines can be composed independently without mutating the queries they were derived from:
212
+
213
+ For example:
214
+
215
+ ```python
216
+ @dataclass
217
+ class Item:
218
+ group: str
219
+ value: int
220
+
221
+ original = flp.it([Item("A", 2), Item("B", 1)])
222
+
223
+ ordered = original.order_by(lambda x: x.group)
224
+
225
+ extended = original.concat([Item("A", 1), Item("B", 2)])
226
+
227
+ ordered_by_group_and_value = ordered.then_by(lambda x: x.value)
228
+ ```
229
+
230
+ The resulting query graph is:
231
+
232
+ ```text
233
+ original
234
+ │
235
+ ├── order_by ──→ ordered
236
+ │ │
237
+ │ └── then_by ──→ ordered_by_group_and_value
238
+ │
239
+ └── concat ─────→ extended
240
+ ```
241
+
242
+ `ordered_by_group_and_value` is derived from `ordered`.
243
+ It does not modify `original`, and it is not affected by the separately created `extended` query.
244
+
245
+ This matches the independent-query behavior of .NET LINQ.
246
+
247
+ ## Execution Characteristics
248
+
249
+ Lazy execution does not mean that every operator is streaming. Some operators must consume and buffer their input before they can produce their result.
250
+ **Sorting requires buffering:** Any exact sort of an arbitrary input must inspect its elements before it can produce the ordered result. This is not specific to FlpIt; it is an inherent characteristic of sorting and applies equally to native Python sorting and .NET LINQ `OrderBy()`.
251
+
252
+ | Operator | Execution deferred | Processes incrementally | Buffers input |
253
+ | ---------------------- | ------------------ | ----------------------- | ------------- |
254
+ | `where()` / `select()` | Yes | Yes | No |
255
+ | `order_by()` | Yes | No | Yes |
256
+ | `group_by()` | Yes | No | Yes |
257
+
258
+ `order_by()` and `group_by()` therefore remain deferred at query construction time, but enumeration of either operator requires buffering its upstream input.
259
+
260
+ `group_by()` produces a query of groupings rather than terminating the pipeline:
261
+
262
+ ```python
263
+ groups = query.group_by(lambda x: x % 3)
264
+
265
+ result = (
266
+ groups
267
+ .where(...)
268
+ .select(...)
269
+ .to_list()
270
+ )
271
+ ```
272
+
273
+ Terminal operators instead evaluate the query and return a result rather than another query:
274
+
275
+ ```python
276
+ query.any()
277
+ query.first()
278
+ query.single()
279
+ query.count()
280
+ query.aggregate(...)
281
+ query.to_list()
282
+ ```
283
+
284
+ For example, `aggregate()` processes its source incrementally but must consume the source completely to produce its final accumulated value.
285
+
286
+ ## 🧠 Lazy Pipelines
287
+
288
+ Streaming operators do not materialize their upstream input:
289
+
290
+ ```python
291
+ query = (
292
+ flp.it(source)
293
+ .where(predicate)
294
+ .select(selector)
295
+ .order_by(key_selector)
296
+ .take(100)
297
+ )
298
+ ```
299
+
300
+ Conceptually:
301
+
302
+ ```text
303
+ source
304
+ │
305
+ ▼
306
+ where ← streaming
307
+ │
308
+ ▼
309
+ select ← streaming
310
+ │
311
+ ▼
312
+ order_by ← lazy, internally buffers when enumerated
313
+ │
314
+ ▼
315
+ take ← streaming
316
+ │
317
+ ▼
318
+ to_list ← terminal materialization
319
+ ```
320
+
321
+ Operations such as `order_by()` necessarily materialize their input internally to establish ordering.
322
+
323
+ ## ⚙️ Installation
324
+
325
+ ```bash
326
+ uv add flpit
327
+ ```
328
+
329
+ ## 📜 License
330
+
331
+ Distributed under the MIT License. See `LICENSE` for more information.
flpit-0.1.6/README.md ADDED
@@ -0,0 +1,311 @@
1
+ # FlpIt
2
+
3
+ > **Fluent LINQ for Python — LINQ semantics, Python-native execution**
4
+
5
+ `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.
6
+
7
+ > **Semantic goal:** `flp` strives to faithfully reproduce .NET LINQ semantics.
8
+ > Where Python's language or type system requires a different API contract, the contract is adapted accordingly.
9
+ > The implementation is actively developed, and semantic deviations may still exist.
10
+
11
+ ## ⚡ Key Features
12
+
13
+ * **⚡ Lazy Evaluation (`FlpIt`):** Deferred, streaming execution where the underlying operation permits it. `FlpIt` wraps a standard Python `Iterable[T]`.
14
+ * **📦 Materialized Container (`FlpList`):** Eager, mutable container backed by `collections.UserList`.
15
+ * **🎯 LINQ Semantics:** Operators such as `.any()`, `.first()`, `.single()`, `.order_by()`, and `.then_by()` follow .NET LINQ semantics.
16
+ * **🔀 Independent Query Pipelines:** Derived queries are independent and never mutate or retroactively affect separately created queries.
17
+ * **🧠 Lazy Internal Materialization:** Some operators, such as ordering and grouping, require internal materialization during deferred execution, while the query itself remains lazy.
18
+ * **🔄 Source Enumeration Semantics** — FlpIt preserves the enumeration characteristics of its source. A query over a re-enumerable source can be enumerated repeatedly; a one-shot source such as a Python generator remains one-shot. FlpIt does not clone or rewind arbitrary Python iterators.
19
+ * **🔹 Static-First Typing:** Designed for precise `pyrefly` inference without plugins (mypy and pyright might give varying results).
20
+ * **🐍 Python-Native:** Built on standard Python iterables, iterators, generators, and callable semantics.
21
+
22
+ ## Scope & Intent
23
+
24
+ FlpIt focuses exclusively on replicating the **`IEnumerable` (`System.Linq.Enumerable`)** model.
25
+
26
+ * **Direct Execution:** It operates directly on standard Python callables (lambdas and functions) over Python iterables.
27
+ * **No Provider Abstraction:** It does not model or emit Expression Trees (`IQueryable`) for translation by external providers. There is no query-provider layer that rewrites FlpIt operations into database queries or other backend-specific operations.
28
+
29
+ FlpIt therefore follows the execution model of in-memory LINQ: query operators build a pipeline, execution is deferred where the operator permits it, and each operator's actual enumeration and buffering behavior is part of the query semantics.
30
+
31
+ ## 🤸 Flipping the Pipeline
32
+
33
+ Native Python functional operations tend to produce an inside-out, right-to-left reading pattern...
34
+
35
+ ```python
36
+ # 🔁 Native Built-ins
37
+ result = list(map(lambda x: x * 10, filter(lambda x: x % 2 == 0, range(1, 11))))
38
+
39
+ # 🤸 FlpIt Pipeline
40
+ result = (
41
+ flp.it(range(1, 11))
42
+ .where(lambda x: x % 2 == 0)
43
+ .select(lambda x: x * 10)
44
+ .to_list()
45
+ )
46
+ ```
47
+
48
+ ## 💡 Quick Start
49
+
50
+ ```python
51
+ from flpit import flp, FlpIt, FlpList
52
+
53
+ # Deferred iterable via shorthand
54
+ data: FlpIt[int] = flp.it(range(1, 11))
55
+
56
+ # Lazy, streaming pipeline
57
+ query: FlpIt[int] = (
58
+ data
59
+ .where(lambda x: x % 2 == 0)
60
+ .select(lambda x: x * 10)
61
+ )
62
+
63
+ # Explicit materialization
64
+ result: FlpList[int] = query.to_list()
65
+ # [20, 40, 60, 80, 100]
66
+
67
+ # Or start directly with an eager container
68
+ eager_list: FlpList[int] = flp.lst([1, 2, 3, 4])
69
+ ```
70
+
71
+ ## Typed Domain Objects
72
+
73
+ ```python
74
+ from dataclasses import dataclass
75
+ from flpit import flp
76
+
77
+
78
+ @dataclass
79
+ class Employee:
80
+ name: str
81
+ role: str
82
+ salary: int
83
+
84
+
85
+ @dataclass
86
+ class Department:
87
+ name: str
88
+ employees: list[Employee]
89
+
90
+
91
+ company = [
92
+ Department(
93
+ "Engineering",
94
+ [
95
+ Employee("Alice", "Backend Engineer", 95_000),
96
+ Employee("Bob", "Frontend Engineer", 90_000),
97
+ ],
98
+ ),
99
+ Department(
100
+ "Design",
101
+ [
102
+ Employee("Charlie", "UI Designer", 82_000),
103
+ ],
104
+ ),
105
+ ]
106
+
107
+ # Extract the names of backend engineers earning at least 90,000.
108
+ backend_engineers = (
109
+ flp.it(company)
110
+ .select_many(lambda dept: dept.employees)
111
+ .where(lambda emp: emp.role == "Backend Engineer")
112
+ .where(lambda emp: emp.salary >= 90_000)
113
+ .select(lambda emp: emp.name)
114
+ )
115
+
116
+ # The query is evaluated when it is enumerated.
117
+ print(list(backend_engineers))
118
+ # ['Alice']
119
+ ```
120
+
121
+ ## 🔬 LINQ Semantics, Python Runtime
122
+
123
+ `flpit` deliberately separates the two:
124
+
125
+ - **LINQ defines the query semantics.**
126
+ - **Python provides the underlying iterable and execution model.**
127
+
128
+ For example:
129
+
130
+ ```python
131
+ query = flp.range(1, 11).where(lambda x: x % 2 == 0)
132
+
133
+ query.any() # LINQ-style terminal operation
134
+ query.first() # LINQ-style terminal operation
135
+ query.single() # LINQ-style terminal operation
136
+ ```
137
+
138
+ The query can also be consumed through normal Python built-ins:
139
+
140
+ ```python
141
+ any(query)
142
+ list(query)
143
+ ```
144
+
145
+ The two forms serve different purposes: LINQ-style operators provide the query API and its associated semantics, while Python built-ins continue to operate through the normal iterable protocol.
146
+
147
+ ## Source Repeatability
148
+
149
+ FlpIt does not imply that every source is repeatable. Re-enumeration depends on the underlying source:
150
+
151
+ ```python
152
+ source = [1, 2, 3]
153
+ query = flp.it(source)
154
+
155
+ list(query) # [1, 2, 3]
156
+ list(query) # [1, 2, 3]
157
+ ```
158
+
159
+ A one-shot iterator remains one-shot:
160
+
161
+ ```python
162
+ source = (x for x in range(3))
163
+ query = flp.it(source)
164
+
165
+ list(query) # [0, 1, 2]
166
+ list(query) # []
167
+ ```
168
+
169
+ ## Intentional Deviation from Python List Contracts (`.count()`)
170
+
171
+ To faithfully provide standard .NET LINQ semantics, `FlpList` deliberately uses LINQ-style count() semantics instead of Python list.count(value) semantics of Python's standard `list.count(value)`.
172
+
173
+ * **Vanilla Python `list.count(value)`** counts occurrences of a specific *value*.
174
+ * **FlpIt `FlpList.count()`** counts all elements.
175
+ * **FlpIt `FlpList.count(predicate)`** counts elements matching the predicate.
176
+
177
+ ```python
178
+ # Pure LINQ semantics instead of standard list behavior
179
+ total_evens = eager_list.count(lambda x: x % 2 == 0)
180
+ ```
181
+
182
+ Because `FlpList` is an explicit, fluent wrapper designed to enforce LINQ behavior, this signature change is a deliberate design choice. If you need the native Python behavior to count occurrences of a specific item value, use the explicitly provided fallback method:
183
+
184
+ ```python
185
+ # Use the fallback for traditional Python value-counting
186
+ occurrences = eager_list.count_item(4)
187
+ ```
188
+
189
+ ## 🔗 Independent Query Pipelines
190
+
191
+ Query pipelines can be composed independently without mutating the queries they were derived from:
192
+
193
+ For example:
194
+
195
+ ```python
196
+ @dataclass
197
+ class Item:
198
+ group: str
199
+ value: int
200
+
201
+ original = flp.it([Item("A", 2), Item("B", 1)])
202
+
203
+ ordered = original.order_by(lambda x: x.group)
204
+
205
+ extended = original.concat([Item("A", 1), Item("B", 2)])
206
+
207
+ ordered_by_group_and_value = ordered.then_by(lambda x: x.value)
208
+ ```
209
+
210
+ The resulting query graph is:
211
+
212
+ ```text
213
+ original
214
+ │
215
+ ├── order_by ──→ ordered
216
+ │ │
217
+ │ └── then_by ──→ ordered_by_group_and_value
218
+ │
219
+ └── concat ─────→ extended
220
+ ```
221
+
222
+ `ordered_by_group_and_value` is derived from `ordered`.
223
+ It does not modify `original`, and it is not affected by the separately created `extended` query.
224
+
225
+ This matches the independent-query behavior of .NET LINQ.
226
+
227
+ ## Execution Characteristics
228
+
229
+ Lazy execution does not mean that every operator is streaming. Some operators must consume and buffer their input before they can produce their result.
230
+ **Sorting requires buffering:** Any exact sort of an arbitrary input must inspect its elements before it can produce the ordered result. This is not specific to FlpIt; it is an inherent characteristic of sorting and applies equally to native Python sorting and .NET LINQ `OrderBy()`.
231
+
232
+ | Operator | Execution deferred | Processes incrementally | Buffers input |
233
+ | ---------------------- | ------------------ | ----------------------- | ------------- |
234
+ | `where()` / `select()` | Yes | Yes | No |
235
+ | `order_by()` | Yes | No | Yes |
236
+ | `group_by()` | Yes | No | Yes |
237
+
238
+ `order_by()` and `group_by()` therefore remain deferred at query construction time, but enumeration of either operator requires buffering its upstream input.
239
+
240
+ `group_by()` produces a query of groupings rather than terminating the pipeline:
241
+
242
+ ```python
243
+ groups = query.group_by(lambda x: x % 3)
244
+
245
+ result = (
246
+ groups
247
+ .where(...)
248
+ .select(...)
249
+ .to_list()
250
+ )
251
+ ```
252
+
253
+ Terminal operators instead evaluate the query and return a result rather than another query:
254
+
255
+ ```python
256
+ query.any()
257
+ query.first()
258
+ query.single()
259
+ query.count()
260
+ query.aggregate(...)
261
+ query.to_list()
262
+ ```
263
+
264
+ For example, `aggregate()` processes its source incrementally but must consume the source completely to produce its final accumulated value.
265
+
266
+ ## 🧠 Lazy Pipelines
267
+
268
+ Streaming operators do not materialize their upstream input:
269
+
270
+ ```python
271
+ query = (
272
+ flp.it(source)
273
+ .where(predicate)
274
+ .select(selector)
275
+ .order_by(key_selector)
276
+ .take(100)
277
+ )
278
+ ```
279
+
280
+ Conceptually:
281
+
282
+ ```text
283
+ source
284
+ │
285
+ ▼
286
+ where ← streaming
287
+ │
288
+ ▼
289
+ select ← streaming
290
+ │
291
+ ▼
292
+ order_by ← lazy, internally buffers when enumerated
293
+ │
294
+ ▼
295
+ take ← streaming
296
+ │
297
+ ▼
298
+ to_list ← terminal materialization
299
+ ```
300
+
301
+ Operations such as `order_by()` necessarily materialize their input internally to establish ordering.
302
+
303
+ ## ⚙️ Installation
304
+
305
+ ```bash
306
+ uv add flpit
307
+ ```
308
+
309
+ ## 📜 License
310
+
311
+ Distributed under the MIT License. See `LICENSE` for more information.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "flpit"
3
- version = "0.1.4.dev7"
3
+ version = "0.1.6"
4
4
  description = "Fluent LINQ for Python — flip your iterables"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -29,7 +29,13 @@ TKey = TypeVar("TKey")
29
29
  TOther = TypeVar("TOther")
30
30
  TAccumulate = TypeVar("TAccumulate")
31
31
 
32
- _SENTINEL = object()
32
+ class _Sentinel:
33
+ __slots__ = ()
34
+
35
+ def __call__(self, *args: object, **kwargs: object) -> Any:
36
+ raise TypeError("_SENTINEL cannot be called")
37
+
38
+ _SENTINEL = _Sentinel()
33
39
  _MISSING = object()
34
40
 
35
41
 
@@ -129,7 +135,7 @@ class FlpIt(Iterable[TItem], Generic[TItem]):
129
135
 
130
136
  def any(
131
137
  self,
132
- predicate: Optional[Callable[[TItem], bool]] = None,
138
+ predicate: Callable[[TItem], bool] | _Sentinel = _SENTINEL,
133
139
  ) -> bool:
134
140
  """
135
141
  Determines whether the sequence contains any elements,
@@ -141,7 +147,7 @@ class FlpIt(Iterable[TItem], Generic[TItem]):
141
147
 
142
148
  Evaluation stops as soon as the result is known.
143
149
  """
144
- if predicate is not None:
150
+ if predicate is not _SENTINEL:
145
151
  return builtins.any(map(predicate, self))
146
152
 
147
153
  iterator = iter(self)
@@ -288,12 +294,12 @@ class FlpIt(Iterable[TItem], Generic[TItem]):
288
294
  def zip(
289
295
  self,
290
296
  second: Iterable[TOther],
291
- result_selector: Optional[Callable[[TItem, TOther], Any]] = None,
297
+ result_selector: Callable[[TItem, TOther], Any] | _Sentinel = _SENTINEL,
292
298
  ) -> FlpIt[Any]:
293
299
  """Applies a specified function to corresponding elements of two sequences."""
294
300
  def _generator() -> Iterator[Any]:
295
301
  for first_item, second_item in zip(self, second):
296
- if result_selector is not None:
302
+ if result_selector is not _SENTINEL:
297
303
  yield result_selector(first_item, second_item)
298
304
  else:
299
305
  yield first_item, second_item
@@ -343,7 +349,7 @@ class FlpIt(Iterable[TItem], Generic[TItem]):
343
349
 
344
350
  return FlpIt(_FactoryIterable(_generator))
345
351
 
346
- # --- Immediate Execution (Materialization & Aggregation) ---
352
+ # --- Immediate Execution (Materialization & Aggregation) ---
347
353
 
348
354
  @overload
349
355
  def aggregate(self, func: Callable[[TItem, TItem], TItem]) -> TItem: ...
@@ -355,9 +361,9 @@ class FlpIt(Iterable[TItem], Generic[TItem]):
355
361
 
356
362
  def aggregate(
357
363
  self,
358
- func: Callable[[Any, TItem], Any],
364
+ func: Callable[[Any, Any], Any],
359
365
  seed: Any = _SENTINEL,
360
- ) -> Any:
366
+ ) -> TItem | TAccumulate:
361
367
  """Applies an accumulator function over a sequence."""
362
368
  it = iter(self)
363
369
  if seed is _SENTINEL:
@@ -381,7 +387,7 @@ class FlpIt(Iterable[TItem], Generic[TItem]):
381
387
 
382
388
  def last(
383
389
  self,
384
- predicate: Optional[Callable[[TItem], bool]] = _SENTINEL,
390
+ predicate: Callable[[TItem], bool] | _Sentinel = _SENTINEL,
385
391
  ) -> TItem:
386
392
  """
387
393
  Returns the last element of the sequence, or the last element
@@ -433,12 +439,12 @@ class FlpIt(Iterable[TItem], Generic[TItem]):
433
439
  return self._extreme(builtins.max, key=key_selector)
434
440
 
435
441
  def average(
436
- self, selector: Optional[Callable[[TItem], Union[int, float]]] = None
442
+ self, selector: Callable[[TItem], Union[int, float]] | _Sentinel = _SENTINEL
437
443
  ) -> float:
438
444
  """Computes the arithmetic mean of the sequence, optionally applying a selector."""
439
445
  total = 0.0
440
446
  count = 0
441
- query = (selector(x) for x in self) if selector is not None else self
447
+ query = (selector(x) for x in self) if selector is not _SENTINEL else self
442
448
  for item in query:
443
449
  total += float(item) # type: ignore[arg-type]
444
450
  count += 1
@@ -462,17 +468,22 @@ class FlpIt(Iterable[TItem], Generic[TItem]):
462
468
  avg_by = average_by
463
469
 
464
470
  def sum(
465
- self, selector: Optional[Callable[[TItem], Union[int, float]]] = None
471
+ self, selector: Callable[[TItem], Union[int, float]] | _Sentinel = _SENTINEL
466
472
  ) -> Union[int, float]:
467
473
  """Calculates the sum of the sequence, optionally applying a selector."""
468
- if selector is not None:
474
+ if selector is not _SENTINEL:
469
475
  return builtins.sum(selector(x) for x in self)
470
476
  return builtins.sum(self) # type: ignore[arg-type]
471
477
 
472
- def count(self, predicate: Optional[Callable[[TItem], bool]] = None) -> int:
473
- """Counts elements in the sequence matching an optional predicate."""
474
- query = self.where(predicate) if predicate is not None else self
475
- return builtins.sum(1 for _ in query)
478
+ def count(
479
+ self,
480
+ predicate: Callable[[TItem], bool] | _Sentinel = _SENTINEL,
481
+ ) -> int:
482
+ """Counts elements in the sequence, optionally matching a predicate."""
483
+ if predicate is _SENTINEL:
484
+ return builtins.sum(1 for _ in self)
485
+
486
+ return builtins.sum(1 for _ in self.where(predicate))
476
487
 
477
488
  def element_at(self, index: int) -> TItem:
478
489
  """Returns the element at a specified index in a sequence."""
@@ -483,15 +494,15 @@ class FlpIt(Iterable[TItem], Generic[TItem]):
483
494
  return item
484
495
  raise IndexError("Index out of range")
485
496
 
486
- def first(self, predicate: Optional[Callable[[TItem], bool]] = None) -> TItem:
497
+ def first(self, predicate: Callable[[TItem], bool] | _Sentinel = _SENTINEL) -> TItem:
487
498
  """Returns the first element matching a predicate, or raises ValueError."""
488
- query = self.where(predicate) if predicate is not None else self
499
+ query = self.where(predicate) if predicate is not _SENTINEL else self
489
500
  for item in query:
490
501
  return item
491
- raise EmptySequenceError() if predicate is None else NoMatchError()
502
+ raise EmptySequenceError() if predicate is _SENTINEL else NoMatchError()
492
503
 
493
504
  def first_or_default(
494
- self, default: TResult, predicate: Optional[Callable[[TItem], bool]] = None
505
+ self, default: TResult, predicate: Callable[[TItem], bool] | _Sentinel = _SENTINEL
495
506
  ) -> Union[TItem, TResult]:
496
507
  """Returns the first element matching a predicate, or a default value."""
497
508
  try:
@@ -499,12 +510,8 @@ class FlpIt(Iterable[TItem], Generic[TItem]):
499
510
  except (EmptySequenceError, NoMatchError):
500
511
  return default
501
512
 
502
- # pyrefly: ignore [bad-function-definition]
503
- def single(self, predicate: Optional[Callable[[TItem], bool]] = _SENTINEL) -> TItem:
513
+ def single(self, predicate: Callable[[TItem], bool] | _Sentinel = _SENTINEL) -> TItem:
504
514
  """Returns the single, unique element matching a predicate."""
505
- if predicate is None:
506
- raise TypeError("Predicate cannot be None")
507
-
508
515
  has_predicate = predicate is not _SENTINEL
509
516
  query = self.where(predicate) if has_predicate else self
510
517
  it = iter(query)
@@ -702,12 +709,16 @@ class FlpList(UserList[TItem], Sequence[TItem], Generic[TItem]):
702
709
  | Fluent List
703
710
  A materialized list extending UserList that yields lazy FlpIt instances for query operations.
704
711
  """
705
- def __init__(self, source: TItem=_SENTINEL):
712
+ def __init__(self, source: Iterable[TItem] | _Sentinel=_SENTINEL):
706
713
  if source is None:
707
714
  raise SourceNoneError()
715
+
708
716
  if source is _SENTINEL:
709
- source = [] # don't depend on the base class to do the right thing
710
- super().__init__(source)
717
+ init_list: Iterable[TItem] = list[TItem]() # don't depend on the base class to do the right thing
718
+ else:
719
+ init_list = typing_cast(Iterable[TItem], source)
720
+
721
+ super().__init__(init_list)
711
722
 
712
723
  def add(self, item: TItem) -> None:
713
724
  """
@@ -752,9 +763,9 @@ class FlpList(UserList[TItem], Sequence[TItem], Generic[TItem]):
752
763
 
753
764
  def any(
754
765
  self,
755
- predicate: Optional[Callable[[TItem], bool]] = None,
766
+ predicate: Callable[[TItem], bool] | _Sentinel = _SENTINEL,
756
767
  ) -> bool:
757
- if predicate is None:
768
+ if predicate is _SENTINEL:
758
769
  return len(self.data) > 0
759
770
 
760
771
  return any(predicate(item) for item in self.data)
@@ -818,9 +829,8 @@ class FlpList(UserList[TItem], Sequence[TItem], Generic[TItem]):
818
829
  def zip(
819
830
  self,
820
831
  second: Iterable[TOther],
821
- result_selector: Optional[Callable[[TItem, TOther], Any]] = None,
832
+ result_selector: Callable[[TItem, TOther], Any] | _Sentinel = _SENTINEL,
822
833
  ) -> FlpIt[Any]:
823
- # pyrefly: ignore [bad-argument-type] # yeah well if overloads are just pretend
824
834
  return FlpIt(self.data).zip(second, result_selector)
825
835
 
826
836
  def chunk(self, size: int) -> FlpIt[FlpList[TItem]]:
@@ -853,7 +863,7 @@ class FlpList(UserList[TItem], Sequence[TItem], Generic[TItem]):
853
863
  self,
854
864
  func: Callable[[Any, Any], Any],
855
865
  seed: Any = _SENTINEL,
856
- ) -> Any:
866
+ ) -> TItem | TAccumulate:
857
867
  return FlpIt(self.data).aggregate(func, seed=seed)
858
868
 
859
869
  @_guard_empty
@@ -873,68 +883,44 @@ class FlpList(UserList[TItem], Sequence[TItem], Generic[TItem]):
873
883
  return builtins.max(self.data, key=key_selector)
874
884
 
875
885
  def sum(
876
- self, selector: Optional[Callable[[TItem], Union[int, float]]] = None
877
- ) -> Union[int, float]:
886
+ self, selector: Callable[[TItem], int | float] | _Sentinel = _SENTINEL
887
+ ) -> int | float:
878
888
  """Calculates the sum of elements, optionally applying a selector."""
879
889
  return FlpIt(self.data).sum(selector)
880
890
 
881
891
  def average(
882
- self, selector: Optional[Callable[[TItem], Union[int, float]]] = None
892
+ self, selector: Callable[[TItem], int | float] | _Sentinel = _SENTINEL
883
893
  ) -> float:
884
894
  """Calculates the arithmetic mean, optionally applying a selector."""
885
895
  return FlpIt(self.data).average(selector)
886
896
 
887
- # --- Overloads for the Type System ---
888
- # 1. Native Python UserList compatibility path (MUST be first): exact value match
889
- @overload
890
- def count(self, item: TItem) -> int: ...
891
-
892
- # 2. LINQ style path: matching via predicate function
893
- @overload
894
- def count(self, item: Callable[[TItem], bool]) -> int: ...
895
-
896
- # 3. LINQ style path: no arguments (counts everything)
897
- @overload
898
- def count(self) -> int: ...
897
+ def count_item(self, item: TItem) -> int:
898
+ return self.data.count(item) # just redirect how the count redirected before
899
899
 
900
- # --- The Clean Implementation ---
901
- # We name the parameter 'item' to perfectly match UserList, but default it to None
902
900
  @override
903
- def count(self, item: Any = _SENTINEL) -> int:
901
+ def count( # type: ignore # pyrefly: ignore [bad-override-param-name] # pyright: ignore[reportIncompatibleMethodOverride]
902
+ self,
903
+ predicate: Callable[[TItem], bool] | _Sentinel = _SENTINEL,
904
+ ) -> int:
904
905
  """
905
- Counts elements using Python/UserList or .NET LINQ conventions.
906
+ Counts elements using LINQ semantics.
906
907
 
907
908
  Supported forms:
908
909
 
909
- count() -> count all elements (.NET LINQ)
910
- count(predicate) -> count elements matching predicate (.NET LINQ)
911
- count(value) -> count occurrences of an exact value (Python)
912
-
913
- Note:
914
- Because Python callables are also valid list values, ``count(value)``
915
- is inherently ambiguous when ``value`` is callable. A callable
916
- argument is interpreted as a LINQ predicate, so counting occurrences
917
- of a callable object via the native ``UserList.count(value)`` behavior
918
- is not distinguishable from ``count(predicate)``.
910
+ count() -> total number of elements
911
+ count(predicate) -> number of elements satisfying the predicate
919
912
  """
920
- # Scenario C: No argument passed (.count()) -> Return total length
921
- if item is _SENTINEL:
913
+ if predicate is _SENTINEL:
922
914
  return len(self.data)
923
915
 
924
- # Scenario A: A LINQ predicate function was passed
925
- if callable(item):
926
- return FlpIt(self.data).count(item)
927
-
928
- # Scenario B: An exact raw value was passed (.count(4))
929
- # Invokes the original parent implementation of UserList to remain 100% compliant
930
- return super().count(item)
916
+ return FlpIt(self.data).count(predicate)
931
917
 
932
918
  def element_at(self, index: int) -> TItem:
933
919
  if index < 0 or index >= len(self.data):
934
920
  raise IndexError("Index out of range")
935
921
  return self.data[index]
936
922
 
937
- def first(self, predicate: Optional[Callable[[TItem], bool]] = None) -> TItem:
923
+ def first(self, predicate: Callable[[TItem], bool] | _Sentinel = _SENTINEL) -> TItem:
938
924
  return FlpIt(self.data).first(predicate)
939
925
 
940
926
  @overload
@@ -945,7 +931,7 @@ class FlpList(UserList[TItem], Sequence[TItem], Generic[TItem]):
945
931
 
946
932
  def last(
947
933
  self,
948
- predicate: Optional[Callable[[TItem], bool]] = _SENTINEL,
934
+ predicate: Callable[[TItem], bool] | _Sentinel = _SENTINEL,
949
935
  ) -> TItem:
950
936
  """
951
937
  Returns the last element of the sequence, or the last element
@@ -970,15 +956,14 @@ class FlpList(UserList[TItem], Sequence[TItem], Generic[TItem]):
970
956
  if predicate(item):
971
957
  return item
972
958
 
973
- raise EmptySequenceError() if predicate is None else NoMatchError()
959
+ raise EmptySequenceError() if predicate is _SENTINEL else NoMatchError()
974
960
 
975
961
  def first_or_default(
976
- self, default: TResult, predicate: Optional[Callable[[TItem], bool]] = None
977
- ) -> Union[TItem, TResult]:
962
+ self, default: TResult, predicate: Callable[[TItem], bool] | _Sentinel = _SENTINEL
963
+ ) -> TItem | TResult:
978
964
  return FlpIt(self.data).first_or_default(default, predicate)
979
965
 
980
- # pyrefly: ignore [bad-function-definition]
981
- def single(self, predicate: Optional[Callable[[TItem], bool]] = _SENTINEL) -> TItem:
966
+ def single(self, predicate: Callable[[TItem], bool] | _Sentinel = _SENTINEL) -> TItem:
982
967
  return FlpIt(self.data).single(predicate)
983
968
 
984
969
  def to_list(self) -> FlpList[TItem]:
flpit-0.1.4.dev7/PKG-INFO DELETED
@@ -1,187 +0,0 @@
1
- Metadata-Version: 2.5
2
- Name: flpit
3
- Version: 0.1.4.dev7
4
- Summary: Fluent LINQ for Python — flip your iterables
5
- License: MIT
6
- License-File: LICENSE
7
- Keywords: fluent,generators,iterable,lazy-evaluation,linq,pipeline,pyrefly,type-safe
8
- Classifier: Development Status :: 3 - Alpha
9
- Classifier: Intended Audience :: Developers
10
- Classifier: License :: OSI Approved :: MIT License
11
- Classifier: Programming Language :: Python :: 3
12
- Classifier: Programming Language :: Python :: 3.11
13
- Classifier: Programming Language :: Python :: 3.12
14
- Classifier: Programming Language :: Python :: 3.13
15
- Classifier: Programming Language :: Python :: 3.14
16
- Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
- Classifier: Typing :: Typed
18
- Requires-Python: >=3.11
19
- Description-Content-Type: text/markdown
20
-
21
- # FlpIt
22
-
23
- > **Fluent LINQ for Python — LINQ semantics, Python-native execution**
24
-
25
- `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.
26
-
27
- > **Semantic goal:** `flp` strives to faithfully reproduce .NET LINQ semantics.
28
- > Where Python's language or type system requires a different API contract, the contract is adapted accordingly.
29
- > The implementation is actively developed, and semantic deviations may still exist.
30
-
31
- ## ⚡ Key Features
32
-
33
- * **⚡ Lazy Evaluation (`FlpIt`):** Deferred, streaming execution where the underlying operation permits it. `FlpIt` wraps a standard Python `Iterable[T]`.
34
- * **📦 Materialized Container (`FlpList`):** Eager, mutable container backed by `collections.UserList`.
35
- * **🎯 LINQ Semantics:** Operators such as `.any()`, `.first()`, `.single()`, `.order_by()`, and `.then_by()` follow .NET LINQ semantics.
36
- * **🔀 Independent Query Pipelines:** Derived queries are independent and never mutate or retroactively affect separately created queries.
37
- * **🧠 Lazy Internal Materialization:** Some operators, such as ordering, require internal materialization during deferred execution, while the query itself remains lazy.
38
- * **🔹 Static-First Typing:** Designed for precise `pyrefly` inference without plugins (mypy and pyright might give varying results).
39
- * **🐍 Python-Native:** Built on standard Python iterables, iterators, generators, and callable semantics.
40
-
41
- ## 🤸 Flipping the Pipeline
42
-
43
- Native Python functional operations tend to produce an inside-out, right-to-left reading pattern...
44
-
45
- ```python
46
- # 🔁 Native Built-ins
47
- result = list(map(lambda x: x * 10, filter(lambda x: x % 2 == 0, range(1, 11))))
48
-
49
- # 🤸 FlpIt Pipeline
50
- result = (
51
- flp.it(range(1, 11))
52
- .where(lambda x: x % 2 == 0)
53
- .select(lambda x: x * 10)
54
- .to_list()
55
- )
56
- ```
57
-
58
- ## 💡 Quick Start
59
-
60
- ```python
61
-
62
- from flpit import flp, FlpIt, FlpList
63
-
64
- # Deferred iterable via shorthand
65
- data: FlpIt[int] = flp.it(range(1, 11))
66
-
67
- # Lazy, streaming pipeline
68
- query: FlpIt[int] = (
69
- data
70
- .where(lambda x: x % 2 == 0)
71
- .select(lambda x: x * 10)
72
- )
73
-
74
- # Explicit materialization
75
- result: FlpList[int] = query.to_list()
76
- # [20, 40, 60, 80, 100]
77
-
78
- # Or start directly with an eager container
79
- eager_list: FlpList[int] = flp.lst([1, 2, 3, 4])
80
- ```
81
-
82
- ## 🔬 LINQ Semantics, Python Runtime
83
-
84
- `flpit` deliberately separates the two:
85
-
86
- - **LINQ defines the query semantics.**
87
- - **Python provides the underlying iterable and execution model.**
88
-
89
- For example:
90
-
91
- ```python
92
- query.any()
93
- query.first()
94
- query.single()
95
- ```
96
-
97
- use LINQ semantics, while native Python operations remain Pythonic:
98
-
99
- ```python
100
- any(query)
101
- list(query)
102
- ```
103
-
104
- This provides a predictable LINQ vocabulary without attempting to replace Python's native iteration model.
105
-
106
- ## 🔗 Independent Queries
107
-
108
- LINQ queries are composable without mutating the query they were derived from.
109
-
110
- For example:
111
-
112
- ```python
113
- @dataclass
114
- class Item: group: str; value: int
115
-
116
- original = flp.it([Item("A", 2), Item("B", 1)])
117
-
118
- ordered = original.order_by(lambda x: x.group)
119
-
120
- extended = original.concat([Item("A", 1), Item("B", 2)])
121
-
122
- ordered_by_group_and_value = ordered.then_by(lambda x: x.value)
123
- ```
124
-
125
- The resulting query graph is:
126
-
127
- ```text
128
- original
129
- │
130
- ├── order_by ──→ ordered
131
- │ │
132
- │ └── then_by ──→ ordered_by_group_and_value
133
- │
134
- └── concat ─────→ extended
135
- ```
136
-
137
- `ordered_by_group_and_value` is derived from `ordered`.
138
- It does not modify `original`, and it is not affected by the separately created `extended` query.
139
-
140
- This matches the independent-query behavior of .NET LINQ.
141
-
142
- ## 🧠 Lazy Pipelines
143
-
144
- Streaming operations do not materialize intermediate results:
145
-
146
- ```python
147
- query = (
148
- flp.it(source)
149
- .where(predicate)
150
- .select(selector)
151
- .order_by(key_selector)
152
- .take(100)
153
- )
154
- ```
155
-
156
- Conceptually:
157
-
158
- ```text
159
- source
160
- │
161
- ▼
162
- where ← streaming
163
- │
164
- ▼
165
- select ← streaming
166
- │
167
- ▼
168
- order_by ← lazy, internally buffers when enumerated
169
- │
170
- ▼
171
- take ← streaming
172
- │
173
- ▼
174
- to_list ← consumer-facing materialization
175
- ```
176
-
177
- Operations such as `order_by()` necessarily materialize their input internally to establish ordering.
178
-
179
- ## ⚙️ Installation
180
-
181
- ```bash
182
- uv add flpit
183
- ```
184
-
185
- ## 📜 License
186
-
187
- Distributed under the MIT License. See `LICENSE` for more information.
@@ -1,167 +0,0 @@
1
- # FlpIt
2
-
3
- > **Fluent LINQ for Python — LINQ semantics, Python-native execution**
4
-
5
- `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.
6
-
7
- > **Semantic goal:** `flp` strives to faithfully reproduce .NET LINQ semantics.
8
- > Where Python's language or type system requires a different API contract, the contract is adapted accordingly.
9
- > The implementation is actively developed, and semantic deviations may still exist.
10
-
11
- ## ⚡ Key Features
12
-
13
- * **⚡ Lazy Evaluation (`FlpIt`):** Deferred, streaming execution where the underlying operation permits it. `FlpIt` wraps a standard Python `Iterable[T]`.
14
- * **📦 Materialized Container (`FlpList`):** Eager, mutable container backed by `collections.UserList`.
15
- * **🎯 LINQ Semantics:** Operators such as `.any()`, `.first()`, `.single()`, `.order_by()`, and `.then_by()` follow .NET LINQ semantics.
16
- * **🔀 Independent Query Pipelines:** Derived queries are independent and never mutate or retroactively affect separately created queries.
17
- * **🧠 Lazy Internal Materialization:** Some operators, such as ordering, require internal materialization during deferred execution, while the query itself remains lazy.
18
- * **🔹 Static-First Typing:** Designed for precise `pyrefly` inference without plugins (mypy and pyright might give varying results).
19
- * **🐍 Python-Native:** Built on standard Python iterables, iterators, generators, and callable semantics.
20
-
21
- ## 🤸 Flipping the Pipeline
22
-
23
- Native Python functional operations tend to produce an inside-out, right-to-left reading pattern...
24
-
25
- ```python
26
- # 🔁 Native Built-ins
27
- result = list(map(lambda x: x * 10, filter(lambda x: x % 2 == 0, range(1, 11))))
28
-
29
- # 🤸 FlpIt Pipeline
30
- result = (
31
- flp.it(range(1, 11))
32
- .where(lambda x: x % 2 == 0)
33
- .select(lambda x: x * 10)
34
- .to_list()
35
- )
36
- ```
37
-
38
- ## 💡 Quick Start
39
-
40
- ```python
41
-
42
- from flpit import flp, FlpIt, FlpList
43
-
44
- # Deferred iterable via shorthand
45
- data: FlpIt[int] = flp.it(range(1, 11))
46
-
47
- # Lazy, streaming pipeline
48
- query: FlpIt[int] = (
49
- data
50
- .where(lambda x: x % 2 == 0)
51
- .select(lambda x: x * 10)
52
- )
53
-
54
- # Explicit materialization
55
- result: FlpList[int] = query.to_list()
56
- # [20, 40, 60, 80, 100]
57
-
58
- # Or start directly with an eager container
59
- eager_list: FlpList[int] = flp.lst([1, 2, 3, 4])
60
- ```
61
-
62
- ## 🔬 LINQ Semantics, Python Runtime
63
-
64
- `flpit` deliberately separates the two:
65
-
66
- - **LINQ defines the query semantics.**
67
- - **Python provides the underlying iterable and execution model.**
68
-
69
- For example:
70
-
71
- ```python
72
- query.any()
73
- query.first()
74
- query.single()
75
- ```
76
-
77
- use LINQ semantics, while native Python operations remain Pythonic:
78
-
79
- ```python
80
- any(query)
81
- list(query)
82
- ```
83
-
84
- This provides a predictable LINQ vocabulary without attempting to replace Python's native iteration model.
85
-
86
- ## 🔗 Independent Queries
87
-
88
- LINQ queries are composable without mutating the query they were derived from.
89
-
90
- For example:
91
-
92
- ```python
93
- @dataclass
94
- class Item: group: str; value: int
95
-
96
- original = flp.it([Item("A", 2), Item("B", 1)])
97
-
98
- ordered = original.order_by(lambda x: x.group)
99
-
100
- extended = original.concat([Item("A", 1), Item("B", 2)])
101
-
102
- ordered_by_group_and_value = ordered.then_by(lambda x: x.value)
103
- ```
104
-
105
- The resulting query graph is:
106
-
107
- ```text
108
- original
109
- │
110
- ├── order_by ──→ ordered
111
- │ │
112
- │ └── then_by ──→ ordered_by_group_and_value
113
- │
114
- └── concat ─────→ extended
115
- ```
116
-
117
- `ordered_by_group_and_value` is derived from `ordered`.
118
- It does not modify `original`, and it is not affected by the separately created `extended` query.
119
-
120
- This matches the independent-query behavior of .NET LINQ.
121
-
122
- ## 🧠 Lazy Pipelines
123
-
124
- Streaming operations do not materialize intermediate results:
125
-
126
- ```python
127
- query = (
128
- flp.it(source)
129
- .where(predicate)
130
- .select(selector)
131
- .order_by(key_selector)
132
- .take(100)
133
- )
134
- ```
135
-
136
- Conceptually:
137
-
138
- ```text
139
- source
140
- │
141
- ▼
142
- where ← streaming
143
- │
144
- ▼
145
- select ← streaming
146
- │
147
- ▼
148
- order_by ← lazy, internally buffers when enumerated
149
- │
150
- ▼
151
- take ← streaming
152
- │
153
- ▼
154
- to_list ← consumer-facing materialization
155
- ```
156
-
157
- Operations such as `order_by()` necessarily materialize their input internally to establish ordering.
158
-
159
- ## ⚙️ Installation
160
-
161
- ```bash
162
- uv add flpit
163
- ```
164
-
165
- ## 📜 License
166
-
167
- Distributed under the MIT License. See `LICENSE` for more information.
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes