flpit 0.2.0__tar.gz → 0.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,27 @@
1
+ Metadata-Version: 2.5
2
+ Name: flpit
3
+ Version: 0.2.1
4
+ Summary: Fluent LINQ for Python — flip your iterables
5
+ Project-URL: Homepage, https://github.com/riemar/flpit
6
+ Project-URL: Repository, https://github.com/riemar/flpit
7
+ Project-URL: Issues, https://github.com/riemar/flpit/issues
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Keywords: fluent,generators,iterable,lazy-evaluation,linq,pipeline,pyrefly,type-safe
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Classifier: Programming Language :: Python :: 3.15
19
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.12
22
+ Requires-Dist: msgspec>=0.22.0
23
+ Description-Content-Type: text/markdown
24
+
1
25
  # FlpIt
2
26
 
3
27
  > **Fluent LINQ for Python — LINQ semantics, Python-native execution**
@@ -130,6 +154,52 @@ print(list(backend_engineers))
130
154
  # ['Alice']
131
155
  ```
132
156
 
157
+ ## JSON Serialization
158
+
159
+ FlpIt provides typed JSON serialization and deserialization through `flpit.json.JsonConvert`,
160
+ backed by [`msgspec`](https://github.com/jcrist/msgspec).
161
+
162
+ It supports dataclasses, nested models, generic containers, configurable property naming, and explicit field metadata.
163
+
164
+ ```python
165
+ from dataclasses import dataclass
166
+ from typing import Annotated
167
+
168
+ from flpit import FlpList
169
+ from flpit.json import JProp, JIgnore, JsonConvert
170
+
171
+ @dataclass
172
+ class Person:
173
+ first_name: str
174
+ internal_id: Annotated[str, JProp("id")]
175
+ password: Annotated[str, JIgnore] = ""
176
+
177
+
178
+ person = Person("Ada", "usr-42", "secret")
179
+ raw = JsonConvert.serialize(person, Person)
180
+ # {"firstName":"Ada","id":"usr-42"}
181
+
182
+ people_json = """
183
+ [
184
+ {"firstName": "Ada", "id": "usr-42"},
185
+ {"firstName": "Grace", "id": "usr-43"}
186
+ ]
187
+ """
188
+
189
+ people: FlpList[Person] = FlpList.from_json(people_json, Person)
190
+ ```
191
+
192
+ ### Mapping and conversion
193
+
194
+ - **Naming policies:** Configure automatic property naming with `JsonSerializerOptions`. The default is camel case.
195
+ - **Explicit metadata:** `JProp` assigns a specific JSON property name, `JIgnore` excludes a field, and `JInclude` explicitly includes otherwise excluded underscore-prefixed fields.
196
+ - **Serialization options:** Control default-value omission and whether unknown JSON fields are accepted or rejected.
197
+ - **Typed collections:** Pass the complete target type, such as `list[Person]`, so nested and generic values can be decoded against the intended schema.
198
+
199
+ For large outputs, `JsonConvert.serialize_to()` writes a JSON array to a file-like stream without first materializing the entire serialized document.
200
+
201
+ By default, deserialization returns instances of the requested model type. Setting `native=True` opts into the generated `msgspec.Struct` representation instead, trading the original model type for the native representation and its performance characteristics.
202
+
133
203
  ## 🔬 LINQ Semantics, Python Runtime
134
204
 
135
205
  `flpit` deliberately separates the two:
@@ -325,6 +395,21 @@ to_list ← terminal materialization
325
395
 
326
396
  Operations such as `order_by()` necessarily materialize their input internally to establish ordering.
327
397
 
398
+ ## ⚙️ Task Runtime
399
+
400
+ FlpIt includes a lightweight, `asyncio`-free task runtime for scheduling and coordinating concurrent work, with synchronous callables executed through a bounded worker pool.
401
+
402
+ Key features:
403
+
404
+ - **Task scheduling:** `Task.run()` schedules coroutines, awaitables, and synchronous callables.
405
+ - **Task coordination:** `Task.when_all()` waits for all tasks while preserving input order; `Task.when_any()` returns the first completed task; `Task.collect()` yields tasks as they complete.
406
+ - **Cancellation:** `CancellationToken` and `CancellationTokenSource` support cooperative cancellation, cancellation callbacks, timeouts, and linked cancellation tokens.
407
+ - **Resource management:** Configure worker capacity and pending background-work limits, handle queue saturation explicitly, and shut down the process-wide runtime with an observable shutdown report.
408
+
409
+ The runtime starts lazily and manages its own lifecycle. It is deliberately not an `asyncio` compatibility layer; cancellation of running synchronous functions is cooperative.
410
+
411
+ See the [Task Runtime examples](https://github.com/riemar/flpit/tree/main/examples/task) for usage examples.
412
+
328
413
  ## ⚙️ Installation
329
414
 
330
415
  ```bash
@@ -1,24 +1,3 @@
1
- Metadata-Version: 2.5
2
- Name: flpit
3
- Version: 0.2.0
4
- Summary: Fluent LINQ for Python — flip your iterables
5
- Project-URL: Homepage, https://github.com/riemar/flpit
6
- License: MIT
7
- License-File: LICENSE
8
- Keywords: fluent,generators,iterable,lazy-evaluation,linq,pipeline,pyrefly,type-safe
9
- Classifier: Development Status :: 3 - Alpha
10
- Classifier: Intended Audience :: Developers
11
- Classifier: License :: OSI Approved :: MIT License
12
- Classifier: Programming Language :: Python :: 3
13
- Classifier: Programming Language :: Python :: 3.11
14
- Classifier: Programming Language :: Python :: 3.12
15
- Classifier: Programming Language :: Python :: 3.13
16
- Classifier: Programming Language :: Python :: 3.14
17
- Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
- Classifier: Typing :: Typed
19
- Requires-Python: >=3.11
20
- Description-Content-Type: text/markdown
21
-
22
1
  # FlpIt
23
2
 
24
3
  > **Fluent LINQ for Python — LINQ semantics, Python-native execution**
@@ -151,6 +130,52 @@ print(list(backend_engineers))
151
130
  # ['Alice']
152
131
  ```
153
132
 
133
+ ## JSON Serialization
134
+
135
+ FlpIt provides typed JSON serialization and deserialization through `flpit.json.JsonConvert`,
136
+ backed by [`msgspec`](https://github.com/jcrist/msgspec).
137
+
138
+ It supports dataclasses, nested models, generic containers, configurable property naming, and explicit field metadata.
139
+
140
+ ```python
141
+ from dataclasses import dataclass
142
+ from typing import Annotated
143
+
144
+ from flpit import FlpList
145
+ from flpit.json import JProp, JIgnore, JsonConvert
146
+
147
+ @dataclass
148
+ class Person:
149
+ first_name: str
150
+ internal_id: Annotated[str, JProp("id")]
151
+ password: Annotated[str, JIgnore] = ""
152
+
153
+
154
+ person = Person("Ada", "usr-42", "secret")
155
+ raw = JsonConvert.serialize(person, Person)
156
+ # {"firstName":"Ada","id":"usr-42"}
157
+
158
+ people_json = """
159
+ [
160
+ {"firstName": "Ada", "id": "usr-42"},
161
+ {"firstName": "Grace", "id": "usr-43"}
162
+ ]
163
+ """
164
+
165
+ people: FlpList[Person] = FlpList.from_json(people_json, Person)
166
+ ```
167
+
168
+ ### Mapping and conversion
169
+
170
+ - **Naming policies:** Configure automatic property naming with `JsonSerializerOptions`. The default is camel case.
171
+ - **Explicit metadata:** `JProp` assigns a specific JSON property name, `JIgnore` excludes a field, and `JInclude` explicitly includes otherwise excluded underscore-prefixed fields.
172
+ - **Serialization options:** Control default-value omission and whether unknown JSON fields are accepted or rejected.
173
+ - **Typed collections:** Pass the complete target type, such as `list[Person]`, so nested and generic values can be decoded against the intended schema.
174
+
175
+ For large outputs, `JsonConvert.serialize_to()` writes a JSON array to a file-like stream without first materializing the entire serialized document.
176
+
177
+ By default, deserialization returns instances of the requested model type. Setting `native=True` opts into the generated `msgspec.Struct` representation instead, trading the original model type for the native representation and its performance characteristics.
178
+
154
179
  ## 🔬 LINQ Semantics, Python Runtime
155
180
 
156
181
  `flpit` deliberately separates the two:
@@ -346,6 +371,21 @@ to_list ← terminal materialization
346
371
 
347
372
  Operations such as `order_by()` necessarily materialize their input internally to establish ordering.
348
373
 
374
+ ## ⚙️ Task Runtime
375
+
376
+ FlpIt includes a lightweight, `asyncio`-free task runtime for scheduling and coordinating concurrent work, with synchronous callables executed through a bounded worker pool.
377
+
378
+ Key features:
379
+
380
+ - **Task scheduling:** `Task.run()` schedules coroutines, awaitables, and synchronous callables.
381
+ - **Task coordination:** `Task.when_all()` waits for all tasks while preserving input order; `Task.when_any()` returns the first completed task; `Task.collect()` yields tasks as they complete.
382
+ - **Cancellation:** `CancellationToken` and `CancellationTokenSource` support cooperative cancellation, cancellation callbacks, timeouts, and linked cancellation tokens.
383
+ - **Resource management:** Configure worker capacity and pending background-work limits, handle queue saturation explicitly, and shut down the process-wide runtime with an observable shutdown report.
384
+
385
+ The runtime starts lazily and manages its own lifecycle. It is deliberately not an `asyncio` compatibility layer; cancellation of running synchronous functions is cooperative.
386
+
387
+ See the [Task Runtime examples](https://github.com/riemar/flpit/tree/main/examples/task) for usage examples.
388
+
349
389
  ## ⚙️ Installation
350
390
 
351
391
  ```bash
@@ -1,9 +1,9 @@
1
1
  [project]
2
2
  name = "flpit"
3
- version = "0.2.0"
3
+ version = "0.2.1"
4
4
  description = "Fluent LINQ for Python — flip your iterables"
5
5
  readme = "README.md"
6
- requires-python = ">=3.11"
6
+ requires-python = ">=3.12"
7
7
  license = { text = "MIT" }
8
8
  keywords = ["linq", "fluent", "iterable", "pipeline", "lazy-evaluation", "pyrefly", "type-safe", "generators"]
9
9
  classifiers = [
@@ -11,17 +11,21 @@ classifiers = [
11
11
  "Intended Audience :: Developers",
12
12
  "License :: OSI Approved :: MIT License",
13
13
  "Programming Language :: Python :: 3",
14
- "Programming Language :: Python :: 3.11",
15
14
  "Programming Language :: Python :: 3.12",
16
15
  "Programming Language :: Python :: 3.13",
17
16
  "Programming Language :: Python :: 3.14",
17
+ "Programming Language :: Python :: 3.15",
18
18
  "Topic :: Software Development :: Libraries :: Python Modules",
19
19
  "Typing :: Typed"
20
20
  ]
21
- dependencies = []
21
+ dependencies = [
22
+ "msgspec>=0.22.0",
23
+ ]
22
24
 
23
25
  [project.urls]
24
26
  Homepage = "https://github.com/riemar/flpit"
27
+ Repository = "https://github.com/riemar/flpit"
28
+ Issues = "https://github.com/riemar/flpit/issues"
25
29
 
26
30
  [dependency-groups]
27
31
  dev = [
@@ -31,6 +35,7 @@ dev = [
31
35
  "pyright>=1.1.414",
32
36
  "pytest-benchmark>=5.3.0",
33
37
  "mypy>=2.4.0",
38
+ "httpx>=0.28.1",
34
39
  ]
35
40
 
36
41
 
@@ -1,7 +1,10 @@
1
1
  from __future__ import annotations
2
- from typing import SupportsIndex
2
+
3
+ from io import BytesIO, StringIO
4
+
3
5
  import decimal
4
6
  from decimal import Decimal, Overflow, Inexact
7
+ from contextlib import AbstractContextManager
5
8
 
6
9
  import builtins
7
10
  from collections import UserList
@@ -15,15 +18,21 @@ from typing import (
15
18
  Iterable,
16
19
  Iterator,
17
20
  Collection,
21
+ SupportsIndex,
18
22
  List,
19
23
  Set,
20
24
  Type,
21
25
  TypeVar,
26
+ ClassVar,
22
27
  Union,
23
28
  override,
24
29
  overload,
25
- cast as typing_cast,
30
+ cast as typing_cast, BinaryIO,
26
31
  )
32
+
33
+ import flpit.json
34
+ from flpit.json import JsonConvert, JsonSerializerOptions
35
+
27
36
  TItem = TypeVar("TItem")
28
37
  TResult = TypeVar("TResult")
29
38
  TKey = TypeVar("TKey")
@@ -118,8 +127,7 @@ def _guard_empty(func: Callable[..., Any]) -> Callable[..., Any]:
118
127
  return wrapper
119
128
 
120
129
 
121
- # noinspection unresolved-references,protected-member
122
- def dotnet_context() -> decimal._ContextManager: # awesome that type was not intended
130
+ def dotnet_context() -> AbstractContextManager[decimal.Context]:
123
131
  # 1. Create and configure pure Context object
124
132
  ctx = decimal.getcontext().copy()
125
133
  ctx.prec = max(ctx.prec, 29)
@@ -246,8 +254,8 @@ class FlpIt(Iterable[TItem], Generic[TItem]):
246
254
  for item in self:
247
255
  if predicate(item):
248
256
  yield item
249
-
250
- return typing_cast(FlpIt[TItem], FlpIt(_FactoryIterable(_generator)))
257
+ # return typing_cast(FlpIt[TItem], FlpIt[TItem](_FactoryIterable(_generator)))
258
+ return FlpIt[TItem](_FactoryIterable(_generator))
251
259
 
252
260
  def select(self, selector: Callable[[TItem], TResult]) -> FlpIt[TResult]:
253
261
  """Projects each element into a new form."""
@@ -617,10 +625,36 @@ class FlpIt(Iterable[TItem], Generic[TItem]):
617
625
 
618
626
  raise MultipleMatchesError() if has_predicate else MultipleElementsError()
619
627
 
620
-
621
628
  def to_list(self) -> FlpList[TItem]:
622
629
  """Explicitly materializes the query into a FlpList."""
623
- return FlpList(self)
630
+ return FlpList[TItem](self)
631
+
632
+ # =====================================================================
633
+ # 5. json serialization
634
+ # =====================================================================
635
+
636
+ @classmethod
637
+ def from_json[T](cls, data: str | bytes, item_type: type[T], jso: JsonSerializerOptions|None=None) -> FlpIt[T]:
638
+ return FlpIt[T](JsonConvert.deserialize(data, list[item_type], jso))
639
+
640
+ def to_json_bytes[T](self, item_type: type[T], jso: JsonSerializerOptions|None=None) -> bytes:
641
+ writer = BytesIO()
642
+ JsonConvert.serialize_to(self, item_type, writer, jso)
643
+ return writer.getvalue()
644
+
645
+ def to_json[T](self, item_type: type[T], jso: JsonSerializerOptions|None=None) -> str:
646
+ writer = StringIO()
647
+ JsonConvert.serialize_to(self, item_type, writer, jso)
648
+ return writer.getvalue()
649
+
650
+ def write_json[T](
651
+ self,
652
+ item_type: type[T],
653
+ output: BinaryIO,
654
+ jso: JsonSerializerOptions | None = None,
655
+ ) -> None:
656
+ JsonConvert.serialize_to(self, item_type, output, jso)
657
+
624
658
 
625
659
  from functools import partial
626
660
 
@@ -731,6 +765,8 @@ class FlpList(Collection[TItem], Generic[TItem]):
731
765
  | Fluent List
732
766
  A materialized list extending UserList that yields lazy FlpIt instances for query operations.
733
767
  """
768
+ __type_args__: ClassVar[tuple[Any, ...]]
769
+
734
770
  def __init__(self, source: Iterable[TItem] | _Sentinel=_SENTINEL):
735
771
  if source is None:
736
772
  raise SourceNoneError()
@@ -1001,7 +1037,7 @@ class FlpList(Collection[TItem], Generic[TItem]):
1001
1037
  return self # type: ignore[return-value]
1002
1038
 
1003
1039
  def where(self, predicate: Callable[[TItem], bool]) -> FlpIt[TItem]:
1004
- return FlpIt(self.__list.data).where(predicate)
1040
+ return FlpIt[TItem](self.__list.data).where(predicate)
1005
1041
 
1006
1042
  def select(self, selector: Callable[[TItem], TResult]) -> FlpIt[TResult]:
1007
1043
  return FlpIt(self.__list.data).select(selector)
@@ -1179,3 +1215,25 @@ class FlpList(Collection[TItem], Generic[TItem]):
1179
1215
  def to_list(self) -> FlpList[TItem]:
1180
1216
  """Explicitly returns a shallow copy instance to isolate mutations."""
1181
1217
  return FlpList(self.__list.data.copy())
1218
+
1219
+ # =====================================================================
1220
+ # 5. json serialization
1221
+ # =====================================================================
1222
+
1223
+ @classmethod
1224
+ def from_json[T](cls, data: str | bytes, item_type: type[T], jso: JsonSerializerOptions|None=None) -> FlpList[T]:
1225
+ return FlpList[T](JsonConvert.deserialize(data, list[item_type], jso))
1226
+
1227
+ def to_json_bytes[T](self, item_type: type[T], jso: JsonSerializerOptions|None=None) -> bytes:
1228
+ return JsonConvert.serialize_bytes(self.__list.data, list[item_type], jso)
1229
+
1230
+ def to_json[T](self, item_type: type[T], jso: JsonSerializerOptions|None=None) -> str:
1231
+ return JsonConvert.serialize(self.__list.data, list[item_type], jso)
1232
+
1233
+ def write_json[T](
1234
+ self,
1235
+ item_type: type[T],
1236
+ output: BinaryIO,
1237
+ jso: JsonSerializerOptions | None = None,
1238
+ ) -> None:
1239
+ JsonConvert.serialize_to(self, item_type, output, jso)