flpit 0.2.0.dev2__py3-none-any.whl → 0.2.1.dev4__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 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,4 @@
1
- from __future__ import annotations
2
- from typing import SupportsIndex
1
+ from flpit.io import StringWriter
3
2
  import decimal
4
3
  from decimal import Decimal, Overflow, Inexact
5
4
 
@@ -15,15 +14,22 @@ from typing import (
15
14
  Iterable,
16
15
  Iterator,
17
16
  Collection,
17
+ SupportsIndex,
18
18
  List,
19
19
  Set,
20
20
  Type,
21
21
  TypeVar,
22
+ ClassVar,
22
23
  Union,
23
24
  override,
24
25
  overload,
25
26
  cast as typing_cast,
27
+ GenericAlias
26
28
  )
29
+
30
+ from flpit.io import MemoryByteWriter
31
+ from flpit.json import JsonConvert, JsonSerializerOptions
32
+
27
33
  TItem = TypeVar("TItem")
28
34
  TResult = TypeVar("TResult")
29
35
  TKey = TypeVar("TKey")
@@ -39,38 +45,70 @@ class _Sentinel:
39
45
  _SENTINEL = _Sentinel()
40
46
  _MISSING = object()
41
47
 
48
+ # =============================================================================
49
+ # Invalid Operation Errors
50
+ # =============================================================================
51
+ class InvalidOperationError(ValueError):
52
+ pass
42
53
 
43
- class EmptySequenceError(ValueError):
54
+ class EmptySequenceError(InvalidOperationError):
44
55
  def __init__(self):
45
56
  super().__init__("Sequence contains no elements")
46
57
 
47
- class NoMatchError(ValueError):
58
+ class NoMatchError(InvalidOperationError):
48
59
  def __init__(self):
49
60
  super().__init__("Sequence contains no matching elements")
50
61
 
51
- class MultipleMatchesError(ValueError):
62
+ class MultipleMatchesError(InvalidOperationError):
52
63
  def __init__(self):
53
64
  super().__init__("Sequence contains more than one matching element")
54
65
 
55
- class MultipleElementsError(ValueError):
66
+ class MultipleElementsError(InvalidOperationError):
56
67
  def __init__(self):
57
68
  super().__init__("Sequence contains more than one element")
58
69
 
59
- class SourceNoneError(TypeError):
70
+
71
+ # =============================================================================
72
+ # Argument Errors
73
+ # =============================================================================
74
+ class ArgumentError(TypeError):
75
+ pass
76
+
77
+ class ArgumentNullError(ArgumentError):
78
+ def __init__(self, argument: str):
79
+ super().__init__(f"Value cannot be None. (Argument '{argument}')")
80
+
81
+ class SourceNoneError(ArgumentNullError):
82
+ def __init__(self):
83
+ super().__init__("source")
84
+
85
+ class PredicateNoneError(ArgumentNullError):
86
+ def __init__(self):
87
+ super().__init__("predicate")
88
+
89
+ class CollectionNoneError(ArgumentNullError):
60
90
  def __init__(self):
61
- super().__init__("Value cannot be None. (Argument 'source')")
91
+ super().__init__("collection")
62
92
 
63
- class PredicateNoneError(TypeError):
93
+ class SelectorNoneError(ArgumentNullError):
64
94
  def __init__(self):
65
- super().__init__("Value cannot be None. (Argument 'predicate')")
95
+ super().__init__("keySelector")
66
96
 
67
- class CollectionNoneError(TypeError):
97
+ class SecondNoneError(ArgumentNullError):
68
98
  def __init__(self):
69
- super().__init__("Value cannot be None. (Argument 'collection')")
99
+ super().__init__("second")
70
100
 
71
- class SelectorNoneError(TypeError):
101
+ class ArgumentOutOfRangeError(ArgumentError):
102
+ def __init__(self, argument: str, pattern:str="Index was out of range. Must be non-negative and less than the size of the collection."):
103
+ super().__init__(f"{pattern} (Parameter '{argument}')")
104
+
105
+ class ArgumentNonNegError(ArgumentOutOfRangeError):
106
+ def __init__(self, argument:str):
107
+ super().__init__(argument, "Non-negative number required.")
108
+
109
+ class ArgumentOutOfBoundsError(ArgumentError):
72
110
  def __init__(self):
73
- super().__init__("Value cannot be None. (Parameter 'keySelector')")
111
+ 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
112
 
75
113
 
76
114
  def _guard_empty(func: Callable[..., Any]) -> Callable[..., Any]:
@@ -191,6 +229,9 @@ class FlpIt(Iterable[TItem], Generic[TItem]):
191
229
  return builtins.all(map(predicate, self))
192
230
 
193
231
  def concat(self, second: Iterable[TItem]) -> "FlpIt[TItem]":
232
+ if second is None:
233
+ raise SecondNoneError()
234
+
194
235
  def _generator() -> Iterator[TItem]:
195
236
  yield from self
196
237
  yield from second
@@ -211,8 +252,8 @@ class FlpIt(Iterable[TItem], Generic[TItem]):
211
252
  for item in self:
212
253
  if predicate(item):
213
254
  yield item
214
-
215
- return typing_cast(FlpIt[TItem], FlpIt(_FactoryIterable(_generator)))
255
+ # return typing_cast(FlpIt[TItem], FlpIt[TItem](_FactoryIterable(_generator)))
256
+ return FlpIt[TItem](_FactoryIterable(_generator))
216
257
 
217
258
  def select(self, selector: Callable[[TItem], TResult]) -> FlpIt[TResult]:
218
259
  """Projects each element into a new form."""
@@ -232,6 +273,18 @@ class FlpIt(Iterable[TItem], Generic[TItem]):
232
273
 
233
274
  return FlpIt(_FactoryIterable(_generator))
234
275
 
276
+ def skip(self, count: int) -> "FlpIt[TItem]":
277
+ """
278
+ |Bypasses a specified number of elements from the start.
279
+ """
280
+ if count <= 0:
281
+ return self
282
+
283
+ def _generator() -> Iterator[TItem]:
284
+ yield from islice(iter(self), count, None)
285
+
286
+ return FlpIt(_FactoryIterable(_generator))
287
+
235
288
  def take(self, count: int) -> "FlpIt[TItem]":
236
289
  """
237
290
  |Returns a specified number of contiguous elements from the start.
@@ -528,11 +581,13 @@ class FlpIt(Iterable[TItem], Generic[TItem]):
528
581
  def element_at(self, index: int) -> TItem:
529
582
  """Returns the element at a specified index in a sequence."""
530
583
  if index < 0:
531
- raise IndexError("Index out of range")
584
+ raise ArgumentOutOfRangeError("index")
585
+
532
586
  for i, item in enumerate(self):
533
587
  if i == index:
534
588
  return item
535
- raise IndexError("Index out of range")
589
+
590
+ raise ArgumentOutOfRangeError("index")
536
591
 
537
592
  def first(self, predicate: Callable[[TItem], bool] | _Sentinel = _SENTINEL) -> TItem:
538
593
  """Returns the first element matching a predicate, or raises ValueError."""
@@ -568,10 +623,28 @@ class FlpIt(Iterable[TItem], Generic[TItem]):
568
623
 
569
624
  raise MultipleMatchesError() if has_predicate else MultipleElementsError()
570
625
 
571
-
572
626
  def to_list(self) -> FlpList[TItem]:
573
627
  """Explicitly materializes the query into a FlpList."""
574
- return FlpList(self)
628
+ return FlpList[TItem](self)
629
+
630
+ # =====================================================================
631
+ # 5. json serialization
632
+ # =====================================================================
633
+
634
+ @classmethod
635
+ def from_json[T](cls, data: str | bytes, item_type: type[T], jso: JsonSerializerOptions|None=None) -> FlpIt[T]:
636
+ return FlpIt[T](JsonConvert.deserialize(data, list[item_type], jso))
637
+
638
+ def to_json_bytes[T](self, item_type: type[T], jso: JsonSerializerOptions|None=None) -> bytes:
639
+ writer = MemoryByteWriter()
640
+ JsonConvert.serialize_to(self, item_type, writer, jso)
641
+ return writer.getvalue()
642
+
643
+ def to_json[T](self, item_type: type[T], jso: JsonSerializerOptions|None=None) -> str:
644
+ writer = StringWriter()
645
+ JsonConvert.serialize_to(self, item_type, writer, jso)
646
+ return writer.getvalue()
647
+
575
648
 
576
649
  from functools import partial
577
650
 
@@ -682,6 +755,8 @@ class FlpList(Collection[TItem], Generic[TItem]):
682
755
  | Fluent List
683
756
  A materialized list extending UserList that yields lazy FlpIt instances for query operations.
684
757
  """
758
+ __type_args__: ClassVar[tuple[Any, ...]]
759
+
685
760
  def __init__(self, source: Iterable[TItem] | _Sentinel=_SENTINEL):
686
761
  if source is None:
687
762
  raise SourceNoneError()
@@ -830,7 +905,7 @@ class FlpList(Collection[TItem], Generic[TItem]):
830
905
 
831
906
  def insert(self, index: int, item: TItem) -> None:
832
907
  if index < 0 or index > len(self.__list.data):
833
- raise IndexError(f"Index {index} is out of range.")
908
+ raise ArgumentOutOfRangeError("index")
834
909
 
835
910
  self.__list.data.insert(index, item)
836
911
 
@@ -839,7 +914,7 @@ class FlpList(Collection[TItem], Generic[TItem]):
839
914
  raise CollectionNoneError()
840
915
 
841
916
  if index < 0 or index > len(self.__list.data):
842
- raise IndexError(f"Index {index} is out of range.")
917
+ raise ArgumentOutOfRangeError("index")
843
918
 
844
919
  self.__list.data[index:index] = list(collection)
845
920
 
@@ -865,22 +940,22 @@ class FlpList(Collection[TItem], Generic[TItem]):
865
940
  return original_length - len(self.__list.data)
866
941
 
867
942
  def remove_at(self, index: int) -> None:
868
- if index < 0 or index > len(self.__list.data):
869
- raise IndexError(f"Index {index} is out of range.")
943
+ if index < 0 or index >= len(self.__list.data):
944
+ raise ArgumentOutOfRangeError("index")
870
945
 
871
946
  del self.__list.data[index]
872
947
 
873
948
  def remove_range(self, index: int, count: int) -> None:
874
949
  size = len(self.__list.data)
875
950
 
876
- if index < 0 or index > size:
877
- raise IndexError("index")
951
+ if index < 0:
952
+ raise ArgumentNonNegError("index")
878
953
 
879
954
  if count < 0:
880
- raise IndexError("count")
955
+ raise ArgumentNonNegError("count")
881
956
 
882
957
  if count > size - index:
883
- raise IndexError("The range is invalid.")
958
+ raise ArgumentOutOfBoundsError()
884
959
 
885
960
  del self.__list.data[index : index + count]
886
961
 
@@ -899,17 +974,13 @@ class FlpList(Collection[TItem], Generic[TItem]):
899
974
 
900
975
  # Apply your List<T>-compatible argument validation here.
901
976
  if index < 0 or index > size:
902
- raise IndexError("Non-negative number required.")
977
+ raise ArgumentNonNegError("index")
903
978
 
904
979
  if count < 0:
905
- raise IndexError("Non-negative number required.")
980
+ raise ArgumentNonNegError("count")
906
981
 
907
982
  if index + count > size:
908
- raise IndexError(
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
- )
983
+ raise ArgumentOutOfBoundsError()
913
984
 
914
985
  self.__list.data[index:index + count] = reversed(
915
986
  self.__list.data[index:index + count]
@@ -948,12 +1019,15 @@ class FlpList(Collection[TItem], Generic[TItem]):
948
1019
  """Prepends an element to the sequence lazily, returning a FlpIt without mutating this list."""
949
1020
  return FlpIt(self.__list.data).prepend(element)
950
1021
 
1022
+ def concat(self, second: Iterable[TItem]) -> FlpIt[TItem]:
1023
+ return FlpIt(self.__list.data).concat(second)
1024
+
951
1025
  # noinspection unused-parameter
952
1026
  def as_type(self, target_type: Type[TResult]) -> FlpList[TResult]:
953
1027
  return self # type: ignore[return-value]
954
1028
 
955
1029
  def where(self, predicate: Callable[[TItem], bool]) -> FlpIt[TItem]:
956
- return FlpIt(self.__list.data).where(predicate)
1030
+ return FlpIt[TItem](self.__list.data).where(predicate)
957
1031
 
958
1032
  def select(self, selector: Callable[[TItem], TResult]) -> FlpIt[TResult]:
959
1033
  return FlpIt(self.__list.data).select(selector)
@@ -963,6 +1037,9 @@ class FlpList(Collection[TItem], Generic[TItem]):
963
1037
  ) -> FlpIt[TResult]:
964
1038
  return FlpIt(self.__list.data).select_many(selector)
965
1039
 
1040
+ def skip(self, count: int) -> FlpIt[TItem]:
1041
+ return FlpIt(self.__list.data).skip(count)
1042
+
966
1043
  def take(self, count: int) -> FlpIt[TItem]:
967
1044
  return FlpIt(self.__list.data).take(count)
968
1045
 
@@ -1076,7 +1153,7 @@ class FlpList(Collection[TItem], Generic[TItem]):
1076
1153
 
1077
1154
  def element_at(self, index: int) -> TItem:
1078
1155
  if index < 0 or index >= len(self.__list.data):
1079
- raise IndexError("Index out of range")
1156
+ raise ArgumentOutOfRangeError("index")
1080
1157
  return self.__list.data[index]
1081
1158
 
1082
1159
  def first(self, predicate: Callable[[TItem], bool] | _Sentinel = _SENTINEL) -> TItem:
@@ -1128,3 +1205,19 @@ class FlpList(Collection[TItem], Generic[TItem]):
1128
1205
  def to_list(self) -> FlpList[TItem]:
1129
1206
  """Explicitly returns a shallow copy instance to isolate mutations."""
1130
1207
  return FlpList(self.__list.data.copy())
1208
+
1209
+ # =====================================================================
1210
+ # 5. json serialization
1211
+ # =====================================================================
1212
+
1213
+ @classmethod
1214
+ def from_json[T](cls, data: str | bytes, item_type: type[T], jso: JsonSerializerOptions|None=None) -> FlpList[T]:
1215
+ return FlpList[T](JsonConvert.deserialize(data, list[item_type], jso))
1216
+
1217
+ def to_json_bytes[T](self, item_type: type[T], jso: JsonSerializerOptions|None=None) -> bytes:
1218
+ return JsonConvert.serialize_bytes(self.__list.data, list[item_type], jso)
1219
+
1220
+ def to_json[T](self, item_type: type[T], jso: JsonSerializerOptions|None=None) -> str:
1221
+ return JsonConvert.serialize(self.__list.data, list[item_type], jso)
1222
+
1223
+
flpit/io.py ADDED
@@ -0,0 +1,212 @@
1
+ from types import TracebackType
2
+ from abc import ABC, abstractmethod
3
+ from io import StringIO, TextIOWrapper
4
+ from typing import Any, BinaryIO, Iterable
5
+
6
+ import msgspec
7
+
8
+
9
+ # ---------- Text layer ----------
10
+
11
+ class StringBuilder:
12
+ __slots__ = ("_buffer", "_length")
13
+
14
+ def __init__(self, initial_value: str = ""):
15
+ self._buffer = StringIO()
16
+ self._length = 0
17
+ self.append(initial_value)
18
+
19
+ def append(self, text: str) -> "StringBuilder":
20
+ self._buffer.write(text)
21
+ self._length += len(text)
22
+ return self
23
+
24
+ def append_line(self, text: str = "") -> "StringBuilder":
25
+ return self.append(text).append("\n")
26
+
27
+ def clear(self) -> None:
28
+ self._buffer.seek(0)
29
+ self._buffer.truncate(0)
30
+ self._length = 0
31
+
32
+ @property
33
+ def length(self) -> int:
34
+ return self._length
35
+
36
+ def to_string(self) -> str:
37
+ return self._buffer.getvalue()
38
+
39
+ def __str__(self) -> str:
40
+ return self.to_string()
41
+
42
+
43
+ class TextWriter(ABC):
44
+ @abstractmethod
45
+ def write(self, text: str) -> int:
46
+ ...
47
+
48
+ def write_line(self, text: str = "") -> None:
49
+ self.write(text)
50
+ self.write("\n")
51
+
52
+ @abstractmethod
53
+ def flush(self) -> None:
54
+ ...
55
+
56
+ @abstractmethod
57
+ def close(self) -> None:
58
+ ...
59
+
60
+ def __enter__(self):
61
+ return self
62
+
63
+ def __exit__(
64
+ self,
65
+ exc_type: type[BaseException] | None,
66
+ exc_val: BaseException | None,
67
+ exc_tb: TracebackType | None,
68
+ ) -> bool | None:
69
+ self.close()
70
+
71
+
72
+ class StringWriter(TextWriter):
73
+ def __init__(self):
74
+ self._builder = StringBuilder()
75
+ self._closed = False
76
+
77
+ def write(self, text: str) -> int:
78
+ if self._closed:
79
+ raise ValueError("Writer is closed")
80
+ self._builder.append(text)
81
+ return len(text)
82
+
83
+ def flush(self) -> None:
84
+ if self._closed:
85
+ raise ValueError("Writer is closed")
86
+
87
+ def close(self) -> None:
88
+ self._closed = True
89
+
90
+ def getvalue(self) -> str:
91
+ return self._builder.to_string()
92
+
93
+
94
+ class StreamWriter(TextWriter):
95
+ """Text writer over a binary stream; closing it closes the stream."""
96
+
97
+ def __init__(self, stream: BinaryIO, encoding: str = "utf-8"):
98
+ self._stream = TextIOWrapper(stream, encoding=encoding)
99
+
100
+ def write(self, text: str) -> int:
101
+ return self._stream.write(text)
102
+
103
+ def flush(self) -> None:
104
+ self._stream.flush()
105
+
106
+ def close(self) -> None:
107
+ self._stream.close()
108
+
109
+ #===================================
110
+ # ---------- Binary layer ----------
111
+ #===================================
112
+
113
+ class ByteWriter(ABC):
114
+ """write() must consume or copy the supplied bytes before returning."""
115
+
116
+ @abstractmethod
117
+ def write(self, data: bytes | bytearray | memoryview) -> None:
118
+ ...
119
+
120
+ @abstractmethod
121
+ def flush(self) -> None:
122
+ ...
123
+
124
+ class MemoryByteWriter(ByteWriter):
125
+ def __init__(self):
126
+ self._buffer = bytearray()
127
+
128
+ def write(self, data: bytes | bytearray | memoryview) -> None:
129
+ self._buffer.extend(data)
130
+
131
+ def flush(self) -> None:
132
+ pass
133
+
134
+ def getvalue(self) -> bytes:
135
+ return bytes(self._buffer)
136
+
137
+
138
+ class StreamByteWriter(ByteWriter):
139
+ """Adapter for a Python binary stream. The caller owns the stream."""
140
+
141
+ def __init__(self, stream: BinaryIO):
142
+ self._stream = stream
143
+
144
+ def write(self, data: bytes | bytearray | memoryview) -> None:
145
+ view = memoryview(data)
146
+
147
+ while view:
148
+ count = self._stream.write(view)
149
+ if count is None or count <= 0:
150
+ raise OSError("Stream failed to write data")
151
+ view = view[count:]
152
+
153
+ def flush(self) -> None:
154
+ self._stream.flush()
155
+
156
+
157
+ class BufferedByteWriter(ByteWriter):
158
+ """Adds explicit, bounded-size output buffering to another writer."""
159
+
160
+ def __init__(self, destination: ByteWriter, chunk_size: int = 1 << 20):
161
+ if chunk_size < 1:
162
+ raise ValueError("chunk_size must be positive")
163
+
164
+ self._destination = destination
165
+ self._chunk_size = chunk_size
166
+ self._buffer = bytearray()
167
+
168
+ def write(self, data: bytes | bytearray | memoryview) -> None:
169
+ view = memoryview(data)
170
+
171
+ while view:
172
+ count = min(
173
+ self._chunk_size - len(self._buffer),
174
+ len(view),
175
+ )
176
+ self._buffer.extend(view[:count])
177
+ view = view[count:]
178
+
179
+ if len(self._buffer) == self._chunk_size:
180
+ self._flush_buffer()
181
+
182
+ def _flush_buffer(self) -> None:
183
+ if self._buffer:
184
+ self._destination.write(self._buffer)
185
+ self._buffer.clear()
186
+
187
+ def flush(self) -> None:
188
+ self._flush_buffer()
189
+ self._destination.flush()
190
+
191
+
192
+ # ---------- JSON serialization policy ----------
193
+
194
+ def write_json_array(
195
+ items: Iterable[Any],
196
+ output: ByteWriter,
197
+ ) -> None:
198
+ encoder = msgspec.json.Encoder()
199
+ scratch = bytearray()
200
+ first = True
201
+
202
+ output.write(b"[")
203
+
204
+ for item in items:
205
+ if not first:
206
+ output.write(b",")
207
+
208
+ encoder.encode_into(item, scratch)
209
+ output.write(scratch)
210
+ first = False
211
+
212
+ output.write(b"]")
flpit/json.py ADDED
@@ -0,0 +1,632 @@
1
+ from collections.abc import Iterable
2
+ import dataclasses
3
+ import re
4
+ import sys
5
+ import threading
6
+ import types
7
+ import typing
8
+ from dataclasses import MISSING, dataclass
9
+ from enum import StrEnum
10
+ from functools import lru_cache
11
+ from hashlib import blake2s
12
+ from typing import Annotated, Any, ForwardRef, get_args, get_origin, get_type_hints, Callable, cast as typing_cast
13
+
14
+ import msgspec
15
+
16
+ from flpit.io import ByteWriter, TextWriter
17
+
18
+
19
+ class JsonNamingPolicy(StrEnum):
20
+ lower = "lower" # pyrefly: ignore [bad-override-mutable-attribute]
21
+ upper = "upper" # pyrefly: ignore [bad-override-mutable-attribute]
22
+ camel = "camel"
23
+ pascal = "pascal"
24
+ kebap = "kebab"
25
+
26
+
27
+ class JsonDefaultHandling(StrEnum):
28
+ include = "include"
29
+ exclude = "exclude"
30
+
31
+ class JsonUnknownFieldHandling(StrEnum):
32
+ allow = "allow"
33
+ disallow = "disallow"
34
+
35
+
36
+ @dataclass(slots=True, frozen=True)
37
+ class JsonSerializerOptions:
38
+ naming_policy: JsonNamingPolicy = JsonNamingPolicy.camel
39
+ default_handling: JsonDefaultHandling = JsonDefaultHandling.exclude
40
+ unknown_field_handling: JsonUnknownFieldHandling = JsonUnknownFieldHandling.allow
41
+
42
+ # write_indented: bool = False # will be supported later
43
+ native: bool = False
44
+ """
45
+ | Set to true for the native speed of msgspec
46
+
47
+ !! only set to true if you know what you are doing !!
48
+ You'll receive the msgspec.msgstruct object that will try to do it's best to behave like your dataclass.
49
+ But remember !! "It's NOT your dataclass" !!
50
+ """
51
+
52
+ @property
53
+ def fingerprint(self) -> str:
54
+ """
55
+ Identifies a unique jso instance for building a struct
56
+ !! All parts of a jso that don't influence the defstruct MUST be ignored!
57
+ """
58
+ settings = (f"jpn-{self.naming_policy.value}:"
59
+ f"dh-{self.default_handling}:"
60
+ f"ufh-{self.unknown_field_handling}:"
61
+ ).encode("utf-8")
62
+ return blake2s(settings, digest_size=6).hexdigest()
63
+
64
+ # ---------------------------------------------------------------------------
65
+ # Metadata Annotation Markers
66
+ # ---------------------------------------------------------------------------
67
+
68
+ class JsonIgnore:
69
+ def __class_getitem__[T](cls, item: type[T]) -> Any:
70
+ return Annotated[item, cls]
71
+
72
+ class JsonInclude:
73
+ def __class_getitem__[T](cls, item: type[T]) -> Any:
74
+ return Annotated[item, cls]
75
+
76
+ class JsonProperty:
77
+ def __class_getitem__[T](cls, item: tuple[type[T], str]) -> Any:
78
+ target_type, json_name = item
79
+ return Annotated[target_type, cls(json_name)]
80
+
81
+ def __init__(self, name: str):
82
+ self.name = name
83
+
84
+ JIgnore = JsonIgnore
85
+ JInclude = JsonInclude
86
+ JProp = JsonProperty
87
+
88
+ # ---------------------------------------------------------------------
89
+ # Generated type metadata
90
+ # ---------------------------------------------------------------------
91
+
92
+ @dataclass(slots=True, eq=False)
93
+ class _WireType:
94
+ source: type
95
+ """The source type to build a msgspec.struct for"""
96
+ hints: dict[str, Any]
97
+ namespace: types.ModuleType
98
+ """the name space we are targeting"""
99
+ struct: type | None = None
100
+
101
+ @property
102
+ def module_name(self: _WireType) -> str:
103
+ return self.namespace.__name__
104
+
105
+ @property
106
+ def class_name(self: _WireType) -> str:
107
+ return self.source.__name__
108
+
109
+ @property
110
+ def ref_name(self: _WireType) -> str:
111
+ identity = (
112
+ f"{self.source.__module__}_{self.source.__qualname__}"
113
+ )
114
+ return "_ref_" + re.sub(r"\W", "_", identity)
115
+
116
+
117
+ # ---------------------------------------------------------------------
118
+ # Never imagined this would be required to have a proper lib behavior
119
+ # ---------------------------------------------------------------------
120
+
121
+ def _apply_naming_policy(name: str, policy: JsonNamingPolicy) -> str:
122
+ """
123
+ Splits any attribute name (snake_case, PascalCase, UPPER_CASE)
124
+ into individual tokens and formats them according to the target policy.
125
+ """
126
+ # 1. Check if there is a leading underscore and save it
127
+ prefix = "_" if name.startswith("_") else ""
128
+ clean_name = name.lstrip("_")
129
+
130
+ # 2. Tokenize only the clean part of the string
131
+ tokens = [t for t in re.split(r'_(?=[a-zA-Z])|(?<=[a-z])(?=[A-Z])', clean_name) if t]
132
+ if not tokens:
133
+ return name
134
+
135
+ if policy == JsonNamingPolicy.lower:
136
+ result = "".join(t.lower() for t in tokens)
137
+
138
+ if policy == JsonNamingPolicy.upper:
139
+ result = "".join(t.upper() for t in tokens)
140
+
141
+ if policy == JsonNamingPolicy.pascal:
142
+ result = "".join(t.capitalize() for t in tokens)
143
+
144
+ if policy == JsonNamingPolicy.kebap:
145
+ result = "-".join(t.lower() for t in tokens)
146
+
147
+ if policy == JsonNamingPolicy.camel:
148
+ # Lowercase the first token completely, capitalize all subsequent tokens
149
+ result = tokens[0].lower() + "".join(t.capitalize() for t in tokens[1:])
150
+
151
+ else:
152
+ result = clean_name
153
+
154
+ return prefix + result
155
+
156
+
157
+
158
+ def _generated_module(source: type) -> types.ModuleType:
159
+ """Generates the required module definition for our runtime only dynamic msgspec.struct"""
160
+ name = f"flpit.json._generated.{source.__module__}"
161
+
162
+ module = sys.modules.get(name)
163
+ if module is None:
164
+ module = types.ModuleType(name)
165
+ sys.modules[name] = module
166
+
167
+ return module
168
+
169
+
170
+ def _is_dataclass_type(tp: Any) -> bool:
171
+ return isinstance(tp, type) and dataclasses.is_dataclass(tp)
172
+
173
+
174
+ def _field_directives(
175
+ annotation: Any,
176
+ ) -> tuple[Any, bool, bool, str | None]:
177
+ """Extract JIgnore, JInclude, and JProp directives from a field annotation."""
178
+
179
+ if get_origin(annotation) is not typing.Annotated:
180
+ return annotation, False, False, None
181
+
182
+ args = get_args(annotation)
183
+ base_type = args[0]
184
+ metadata = args[1:]
185
+ ignored = any(item is JsonIgnore for item in metadata)
186
+ included = any(item is JsonInclude for item in metadata)
187
+ json_name = next(
188
+ (item.name for item in metadata if isinstance(item, JsonProperty)),
189
+ None,
190
+ )
191
+ remaining = tuple(
192
+ item
193
+ for item in metadata
194
+ if item is not JsonIgnore
195
+ and item is not JsonInclude
196
+ and not isinstance(item, JsonProperty)
197
+ )
198
+
199
+ if remaining:
200
+ base_type = Annotated[(base_type, *remaining)]
201
+
202
+ return base_type, ignored, included, json_name
203
+
204
+
205
+ def _get_property_directive(prop_func: Callable[[Any, Any], None] | Callable[[Any], Any] | None):
206
+ if prop_func is not None:
207
+ getter_hints = get_type_hints(prop_func, include_extras=True)
208
+ source_annotation = getter_hints.get("return", Any)
209
+ else:
210
+ source_annotation = Any
211
+
212
+ return _field_directives(source_annotation)
213
+
214
+
215
+ def _nested_dataclasses(tp: Any):
216
+ """Yield dataclass types referenced by an annotation."""
217
+ if _is_dataclass_type(tp):
218
+ yield tp
219
+ return
220
+
221
+ if isinstance(tp, (list, tuple)):
222
+ for item in tp:
223
+ yield from _nested_dataclasses(item)
224
+ return
225
+
226
+ origin = get_origin(tp)
227
+
228
+ if origin is typing.Annotated:
229
+ yield from _nested_dataclasses(get_args(tp)[0])
230
+ return
231
+
232
+ if origin is typing.Literal:
233
+ return
234
+
235
+ for arg in get_args(tp):
236
+ yield from _nested_dataclasses(arg)
237
+
238
+
239
+ def _rewrite_type(
240
+ tp: Any,
241
+ owner: _WireType,
242
+ graph: dict[type, _WireType],
243
+ *,
244
+ resolve: bool = False,
245
+ ) -> Any:
246
+ """Replace source dataclasses with generated Structs or ForwardRefs."""
247
+ if tp is getattr(typing, "Self", None):
248
+ info = owner
249
+ return info.struct if resolve else ForwardRef(info.ref_name)
250
+
251
+ if _is_dataclass_type(tp):
252
+ info = graph[tp]
253
+ return info.struct if resolve else ForwardRef(info.ref_name)
254
+
255
+ if isinstance(tp, list):
256
+ return [
257
+ _rewrite_type(item, owner, graph, resolve=resolve)
258
+ for item in tp
259
+ ]
260
+
261
+ if isinstance(tp, tuple):
262
+ return tuple(
263
+ _rewrite_type(item, owner, graph, resolve=resolve)
264
+ for item in tp
265
+ )
266
+
267
+ origin = get_origin(tp)
268
+
269
+ if origin is None or origin is typing.Literal:
270
+ return tp
271
+
272
+ args = get_args(tp)
273
+
274
+ if origin is typing.Annotated:
275
+ base = _rewrite_type(
276
+ args[0], owner, graph, resolve=resolve
277
+ )
278
+ return typing.Annotated[(base, *args[1:])]
279
+
280
+ rewritten: tuple[Any, ...] = tuple(
281
+ _rewrite_type(arg, owner, graph, resolve=resolve)
282
+ for arg in args
283
+ )
284
+
285
+ if origin in (typing.Union, types.UnionType):
286
+ union: Any = typing.Union # with all the magic here, there's no way to make typing really happy
287
+ return union[rewritten]
288
+
289
+ params = rewritten[0] if len(rewritten) == 1 else rewritten
290
+
291
+ try:
292
+ return origin[params]
293
+ except (TypeError, AttributeError) as exc:
294
+ copier = getattr(tp, "copy_with", None)
295
+ if copier is not None:
296
+ return copier(rewritten)
297
+
298
+ raise TypeError(
299
+ f"Cannot rewrite annotation {tp!r}"
300
+ ) from exc
301
+
302
+
303
+ # ---------------------------------------------------------------------
304
+ # Graph discovery
305
+ # ---------------------------------------------------------------------
306
+
307
+
308
+ def _discover_graph(
309
+ roots: tuple[type, ...],
310
+ ) -> dict[type, _WireType]:
311
+ """Discover reachable dataclasses and resolve their annotations."""
312
+ graph: dict[type, _WireType] = {}
313
+ pending = list(roots)
314
+
315
+ while pending:
316
+ source = pending.pop()
317
+
318
+ if source in graph:
319
+ continue
320
+
321
+ info = _WireType(
322
+ source=source,
323
+ hints=get_type_hints(source, include_extras=True),
324
+ namespace=_generated_module(source),
325
+ )
326
+ graph[source] = info
327
+
328
+ for field in dataclasses.fields(source):
329
+ annotation, ignored, included, _ = _field_directives(
330
+ info.hints[field.name]
331
+ )
332
+ if ignored:
333
+ continue
334
+
335
+ pending.extend(_nested_dataclasses(annotation))
336
+
337
+ return graph
338
+
339
+
340
+ def _allows_none(tp: Any) -> bool:
341
+ if get_origin(tp) is typing.Annotated:
342
+ tp = get_args(tp)[0]
343
+
344
+ return type(None) in get_args(tp)
345
+
346
+ # ---------------------------------------------------------------------
347
+ # Struct construction
348
+ # ---------------------------------------------------------------------
349
+
350
+
351
+ def _struct_fields(
352
+ info: _WireType,
353
+ graph: dict[type, _WireType],
354
+ jso: JsonSerializerOptions,
355
+ ) -> list[tuple[Any, ...]]:
356
+ definitions: list[tuple[Any, ...]] = []
357
+ dataclass_fields = {f.name for f in dataclasses.fields(info.source)}
358
+
359
+ # 1. Process regular fields (regardless of init=True or init=False)
360
+ for field in dataclasses.fields(info.source):
361
+ source_annotation = info.hints[field.name]
362
+ annotation, ignored, included, json_name = _field_directives(source_annotation)
363
+
364
+ if ignored:
365
+ continue
366
+
367
+ # If the field starts with an underscore, it MUST have JInclude or JProp attached!
368
+ if field.name.startswith("_") and not (included or json_name is not None):
369
+ continue
370
+
371
+ annotation = _rewrite_type(annotation, info, graph)
372
+ wire_name = json_name if json_name is not None else _apply_naming_policy(field.name, jso.naming_policy)
373
+
374
+ # Resolve structural field default values cleanly
375
+ if field.default is not MISSING:
376
+ default_val = field.default
377
+ has_default = True
378
+ elif field.default_factory is not MISSING:
379
+ default_val = msgspec.field(default_factory=field.default_factory)
380
+ has_default = True
381
+ elif _allows_none(annotation):
382
+ default_val = None
383
+ has_default = True
384
+ else:
385
+ default_val = MISSING
386
+ has_default = False
387
+
388
+ field_options: dict[str, Any] = {"name": wire_name}
389
+ if has_default:
390
+ if field.default_factory is not MISSING:
391
+ field_options["default_factory"] = field.default_factory
392
+ else:
393
+ field_options["default"] = default_val
394
+
395
+ spec = (field.name, annotation, msgspec.field(**field_options))
396
+ definitions.append(spec)
397
+
398
+ # 2. Process properties automatically
399
+ for name, value in vars(info.source).items():
400
+ if isinstance(value, property) and name not in dataclass_fields:
401
+ # Extract the type annotation directly from the getter function signature
402
+ if value.fset is not None:
403
+ _, _, included, json_name = _get_property_directive(value.fset)
404
+
405
+ if included or json_name is not None:
406
+ raise ValueError(
407
+ f"{info.source.__qualname__}.{name}: "
408
+ "fplit.json with msgspec backend doesn't support "
409
+ "JProp/JInclude on property setters"
410
+ )
411
+
412
+ annotation, ignored, included, json_name = _get_property_directive(value.fget)
413
+
414
+ if not included and json_name is None:
415
+ continue
416
+
417
+ annotation = _rewrite_type(annotation, info, graph)
418
+ wire_name = json_name if json_name is not None else _apply_naming_policy(name, jso.naming_policy)
419
+
420
+ # Map the field into the C-struct. Default to None so the inbound parser never complains.
421
+ spec = (name, annotation, msgspec.field(name=wire_name, default=None))
422
+ definitions.append(spec)
423
+
424
+
425
+ return definitions
426
+
427
+
428
+ def _register_forward_refs(
429
+ graph: dict[type, _WireType],
430
+ ) -> None:
431
+ """Expose every graph reference in each participating module."""
432
+ namespaces = []
433
+
434
+ for info in graph.values():
435
+ if not any(info.namespace is ns for ns in namespaces):
436
+ namespaces.append(info.namespace)
437
+
438
+ for namespace in namespaces:
439
+ for info in graph.values():
440
+ setattr(namespace, info.ref_name, info.struct)
441
+
442
+
443
+ def _behavior_namespace(source: type) -> dict[str, Any]:
444
+ return {
445
+ name: value
446
+ for name, value in vars(source).items()
447
+ if (
448
+ not name.startswith("__")
449
+ and (callable(value) or isinstance(value, property))
450
+ )
451
+ # pyrefly: ignore [implicit-any-lambda]
452
+ } | { "__class__": property(lambda self, cls=source: cls) }
453
+
454
+
455
+ def _create_structs(
456
+ graph: dict[type, _WireType],
457
+ jso: JsonSerializerOptions,
458
+ ) -> None:
459
+ """Create the generated msgspec structs for every node in the graph."""
460
+ for wt in graph.values():
461
+ fields = _struct_fields(wt, graph, jso)
462
+ wt.struct = msgspec.defstruct(
463
+ wt.class_name,
464
+ fields,
465
+ module=wt.module_name,
466
+ namespace=_behavior_namespace(wt.source),
467
+ rename=None,
468
+ kw_only=False,
469
+ omit_defaults=jso.default_handling.value == JsonDefaultHandling.exclude,
470
+ forbid_unknown_fields=jso.unknown_field_handling.value == JsonUnknownFieldHandling.disallow,
471
+ )
472
+
473
+
474
+ def _validate_structs(graph: dict[type, _WireType]) -> None:
475
+ """Force msgspec to inspect each generated struct after refs are registered."""
476
+ for wt in graph.values():
477
+ msgspec.inspect.type_info(wt.struct)
478
+
479
+
480
+ def _publish_structs(graph: dict[type, _WireType]) -> None:
481
+ # Publish the readable generated class names.
482
+ for wt in graph.values():
483
+ setattr(wt.namespace, wt.class_name, wt.struct)
484
+
485
+
486
+ def _build_graph(
487
+ roots: tuple[type, ...],
488
+ jso: JsonSerializerOptions,
489
+ ) -> dict[type, _WireType]:
490
+ graph = _discover_graph(roots)
491
+ _create_structs(graph, jso)
492
+ _register_forward_refs(graph)
493
+ _validate_structs(graph)
494
+ _publish_structs(graph)
495
+ return graph
496
+
497
+
498
+ # ---------------------------------------------------------------------
499
+ # Cache and public API
500
+ # ---------------------------------------------------------------------
501
+
502
+ # we must only build new when the
503
+ _jso_registry: dict[str, JsonSerializerOptions] = {}
504
+ _registry_lock = threading.Lock()
505
+
506
+
507
+ @lru_cache(maxsize=None)
508
+ def _wrapped_lru(roots: tuple[type, ...], jso_id: str):
509
+ with _registry_lock:
510
+ jso = _jso_registry[jso_id]
511
+
512
+ return _build_graph(roots, jso)
513
+
514
+
515
+ @lru_cache(maxsize=None)
516
+ def _cached_wire_type(target_type: object, jso_id: str):
517
+ roots = tuple(_nested_dataclasses(target_type))
518
+
519
+ if not roots:
520
+ raise TypeError(
521
+ "Only @dataclass are supported!, "
522
+ f"got {target_type!r}"
523
+ )
524
+
525
+ graph_roots = tuple(sorted(set(roots), key=id))
526
+ graph = _wrapped_lru(graph_roots, jso_id)
527
+
528
+ return _rewrite_type(
529
+ target_type,
530
+ graph[roots[0]],
531
+ graph,
532
+ resolve=True,
533
+ )
534
+
535
+
536
+ def wire_type_for(target_type: type, jso: JsonSerializerOptions) -> type:
537
+ jso_id = jso.fingerprint
538
+
539
+ with _registry_lock:
540
+ _jso_registry[jso_id] = jso
541
+
542
+ return _cached_wire_type(target_type, jso_id)
543
+
544
+
545
+ @lru_cache(maxsize=256)
546
+ def _get_decoder(wire_type: Any):
547
+ return msgspec.json.Decoder(type=wire_type)
548
+
549
+
550
+ _JSON_ENCODER = msgspec.json.Encoder()
551
+
552
+
553
+ class JsonConvert:
554
+
555
+ @staticmethod
556
+ def deserialize[T](
557
+ data: bytes | str,
558
+ target: type[T],
559
+ jso: JsonSerializerOptions | None = None
560
+ ) -> T:
561
+ if jso is None:
562
+ jso = JsonSerializerOptions()
563
+
564
+ wire_type = wire_type_for(target, jso)
565
+ parsed = _get_decoder(wire_type).decode(data)
566
+
567
+ if jso.native:
568
+ return parsed
569
+
570
+ result = msgspec.convert(
571
+ parsed,
572
+ target,
573
+ from_attributes=True,
574
+ )
575
+
576
+ return result
577
+
578
+ @staticmethod
579
+ def serialize[T](
580
+ data: Any,
581
+ target: type[T],
582
+ jso: JsonSerializerOptions | None = None
583
+ ) -> str:
584
+ return JsonConvert.serialize_bytes(data, target, jso).decode("utf-8")
585
+
586
+ @staticmethod
587
+ def serialize_bytes[T](
588
+ data: Any,
589
+ target: type[T],
590
+ jso: JsonSerializerOptions | None = None
591
+ ) -> bytes:
592
+ if jso is None:
593
+ jso = JsonSerializerOptions()
594
+
595
+ wire_type = wire_type_for(target, jso)
596
+ wire_obj = msgspec.convert(data, wire_type, from_attributes=True)
597
+ return _JSON_ENCODER.encode(wire_obj)
598
+
599
+ @staticmethod
600
+ def serialize_to[T](
601
+ it: Iterable[T],
602
+ target: type[T],
603
+ output: ByteWriter | TextWriter,
604
+ jso: JsonSerializerOptions | None = None
605
+ ) -> None:
606
+ if jso is None:
607
+ jso = JsonSerializerOptions()
608
+
609
+ if isinstance(output, ByteWriter):
610
+ write = output.write
611
+ elif isinstance(output, TextWriter):
612
+ def write(data: bytes | bytearray) -> int:
613
+ return output.write(data.decode("utf-8"))
614
+ else:
615
+ raise TypeError("output must be ByteWriter or TextWriter")
616
+
617
+ first = True
618
+ wire_type = wire_type_for(target, jso)
619
+
620
+ write(b"[")
621
+
622
+ for item in iter(it):
623
+ if not first:
624
+ write(b",")
625
+
626
+ wire_obj = msgspec.convert(item, wire_type, from_attributes=True)
627
+ write(_JSON_ENCODER.encode(wire_obj))
628
+ first = False
629
+
630
+ write(b']')
631
+
632
+
@@ -1,7 +1,9 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: flpit
3
- Version: 0.2.0.dev2
3
+ Version: 0.2.1.dev4
4
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
5
7
  License: MIT
6
8
  License-File: LICENSE
7
9
  Keywords: fluent,generators,iterable,lazy-evaluation,linq,pipeline,pyrefly,type-safe
@@ -9,13 +11,13 @@ Classifier: Development Status :: 3 - Alpha
9
11
  Classifier: Intended Audience :: Developers
10
12
  Classifier: License :: OSI Approved :: MIT License
11
13
  Classifier: Programming Language :: Python :: 3
12
- Classifier: Programming Language :: Python :: 3.11
13
14
  Classifier: Programming Language :: Python :: 3.12
14
15
  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.11
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 | 1295 |
386
+ | GRAND TOTALS | 1434 |
383
387
  ================================================================
384
388
  ```
@@ -0,0 +1,12 @@
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/io.py,sha256=ATXt0H7DpKt9aC7-td5FhR601alVDsAPZ1tFJ9tVYvM,5259
5
+ flpit/json.py,sha256=Ujlk8jo6kCIaVwhYRwDV17PQ6rmCVa8G9IjPz_Tvfsw,19115
6
+ flpit/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ flpit/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ flpit/core/linq.py,sha256=h2KKK9wP77cGFKsqDtCRlulSlhKj40DCVZj4D9tpK2s,41049
9
+ flpit-0.2.1.dev4.dist-info/METADATA,sha256=1O1rnsiWRbdz8f1bJbgKMrgCyDK9wlbsNmwz-IF9epk,13047
10
+ flpit-0.2.1.dev4.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
11
+ flpit-0.2.1.dev4.dist-info/licenses/LICENSE,sha256=bi5Vg_YP3Oq3jaOMQT4b7WZu0NfBcZr-nRyOExUKJrQ,1063
12
+ flpit-0.2.1.dev4.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,,