flpit 0.2.0.dev2__py3-none-any.whl → 0.2.1.dev3__py3-none-any.whl
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/__init__.py +29 -3
- flpit/core/linq.py +110 -37
- flpit/json.py +427 -0
- {flpit-0.2.0.dev2.dist-info → flpit-0.2.1.dev3.dist-info}/METADATA +7 -3
- flpit-0.2.1.dev3.dist-info/RECORD +11 -0
- flpit-0.2.0.dev2.dist-info/RECORD +0 -10
- {flpit-0.2.0.dev2.dist-info → flpit-0.2.1.dev3.dist-info}/WHEEL +0 -0
- {flpit-0.2.0.dev2.dist-info → flpit-0.2.1.dev3.dist-info}/licenses/LICENSE +0 -0
flpit/__init__.py
CHANGED
|
@@ -5,33 +5,59 @@ from __future__ import annotations
|
|
|
5
5
|
from importlib.metadata import version
|
|
6
6
|
__version__ = version("flpit")
|
|
7
7
|
|
|
8
|
-
import builtins
|
|
9
|
-
from typing import Iterable as _Iterable, TypeVar
|
|
10
|
-
|
|
11
8
|
from flpit import flp
|
|
12
9
|
from flpit.core.linq import (
|
|
13
10
|
FlpIt,
|
|
14
11
|
FlpList,
|
|
15
12
|
Grouping,
|
|
16
13
|
OrderedIt,
|
|
14
|
+
|
|
15
|
+
#====================
|
|
16
|
+
# InvalidOperationException
|
|
17
|
+
#====================
|
|
18
|
+
InvalidOperationError,
|
|
17
19
|
EmptySequenceError,
|
|
18
20
|
NoMatchError,
|
|
19
21
|
MultipleElementsError,
|
|
20
22
|
MultipleMatchesError,
|
|
23
|
+
|
|
24
|
+
#====================
|
|
25
|
+
# ArgumentExceptions
|
|
26
|
+
#====================
|
|
27
|
+
ArgumentError,
|
|
28
|
+
ArgumentNullError,
|
|
21
29
|
SourceNoneError,
|
|
22
30
|
PredicateNoneError,
|
|
31
|
+
CollectionNoneError,
|
|
32
|
+
SelectorNoneError,
|
|
33
|
+
SecondNoneError,
|
|
34
|
+
ArgumentOutOfRangeError,
|
|
35
|
+
ArgumentNonNegError,
|
|
36
|
+
ArgumentOutOfBoundsError
|
|
23
37
|
)
|
|
24
38
|
|
|
25
39
|
__all__ = [
|
|
40
|
+
"flp",
|
|
26
41
|
"FlpIt",
|
|
27
42
|
"FlpList",
|
|
28
43
|
"Grouping",
|
|
29
44
|
"OrderedIt",
|
|
45
|
+
|
|
46
|
+
"InvalidOperationError",
|
|
30
47
|
"EmptySequenceError",
|
|
31
48
|
"NoMatchError",
|
|
32
49
|
"MultipleElementsError",
|
|
33
50
|
"MultipleMatchesError",
|
|
51
|
+
|
|
52
|
+
"ArgumentError",
|
|
53
|
+
"ArgumentNullError",
|
|
34
54
|
"SourceNoneError",
|
|
35
55
|
"PredicateNoneError",
|
|
56
|
+
"CollectionNoneError",
|
|
57
|
+
"SelectorNoneError",
|
|
58
|
+
"SecondNoneError",
|
|
59
|
+
"ArgumentOutOfRangeError",
|
|
60
|
+
"ArgumentNonNegError",
|
|
61
|
+
"ArgumentOutOfBoundsError",
|
|
36
62
|
]
|
|
37
63
|
|
flpit/core/linq.py
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
from typing import SupportsIndex
|
|
3
1
|
import decimal
|
|
4
2
|
from decimal import Decimal, Overflow, Inexact
|
|
5
3
|
|
|
@@ -15,15 +13,21 @@ from typing import (
|
|
|
15
13
|
Iterable,
|
|
16
14
|
Iterator,
|
|
17
15
|
Collection,
|
|
16
|
+
SupportsIndex,
|
|
18
17
|
List,
|
|
19
18
|
Set,
|
|
20
19
|
Type,
|
|
21
20
|
TypeVar,
|
|
21
|
+
ClassVar,
|
|
22
22
|
Union,
|
|
23
23
|
override,
|
|
24
24
|
overload,
|
|
25
25
|
cast as typing_cast,
|
|
26
|
+
GenericAlias
|
|
26
27
|
)
|
|
28
|
+
|
|
29
|
+
from flpit.json import JsonSerializerOptions, decode_dataclass, encode_dataclass
|
|
30
|
+
|
|
27
31
|
TItem = TypeVar("TItem")
|
|
28
32
|
TResult = TypeVar("TResult")
|
|
29
33
|
TKey = TypeVar("TKey")
|
|
@@ -39,38 +43,70 @@ class _Sentinel:
|
|
|
39
43
|
_SENTINEL = _Sentinel()
|
|
40
44
|
_MISSING = object()
|
|
41
45
|
|
|
46
|
+
# =============================================================================
|
|
47
|
+
# Invalid Operation Errors
|
|
48
|
+
# =============================================================================
|
|
49
|
+
class InvalidOperationError(ValueError):
|
|
50
|
+
pass
|
|
42
51
|
|
|
43
|
-
class EmptySequenceError(
|
|
52
|
+
class EmptySequenceError(InvalidOperationError):
|
|
44
53
|
def __init__(self):
|
|
45
54
|
super().__init__("Sequence contains no elements")
|
|
46
55
|
|
|
47
|
-
class NoMatchError(
|
|
56
|
+
class NoMatchError(InvalidOperationError):
|
|
48
57
|
def __init__(self):
|
|
49
58
|
super().__init__("Sequence contains no matching elements")
|
|
50
59
|
|
|
51
|
-
class MultipleMatchesError(
|
|
60
|
+
class MultipleMatchesError(InvalidOperationError):
|
|
52
61
|
def __init__(self):
|
|
53
62
|
super().__init__("Sequence contains more than one matching element")
|
|
54
63
|
|
|
55
|
-
class MultipleElementsError(
|
|
64
|
+
class MultipleElementsError(InvalidOperationError):
|
|
56
65
|
def __init__(self):
|
|
57
66
|
super().__init__("Sequence contains more than one element")
|
|
58
67
|
|
|
59
|
-
|
|
68
|
+
|
|
69
|
+
# =============================================================================
|
|
70
|
+
# Argument Errors
|
|
71
|
+
# =============================================================================
|
|
72
|
+
class ArgumentError(TypeError):
|
|
73
|
+
pass
|
|
74
|
+
|
|
75
|
+
class ArgumentNullError(ArgumentError):
|
|
76
|
+
def __init__(self, argument: str):
|
|
77
|
+
super().__init__(f"Value cannot be None. (Argument '{argument}')")
|
|
78
|
+
|
|
79
|
+
class SourceNoneError(ArgumentNullError):
|
|
60
80
|
def __init__(self):
|
|
61
|
-
super().__init__("
|
|
81
|
+
super().__init__("source")
|
|
62
82
|
|
|
63
|
-
class PredicateNoneError(
|
|
83
|
+
class PredicateNoneError(ArgumentNullError):
|
|
64
84
|
def __init__(self):
|
|
65
|
-
super().__init__("
|
|
85
|
+
super().__init__("predicate")
|
|
66
86
|
|
|
67
|
-
class CollectionNoneError(
|
|
87
|
+
class CollectionNoneError(ArgumentNullError):
|
|
68
88
|
def __init__(self):
|
|
69
|
-
super().__init__("
|
|
89
|
+
super().__init__("collection")
|
|
90
|
+
|
|
91
|
+
class SelectorNoneError(ArgumentNullError):
|
|
92
|
+
def __init__(self):
|
|
93
|
+
super().__init__("keySelector")
|
|
94
|
+
|
|
95
|
+
class SecondNoneError(ArgumentNullError):
|
|
96
|
+
def __init__(self):
|
|
97
|
+
super().__init__("second")
|
|
98
|
+
|
|
99
|
+
class ArgumentOutOfRangeError(ArgumentError):
|
|
100
|
+
def __init__(self, argument: str, pattern:str="Index was out of range. Must be non-negative and less than the size of the collection."):
|
|
101
|
+
super().__init__(f"{pattern} (Parameter '{argument}')")
|
|
102
|
+
|
|
103
|
+
class ArgumentNonNegError(ArgumentOutOfRangeError):
|
|
104
|
+
def __init__(self, argument:str):
|
|
105
|
+
super().__init__(argument, "Non-negative number required.")
|
|
70
106
|
|
|
71
|
-
class
|
|
107
|
+
class ArgumentOutOfBoundsError(ArgumentError):
|
|
72
108
|
def __init__(self):
|
|
73
|
-
super().__init__("
|
|
109
|
+
super().__init__("Offset and length were out of bounds for the array or count is greater than the number of elements from index to the end of the source collection.")
|
|
74
110
|
|
|
75
111
|
|
|
76
112
|
def _guard_empty(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
@@ -191,6 +227,9 @@ class FlpIt(Iterable[TItem], Generic[TItem]):
|
|
|
191
227
|
return builtins.all(map(predicate, self))
|
|
192
228
|
|
|
193
229
|
def concat(self, second: Iterable[TItem]) -> "FlpIt[TItem]":
|
|
230
|
+
if second is None:
|
|
231
|
+
raise SecondNoneError()
|
|
232
|
+
|
|
194
233
|
def _generator() -> Iterator[TItem]:
|
|
195
234
|
yield from self
|
|
196
235
|
yield from second
|
|
@@ -211,8 +250,8 @@ class FlpIt(Iterable[TItem], Generic[TItem]):
|
|
|
211
250
|
for item in self:
|
|
212
251
|
if predicate(item):
|
|
213
252
|
yield item
|
|
214
|
-
|
|
215
|
-
return
|
|
253
|
+
# return typing_cast(FlpIt[TItem], FlpIt[TItem](_FactoryIterable(_generator)))
|
|
254
|
+
return FlpIt[TItem](_FactoryIterable(_generator))
|
|
216
255
|
|
|
217
256
|
def select(self, selector: Callable[[TItem], TResult]) -> FlpIt[TResult]:
|
|
218
257
|
"""Projects each element into a new form."""
|
|
@@ -232,6 +271,18 @@ class FlpIt(Iterable[TItem], Generic[TItem]):
|
|
|
232
271
|
|
|
233
272
|
return FlpIt(_FactoryIterable(_generator))
|
|
234
273
|
|
|
274
|
+
def skip(self, count: int) -> "FlpIt[TItem]":
|
|
275
|
+
"""
|
|
276
|
+
|Bypasses a specified number of elements from the start.
|
|
277
|
+
"""
|
|
278
|
+
if count <= 0:
|
|
279
|
+
return self
|
|
280
|
+
|
|
281
|
+
def _generator() -> Iterator[TItem]:
|
|
282
|
+
yield from islice(iter(self), count, None)
|
|
283
|
+
|
|
284
|
+
return FlpIt(_FactoryIterable(_generator))
|
|
285
|
+
|
|
235
286
|
def take(self, count: int) -> "FlpIt[TItem]":
|
|
236
287
|
"""
|
|
237
288
|
|Returns a specified number of contiguous elements from the start.
|
|
@@ -528,11 +579,13 @@ class FlpIt(Iterable[TItem], Generic[TItem]):
|
|
|
528
579
|
def element_at(self, index: int) -> TItem:
|
|
529
580
|
"""Returns the element at a specified index in a sequence."""
|
|
530
581
|
if index < 0:
|
|
531
|
-
raise
|
|
582
|
+
raise ArgumentOutOfRangeError("index")
|
|
583
|
+
|
|
532
584
|
for i, item in enumerate(self):
|
|
533
585
|
if i == index:
|
|
534
586
|
return item
|
|
535
|
-
|
|
587
|
+
|
|
588
|
+
raise ArgumentOutOfRangeError("index")
|
|
536
589
|
|
|
537
590
|
def first(self, predicate: Callable[[TItem], bool] | _Sentinel = _SENTINEL) -> TItem:
|
|
538
591
|
"""Returns the first element matching a predicate, or raises ValueError."""
|
|
@@ -568,10 +621,10 @@ class FlpIt(Iterable[TItem], Generic[TItem]):
|
|
|
568
621
|
|
|
569
622
|
raise MultipleMatchesError() if has_predicate else MultipleElementsError()
|
|
570
623
|
|
|
571
|
-
|
|
572
624
|
def to_list(self) -> FlpList[TItem]:
|
|
573
625
|
"""Explicitly materializes the query into a FlpList."""
|
|
574
|
-
return FlpList(self)
|
|
626
|
+
return FlpList[TItem](self)
|
|
627
|
+
|
|
575
628
|
|
|
576
629
|
from functools import partial
|
|
577
630
|
|
|
@@ -682,6 +735,8 @@ class FlpList(Collection[TItem], Generic[TItem]):
|
|
|
682
735
|
| Fluent List
|
|
683
736
|
A materialized list extending UserList that yields lazy FlpIt instances for query operations.
|
|
684
737
|
"""
|
|
738
|
+
__type_args__: ClassVar[tuple[Any, ...]]
|
|
739
|
+
|
|
685
740
|
def __init__(self, source: Iterable[TItem] | _Sentinel=_SENTINEL):
|
|
686
741
|
if source is None:
|
|
687
742
|
raise SourceNoneError()
|
|
@@ -830,7 +885,7 @@ class FlpList(Collection[TItem], Generic[TItem]):
|
|
|
830
885
|
|
|
831
886
|
def insert(self, index: int, item: TItem) -> None:
|
|
832
887
|
if index < 0 or index > len(self.__list.data):
|
|
833
|
-
raise
|
|
888
|
+
raise ArgumentOutOfRangeError("index")
|
|
834
889
|
|
|
835
890
|
self.__list.data.insert(index, item)
|
|
836
891
|
|
|
@@ -839,7 +894,7 @@ class FlpList(Collection[TItem], Generic[TItem]):
|
|
|
839
894
|
raise CollectionNoneError()
|
|
840
895
|
|
|
841
896
|
if index < 0 or index > len(self.__list.data):
|
|
842
|
-
raise
|
|
897
|
+
raise ArgumentOutOfRangeError("index")
|
|
843
898
|
|
|
844
899
|
self.__list.data[index:index] = list(collection)
|
|
845
900
|
|
|
@@ -865,22 +920,22 @@ class FlpList(Collection[TItem], Generic[TItem]):
|
|
|
865
920
|
return original_length - len(self.__list.data)
|
|
866
921
|
|
|
867
922
|
def remove_at(self, index: int) -> None:
|
|
868
|
-
if index < 0 or index
|
|
869
|
-
raise
|
|
923
|
+
if index < 0 or index >= len(self.__list.data):
|
|
924
|
+
raise ArgumentOutOfRangeError("index")
|
|
870
925
|
|
|
871
926
|
del self.__list.data[index]
|
|
872
927
|
|
|
873
928
|
def remove_range(self, index: int, count: int) -> None:
|
|
874
929
|
size = len(self.__list.data)
|
|
875
930
|
|
|
876
|
-
if index < 0
|
|
877
|
-
raise
|
|
931
|
+
if index < 0:
|
|
932
|
+
raise ArgumentNonNegError("index")
|
|
878
933
|
|
|
879
934
|
if count < 0:
|
|
880
|
-
raise
|
|
935
|
+
raise ArgumentNonNegError("count")
|
|
881
936
|
|
|
882
937
|
if count > size - index:
|
|
883
|
-
raise
|
|
938
|
+
raise ArgumentOutOfBoundsError()
|
|
884
939
|
|
|
885
940
|
del self.__list.data[index : index + count]
|
|
886
941
|
|
|
@@ -899,17 +954,13 @@ class FlpList(Collection[TItem], Generic[TItem]):
|
|
|
899
954
|
|
|
900
955
|
# Apply your List<T>-compatible argument validation here.
|
|
901
956
|
if index < 0 or index > size:
|
|
902
|
-
raise
|
|
957
|
+
raise ArgumentNonNegError("index")
|
|
903
958
|
|
|
904
959
|
if count < 0:
|
|
905
|
-
raise
|
|
960
|
+
raise ArgumentNonNegError("count")
|
|
906
961
|
|
|
907
962
|
if index + count > size:
|
|
908
|
-
raise
|
|
909
|
-
"Offset and length were out of bounds for the array or "
|
|
910
|
-
"count is greater than the number of elements from index "
|
|
911
|
-
"to the end of the source collection."
|
|
912
|
-
)
|
|
963
|
+
raise ArgumentOutOfBoundsError()
|
|
913
964
|
|
|
914
965
|
self.__list.data[index:index + count] = reversed(
|
|
915
966
|
self.__list.data[index:index + count]
|
|
@@ -948,12 +999,15 @@ class FlpList(Collection[TItem], Generic[TItem]):
|
|
|
948
999
|
"""Prepends an element to the sequence lazily, returning a FlpIt without mutating this list."""
|
|
949
1000
|
return FlpIt(self.__list.data).prepend(element)
|
|
950
1001
|
|
|
1002
|
+
def concat(self, second: Iterable[TItem]) -> FlpIt[TItem]:
|
|
1003
|
+
return FlpIt(self.__list.data).concat(second)
|
|
1004
|
+
|
|
951
1005
|
# noinspection unused-parameter
|
|
952
1006
|
def as_type(self, target_type: Type[TResult]) -> FlpList[TResult]:
|
|
953
1007
|
return self # type: ignore[return-value]
|
|
954
1008
|
|
|
955
1009
|
def where(self, predicate: Callable[[TItem], bool]) -> FlpIt[TItem]:
|
|
956
|
-
return FlpIt(self.__list.data).where(predicate)
|
|
1010
|
+
return FlpIt[TItem](self.__list.data).where(predicate)
|
|
957
1011
|
|
|
958
1012
|
def select(self, selector: Callable[[TItem], TResult]) -> FlpIt[TResult]:
|
|
959
1013
|
return FlpIt(self.__list.data).select(selector)
|
|
@@ -963,6 +1017,9 @@ class FlpList(Collection[TItem], Generic[TItem]):
|
|
|
963
1017
|
) -> FlpIt[TResult]:
|
|
964
1018
|
return FlpIt(self.__list.data).select_many(selector)
|
|
965
1019
|
|
|
1020
|
+
def skip(self, count: int) -> FlpIt[TItem]:
|
|
1021
|
+
return FlpIt(self.__list.data).skip(count)
|
|
1022
|
+
|
|
966
1023
|
def take(self, count: int) -> FlpIt[TItem]:
|
|
967
1024
|
return FlpIt(self.__list.data).take(count)
|
|
968
1025
|
|
|
@@ -1076,7 +1133,7 @@ class FlpList(Collection[TItem], Generic[TItem]):
|
|
|
1076
1133
|
|
|
1077
1134
|
def element_at(self, index: int) -> TItem:
|
|
1078
1135
|
if index < 0 or index >= len(self.__list.data):
|
|
1079
|
-
raise
|
|
1136
|
+
raise ArgumentOutOfRangeError("index")
|
|
1080
1137
|
return self.__list.data[index]
|
|
1081
1138
|
|
|
1082
1139
|
def first(self, predicate: Callable[[TItem], bool] | _Sentinel = _SENTINEL) -> TItem:
|
|
@@ -1128,3 +1185,19 @@ class FlpList(Collection[TItem], Generic[TItem]):
|
|
|
1128
1185
|
def to_list(self) -> FlpList[TItem]:
|
|
1129
1186
|
"""Explicitly returns a shallow copy instance to isolate mutations."""
|
|
1130
1187
|
return FlpList(self.__list.data.copy())
|
|
1188
|
+
|
|
1189
|
+
# =====================================================================
|
|
1190
|
+
# 5. json serialization
|
|
1191
|
+
# =====================================================================
|
|
1192
|
+
|
|
1193
|
+
@classmethod
|
|
1194
|
+
def from_json[T](cls, data: str | bytes, item_type: type[T], jso: JsonSerializerOptions|None=None) -> FlpList[T]:
|
|
1195
|
+
return FlpList[T](decode_dataclass(data, list[item_type], jso))
|
|
1196
|
+
|
|
1197
|
+
def to_json_bytes[T](self, item_type: type[T], jso: JsonSerializerOptions|None=None) -> bytes:
|
|
1198
|
+
return encode_dataclass(self.__list.data, list[item_type], jso)
|
|
1199
|
+
|
|
1200
|
+
def to_json[T](self, item_type: type[T], jso: JsonSerializerOptions|None=None) -> str:
|
|
1201
|
+
return self.to_json_bytes(item_type, jso).decode(encoding="utf-8")
|
|
1202
|
+
|
|
1203
|
+
|
flpit/json.py
ADDED
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
import dataclasses
|
|
3
|
+
import re
|
|
4
|
+
import sys
|
|
5
|
+
import types
|
|
6
|
+
import typing
|
|
7
|
+
|
|
8
|
+
import threading
|
|
9
|
+
from dataclasses import MISSING, dataclass
|
|
10
|
+
from functools import lru_cache
|
|
11
|
+
from typing import Any, ForwardRef, get_args, get_origin, get_type_hints
|
|
12
|
+
from hashlib import blake2s
|
|
13
|
+
|
|
14
|
+
import msgspec
|
|
15
|
+
|
|
16
|
+
from enum import StrEnum
|
|
17
|
+
|
|
18
|
+
class JsonNamingPolicy(StrEnum):
|
|
19
|
+
lower_case = "lower"
|
|
20
|
+
upper_case = "upper"
|
|
21
|
+
camel_case = "camel"
|
|
22
|
+
pascal_case = "pascal"
|
|
23
|
+
kebap_case = "kebab"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(slots=True, frozen=True)
|
|
27
|
+
class JsonSerializerOptions:
|
|
28
|
+
naming_policy: JsonNamingPolicy = JsonNamingPolicy.camel_case
|
|
29
|
+
write_indented: bool = False
|
|
30
|
+
native:bool=False
|
|
31
|
+
"""
|
|
32
|
+
| Set to true for the native speed of msgspec
|
|
33
|
+
|
|
34
|
+
!! only set to true if you know what you are doing !!
|
|
35
|
+
You'll receive the msgspec.msgstruct object that will try to do it's best to behave like your dataclass.
|
|
36
|
+
But remember !! "It's NOT your dataclass" !!
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def fingerprint(self) -> str:
|
|
41
|
+
"""
|
|
42
|
+
Identifies a unique jso instance for building a struct
|
|
43
|
+
!! All parts of a jso that don't influence the defstruct MUST be ignored!
|
|
44
|
+
"""
|
|
45
|
+
settings = self.naming_policy.value
|
|
46
|
+
return blake2s(settings.encode(), digest_size=6).hexdigest()
|
|
47
|
+
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
# Metadata Annotation Markers
|
|
50
|
+
# ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
class JsonIgnore:
|
|
53
|
+
def __class_getitem__[T](cls, item: type[T]) -> Any:
|
|
54
|
+
return Annotated[item, cls]
|
|
55
|
+
|
|
56
|
+
class JsonInclude:
|
|
57
|
+
def __class_getitem__[T](cls, item: type[T]) -> Any:
|
|
58
|
+
return Annotated[item, cls]
|
|
59
|
+
|
|
60
|
+
class JsonProperty:
|
|
61
|
+
def __class_getitem__[T](cls, item: tuple[type[T], str]) -> Any:
|
|
62
|
+
target_type, json_name = item
|
|
63
|
+
return Annotated[target_type, cls(json_name)]
|
|
64
|
+
|
|
65
|
+
def __init__(self, name: str):
|
|
66
|
+
self.name = name
|
|
67
|
+
|
|
68
|
+
JIgnore = JsonIgnore
|
|
69
|
+
JInclude = JsonInclude
|
|
70
|
+
JProp = JsonProperty
|
|
71
|
+
|
|
72
|
+
# ---------------------------------------------------------------------
|
|
73
|
+
# Generated type metadata
|
|
74
|
+
# ---------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
@dataclass(slots=True, eq=False)
|
|
77
|
+
class _WireType:
|
|
78
|
+
source: type
|
|
79
|
+
"""The source type to build a msgspec.struct for"""
|
|
80
|
+
hints: dict[str, Any]
|
|
81
|
+
namespace: types.ModuleType
|
|
82
|
+
"""the name space we are targeting"""
|
|
83
|
+
struct: type | None = None
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def module_name(self: _WireType) -> str:
|
|
87
|
+
return self.namespace.__name__
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def class_name(self: _WireType) -> str:
|
|
91
|
+
return self.source.__name__
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def ref_name(self: _WireType) -> str:
|
|
95
|
+
identity = (
|
|
96
|
+
f"{self.source.__module__}_{self.source.__qualname__}"
|
|
97
|
+
)
|
|
98
|
+
return "_ref_" + re.sub(r"\W", "_", identity)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# ---------------------------------------------------------------------
|
|
102
|
+
# Module and annotation utilities
|
|
103
|
+
# ---------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
def _generated_module(source: type) -> types.ModuleType:
|
|
106
|
+
"""Generates the required module definition for our runtime only dynamic msgspec.struct"""
|
|
107
|
+
name = f"flpit.json._generated.{source.__module__}"
|
|
108
|
+
|
|
109
|
+
module = sys.modules.get(name)
|
|
110
|
+
if module is None:
|
|
111
|
+
module = types.ModuleType(name)
|
|
112
|
+
sys.modules[name] = module
|
|
113
|
+
|
|
114
|
+
return module
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _is_dataclass_type(tp: Any) -> bool:
|
|
118
|
+
return isinstance(tp, type) and dataclasses.is_dataclass(tp)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _nested_dataclasses(tp: Any):
|
|
122
|
+
"""Yield dataclass types referenced by an annotation."""
|
|
123
|
+
if _is_dataclass_type(tp):
|
|
124
|
+
yield tp
|
|
125
|
+
return
|
|
126
|
+
|
|
127
|
+
if isinstance(tp, (list, tuple)):
|
|
128
|
+
for item in tp:
|
|
129
|
+
yield from _nested_dataclasses(item)
|
|
130
|
+
return
|
|
131
|
+
|
|
132
|
+
origin = get_origin(tp)
|
|
133
|
+
|
|
134
|
+
if origin is typing.Annotated:
|
|
135
|
+
yield from _nested_dataclasses(get_args(tp)[0])
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
if origin is typing.Literal:
|
|
139
|
+
return
|
|
140
|
+
|
|
141
|
+
for arg in get_args(tp):
|
|
142
|
+
yield from _nested_dataclasses(arg)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _rewrite_type(
|
|
146
|
+
tp: Any,
|
|
147
|
+
owner: _WireType,
|
|
148
|
+
graph: dict[type, _WireType],
|
|
149
|
+
*,
|
|
150
|
+
resolve: bool = False,
|
|
151
|
+
) -> Any:
|
|
152
|
+
"""Replace source dataclasses with generated Structs or ForwardRefs."""
|
|
153
|
+
if tp is getattr(typing, "Self", None):
|
|
154
|
+
info = owner
|
|
155
|
+
return info.struct if resolve else ForwardRef(info.ref_name)
|
|
156
|
+
|
|
157
|
+
if _is_dataclass_type(tp):
|
|
158
|
+
info = graph[tp]
|
|
159
|
+
return info.struct if resolve else ForwardRef(info.ref_name)
|
|
160
|
+
|
|
161
|
+
if isinstance(tp, list):
|
|
162
|
+
return [
|
|
163
|
+
_rewrite_type(item, owner, graph, resolve=resolve)
|
|
164
|
+
for item in tp
|
|
165
|
+
]
|
|
166
|
+
|
|
167
|
+
if isinstance(tp, tuple):
|
|
168
|
+
return tuple(
|
|
169
|
+
_rewrite_type(item, owner, graph, resolve=resolve)
|
|
170
|
+
for item in tp
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
origin = get_origin(tp)
|
|
174
|
+
|
|
175
|
+
if origin is None or origin is typing.Literal:
|
|
176
|
+
return tp
|
|
177
|
+
|
|
178
|
+
args = get_args(tp)
|
|
179
|
+
|
|
180
|
+
if origin is typing.Annotated:
|
|
181
|
+
base = _rewrite_type(
|
|
182
|
+
args[0], owner, graph, resolve=resolve
|
|
183
|
+
)
|
|
184
|
+
return typing.Annotated[(base, *args[1:])]
|
|
185
|
+
|
|
186
|
+
rewritten: tuple[Any, ...] = tuple(
|
|
187
|
+
_rewrite_type(arg, owner, graph, resolve=resolve)
|
|
188
|
+
for arg in args
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
if origin in (typing.Union, types.UnionType):
|
|
192
|
+
union: Any = typing.Union # with all the magic here, there's no way to make typing really happy
|
|
193
|
+
return union[rewritten]
|
|
194
|
+
|
|
195
|
+
params = rewritten[0] if len(rewritten) == 1 else rewritten
|
|
196
|
+
|
|
197
|
+
try:
|
|
198
|
+
return origin[params]
|
|
199
|
+
except (TypeError, AttributeError) as exc:
|
|
200
|
+
copier = getattr(tp, "copy_with", None)
|
|
201
|
+
if copier is not None:
|
|
202
|
+
return copier(rewritten)
|
|
203
|
+
|
|
204
|
+
raise TypeError(
|
|
205
|
+
f"Cannot rewrite annotation {tp!r}"
|
|
206
|
+
) from exc
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
# ---------------------------------------------------------------------
|
|
210
|
+
# Graph discovery
|
|
211
|
+
# ---------------------------------------------------------------------
|
|
212
|
+
|
|
213
|
+
def _discover_graph(
|
|
214
|
+
roots: tuple[type, ...],
|
|
215
|
+
) -> dict[type, _WireType]:
|
|
216
|
+
"""Discover reachable dataclasses and resolve their annotations."""
|
|
217
|
+
graph: dict[type, _WireType] = {}
|
|
218
|
+
pending = list(roots)
|
|
219
|
+
|
|
220
|
+
while pending:
|
|
221
|
+
source = pending.pop()
|
|
222
|
+
|
|
223
|
+
if source in graph:
|
|
224
|
+
continue
|
|
225
|
+
|
|
226
|
+
info = _WireType(
|
|
227
|
+
source=source,
|
|
228
|
+
hints=get_type_hints(source, include_extras=True),
|
|
229
|
+
namespace=_generated_module(source),
|
|
230
|
+
)
|
|
231
|
+
graph[source] = info
|
|
232
|
+
|
|
233
|
+
for field in dataclasses.fields(source):
|
|
234
|
+
if not field.init:
|
|
235
|
+
continue
|
|
236
|
+
|
|
237
|
+
pending.extend(
|
|
238
|
+
_nested_dataclasses(info.hints[field.name])
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
return graph
|
|
242
|
+
|
|
243
|
+
def _allows_none(tp: Any) -> bool:
|
|
244
|
+
if get_origin(tp) is typing.Annotated:
|
|
245
|
+
tp = get_args(tp)[0]
|
|
246
|
+
|
|
247
|
+
return type(None) in get_args(tp)
|
|
248
|
+
|
|
249
|
+
# ---------------------------------------------------------------------
|
|
250
|
+
# Struct construction
|
|
251
|
+
# ---------------------------------------------------------------------
|
|
252
|
+
|
|
253
|
+
def _struct_fields(
|
|
254
|
+
info: _WireType,
|
|
255
|
+
graph: dict[type, _WireType],
|
|
256
|
+
) -> list[tuple[Any, ...]]:
|
|
257
|
+
definitions: list[tuple[Any, ...]] = []
|
|
258
|
+
|
|
259
|
+
for field in dataclasses.fields(info.source):
|
|
260
|
+
if not field.init:
|
|
261
|
+
continue
|
|
262
|
+
|
|
263
|
+
annotation = _rewrite_type(
|
|
264
|
+
info.hints[field.name],
|
|
265
|
+
info,
|
|
266
|
+
graph,
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
if field.default is not MISSING:
|
|
270
|
+
spec = (field.name, annotation, field.default)
|
|
271
|
+
|
|
272
|
+
elif field.default_factory is not MISSING:
|
|
273
|
+
spec = (
|
|
274
|
+
field.name,
|
|
275
|
+
annotation,
|
|
276
|
+
msgspec.field(
|
|
277
|
+
default_factory=field.default_factory
|
|
278
|
+
),
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
# for now... no enforcing of explicit null in json
|
|
282
|
+
elif _allows_none(info.hints[field.name]):
|
|
283
|
+
spec = (field.name, annotation, None)
|
|
284
|
+
|
|
285
|
+
else:
|
|
286
|
+
spec = (field.name, annotation)
|
|
287
|
+
|
|
288
|
+
definitions.append(spec)
|
|
289
|
+
|
|
290
|
+
return definitions
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _register_forward_refs(
|
|
294
|
+
graph: dict[type, _WireType],
|
|
295
|
+
) -> None:
|
|
296
|
+
"""Expose every graph reference in each participating module."""
|
|
297
|
+
namespaces = []
|
|
298
|
+
|
|
299
|
+
for info in graph.values():
|
|
300
|
+
if not any(info.namespace is ns for ns in namespaces):
|
|
301
|
+
namespaces.append(info.namespace)
|
|
302
|
+
|
|
303
|
+
for namespace in namespaces:
|
|
304
|
+
for info in graph.values():
|
|
305
|
+
setattr(namespace, info.ref_name, info.struct)
|
|
306
|
+
|
|
307
|
+
def _behavior_namespace(source: type) -> dict[str, Any]:
|
|
308
|
+
return {
|
|
309
|
+
name: value
|
|
310
|
+
for name, value in vars(source).items()
|
|
311
|
+
if (
|
|
312
|
+
not name.startswith("__")
|
|
313
|
+
and callable(value)
|
|
314
|
+
)
|
|
315
|
+
# pyrefly: ignore [implicit-any-lambda]
|
|
316
|
+
} | { "__class__": property(lambda self, cls=source: cls) }
|
|
317
|
+
|
|
318
|
+
def _build_graph(
|
|
319
|
+
roots: tuple[type, ...],
|
|
320
|
+
jso: JsonSerializerOptions,
|
|
321
|
+
) -> dict[type, _WireType]:
|
|
322
|
+
graph = _discover_graph(roots)
|
|
323
|
+
|
|
324
|
+
for wt in graph.values():
|
|
325
|
+
wt.struct = msgspec.defstruct(
|
|
326
|
+
wt.class_name,
|
|
327
|
+
_struct_fields(wt, graph),
|
|
328
|
+
module=wt.module_name,
|
|
329
|
+
namespace=_behavior_namespace(wt.source),
|
|
330
|
+
rename=jso.naming_policy.value,
|
|
331
|
+
kw_only=True,
|
|
332
|
+
omit_defaults=True, # don't render nulls
|
|
333
|
+
forbid_unknown_fields=False, # ensure ignoring unknow fields!
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
_register_forward_refs(graph)
|
|
337
|
+
|
|
338
|
+
for wt in graph.values():
|
|
339
|
+
msgspec.inspect.type_info(wt.struct)
|
|
340
|
+
|
|
341
|
+
# Publish the readable generated class names.
|
|
342
|
+
for wt in graph.values():
|
|
343
|
+
setattr(wt.namespace, wt.class_name, wt.struct)
|
|
344
|
+
|
|
345
|
+
return graph
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
# ---------------------------------------------------------------------
|
|
349
|
+
# Cache and public API
|
|
350
|
+
# ---------------------------------------------------------------------
|
|
351
|
+
|
|
352
|
+
# we must only build new when the
|
|
353
|
+
_jso_registry: dict[str, JsonSerializerOptions] = {}
|
|
354
|
+
_registry_lock = threading.Lock()
|
|
355
|
+
|
|
356
|
+
@lru_cache(maxsize=None)
|
|
357
|
+
def _wrapped_lru(roots: tuple[type, ...], jso_id: str):
|
|
358
|
+
with _registry_lock:
|
|
359
|
+
jso = _jso_registry[jso_id]
|
|
360
|
+
return _build_graph(roots, jso)
|
|
361
|
+
|
|
362
|
+
def _cached_graph(roots:tuple[object, ...], jso: JsonSerializerOptions):
|
|
363
|
+
with _registry_lock:
|
|
364
|
+
_jso_registry[jso.fingerprint] = jso
|
|
365
|
+
|
|
366
|
+
return _wrapped_lru(roots, jso.fingerprint)
|
|
367
|
+
|
|
368
|
+
def wire_type_for(
|
|
369
|
+
target_type: type,
|
|
370
|
+
jso: JsonSerializerOptions,
|
|
371
|
+
):
|
|
372
|
+
roots = tuple(_nested_dataclasses(target_type))
|
|
373
|
+
|
|
374
|
+
if not roots:
|
|
375
|
+
raise TypeError(
|
|
376
|
+
"Only @dataclass are supported!, "
|
|
377
|
+
f"got {target_type!r}"
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
# Normalize the root set so User and list[User] share the graph.
|
|
381
|
+
graph_roots = tuple(sorted(set(roots), key=id))
|
|
382
|
+
|
|
383
|
+
graph = _cached_graph(
|
|
384
|
+
graph_roots,
|
|
385
|
+
jso,
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
return _rewrite_type(
|
|
389
|
+
target_type,
|
|
390
|
+
graph[roots[0]],
|
|
391
|
+
graph,
|
|
392
|
+
resolve=True,
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def decode_dataclass(
|
|
397
|
+
data: bytes | str,
|
|
398
|
+
target: type,
|
|
399
|
+
jso: JsonSerializerOptions|None=None
|
|
400
|
+
):
|
|
401
|
+
if jso is None:
|
|
402
|
+
jso = JsonSerializerOptions()
|
|
403
|
+
|
|
404
|
+
wire_type = wire_type_for(target, jso)
|
|
405
|
+
parsed = msgspec.json.decode(data, type=wire_type)
|
|
406
|
+
|
|
407
|
+
if jso.native:
|
|
408
|
+
return parsed
|
|
409
|
+
|
|
410
|
+
return msgspec.convert(
|
|
411
|
+
parsed,
|
|
412
|
+
target,
|
|
413
|
+
from_attributes=True,
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
def encode_dataclass[T](
|
|
417
|
+
data: Any,
|
|
418
|
+
target: type[T],
|
|
419
|
+
jso: JsonSerializerOptions|None=None
|
|
420
|
+
) -> bytes:
|
|
421
|
+
if jso is None:
|
|
422
|
+
jso = JsonSerializerOptions()
|
|
423
|
+
|
|
424
|
+
wire_type = wire_type_for(target, jso)
|
|
425
|
+
wire_obj = msgspec.convert(data, wire_type, from_attributes=True)
|
|
426
|
+
return msgspec.json.encode(wire_obj)
|
|
427
|
+
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: flpit
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1.dev3
|
|
4
4
|
Summary: Fluent LINQ for Python — flip your iterables
|
|
5
|
+
Project-URL: Homepage, https://github.com/riemar/flpit
|
|
5
6
|
License: MIT
|
|
6
7
|
License-File: LICENSE
|
|
7
8
|
Keywords: fluent,generators,iterable,lazy-evaluation,linq,pipeline,pyrefly,type-safe
|
|
@@ -15,7 +16,8 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
15
16
|
Classifier: Programming Language :: Python :: 3.14
|
|
16
17
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
17
18
|
Classifier: Typing :: Typed
|
|
18
|
-
Requires-Python: >=3.
|
|
19
|
+
Requires-Python: >=3.12
|
|
20
|
+
Requires-Dist: msgspec>=0.22.0
|
|
19
21
|
Description-Content-Type: text/markdown
|
|
20
22
|
|
|
21
23
|
# FlpIt
|
|
@@ -369,16 +371,18 @@ Distributed under the MIT License. See `LICENSE` for more information.
|
|
|
369
371
|
| ├── test_all.py | 80 |
|
|
370
372
|
| ├── test_any.py | 90 |
|
|
371
373
|
| ├── test_average.py | 572 |
|
|
374
|
+
| ├── test_concat.py | 64 |
|
|
372
375
|
| ├── test_count.py | 37 |
|
|
373
376
|
| ├── test_generic_list.py | 95 |
|
|
374
377
|
| ├── test_last.py | 29 |
|
|
375
378
|
| ├── test_order_by.py | 100 |
|
|
376
379
|
| ├── test_order_descending_by.py | 45 |
|
|
380
|
+
| ├── test_skip.py | 75 |
|
|
377
381
|
| ├── test_sum.py | 134 |
|
|
378
382
|
| ├── test_take.py | 39 |
|
|
379
383
|
| ├── test_then_by.py | 40 |
|
|
380
384
|
| └── test_then_descending_by.py | 34 |
|
|
381
385
|
----------------------------------------------------------------
|
|
382
|
-
| GRAND TOTALS |
|
|
386
|
+
| GRAND TOTALS | 1434 |
|
|
383
387
|
================================================================
|
|
384
388
|
```
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
flp/__init__.py,sha256=reAHhZtYt0f2LHeIYJbklta6Vatr6yeQCL1R-9TTyY0,688
|
|
2
|
+
flpit/__init__.py,sha256=tx4tkpLxoKQTP7wu2PyUtQtLs9_pWHbx1jHh4Vdsrus,1275
|
|
3
|
+
flpit/flp.py,sha256=i-FbnMeO92cxNRssYsgdlm2UGjILpveXgYUT468fWvY,999
|
|
4
|
+
flpit/json.py,sha256=KHACbdvunJyyuVt18AZjC_L-CGh0aptkem0wiJtJx0s,11687
|
|
5
|
+
flpit/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
flpit/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
flpit/core/linq.py,sha256=ekihrga5XEhWUyG5pIRl3LJBrhr3V0cv498jFfyZhKg,40136
|
|
8
|
+
flpit-0.2.1.dev3.dist-info/METADATA,sha256=KqNxb2w0XNQsTcNVNVyGCX4nqnWHhCtH6-RvRFn5XXg,13041
|
|
9
|
+
flpit-0.2.1.dev3.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
10
|
+
flpit-0.2.1.dev3.dist-info/licenses/LICENSE,sha256=bi5Vg_YP3Oq3jaOMQT4b7WZu0NfBcZr-nRyOExUKJrQ,1063
|
|
11
|
+
flpit-0.2.1.dev3.dist-info/RECORD,,
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
flp/__init__.py,sha256=reAHhZtYt0f2LHeIYJbklta6Vatr6yeQCL1R-9TTyY0,688
|
|
2
|
-
flpit/__init__.py,sha256=T8tdJmfJiYiBuUszbACGan0U1meuBQa7IQpfIncADLA,705
|
|
3
|
-
flpit/flp.py,sha256=i-FbnMeO92cxNRssYsgdlm2UGjILpveXgYUT468fWvY,999
|
|
4
|
-
flpit/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
-
flpit/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
-
flpit/core/linq.py,sha256=S4rbqA4UTIze_NOeSKLJMm1gWcWWS_voHdHhEVmlZ8U,37560
|
|
7
|
-
flpit-0.2.0.dev2.dist-info/METADATA,sha256=IgRIlar-JXbxGK2Dt1Jcj8Sbf_GZNmp6X3yl9JjLJaU,12813
|
|
8
|
-
flpit-0.2.0.dev2.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
9
|
-
flpit-0.2.0.dev2.dist-info/licenses/LICENSE,sha256=bi5Vg_YP3Oq3jaOMQT4b7WZu0NfBcZr-nRyOExUKJrQ,1063
|
|
10
|
-
flpit-0.2.0.dev2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|