flpit 0.2.1.dev3__py3-none-any.whl → 0.2.1.dev5__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/core/linq.py CHANGED
@@ -1,5 +1,10 @@
1
+ from __future__ import annotations
2
+
3
+ from io import BytesIO, StringIO
4
+
1
5
  import decimal
2
6
  from decimal import Decimal, Overflow, Inexact
7
+ from contextlib import AbstractContextManager
3
8
 
4
9
  import builtins
5
10
  from collections import UserList
@@ -22,11 +27,10 @@ from typing import (
22
27
  Union,
23
28
  override,
24
29
  overload,
25
- cast as typing_cast,
26
- GenericAlias
30
+ cast as typing_cast, BinaryIO,
27
31
  )
28
32
 
29
- from flpit.json import JsonSerializerOptions, decode_dataclass, encode_dataclass
33
+ from flpit.json import JsonConvert, JsonSerializerOptions
30
34
 
31
35
  TItem = TypeVar("TItem")
32
36
  TResult = TypeVar("TResult")
@@ -122,8 +126,7 @@ def _guard_empty(func: Callable[..., Any]) -> Callable[..., Any]:
122
126
  return wrapper
123
127
 
124
128
 
125
- # noinspection unresolved-references,protected-member
126
- def dotnet_context() -> decimal._ContextManager: # awesome that type was not intended
129
+ def dotnet_context() -> AbstractContextManager[decimal.Context]:
127
130
  # 1. Create and configure pure Context object
128
131
  ctx = decimal.getcontext().copy()
129
132
  ctx.prec = max(ctx.prec, 29)
@@ -625,6 +628,32 @@ class FlpIt(Iterable[TItem], Generic[TItem]):
625
628
  """Explicitly materializes the query into a FlpList."""
626
629
  return FlpList[TItem](self)
627
630
 
631
+ # =====================================================================
632
+ # 5. json serialization
633
+ # =====================================================================
634
+
635
+ @classmethod
636
+ def from_json[T](cls, data: str | bytes, item_type: type[T], jso: JsonSerializerOptions|None=None) -> FlpIt[T]:
637
+ return FlpIt[T](JsonConvert.deserialize(data, list[item_type], jso))
638
+
639
+ def to_json_bytes[T](self, item_type: type[T], jso: JsonSerializerOptions|None=None) -> bytes:
640
+ writer = BytesIO()
641
+ JsonConvert.serialize_to(self, item_type, writer, jso)
642
+ return writer.getvalue()
643
+
644
+ def to_json[T](self, item_type: type[T], jso: JsonSerializerOptions|None=None) -> str:
645
+ writer = StringIO()
646
+ JsonConvert.serialize_to(self, item_type, writer, jso)
647
+ return writer.getvalue()
648
+
649
+ def write_json[T](
650
+ self,
651
+ item_type: type[T],
652
+ output: BinaryIO,
653
+ jso: JsonSerializerOptions | None = None,
654
+ ) -> None:
655
+ JsonConvert.serialize_to(self, item_type, output, jso)
656
+
628
657
 
629
658
  from functools import partial
630
659
 
@@ -1192,12 +1221,18 @@ class FlpList(Collection[TItem], Generic[TItem]):
1192
1221
 
1193
1222
  @classmethod
1194
1223
  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))
1224
+ return FlpList[T](JsonConvert.deserialize(data, list[item_type], jso))
1196
1225
 
1197
1226
  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)
1227
+ return JsonConvert.serialize_bytes(self.__list.data, list[item_type], jso)
1199
1228
 
1200
1229
  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
-
1230
+ return JsonConvert.serialize(self.__list.data, list[item_type], jso)
1203
1231
 
1232
+ def write_json[T](
1233
+ self,
1234
+ item_type: type[T],
1235
+ output: BinaryIO,
1236
+ jso: JsonSerializerOptions | None = None,
1237
+ ) -> None:
1238
+ JsonConvert.serialize_to(self, item_type, output, jso)
flpit/json.py CHANGED
@@ -1,33 +1,46 @@
1
- from typing import Annotated
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterable
2
4
  import dataclasses
3
5
  import re
4
6
  import sys
7
+ import threading
5
8
  import types
6
9
  import typing
7
-
8
- import threading
9
10
  from dataclasses import MISSING, dataclass
11
+ from enum import StrEnum
10
12
  from functools import lru_cache
11
- from typing import Any, ForwardRef, get_args, get_origin, get_type_hints
12
13
  from hashlib import blake2s
14
+ from typing import Annotated, Any, ForwardRef, get_args, get_origin, get_type_hints, Callable
13
15
 
14
16
  import msgspec
15
17
 
16
- from enum import StrEnum
17
18
 
18
19
  class JsonNamingPolicy(StrEnum):
19
- lower_case = "lower"
20
- upper_case = "upper"
21
- camel_case = "camel"
22
- pascal_case = "pascal"
23
- kebap_case = "kebab"
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"
24
34
 
25
35
 
26
36
  @dataclass(slots=True, frozen=True)
27
37
  class JsonSerializerOptions:
28
- naming_policy: JsonNamingPolicy = JsonNamingPolicy.camel_case
29
- write_indented: bool = False
30
- native:bool=False
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
31
44
  """
32
45
  | Set to true for the native speed of msgspec
33
46
 
@@ -42,8 +55,11 @@ class JsonSerializerOptions:
42
55
  Identifies a unique jso instance for building a struct
43
56
  !! All parts of a jso that don't influence the defstruct MUST be ignored!
44
57
  """
45
- settings = self.naming_policy.value
46
- return blake2s(settings.encode(), digest_size=6).hexdigest()
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()
47
63
 
48
64
  # ---------------------------------------------------------------------------
49
65
  # Metadata Annotation Markers
@@ -99,9 +115,46 @@ class _WireType:
99
115
 
100
116
 
101
117
  # ---------------------------------------------------------------------
102
- # Module and annotation utilities
118
+ # Never imagined this would be required to have a proper lib behavior
103
119
  # ---------------------------------------------------------------------
104
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
+
105
158
  def _generated_module(source: type) -> types.ModuleType:
106
159
  """Generates the required module definition for our runtime only dynamic msgspec.struct"""
107
160
  name = f"flpit.json._generated.{source.__module__}"
@@ -118,6 +171,47 @@ def _is_dataclass_type(tp: Any) -> bool:
118
171
  return isinstance(tp, type) and dataclasses.is_dataclass(tp)
119
172
 
120
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
+
121
215
  def _nested_dataclasses(tp: Any):
122
216
  """Yield dataclass types referenced by an annotation."""
123
217
  if _is_dataclass_type(tp):
@@ -210,6 +304,7 @@ def _rewrite_type(
210
304
  # Graph discovery
211
305
  # ---------------------------------------------------------------------
212
306
 
307
+
213
308
  def _discover_graph(
214
309
  roots: tuple[type, ...],
215
310
  ) -> dict[type, _WireType]:
@@ -231,15 +326,17 @@ def _discover_graph(
231
326
  graph[source] = info
232
327
 
233
328
  for field in dataclasses.fields(source):
234
- if not field.init:
329
+ annotation, ignored, included, _ = _field_directives(
330
+ info.hints[field.name]
331
+ )
332
+ if ignored:
235
333
  continue
236
334
 
237
- pending.extend(
238
- _nested_dataclasses(info.hints[field.name])
239
- )
335
+ pending.extend(_nested_dataclasses(annotation))
240
336
 
241
337
  return graph
242
338
 
339
+
243
340
  def _allows_none(tp: Any) -> bool:
244
341
  if get_origin(tp) is typing.Annotated:
245
342
  tp = get_args(tp)[0]
@@ -250,43 +347,81 @@ def _allows_none(tp: Any) -> bool:
250
347
  # Struct construction
251
348
  # ---------------------------------------------------------------------
252
349
 
350
+
253
351
  def _struct_fields(
254
352
  info: _WireType,
255
353
  graph: dict[type, _WireType],
354
+ jso: JsonSerializerOptions,
256
355
  ) -> list[tuple[Any, ...]]:
257
356
  definitions: list[tuple[Any, ...]] = []
357
+ dataclass_fields = {f.name for f in dataclasses.fields(info.source)}
258
358
 
359
+ # 1. Process regular fields (regardless of init=True or init=False)
259
360
  for field in dataclasses.fields(info.source):
260
- if not field.init:
361
+ source_annotation = info.hints[field.name]
362
+ annotation, ignored, included, json_name = _field_directives(source_annotation)
363
+
364
+ if ignored:
261
365
  continue
262
366
 
263
- annotation = _rewrite_type(
264
- info.hints[field.name],
265
- info,
266
- graph,
267
- )
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
268
370
 
269
- if field.default is not MISSING:
270
- spec = (field.name, annotation, field.default)
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)
271
373
 
374
+ # Resolve structural field default values cleanly
375
+ if field.default is not MISSING:
376
+ default_val = field.default
377
+ has_default = True
272
378
  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
-
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
285
384
  else:
286
- spec = (field.name, annotation)
385
+ default_val = MISSING
386
+ has_default = False
287
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))
288
396
  definitions.append(spec)
289
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
+
290
425
  return definitions
291
426
 
292
427
 
@@ -304,44 +439,59 @@ def _register_forward_refs(
304
439
  for info in graph.values():
305
440
  setattr(namespace, info.ref_name, info.struct)
306
441
 
442
+
307
443
  def _behavior_namespace(source: type) -> dict[str, Any]:
308
444
  return {
309
445
  name: value
310
446
  for name, value in vars(source).items()
311
447
  if (
312
448
  not name.startswith("__")
313
- and callable(value)
449
+ and (callable(value) or isinstance(value, property))
314
450
  )
315
- # pyrefly: ignore [implicit-any-lambda]
451
+ # pyrefly: ignore [implicit-any-lambda]
316
452
  } | { "__class__": property(lambda self, cls=source: cls) }
317
453
 
318
- def _build_graph(
319
- roots: tuple[type, ...],
320
- jso: JsonSerializerOptions,
321
- ) -> dict[type, _WireType]:
322
- graph = _discover_graph(roots)
323
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."""
324
460
  for wt in graph.values():
461
+ fields = _struct_fields(wt, graph, jso)
325
462
  wt.struct = msgspec.defstruct(
326
463
  wt.class_name,
327
- _struct_fields(wt, graph),
464
+ fields,
328
465
  module=wt.module_name,
329
466
  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!
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,
334
471
  )
335
472
 
336
- _register_forward_refs(graph)
337
473
 
474
+ def _validate_structs(graph: dict[type, _WireType]) -> None:
475
+ """Force msgspec to inspect each generated struct after refs are registered."""
338
476
  for wt in graph.values():
339
477
  msgspec.inspect.type_info(wt.struct)
340
478
 
479
+
480
+ def _publish_structs(graph: dict[type, _WireType]) -> None:
341
481
  # Publish the readable generated class names.
342
482
  for wt in graph.values():
343
483
  setattr(wt.namespace, wt.class_name, wt.struct)
344
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)
345
495
  return graph
346
496
 
347
497
 
@@ -353,22 +503,17 @@ def _build_graph(
353
503
  _jso_registry: dict[str, JsonSerializerOptions] = {}
354
504
  _registry_lock = threading.Lock()
355
505
 
506
+
356
507
  @lru_cache(maxsize=None)
357
508
  def _wrapped_lru(roots: tuple[type, ...], jso_id: str):
358
509
  with _registry_lock:
359
510
  jso = _jso_registry[jso_id]
360
- return _build_graph(roots, jso)
361
511
 
362
- def _cached_graph(roots:tuple[object, ...], jso: JsonSerializerOptions):
363
- with _registry_lock:
364
- _jso_registry[jso.fingerprint] = jso
512
+ return _build_graph(roots, jso)
365
513
 
366
- return _wrapped_lru(roots, jso.fingerprint)
367
514
 
368
- def wire_type_for(
369
- target_type: type,
370
- jso: JsonSerializerOptions,
371
- ):
515
+ @lru_cache(maxsize=None)
516
+ def _cached_wire_type(target_type: object, jso_id: str):
372
517
  roots = tuple(_nested_dataclasses(target_type))
373
518
 
374
519
  if not roots:
@@ -377,13 +522,8 @@ def wire_type_for(
377
522
  f"got {target_type!r}"
378
523
  )
379
524
 
380
- # Normalize the root set so User and list[User] share the graph.
381
525
  graph_roots = tuple(sorted(set(roots), key=id))
382
-
383
- graph = _cached_graph(
384
- graph_roots,
385
- jso,
386
- )
526
+ graph = _wrapped_lru(graph_roots, jso_id)
387
527
 
388
528
  return _rewrite_type(
389
529
  target_type,
@@ -393,35 +533,106 @@ def wire_type_for(
393
533
  )
394
534
 
395
535
 
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()
536
+ def wire_type_for(target_type: type, jso: JsonSerializerOptions) -> type:
537
+ jso_id = jso.fingerprint
403
538
 
404
- wire_type = wire_type_for(target, jso)
405
- parsed = msgspec.json.decode(data, type=wire_type)
539
+ with _registry_lock:
540
+ _jso_registry[jso_id] = jso
406
541
 
407
- if jso.native:
408
- return parsed
542
+ return _cached_wire_type(target_type, jso_id)
409
543
 
410
- return msgspec.convert(
411
- parsed,
412
- target,
413
- from_attributes=True,
414
- )
415
544
 
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)
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: typing.BinaryIO | typing.TextIO,
604
+ jso: JsonSerializerOptions | None = None,
605
+ ) -> None:
606
+ import io
607
+ if jso is None:
608
+ jso = JsonSerializerOptions()
609
+
610
+ if isinstance(output, io.TextIOBase):
611
+ def write(data: bytes) -> None:
612
+ output.write(data.decode("utf-8"))
613
+
614
+ elif isinstance(
615
+ output,
616
+ (io.BufferedIOBase, io.RawIOBase),
617
+ ):
618
+ def write(data: bytes) -> None:
619
+ output.write(data)
620
+ else:
621
+ raise TypeError("output must be ByteWriter or TextWriter")
622
+
623
+ first = True
624
+ wire_type = wire_type_for(target, jso)
625
+
626
+ write(b"[")
627
+
628
+ for item in iter(it):
629
+ if not first:
630
+ write(b",")
631
+
632
+ wire_obj = msgspec.convert(item, wire_type, from_attributes=True)
633
+ write(_JSON_ENCODER.encode(wire_obj))
634
+ first = False
635
+
636
+ write(b']')
637
+
427
638
 
@@ -1,8 +1,9 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: flpit
3
- Version: 0.2.1.dev3
3
+ Version: 0.2.1.dev5
4
4
  Summary: Fluent LINQ for Python — flip your iterables
5
5
  Project-URL: Homepage, https://github.com/riemar/flpit
6
+ Project-URL: Repository, https://github.com/riemar/flpit
6
7
  License: MIT
7
8
  License-File: LICENSE
8
9
  Keywords: fluent,generators,iterable,lazy-evaluation,linq,pipeline,pyrefly,type-safe
@@ -10,10 +11,10 @@ Classifier: Development Status :: 3 - Alpha
10
11
  Classifier: Intended Audience :: Developers
11
12
  Classifier: License :: OSI Approved :: MIT License
12
13
  Classifier: Programming Language :: Python :: 3
13
- Classifier: Programming Language :: Python :: 3.11
14
14
  Classifier: Programming Language :: Python :: 3.12
15
15
  Classifier: Programming Language :: Python :: 3.13
16
16
  Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Programming Language :: Python :: 3.15
17
18
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
19
  Classifier: Typing :: Typed
19
20
  Requires-Python: >=3.12
@@ -1,11 +1,11 @@
1
1
  flp/__init__.py,sha256=reAHhZtYt0f2LHeIYJbklta6Vatr6yeQCL1R-9TTyY0,688
2
2
  flpit/__init__.py,sha256=tx4tkpLxoKQTP7wu2PyUtQtLs9_pWHbx1jHh4Vdsrus,1275
3
3
  flpit/flp.py,sha256=i-FbnMeO92cxNRssYsgdlm2UGjILpveXgYUT468fWvY,999
4
- flpit/json.py,sha256=KHACbdvunJyyuVt18AZjC_L-CGh0aptkem0wiJtJx0s,11687
4
+ flpit/json.py,sha256=HYuZl_xLUTacsUusCct0PYhC0s2iVYghjTggay4ehjI,19210
5
5
  flpit/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
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,,
7
+ flpit/core/linq.py,sha256=USO-L0z54pXcq-71j6kKzzFp0clHDMBRuhxOTC9XrdY,41468
8
+ flpit-0.2.1.dev5.dist-info/METADATA,sha256=LHIiPg44E6DSMs51-K0asTs2qu2U7ASXNPH92X_x2uQ,13098
9
+ flpit-0.2.1.dev5.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
10
+ flpit-0.2.1.dev5.dist-info/licenses/LICENSE,sha256=bi5Vg_YP3Oq3jaOMQT4b7WZu0NfBcZr-nRyOExUKJrQ,1063
11
+ flpit-0.2.1.dev5.dist-info/RECORD,,