flpit 0.1.4.dev5__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/.gitignore ADDED
@@ -0,0 +1,10 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
flpit-0.1.6/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 riemar
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
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.dev5"
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,14 +29,19 @@ dev = [
29
29
  "pytest-benchmark>=5.3.0",
30
30
  ]
31
31
 
32
+
32
33
  [build-system]
33
- requires = ["uv_build>=0.12.10,<0.13.0"]
34
- build-backend = "uv_build"
34
+ requires = ["hatchling"]
35
+ build-backend = "hatchling.build"
36
+
37
+ [tool.hatch.build.targets.wheel]
38
+ # This tells Hatchling exactly which folders to grab
39
+ packages = ["src/flp", "src/flpit"]
40
+
41
+ [tool.hatch.build.targets.sdist]
42
+ # This keeps the exact working sdist inclusion rule you had earlier
43
+ include = ["src/flp", "src/flpit"]
35
44
 
36
- [tool.uv.build-backend]
37
- module-name = "flpit"
38
- namespace = true
39
- source-include = ["src/flp/**/*", "src/flpit/**/*"]
40
45
 
41
46
  [tool.pyrefly]
42
47
  project-includes = ["src"]