flpit 0.2.1.dev8__tar.gz → 0.2.2__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.2.1.dev8 → flpit-0.2.2}/PKG-INFO +63 -1
- {flpit-0.2.1.dev8 → flpit-0.2.2}/README.md +61 -0
- {flpit-0.2.1.dev8 → flpit-0.2.2}/pyproject.toml +2 -1
- {flpit-0.2.1.dev8 → flpit-0.2.2}/src/flpit/core/linq.py +2 -1
- flpit-0.2.2/src/flpit/csv.py +333 -0
- {flpit-0.2.1.dev8 → flpit-0.2.2}/src/flpit/json.py +64 -32
- {flpit-0.2.1.dev8 → flpit-0.2.2}/src/flpit/task.py +50 -22
- {flpit-0.2.1.dev8 → flpit-0.2.2}/.gitignore +0 -0
- {flpit-0.2.1.dev8 → flpit-0.2.2}/LICENSE +0 -0
- {flpit-0.2.1.dev8 → flpit-0.2.2}/src/flp/__init__.py +0 -0
- {flpit-0.2.1.dev8 → flpit-0.2.2}/src/flpit/__init__.py +0 -0
- {flpit-0.2.1.dev8 → flpit-0.2.2}/src/flpit/core/__init__.py +0 -0
- {flpit-0.2.1.dev8 → flpit-0.2.2}/src/flpit/flp.py +0 -0
- {flpit-0.2.1.dev8 → flpit-0.2.2}/src/flpit/py.typed +0 -0
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: flpit
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Fluent LINQ for Python — flip your iterables
|
|
5
5
|
Project-URL: Homepage, https://github.com/riemar/flpit
|
|
6
6
|
Project-URL: Repository, https://github.com/riemar/flpit
|
|
7
|
+
Project-URL: Issues, https://github.com/riemar/flpit/issues
|
|
7
8
|
License: MIT
|
|
8
9
|
License-File: LICENSE
|
|
9
10
|
Keywords: fluent,generators,iterable,lazy-evaluation,linq,pipeline,pyrefly,type-safe
|
|
@@ -153,6 +154,52 @@ print(list(backend_engineers))
|
|
|
153
154
|
# ['Alice']
|
|
154
155
|
```
|
|
155
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
|
+
|
|
156
203
|
## 🔬 LINQ Semantics, Python Runtime
|
|
157
204
|
|
|
158
205
|
`flpit` deliberately separates the two:
|
|
@@ -348,6 +395,21 @@ to_list ← terminal materialization
|
|
|
348
395
|
|
|
349
396
|
Operations such as `order_by()` necessarily materialize their input internally to establish ordering.
|
|
350
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
|
+
|
|
351
413
|
## ⚙️ Installation
|
|
352
414
|
|
|
353
415
|
```bash
|
|
@@ -130,6 +130,52 @@ print(list(backend_engineers))
|
|
|
130
130
|
# ['Alice']
|
|
131
131
|
```
|
|
132
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
|
+
|
|
133
179
|
## 🔬 LINQ Semantics, Python Runtime
|
|
134
180
|
|
|
135
181
|
`flpit` deliberately separates the two:
|
|
@@ -325,6 +371,21 @@ to_list ← terminal materialization
|
|
|
325
371
|
|
|
326
372
|
Operations such as `order_by()` necessarily materialize their input internally to establish ordering.
|
|
327
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
|
+
|
|
328
389
|
## ⚙️ Installation
|
|
329
390
|
|
|
330
391
|
```bash
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "flpit"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.2"
|
|
4
4
|
description = "Fluent LINQ for Python — flip your iterables"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.12"
|
|
@@ -25,6 +25,7 @@ dependencies = [
|
|
|
25
25
|
[project.urls]
|
|
26
26
|
Homepage = "https://github.com/riemar/flpit"
|
|
27
27
|
Repository = "https://github.com/riemar/flpit"
|
|
28
|
+
Issues = "https://github.com/riemar/flpit/issues"
|
|
28
29
|
|
|
29
30
|
[dependency-groups]
|
|
30
31
|
dev = [
|
|
@@ -30,6 +30,7 @@ from typing import (
|
|
|
30
30
|
cast as typing_cast, BinaryIO,
|
|
31
31
|
)
|
|
32
32
|
|
|
33
|
+
import flpit.json
|
|
33
34
|
from flpit.json import JsonConvert, JsonSerializerOptions
|
|
34
35
|
|
|
35
36
|
TItem = TypeVar("TItem")
|
|
@@ -1235,4 +1236,4 @@ class FlpList(Collection[TItem], Generic[TItem]):
|
|
|
1235
1236
|
output: BinaryIO,
|
|
1236
1237
|
jso: JsonSerializerOptions | None = None,
|
|
1237
1238
|
) -> None:
|
|
1238
|
-
JsonConvert.serialize_to(self, item_type, output, jso)
|
|
1239
|
+
JsonConvert.serialize_to(self, item_type, output, jso)
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import csv
|
|
4
|
+
import io
|
|
5
|
+
from csv import DictReader
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from functools import lru_cache
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Iterator, TextIO
|
|
10
|
+
|
|
11
|
+
import msgspec
|
|
12
|
+
|
|
13
|
+
from flpit import FlpIt, FlpList
|
|
14
|
+
from flpit.json import (
|
|
15
|
+
JProp,
|
|
16
|
+
JsonNamingPolicy,
|
|
17
|
+
JsonSerializerOptions,
|
|
18
|
+
wire_type_for,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class CsvCol(JProp):
|
|
23
|
+
"""Explicit CSV column name.
|
|
24
|
+
|
|
25
|
+
Inherits JProp so the existing wire-schema machinery recognizes
|
|
26
|
+
the metadata without a separate conversion implementation.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True, slots=True)
|
|
31
|
+
class CsvOptions:
|
|
32
|
+
"""CSV parsing and property-mapping options.
|
|
33
|
+
|
|
34
|
+
Naming defaults to none: headers match declared property names unless
|
|
35
|
+
CsvCol specifies an explicit column name.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
delimiter: str = ","
|
|
39
|
+
quotechar: str = '"'
|
|
40
|
+
escapechar: str | None = None
|
|
41
|
+
doublequote: bool = True
|
|
42
|
+
skipinitialspace: bool = False
|
|
43
|
+
strict: bool = False
|
|
44
|
+
encoding: str = "utf-8-sig"
|
|
45
|
+
naming_policy: JsonNamingPolicy = JsonNamingPolicy.none
|
|
46
|
+
|
|
47
|
+
def __post_init__(self) -> None:
|
|
48
|
+
if len(self.delimiter) != 1:
|
|
49
|
+
raise ValueError("delimiter must be exactly one character")
|
|
50
|
+
if len(self.quotechar) != 1:
|
|
51
|
+
raise ValueError("quotechar must be exactly one character")
|
|
52
|
+
if self.escapechar is not None and len(self.escapechar) != 1:
|
|
53
|
+
raise ValueError("escapechar must be exactly one character")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@lru_cache(maxsize=None)
|
|
57
|
+
def _json_options_for_csv(
|
|
58
|
+
naming_policy: JsonNamingPolicy,
|
|
59
|
+
) -> JsonSerializerOptions:
|
|
60
|
+
"""Reuse options identity because the wire-type cache keys by options ID."""
|
|
61
|
+
return JsonSerializerOptions(naming_policy=naming_policy)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class CsvLoader:
|
|
65
|
+
"""Load typed records from CSV without adding format methods to FlpList/FlpIt."""
|
|
66
|
+
|
|
67
|
+
@staticmethod
|
|
68
|
+
def load_list[T](
|
|
69
|
+
data: str | Path,
|
|
70
|
+
item_type: type[T],
|
|
71
|
+
options: CsvOptions | None = None,
|
|
72
|
+
) -> FlpList[T]:
|
|
73
|
+
"""Load CSV rows eagerly using bulk conversion.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
data: CSV text, or a Path to a CSV file.
|
|
77
|
+
item_type: Type of each resulting model.
|
|
78
|
+
options: CSV dialect, encoding, and naming policy.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
An eagerly populated FlpList of item_type instances.
|
|
82
|
+
"""
|
|
83
|
+
opts = options or CsvOptions()
|
|
84
|
+
jso = _json_options_for_csv(opts.naming_policy)
|
|
85
|
+
target: type[list[T]] = list[item_type]
|
|
86
|
+
wire_type = wire_type_for(target, jso)
|
|
87
|
+
|
|
88
|
+
with CsvLoader._open_text(data, opts) as stream:
|
|
89
|
+
reader: DictReader[Any] = CsvLoader._reader(stream, opts)
|
|
90
|
+
rows = CsvLoader._read_rows(reader)
|
|
91
|
+
|
|
92
|
+
output = msgspec.convert(
|
|
93
|
+
rows,
|
|
94
|
+
wire_type,
|
|
95
|
+
from_attributes=True,
|
|
96
|
+
strict=False,
|
|
97
|
+
)
|
|
98
|
+
del rows
|
|
99
|
+
|
|
100
|
+
return FlpList[T](output)
|
|
101
|
+
|
|
102
|
+
@staticmethod
|
|
103
|
+
def load_it[T](
|
|
104
|
+
data: str | Path,
|
|
105
|
+
item_type: type[T],
|
|
106
|
+
options: CsvOptions | None = None,
|
|
107
|
+
) -> FlpIt[T]:
|
|
108
|
+
"""Load CSV rows lazily, converting one row when it is consumed.
|
|
109
|
+
|
|
110
|
+
For Path input, the file is opened on iteration and closed when
|
|
111
|
+
the iterator is exhausted or closed. CSV parsing and conversion
|
|
112
|
+
errors therefore occur during iteration, not at this call.
|
|
113
|
+
"""
|
|
114
|
+
opts = options or CsvOptions()
|
|
115
|
+
jso = _json_options_for_csv(opts.naming_policy)
|
|
116
|
+
wire_item_type = wire_type_for(item_type, jso)
|
|
117
|
+
|
|
118
|
+
return FlpIt[T](
|
|
119
|
+
CsvLoader._iter_items(data, item_type, wire_item_type, opts)
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
@staticmethod
|
|
123
|
+
def _open_text(data: str | Path, options: CsvOptions) -> TextIO:
|
|
124
|
+
if isinstance(data, Path):
|
|
125
|
+
return data.open("r", newline="", encoding=options.encoding)
|
|
126
|
+
return io.StringIO(data.removeprefix("\ufeff"), newline="")
|
|
127
|
+
|
|
128
|
+
@staticmethod
|
|
129
|
+
def _reader(stream: TextIO, options: CsvOptions) -> csv.DictReader[Any]:
|
|
130
|
+
reader = csv.DictReader(
|
|
131
|
+
stream,
|
|
132
|
+
delimiter=options.delimiter,
|
|
133
|
+
quotechar=options.quotechar,
|
|
134
|
+
escapechar=options.escapechar,
|
|
135
|
+
doublequote=options.doublequote,
|
|
136
|
+
skipinitialspace=options.skipinitialspace,
|
|
137
|
+
strict=options.strict,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
headers = reader.fieldnames
|
|
141
|
+
if headers is None:
|
|
142
|
+
raise ValueError("CSV input must contain a header row")
|
|
143
|
+
|
|
144
|
+
if any(header == "" for header in headers):
|
|
145
|
+
raise ValueError("CSV column names must not be empty")
|
|
146
|
+
|
|
147
|
+
if len(headers) != len(set(headers)):
|
|
148
|
+
raise ValueError("CSV column names must be unique")
|
|
149
|
+
|
|
150
|
+
return reader
|
|
151
|
+
|
|
152
|
+
@staticmethod
|
|
153
|
+
def _read_rows(reader: csv.DictReader[Any]) -> list[dict[str, Any]]:
|
|
154
|
+
rows: list[dict[str, Any]] = []
|
|
155
|
+
|
|
156
|
+
for row in reader:
|
|
157
|
+
if None in row:
|
|
158
|
+
raise ValueError(
|
|
159
|
+
f"CSV row ending at line {reader.line_num} "
|
|
160
|
+
"contains more values than the header"
|
|
161
|
+
)
|
|
162
|
+
rows.append(row)
|
|
163
|
+
|
|
164
|
+
return rows
|
|
165
|
+
|
|
166
|
+
@staticmethod
|
|
167
|
+
def _iter_items[T](
|
|
168
|
+
data: str | Path,
|
|
169
|
+
item_type: type[T],
|
|
170
|
+
wire_item_type: Any,
|
|
171
|
+
options: CsvOptions,
|
|
172
|
+
) -> Iterator[T]:
|
|
173
|
+
with CsvLoader._open_text(data, options) as stream:
|
|
174
|
+
reader = CsvLoader._reader(stream, options)
|
|
175
|
+
|
|
176
|
+
for row in reader:
|
|
177
|
+
if None in row:
|
|
178
|
+
raise ValueError(
|
|
179
|
+
f"CSV row ending at line {reader.line_num} "
|
|
180
|
+
"contains more values than the header"
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
yield msgspec.convert(
|
|
184
|
+
row,
|
|
185
|
+
wire_item_type,
|
|
186
|
+
from_attributes=True,
|
|
187
|
+
strict=False,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
# import csv
|
|
191
|
+
# import io
|
|
192
|
+
# from dataclasses import dataclass
|
|
193
|
+
# from pathlib import Path
|
|
194
|
+
# from typing import TextIO
|
|
195
|
+
#
|
|
196
|
+
# import msgspec
|
|
197
|
+
#
|
|
198
|
+
# from flpit.core.linq import FlpList
|
|
199
|
+
# from flpit.json import (
|
|
200
|
+
# JProp,
|
|
201
|
+
# JsonNamingPolicy,
|
|
202
|
+
# JsonSerializerOptions,
|
|
203
|
+
# wire_type_for,
|
|
204
|
+
# )
|
|
205
|
+
#
|
|
206
|
+
#
|
|
207
|
+
# class CsvCol(JProp):
|
|
208
|
+
# """Explicit CSV column name using the existing property metadata."""
|
|
209
|
+
#
|
|
210
|
+
#
|
|
211
|
+
# @dataclass(frozen=True, slots=True)
|
|
212
|
+
# class CsvOptions:
|
|
213
|
+
# """CSV parsing and field-mapping options.
|
|
214
|
+
#
|
|
215
|
+
# The naming policy defaults to none, so CSV headers match Python field
|
|
216
|
+
# names unless an explicit CsvCol/JProp name is supplied.
|
|
217
|
+
# """
|
|
218
|
+
#
|
|
219
|
+
# delimiter: str = ","
|
|
220
|
+
# quotechar: str = '"'
|
|
221
|
+
# escapechar: str | None = None
|
|
222
|
+
# doublequote: bool = True
|
|
223
|
+
# skipinitialspace: bool = False
|
|
224
|
+
# strict: bool = False
|
|
225
|
+
# encoding: str = "utf-8-sig"
|
|
226
|
+
# naming_policy: JsonNamingPolicy = JsonNamingPolicy.none
|
|
227
|
+
#
|
|
228
|
+
# def __post_init__(self) -> None:
|
|
229
|
+
# if len(self.delimiter) != 1:
|
|
230
|
+
# raise ValueError("delimiter must be exactly one character")
|
|
231
|
+
# if len(self.quotechar) != 1:
|
|
232
|
+
# raise ValueError("quotechar must be exactly one character")
|
|
233
|
+
# if self.escapechar is not None and len(self.escapechar) != 1:
|
|
234
|
+
# raise ValueError("escapechar must be exactly one character")
|
|
235
|
+
#
|
|
236
|
+
# def to_json_options(self) -> JsonSerializerOptions:
|
|
237
|
+
# """Map CSV field naming to the shared wire-schema builder."""
|
|
238
|
+
# return JsonSerializerOptions(naming_policy=self.naming_policy)
|
|
239
|
+
#
|
|
240
|
+
#
|
|
241
|
+
# class CsvLoader:
|
|
242
|
+
# @staticmethod
|
|
243
|
+
# def load[T](
|
|
244
|
+
# data: str | Path,
|
|
245
|
+
# item_type: type[T],
|
|
246
|
+
# options: CsvOptions | None = None,
|
|
247
|
+
# ) -> FlpList[T]:
|
|
248
|
+
# """Load CSV content or a CSV file into a FlpList.
|
|
249
|
+
#
|
|
250
|
+
# CSV headers are matched using the configured naming policy and
|
|
251
|
+
# explicit property metadata. The naming policy is passed to the
|
|
252
|
+
# existing JSON wire-schema builder so annotation handling stays
|
|
253
|
+
# consistent with JsonConvert.
|
|
254
|
+
#
|
|
255
|
+
# CSV values are converted in lax mode because CSV supplies text
|
|
256
|
+
# values, including numeric values represented as strings.
|
|
257
|
+
#
|
|
258
|
+
# Args:
|
|
259
|
+
# data: CSV content as a string, or a Path to a CSV file.
|
|
260
|
+
# item_type: Model type to construct for each row.
|
|
261
|
+
# options: CSV dialect, encoding and naming options.
|
|
262
|
+
#
|
|
263
|
+
# Returns:
|
|
264
|
+
# A FlpList containing instances of item_type.
|
|
265
|
+
# """
|
|
266
|
+
# csv_options = options or CsvOptions()
|
|
267
|
+
# json_options = csv_options.to_json_options()
|
|
268
|
+
# wire_type = wire_type_for(item_type, json_options)
|
|
269
|
+
#
|
|
270
|
+
# if isinstance(data, Path):
|
|
271
|
+
# with data.open(
|
|
272
|
+
# "r",
|
|
273
|
+
# newline="",
|
|
274
|
+
# encoding=csv_options.encoding,
|
|
275
|
+
# ) as stream:
|
|
276
|
+
# return CsvLoader._load_stream(
|
|
277
|
+
# stream, item_type, wire_type, csv_options
|
|
278
|
+
# )
|
|
279
|
+
#
|
|
280
|
+
# with io.StringIO(data, newline="") as stream:
|
|
281
|
+
# return CsvLoader._load_stream(
|
|
282
|
+
# stream, item_type, wire_type, csv_options
|
|
283
|
+
# )
|
|
284
|
+
#
|
|
285
|
+
# @staticmethod
|
|
286
|
+
# def _load_stream[T](
|
|
287
|
+
# stream: TextIO,
|
|
288
|
+
# item_type: type[T],
|
|
289
|
+
# wire_type: type,
|
|
290
|
+
# options: CsvOptions,
|
|
291
|
+
# ) -> FlpList[T]:
|
|
292
|
+
# reader = csv.DictReader(
|
|
293
|
+
# stream,
|
|
294
|
+
# delimiter=options.delimiter,
|
|
295
|
+
# quotechar=options.quotechar,
|
|
296
|
+
# escapechar=options.escapechar,
|
|
297
|
+
# doublequote=options.doublequote,
|
|
298
|
+
# skipinitialspace=options.skipinitialspace,
|
|
299
|
+
# strict=options.strict,
|
|
300
|
+
# )
|
|
301
|
+
#
|
|
302
|
+
# if reader.fieldnames is None:
|
|
303
|
+
# raise ValueError("CSV input must contain a header row")
|
|
304
|
+
#
|
|
305
|
+
# if len(reader.fieldnames) != len(set(reader.fieldnames)):
|
|
306
|
+
# raise ValueError("CSV column names must be unique")
|
|
307
|
+
#
|
|
308
|
+
# def convert_rows():
|
|
309
|
+
# for row in reader:
|
|
310
|
+
# if None in row:
|
|
311
|
+
# raise ValueError(
|
|
312
|
+
# f"CSV row ending at line {reader.line_num} "
|
|
313
|
+
# "contains more values than the header"
|
|
314
|
+
# )
|
|
315
|
+
#
|
|
316
|
+
# wire_obj = msgspec.convert(
|
|
317
|
+
# row,
|
|
318
|
+
# wire_type,
|
|
319
|
+
# from_attributes=True,
|
|
320
|
+
# strict=False,
|
|
321
|
+
# )
|
|
322
|
+
#
|
|
323
|
+
# # Reconstruct the declared model rather than leaking the
|
|
324
|
+
# # dynamically generated wire type to the caller.
|
|
325
|
+
# yield msgspec.convert(
|
|
326
|
+
# wire_obj,
|
|
327
|
+
# item_type,
|
|
328
|
+
# from_attributes=True,
|
|
329
|
+
# strict=False,
|
|
330
|
+
# )
|
|
331
|
+
#
|
|
332
|
+
# return FlpList[T](convert_rows())
|
|
333
|
+
#
|
|
@@ -67,37 +67,49 @@ class JsonSerializerOptions:
|
|
|
67
67
|
# ---------------------------------------------------------------------------
|
|
68
68
|
|
|
69
69
|
class JIgnore:
|
|
70
|
-
def __class_getitem__[T](cls, item: type[T]) ->
|
|
71
|
-
return Annotated[item, cls]
|
|
70
|
+
def __class_getitem__[T](cls, item: type[T]) -> Annotated[type[T], JIgnore]:
|
|
71
|
+
return typing.cast(Annotated[type[T], JIgnore], Annotated[item, cls])
|
|
72
72
|
|
|
73
73
|
class JInclude:
|
|
74
|
-
def __class_getitem__[T](cls, item: type[T]) ->
|
|
75
|
-
return Annotated[item, cls]
|
|
74
|
+
def __class_getitem__[T](cls, item: type[T]) -> Annotated[type[T], JInclude]:
|
|
75
|
+
return typing.cast(Annotated[type[T], JInclude], Annotated[item, cls])
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class JPropMeta(type):
|
|
79
|
+
def __getattr__(cls, name: str) -> Any:
|
|
80
|
+
class SpecializedJProp:
|
|
81
|
+
def __class_getitem__[T](sub_cls, item: type[T]) -> Annotated[type[T], JProp]:
|
|
82
|
+
return typing.cast(Annotated[type[T], JProp], Annotated[item, JProp(json_name=name)])
|
|
83
|
+
return SpecializedJProp
|
|
76
84
|
|
|
77
85
|
class JProp:
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
return Annotated[target_type, cls(json_name)]
|
|
86
|
+
__slots__ = ("json_name",)
|
|
87
|
+
json_name: str
|
|
81
88
|
|
|
82
|
-
def __init__(self,
|
|
83
|
-
self.
|
|
89
|
+
def __init__(self, json_name: str):
|
|
90
|
+
self.json_name = json_name
|
|
84
91
|
|
|
85
|
-
|
|
86
|
-
def
|
|
87
|
-
|
|
88
|
-
return Annotated[target_type, self]
|
|
92
|
+
# 1. This handles the standard initialization/metadata if needed
|
|
93
|
+
def __repr__(self):
|
|
94
|
+
return f"JProp(json_name={self.json_name!r})"
|
|
89
95
|
|
|
90
|
-
|
|
91
|
-
|
|
96
|
+
# 2. This intercepts JProp[str] (the type subscription first)
|
|
97
|
+
def __class_getitem__[T](cls, item: type[T]) -> Any:
|
|
98
|
+
# We return a proxy class that remembers the type 'item' (e.g., str | None)
|
|
99
|
+
class JPropTypeProxy:
|
|
100
|
+
def __getattr__(self, name: str) -> Annotated[type[T], JProp]:
|
|
101
|
+
# 3. This intercepts .JsonPropNameToUse and returns the Annotated type
|
|
102
|
+
return typing.cast(
|
|
103
|
+
Annotated[type[T], JProp],
|
|
104
|
+
Annotated[item, JProp(json_name=name)]
|
|
105
|
+
)
|
|
92
106
|
|
|
93
|
-
|
|
94
|
-
J = Annotated
|
|
107
|
+
return JPropTypeProxy()
|
|
95
108
|
|
|
96
109
|
@dataclass
|
|
97
110
|
class AnnotationModel:
|
|
98
|
-
|
|
99
|
-
display_name:
|
|
100
|
-
display_name_2: Dummy("name")[str]
|
|
111
|
+
other_name: Annotated[str, JProp("WorksToo")]
|
|
112
|
+
display_name: JProp[str].meta
|
|
101
113
|
secret: JIgnore[str] = ""
|
|
102
114
|
always_emit: JInclude[int] = 0
|
|
103
115
|
|
|
@@ -209,7 +221,7 @@ def _field_directives(
|
|
|
209
221
|
ignored = any(item is JIgnore for item in metadata)
|
|
210
222
|
included = any(item is JInclude for item in metadata)
|
|
211
223
|
json_name = next(
|
|
212
|
-
(item.
|
|
224
|
+
(item.json_name for item in metadata if isinstance(item, JProp)),
|
|
213
225
|
None,
|
|
214
226
|
)
|
|
215
227
|
remaining = tuple(
|
|
@@ -574,6 +586,13 @@ def _get_decoder(wire_type: Any):
|
|
|
574
586
|
_JSON_ENCODER = msgspec.json.Encoder()
|
|
575
587
|
|
|
576
588
|
|
|
589
|
+
class JsonSerializationError(Exception):
|
|
590
|
+
pass
|
|
591
|
+
|
|
592
|
+
class JsonDeserializationError(Exception):
|
|
593
|
+
pass
|
|
594
|
+
|
|
595
|
+
|
|
577
596
|
class JsonConvert:
|
|
578
597
|
|
|
579
598
|
@staticmethod
|
|
@@ -582,22 +601,29 @@ class JsonConvert:
|
|
|
582
601
|
target: type[T],
|
|
583
602
|
jso: JsonSerializerOptions | None = None
|
|
584
603
|
) -> T:
|
|
604
|
+
if data is None:
|
|
605
|
+
raise JsonDeserializationError("data must not be None")
|
|
606
|
+
|
|
585
607
|
if jso is None:
|
|
586
608
|
jso = JsonSerializerOptions()
|
|
587
609
|
|
|
588
610
|
wire_type = wire_type_for(target, jso)
|
|
589
|
-
parsed = _get_decoder(wire_type).decode(data)
|
|
590
611
|
|
|
591
|
-
|
|
592
|
-
|
|
612
|
+
try:
|
|
613
|
+
parsed = _get_decoder(wire_type).decode(data)
|
|
593
614
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
target,
|
|
597
|
-
from_attributes=True,
|
|
598
|
-
)
|
|
615
|
+
if jso.native:
|
|
616
|
+
return parsed
|
|
599
617
|
|
|
600
|
-
|
|
618
|
+
result = msgspec.convert(
|
|
619
|
+
parsed,
|
|
620
|
+
target,
|
|
621
|
+
from_attributes=True,
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
return result
|
|
625
|
+
except msgspec.MsgspecError as exc:
|
|
626
|
+
raise JsonDeserializationError(str(exc)) from exc
|
|
601
627
|
|
|
602
628
|
@staticmethod
|
|
603
629
|
def serialize[T](
|
|
@@ -605,6 +631,9 @@ class JsonConvert:
|
|
|
605
631
|
target: type[T],
|
|
606
632
|
jso: JsonSerializerOptions | None = None
|
|
607
633
|
) -> str:
|
|
634
|
+
if data is None:
|
|
635
|
+
raise JsonSerializationError("data must not be None")
|
|
636
|
+
|
|
608
637
|
return JsonConvert.serialize_bytes(data, target, jso).decode("utf-8")
|
|
609
638
|
|
|
610
639
|
@staticmethod
|
|
@@ -617,8 +646,11 @@ class JsonConvert:
|
|
|
617
646
|
jso = JsonSerializerOptions()
|
|
618
647
|
|
|
619
648
|
wire_type = wire_type_for(target, jso)
|
|
620
|
-
|
|
621
|
-
|
|
649
|
+
try:
|
|
650
|
+
wire_obj = msgspec.convert(data, wire_type, from_attributes=True)
|
|
651
|
+
return _JSON_ENCODER.encode(wire_obj)
|
|
652
|
+
except msgspec.MsgspecError as exc:
|
|
653
|
+
raise JsonSerializationError(str(exc)) from exc
|
|
622
654
|
|
|
623
655
|
@staticmethod
|
|
624
656
|
def serialize_to[T](
|
|
@@ -36,11 +36,15 @@ import socket
|
|
|
36
36
|
import threading
|
|
37
37
|
import time
|
|
38
38
|
from collections import deque
|
|
39
|
+
from collections.abc import Generator
|
|
39
40
|
from concurrent.futures import Future, ThreadPoolExecutor
|
|
40
41
|
from dataclasses import dataclass
|
|
41
42
|
from itertools import count
|
|
42
|
-
from typing import Any,
|
|
43
|
+
from typing import Any, Generic, TypeVar, cast, Iterable, Final, overload
|
|
43
44
|
import functools
|
|
45
|
+
from functools import wraps
|
|
46
|
+
from collections.abc import Awaitable, Callable, Coroutine
|
|
47
|
+
|
|
44
48
|
|
|
45
49
|
import flpit
|
|
46
50
|
|
|
@@ -409,17 +413,17 @@ class _WaitIO(_Operation):
|
|
|
409
413
|
self.mode = mode
|
|
410
414
|
|
|
411
415
|
|
|
412
|
-
class _WaitTask(_Operation):
|
|
413
|
-
def __init__(self, runtime: "_Runtime",
|
|
416
|
+
class _WaitTask[T](_Operation):
|
|
417
|
+
def __init__(self, runtime: "_Runtime", ttw: "Task[T]") -> None:
|
|
414
418
|
super().__init__(runtime)
|
|
415
|
-
self.task =
|
|
419
|
+
self.task = ttw
|
|
416
420
|
|
|
417
421
|
|
|
418
|
-
class _WaitMany(_Operation):
|
|
422
|
+
class _WaitMany[T](_Operation):
|
|
419
423
|
"""Wait for all or any of a set of tasks to complete."""
|
|
420
424
|
|
|
421
425
|
def __init__(
|
|
422
|
-
self, runtime: "_Runtime", tasks: tuple["Task[
|
|
426
|
+
self, runtime: "_Runtime", tasks: tuple["Task[T]", ...], mode: str
|
|
423
427
|
) -> None:
|
|
424
428
|
super().__init__(runtime)
|
|
425
429
|
if mode not in ("all", "any"):
|
|
@@ -559,14 +563,14 @@ class Task(Generic[T]):
|
|
|
559
563
|
@classmethod
|
|
560
564
|
def run(
|
|
561
565
|
cls,
|
|
562
|
-
work:
|
|
566
|
+
work: Callable[..., T],
|
|
563
567
|
/,
|
|
564
568
|
*args: Any,
|
|
565
569
|
name: str | None = None,
|
|
566
570
|
token: CancellationToken | None = None,
|
|
567
571
|
pass_token: bool = False,
|
|
568
572
|
**kwargs: Any,
|
|
569
|
-
) -> "Task[
|
|
573
|
+
) -> "Task[T]":
|
|
570
574
|
"""Schedule a coroutine/awaitable or ordinary callable.
|
|
571
575
|
|
|
572
576
|
Ordinary functions run in the bounded worker pool. ``token`` controls
|
|
@@ -616,6 +620,8 @@ class Task(Generic[T]):
|
|
|
616
620
|
normalized = _normalize_tasks(tasks, allow_empty=True)
|
|
617
621
|
return cast(
|
|
618
622
|
"Task[list[Any]]",
|
|
623
|
+
# todo: make run type compatible via Generator[Y, S, R]
|
|
624
|
+
# pyrefly: ignore [bad-argument-type]
|
|
619
625
|
cls.run(_when_all_coroutine, normalized, name=name, token=token),
|
|
620
626
|
)
|
|
621
627
|
|
|
@@ -641,6 +647,8 @@ class Task(Generic[T]):
|
|
|
641
647
|
normalized = _normalize_tasks(tasks, allow_empty=False)
|
|
642
648
|
return cast(
|
|
643
649
|
"Task[Task[Any]]",
|
|
650
|
+
# todo: make run type compatible via Generator[Y, S, R]
|
|
651
|
+
# pyrefly: ignore [bad-argument-type]
|
|
644
652
|
cls.run(_when_any_coroutine, normalized, name=name, token=token),
|
|
645
653
|
)
|
|
646
654
|
|
|
@@ -726,19 +734,17 @@ class Task(Generic[T]):
|
|
|
726
734
|
def cancelled(self) -> bool:
|
|
727
735
|
return self._completed.is_set() and self._state == self.CANCELLED
|
|
728
736
|
|
|
729
|
-
def result(self
|
|
737
|
+
def result(self) -> T:
|
|
730
738
|
"""Block an external thread until completion and return/raise the result."""
|
|
731
739
|
if not self.done() and self._runtime.is_runtime_thread():
|
|
732
740
|
raise RuntimeError("Task.result() would block the runtime thread; use 'await task'")
|
|
733
|
-
|
|
734
|
-
raise TimeoutError(f"{self.name} did not complete before the timeout")
|
|
741
|
+
self._completed.wait()
|
|
735
742
|
return self._raise_or_return()
|
|
736
743
|
|
|
737
|
-
def exception(self
|
|
744
|
+
def exception(self) -> BaseException | None:
|
|
738
745
|
if not self.done() and self._runtime.is_runtime_thread():
|
|
739
746
|
raise RuntimeError("Task.exception() would block the runtime thread")
|
|
740
|
-
|
|
741
|
-
raise TimeoutError(f"{self.name} did not complete before the timeout")
|
|
747
|
+
self._completed.wait()
|
|
742
748
|
if self._state == self.CANCELLED:
|
|
743
749
|
raise self._cancellation_exception()
|
|
744
750
|
return self._exception
|
|
@@ -758,10 +764,10 @@ class Task(Generic[T]):
|
|
|
758
764
|
return False
|
|
759
765
|
return True
|
|
760
766
|
|
|
761
|
-
def __await__(self):
|
|
767
|
+
def __await__(self) -> Generator[Any, T, T]:
|
|
762
768
|
if self.done():
|
|
763
769
|
return self._raise_or_return()
|
|
764
|
-
result = yield _WaitTask(self._runtime, self)
|
|
770
|
+
result = yield _WaitTask[T](self._runtime, self)
|
|
765
771
|
return result
|
|
766
772
|
|
|
767
773
|
def _raise_or_return(self) -> T:
|
|
@@ -1088,9 +1094,10 @@ def _normalize_tasks(items: tuple[Any, ...], *, allow_empty: bool) -> tuple[Task
|
|
|
1088
1094
|
return tuple(normalized)
|
|
1089
1095
|
|
|
1090
1096
|
|
|
1091
|
-
async def _when_all_coroutine(tasks: tuple[Task[
|
|
1097
|
+
async def _when_all_coroutine(tasks: tuple[Task[T], ...]) -> list[T]:
|
|
1092
1098
|
await _WaitMany(_get_runtime(), tasks, "all")
|
|
1093
1099
|
failures = tuple(task._exception for task in tasks if task._state == Task.FAILED)
|
|
1100
|
+
|
|
1094
1101
|
if len(failures) == 1:
|
|
1095
1102
|
assert failures[0] is not None
|
|
1096
1103
|
raise failures[0]
|
|
@@ -1101,7 +1108,7 @@ async def _when_all_coroutine(tasks: tuple[Task[Any], ...]) -> list[Any]:
|
|
|
1101
1108
|
return [task._result for task in tasks]
|
|
1102
1109
|
|
|
1103
1110
|
|
|
1104
|
-
async def _when_any_coroutine(tasks: tuple[Task[
|
|
1111
|
+
async def _when_any_coroutine(tasks: tuple[Task[T], ...]) -> Task[T]:
|
|
1105
1112
|
winner = await _WaitMany(_get_runtime(), tasks, "any")
|
|
1106
1113
|
return cast(Task[Any], winner)
|
|
1107
1114
|
|
|
@@ -1823,17 +1830,38 @@ def _shutdown_at_exit() -> None:
|
|
|
1823
1830
|
|
|
1824
1831
|
atexit.register(_shutdown_at_exit)
|
|
1825
1832
|
|
|
1826
|
-
|
|
1833
|
+
|
|
1834
|
+
@overload
|
|
1835
|
+
def task[**P, T](
|
|
1836
|
+
function: Callable[P, Awaitable[T]]
|
|
1837
|
+
) -> Callable[P, Task[T]]: ...
|
|
1838
|
+
|
|
1839
|
+
@overload
|
|
1840
|
+
def task[**P, T](
|
|
1841
|
+
function: Callable[P, T]
|
|
1842
|
+
) -> Callable[P, Task[T]]: ...
|
|
1843
|
+
|
|
1844
|
+
def task(function: Callable[..., Any]) -> Callable[..., Task[Any]]:
|
|
1827
1845
|
"""
|
|
1828
1846
|
| Wraps sync functions and non-asyncio dependent coroutines to eagerly executed await-able tasks
|
|
1829
1847
|
"""
|
|
1830
|
-
@
|
|
1848
|
+
@wraps(function)
|
|
1831
1849
|
def wrapper(*args: Any, **kwargs: Any) -> Task[Any]:
|
|
1832
|
-
return Task.run(
|
|
1850
|
+
return Task.run(function, *args, **kwargs)
|
|
1833
1851
|
|
|
1834
1852
|
return wrapper
|
|
1835
1853
|
|
|
1836
1854
|
|
|
1855
|
+
@overload
|
|
1856
|
+
def asyncio_task[**P, T](
|
|
1857
|
+
async_function: Callable[P, Awaitable[T]]
|
|
1858
|
+
) -> Callable[P, Task[T]]: ...
|
|
1859
|
+
|
|
1860
|
+
@overload
|
|
1861
|
+
def asyncio_task[**P, T](
|
|
1862
|
+
async_function: Callable[P, T], /
|
|
1863
|
+
) -> Callable[P, Task[T]]: ...
|
|
1864
|
+
|
|
1837
1865
|
def asyncio_task(async_function: Callable[..., Any]) -> Callable[..., Task[Any]]:
|
|
1838
1866
|
"""
|
|
1839
1867
|
| Wraps async functions with a dependency on asyncio to be compatible with Task.run(coro)
|
|
@@ -1852,7 +1880,7 @@ def asyncio_task(async_function: Callable[..., Any]) -> Callable[..., Task[Any]]
|
|
|
1852
1880
|
|
|
1853
1881
|
|
|
1854
1882
|
class _SyncBridge:
|
|
1855
|
-
def __rshift__(self, ttwa: Task[T]) ->
|
|
1883
|
+
def __rshift__(self, ttwa: Task[T]) -> T:
|
|
1856
1884
|
frame = inspect.currentframe()
|
|
1857
1885
|
try:
|
|
1858
1886
|
assert frame is not None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|