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 +10 -0
- flpit-0.1.6/LICENSE +21 -0
- flpit-0.1.6/PKG-INFO +331 -0
- flpit-0.1.6/README.md +311 -0
- flpit-0.1.4.dev5/pyproject.toml.orig → flpit-0.1.6/pyproject.toml +12 -7
- {flpit-0.1.4.dev5 → flpit-0.1.6}/src/flpit/core/linq.py +64 -79
- flpit-0.1.4.dev5/PKG-INFO +0 -186
- flpit-0.1.4.dev5/README.md +0 -167
- flpit-0.1.4.dev5/pyproject.toml +0 -70
- {flpit-0.1.4.dev5 → flpit-0.1.6}/src/flp/__init__.py +0 -0
- {flpit-0.1.4.dev5 → flpit-0.1.6}/src/flpit/__init__.py +0 -0
- {flpit-0.1.4.dev5 → flpit-0.1.6}/src/flpit/core/__init__.py +0 -0
- {flpit-0.1.4.dev5 → flpit-0.1.6}/src/flpit/flp.py +0 -0
- {flpit-0.1.4.dev5 → flpit-0.1.6}/src/flpit/py.typed +0 -0
flpit-0.1.6/.gitignore
ADDED
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.
|
|
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 = ["
|
|
34
|
-
build-backend = "
|
|
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"]
|