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 +331 -0
- flpit-0.1.6/README.md +311 -0
- {flpit-0.1.4.dev7 → flpit-0.1.6}/pyproject.toml +1 -1
- {flpit-0.1.4.dev7 → flpit-0.1.6}/src/flpit/core/linq.py +64 -79
- flpit-0.1.4.dev7/PKG-INFO +0 -187
- flpit-0.1.4.dev7/README.md +0 -167
- {flpit-0.1.4.dev7 → flpit-0.1.6}/.gitignore +0 -0
- {flpit-0.1.4.dev7 → flpit-0.1.6}/LICENSE +0 -0
- {flpit-0.1.4.dev7 → flpit-0.1.6}/src/flp/__init__.py +0 -0
- {flpit-0.1.4.dev7 → flpit-0.1.6}/src/flpit/__init__.py +0 -0
- {flpit-0.1.4.dev7 → flpit-0.1.6}/src/flpit/core/__init__.py +0 -0
- {flpit-0.1.4.dev7 → flpit-0.1.6}/src/flpit/flp.py +0 -0
- {flpit-0.1.4.dev7 → flpit-0.1.6}/src/flpit/py.typed +0 -0
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.
|
|
@@ -29,7 +29,13 @@ TKey = TypeVar("TKey")
|
|
|
29
29
|
TOther = TypeVar("TOther")
|
|
30
30
|
TAccumulate = TypeVar("TAccumulate")
|
|
31
31
|
|
|
32
|
-
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
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
|
-
|
|
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,
|
|
364
|
+
func: Callable[[Any, Any], Any],
|
|
359
365
|
seed: Any = _SENTINEL,
|
|
360
|
-
) ->
|
|
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:
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
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(
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
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:
|
|
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
|
|
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
|
|
502
|
+
raise EmptySequenceError() if predicate is _SENTINEL else NoMatchError()
|
|
492
503
|
|
|
493
504
|
def first_or_default(
|
|
494
|
-
self, default: TResult, predicate:
|
|
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
|
-
|
|
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
|
-
|
|
710
|
-
|
|
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:
|
|
766
|
+
predicate: Callable[[TItem], bool] | _Sentinel = _SENTINEL,
|
|
756
767
|
) -> bool:
|
|
757
|
-
if predicate is
|
|
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:
|
|
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
|
-
) ->
|
|
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:
|
|
877
|
-
) ->
|
|
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:
|
|
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
|
-
|
|
888
|
-
|
|
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(
|
|
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
|
|
906
|
+
Counts elements using LINQ semantics.
|
|
906
907
|
|
|
907
908
|
Supported forms:
|
|
908
909
|
|
|
909
|
-
count() ->
|
|
910
|
-
count(predicate) ->
|
|
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
|
-
|
|
921
|
-
if item is _SENTINEL:
|
|
913
|
+
if predicate is _SENTINEL:
|
|
922
914
|
return len(self.data)
|
|
923
915
|
|
|
924
|
-
|
|
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:
|
|
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:
|
|
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
|
|
959
|
+
raise EmptySequenceError() if predicate is _SENTINEL else NoMatchError()
|
|
974
960
|
|
|
975
961
|
def first_or_default(
|
|
976
|
-
self, default: TResult, predicate:
|
|
977
|
-
) ->
|
|
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
|
-
|
|
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.
|
flpit-0.1.4.dev7/README.md
DELETED
|
@@ -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
|
|
File without changes
|