edri 2025.12.3rc1__py3-none-any.whl → 2025.12.3rc2__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.
@@ -634,7 +634,7 @@ class ManagerBase(ABC, Process, metaclass=ManagerBaseMeta):
634
634
  if cache_key:
635
635
  etag = self._cache.tag(cache_key)
636
636
  method = self._cache_methods[event.__class__]
637
- if etag and hasattr(event, "etag") and event.etag and etag in event.etag and method == HTTPMethod.GET:
637
+ if etag and hasattr(event, "etag") and event.etag and f"\"{etag}\"" in event.etag and method == HTTPMethod.GET:
638
638
  event.response.add_directive(NotModifiedResponseDirective())
639
639
  self.router_queue.put(event)
640
640
  return
@@ -646,7 +646,7 @@ class ManagerBase(ABC, Process, metaclass=ManagerBaseMeta):
646
646
  if event.response.get_status() == ResponseStatus.OK and method in (HTTPMethod.POST, HTTPMethod.PUT, HTTPMethod.PATCH, HTTPMethod.DELETE):
647
647
  self._cache.renew(cache_key)
648
648
  else:
649
- event.response.add_directive(HeaderResponseDirective(name="ETag", value=etag))
649
+ event.response.add_directive(HeaderResponseDirective(name="ETag", value=f"\"{etag}\""))
650
650
  event.response.add_directive(HeaderResponseDirective(name="Cache-Control", value=API_CACHE_CONTROL))
651
651
  event.response.add_directive(HeaderResponseDirective(name="Vary", value=self._cache_vary))
652
652
  self.router_queue.put(event)
@@ -1,5 +1,6 @@
1
1
  from .file import File
2
2
  from .client import Client
3
+ from .range import RangeSpec, RangeValue
3
4
 
4
5
  class Cookie:
5
6
  def __init__(self, name: str):
@@ -9,3 +9,4 @@ class File:
9
9
  file_name: str | None
10
10
  mime_type: str
11
11
  path: Path | SharedMemoryPipe
12
+ fingerprint: str | None = None
@@ -0,0 +1,71 @@
1
+ from dataclasses import dataclass
2
+
3
+
4
+ @dataclass(slots=True)
5
+ class RangeSpec:
6
+ first: int | None = None
7
+ last: int | None = None
8
+ suffix_length: int | None = None
9
+
10
+
11
+ @dataclass(slots=True)
12
+ class RangeValue:
13
+ unit: str
14
+ specs: tuple[RangeSpec, ...]
15
+
16
+ def content_range(
17
+ self,
18
+ full_length: int | None,
19
+ start: int,
20
+ end: int,
21
+ ) -> str:
22
+ """
23
+ Build Content-Range for a *single* range response (206).
24
+
25
+ Args:
26
+ full_length: total representation length, or None if unknown.
27
+ start, end: inclusive byte positions of the payload actually served.
28
+ Returns:
29
+ e.g. "bytes 0-99/1234" or "bytes 0-99/*"
30
+ """
31
+ if self.unit != "bytes":
32
+ raise ValueError(f"Content-Range generation not implemented for unit={self.unit!r}")
33
+
34
+ if start < 0 or end < start:
35
+ raise ValueError("Invalid start/end")
36
+
37
+ if full_length is not None:
38
+ if full_length < 0:
39
+ raise ValueError("full_length must be >= 0")
40
+ # When length known, served range must be within it (end inclusive)
41
+ if full_length == 0:
42
+ raise ValueError("Cannot serve a non-empty range for full_length=0")
43
+ if end >= full_length:
44
+ raise ValueError("end must be < full_length when full_length is known")
45
+
46
+ return f"bytes {start}-{end}/{full_length}"
47
+
48
+ return f"bytes {start}-{end}/*"
49
+
50
+ @staticmethod
51
+ def content_range_unsatisfied(*, unit: str = "bytes", full_length: int) -> str:
52
+ """
53
+ Build Content-Range for 416 Range Not Satisfiable.
54
+
55
+ Returns:
56
+ e.g. "bytes */1234"
57
+ """
58
+ if full_length < 0:
59
+ raise ValueError("full_length must be >= 0")
60
+ return f"{unit} */{full_length}"
61
+
62
+
63
+ def content_length(self, start: int, end: int) -> int:
64
+ """
65
+ Content-Length for a *single* resolved range body (206).
66
+ """
67
+ if self.unit != "bytes":
68
+ raise ValueError(f"Content-Length not implemented for unit={self.unit!r}")
69
+ if start < 0 or end < start:
70
+ raise ValueError("Invalid start/end")
71
+ return (end - start) + 1
@@ -9,7 +9,7 @@ from json import loads, JSONDecodeError
9
9
  from logging import warning
10
10
  from mimetypes import guess_type
11
11
  from pathlib import Path
12
- from re import compile, escape, sub
12
+ from re import compile, escape, sub, VERBOSE, split
13
13
  from tempfile import NamedTemporaryFile, TemporaryFile
14
14
  from types import NoneType
15
15
  from typing import Callable, Type, Pattern, Any, Unpack, TypedDict, NotRequired, AnyStr, BinaryIO, Self, Literal, get_args, get_origin, \
@@ -20,7 +20,7 @@ from uuid import UUID
20
20
  from multipart import MultipartParser
21
21
 
22
22
  from edri.api import Headers
23
- from edri.api.dataclass import File
23
+ from edri.api.dataclass import File, RangeSpec, RangeValue
24
24
  from edri.api.dataclass.api_event import api_events
25
25
  from edri.api.handlers import BaseHandler
26
26
  from edri.api.handlers.base_handler import BaseDirectiveHandlerDict
@@ -30,15 +30,14 @@ from edri.config.setting import MAX_BODY_SIZE, ASSETS_PATH, UPLOAD_FILES_PREFIX,
30
30
  from edri.dataclass.directive import HTTPResponseDirective, ResponseDirective
31
31
  from edri.dataclass.directive.base import InternalServerErrorResponseDirective, UnauthorizedResponseDirective
32
32
  from edri.dataclass.directive.http import CookieResponseDirective, AccessDeniedResponseDirective, \
33
- NotFoundResponseDirective, \
34
- ConflictResponseDirective, HeaderResponseDirective, UnprocessableContentResponseDirective, \
35
- BadRequestResponseDirective, NotModifiedResponseDirective, ServiceUnavailableResponseDirective
33
+ NotFoundResponseDirective, ConflictResponseDirective, HeaderResponseDirective, \
34
+ UnprocessableContentResponseDirective, BadRequestResponseDirective, NotModifiedResponseDirective, \
35
+ ServiceUnavailableResponseDirective, PartialContentResponseDirective, RangeNotSatisfiableResponseDirective
36
36
  from edri.dataclass.event import Event
37
37
  from edri.dataclass.injection import Injection
38
38
  from edri.utility import NormalizedDefaultDict
39
39
  from edri.utility.function import camel2snake
40
40
  from edri.utility.shared_memory_pipe import SharedMemoryPipe
41
- from edri.utility.validation import StringValidator
42
41
 
43
42
 
44
43
  class EventTypesExtensionsDict(TypedDict):
@@ -236,6 +235,23 @@ class URLNode:
236
235
 
237
236
  return repr_str
238
237
 
238
+ _RANGE_VALUE_RE = compile(
239
+ r"""
240
+ ^\s*
241
+ (?P<unit>[A-Za-z][A-Za-z0-9._-]*) # range-unit
242
+ \s*=\s*
243
+ (?P<specs>.+?) # comma-separated specs (validated below)
244
+ \s*$
245
+ """,
246
+ VERBOSE,
247
+ )
248
+
249
+ # "bytes" token grammar: 1) first-last / first- OR 2) -suffix
250
+ _BYTES_SPEC_RE = compile(
251
+ r"^\s*(?:(?P<first>\d+)\s*-\s*(?P<last>\d*)|-\s*(?P<suffix>\d+))\s*$"
252
+ )
253
+
254
+
239
255
 
240
256
  class HTTPHandler[T: HTTPResponseDirective](BaseHandler, ABC):
241
257
  _directive_handlers: dict[Type[T], HTTPDirectiveHandlerDict[T]] = {
@@ -276,6 +292,12 @@ class HTTPHandler[T: HTTPResponseDirective](BaseHandler, ABC):
276
292
  },
277
293
  ServiceUnavailableResponseDirective: {
278
294
  "status": HTTPStatus.SERVICE_UNAVAILABLE,
295
+ },
296
+ PartialContentResponseDirective: {
297
+ "status": HTTPStatus.PARTIAL_CONTENT,
298
+ },
299
+ RangeNotSatisfiableResponseDirective: {
300
+ "status": HTTPStatus.REQUESTED_RANGE_NOT_SATISFIABLE,
279
301
  }
280
302
  }
281
303
 
@@ -618,16 +640,39 @@ class HTTPHandler[T: HTTPResponseDirective](BaseHandler, ABC):
618
640
 
619
641
  async def response_file(self, event: Event, *args, **kwargs: Unpack[ResponseKW]):
620
642
  headers = kwargs["headers"]
643
+ request_headers = kwargs["request_headers"]
621
644
  file = event.response.file
622
645
 
623
646
  headers["Content-Type"].append(file.mime_type)
624
647
  headers["Content-Disposition"].append(f"attachment;filename*=UTF-8''{quote(file.file_name, encoding='utf-8')}")
625
648
 
626
649
  if isinstance(file.path, Path):
627
- headers["Content-Length"].append(str(file.path.lstat().st_size))
650
+ file_size = file.path.stat().st_size
651
+ headers["Accept-Ranges"].append("bytes")
652
+ if file.fingerprint:
653
+ headers["ETag"].append(f"\"{file.fingerprint}\"")
628
654
  try:
629
655
  with file.path.open("rb") as data:
630
- await self.response_file_path(data, headers)
656
+ # Only honor Range if If-Range matches current ETag (and we actually got a Range header).
657
+ if request_headers["If-Range"] == headers["ETag"] and "range" in request_headers and len(request_headers["range"]) == 1:
658
+ try:
659
+ bytes_ranges = await self.parse_range_values(request_headers["Range"][0])
660
+
661
+ # Handle only the first range spec (multipart/byteranges is a separate response type).
662
+ if bytes_ranges.specs:
663
+ spec = bytes_ranges.specs[0]
664
+ start, end = self.compute_range(spec, file_size)
665
+ data.seek(start)
666
+ headers["Content-Length"].append(str(bytes_ranges.content_length(start, end)))
667
+ headers["Content-Range"].append(bytes_ranges.content_range(file_size, start, end))
668
+ return await self.response_file_path(data, headers, max_bytes=(end - start) + 1, http_status=HTTPStatus.REQUESTED_RANGE_NOT_SATISFIABLE)
669
+
670
+ except Exception as e:
671
+ self.logger.warning("Wrong Range header", exc_info=e)
672
+
673
+ headers["Content-Length"].append(str(file_size))
674
+ return await self.response_file_path(data, headers)
675
+
631
676
  except FileNotFoundError as e:
632
677
  await self.response_error(HTTPStatus.INTERNAL_SERVER_ERROR, {
633
678
  "reasons": [{
@@ -680,22 +725,60 @@ class HTTPHandler[T: HTTPResponseDirective](BaseHandler, ABC):
680
725
  }]
681
726
  })
682
727
 
683
- async def response_file_path(self, file: BinaryIO, headers: NormalizedDefaultDict[str, Headers]):
728
+ async def response_file_path(
729
+ self,
730
+ file: BinaryIO,
731
+ headers: "NormalizedDefaultDict[str, Headers]",
732
+ *,
733
+ max_bytes: int | None = None,
734
+ http_status: HTTPStatus = HTTPStatus.OK,
735
+ ):
684
736
  try:
685
737
  await self.send({
686
- 'type': 'http.response.start',
687
- 'status': HTTPStatus.OK,
688
- 'headers': self.get_headers_binary(headers),
738
+ "type": "http.response.start",
739
+ "status": http_status,
740
+ "headers": self.get_headers_binary(headers),
689
741
  })
742
+
690
743
  chunk_size = 1024 * 1024
691
- while data := file.read(chunk_size):
744
+ remaining = max_bytes
745
+
746
+ def read_size() -> int:
747
+ if remaining is None:
748
+ return chunk_size
749
+ return min(chunk_size, remaining)
750
+
751
+ sent_any = False
752
+
753
+ # Lookahead loop so we always send a final more_body=False
754
+ if remaining == 0:
755
+ await self.send({"type": "http.response.body", "body": b"", "more_body": False})
756
+ return
757
+
758
+ data = file.read(read_size())
759
+ while data:
760
+ sent_any = True
761
+
762
+ if remaining is not None:
763
+ remaining -= len(data)
764
+ if remaining <= 0:
765
+ await self.send({"type": "http.response.body", "body": data, "more_body": False})
766
+ return
767
+
768
+ next_data = file.read(read_size())
692
769
  await self.send({
693
- 'type': 'http.response.body',
694
- 'body': data,
695
- 'more_body': len(data) == chunk_size
770
+ "type": "http.response.body",
771
+ "body": data,
772
+ "more_body": bool(next_data),
696
773
  })
774
+ data = next_data
775
+
776
+ if not sent_any:
777
+ await self.send({"type": "http.response.body", "body": b"", "more_body": False})
778
+
697
779
  except Exception as e:
698
780
  self.logger.error(e, exc_info=e)
781
+ raise
699
782
 
700
783
  async def response_file_shared_memory_pipe(self, smp: SharedMemoryPipe, headers: NormalizedDefaultDict[str, Headers]):
701
784
  try:
@@ -821,3 +904,98 @@ class HTTPHandler[T: HTTPResponseDirective](BaseHandler, ABC):
821
904
  return super().convert_type(value.value, annotation)
822
905
 
823
906
  return super().convert_type(value, annotation)
907
+
908
+ @staticmethod
909
+ async def parse_range_values(value: str, *, max_ranges: int = 100, max_digits: int = 50) -> RangeValue:
910
+ """
911
+ Parse + validate a Range header *value* (field-value only), e.g.:
912
+ "bytes=0-99,200-299"
913
+ "bytes=9500-"
914
+ "bytes=-500"
915
+ "items=1-10" (prepared for other units -> currently throws)
916
+
917
+ Returns RangeValue(unit, specs). Raises ValueError on any validation failure.
918
+ """
919
+ if value is None:
920
+ raise ValueError("Range value is None")
921
+
922
+ m = _RANGE_VALUE_RE.match(value)
923
+ if not m:
924
+ raise ValueError("Invalid Range value (expected <unit>=<spec>[,<spec>...])")
925
+
926
+ unit = m.group("unit").lower()
927
+ specs_blob = m.group("specs").strip()
928
+ if not specs_blob:
929
+ raise ValueError("Range value missing specs after '='")
930
+
931
+ parts = split(r"\s*,\s*", specs_blob)
932
+ if any(p == "" for p in parts):
933
+ raise ValueError("Invalid Range value (empty range-spec)")
934
+ if len(parts) > max_ranges:
935
+ raise ValueError(f"Too many ranges: {len(parts)} > {max_ranges}")
936
+
937
+ def to_int(s: str) -> int:
938
+ s = s.strip()
939
+ if not s.isdigit():
940
+ raise ValueError(f"Invalid numeric value: {s!r}")
941
+ if len(s) > max_digits:
942
+ raise ValueError(f"Numeric value too large (>{max_digits} digits)")
943
+ return int(s)
944
+
945
+ if unit != "bytes":
946
+ # Prepared for other units: we parse the unit name, but we don't implement its grammar yet.
947
+ raise ValueError(f"Unsupported range unit: {unit!r}")
948
+
949
+ parsed: list[RangeSpec] = []
950
+ for p in parts:
951
+ sm = _BYTES_SPEC_RE.match(p)
952
+ if not sm:
953
+ raise ValueError(f"Invalid bytes range-spec: {p!r}")
954
+
955
+ suffix = sm.group("suffix")
956
+ if suffix is not None:
957
+ # "-0" is syntactically valid; satisfiable vs unsatisfiable needs representation length.
958
+ parsed.append(RangeSpec(suffix_length=to_int(suffix)))
959
+ continue
960
+
961
+ first = to_int(sm.group("first"))
962
+ last_s = (sm.group("last") or "").strip()
963
+ if last_s == "":
964
+ parsed.append(RangeSpec(first=first, last=None))
965
+ else:
966
+ last = to_int(last_s)
967
+ if last < first:
968
+ raise ValueError(f"Invalid bytes range-spec (last < first): {p!r}")
969
+ parsed.append(RangeSpec(first=first, last=last))
970
+
971
+ return RangeValue(unit=unit, specs=tuple(parsed))
972
+
973
+ @staticmethod
974
+ def compute_range(spec: RangeSpec, file_size: int) -> tuple[int, int]:
975
+ """
976
+ Compute (start, end) inclusive for a single byte-range spec.
977
+ """
978
+
979
+ if spec.suffix_length:
980
+ if spec.suffix_length <= 0:
981
+ raise ValueError("Invalid suffix length")
982
+ start = max(file_size - spec.suffix_length, 0)
983
+ end = file_size - 1
984
+
985
+ # Open-ended range: bytes=START-
986
+ elif spec.first is not None and spec.last is None:
987
+ start = spec.first
988
+ end = file_size - 1
989
+
990
+ # Normal range: bytes=START-END
991
+ elif spec.first is not None and spec.last is not None:
992
+ start = spec.first
993
+ end = min(spec.last, file_size - 1)
994
+
995
+ else:
996
+ raise ValueError("Invalid range spec")
997
+
998
+ if start < 0 or start >= file_size or end < start:
999
+ raise ValueError("Range out of bounds")
1000
+
1001
+ return start, end
edri/api/listener.py CHANGED
@@ -267,7 +267,7 @@ class Listener(Process):
267
267
  status, headers = handler.handle_directives(event_response.get_response().get_directives())
268
268
  if status.is_success:
269
269
  if event_response.get_response().is_file_only:
270
- await handler.response_file(event_response, headers=headers)
270
+ await handler.response_file(event_response, headers=headers, request_headers=handler.headers)
271
271
  else:
272
272
  await handler.response(status, event_response, headers=headers)
273
273
  elif status.is_redirection:
@@ -80,3 +80,8 @@ class ServiceUnavailableResponseDirective(HTTPResponseDirective):
80
80
  @dataclass
81
81
  class PartialContentResponseDirective(HTTPResponseDirective):
82
82
  pass
83
+
84
+
85
+ @dataclass
86
+ class RangeNotSatisfiableResponseDirective(HTTPResponseDirective):
87
+ pass
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: edri
3
- Version: 2025.12.3rc1
3
+ Version: 2025.12.3rc2
4
4
  Summary: Event Driven Routing Infrastructure
5
5
  Author: Marek Olšan
6
6
  Author-email: marek.olsan@gmail.com
@@ -1,7 +1,7 @@
1
1
  edri/__init__.py,sha256=bBVs4ynkUzMRudKZyuRScpPmUZTIL1LDfIytZ_L7oKE,6257
2
2
  edri/abstract/__init__.py,sha256=C6ew041GNVcQlfkE77kHeZ9Ts1rIaWRqqh3bKz7Kb1E,488
3
3
  edri/abstract/manager/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
- edri/abstract/manager/manager_base.py,sha256=mguHncpNnx-5RSAPdkzzl74NpOghpl2pI76H0biyZEo,42373
4
+ edri/abstract/manager/manager_base.py,sha256=aEVzFzzl6XfLf3A7I8yAUyBpRbX3d3H6R2b0IRkNQNA,42391
5
5
  edri/abstract/manager/manager_priority_base.py,sha256=1bUGsIr6MUrCNsJsLNF_tYaiFDX5t8vK2794vdZumQo,8219
6
6
  edri/abstract/manager/worker.py,sha256=xMJMDQtcH-j8s37cUoMsF_ZrWS4elbU7vnjdKfAv0gM,403
7
7
  edri/abstract/worker/__init__.py,sha256=qcCF2MnReil9G9ojrr7I3hjMks-1tsap4yrYH5Vr07Q,164
@@ -10,19 +10,20 @@ edri/abstract/worker/worker_process.py,sha256=QiNxOuwkMds0sV2MBLyp7bjrovm5xColC7
10
10
  edri/abstract/worker/worker_thread.py,sha256=xoMPuDn-hAkWk6kFY3Xf8mxOVP__5t7-x7f-b396-8M,2176
11
11
  edri/api/__init__.py,sha256=ZDxCpHKFGajJ1RwDpV7CzxLDUaKpozJRfOCv1OPv5ZY,142
12
12
  edri/api/broker.py,sha256=I3z_bKbcTDnKXk82yteGEQmuxpqHgp5KrhQaJmk3US0,37258
13
- edri/api/listener.py,sha256=vXvLx2agQG43BIaLOnVpu9ATXhLvKon8BVRLCbVRs_Q,20409
13
+ edri/api/listener.py,sha256=-29ROnrACEpGPtWuBaikzzxJZxrq2jU_twYiU__GgCA,20442
14
14
  edri/api/middleware.py,sha256=6_x55swthVDczT-fu_1ufY1cDsHTZ04jMx6J6xfjbsM,5483
15
- edri/api/dataclass/__init__.py,sha256=8Y-zcaJtzMdALnNG7M9jsCaB1qAJKM8Ld3h9MDajYjA,292
15
+ edri/api/dataclass/__init__.py,sha256=u3520rEHLLetRI--OhTXwRmhv-ydMhuMhDPDx6_7KG0,333
16
16
  edri/api/dataclass/api_event.py,sha256=43pCg4Whm3Y1ANUomu8pXbXIRtWQkrDDqNsr_Qnt_-Y,6629
17
17
  edri/api/dataclass/client.py,sha256=ctc2G4mXJR2wUSujANudT3LqxW7qxk_YkpM_TEXD0tM,216
18
- edri/api/dataclass/file.py,sha256=OJfJlrCTjSnzCF8yFVnxr8rGeL0l08WVMsXJx00S4qc,225
18
+ edri/api/dataclass/file.py,sha256=fKQDWBYOSMj0CHXjgkY50wjgvnUlVhaB3rqK9mgp0aE,260
19
+ edri/api/dataclass/range.py,sha256=VaNZTZHtzzKimyg0mdW5auBF7q9ot1JHX5V2h2uw2NE,2222
19
20
  edri/api/extensions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
21
  edri/api/extensions/url_extension.py,sha256=rZKumjR7J6pDTiSLIZf8IzxGgDZP7p2g0Kgs0USug_U,1971
21
22
  edri/api/extensions/url_prefix.py,sha256=kNI6g5ZlW0w-J_IMacYLco1EQvmTtMJyEkN6-SK1wC0,491
22
23
  edri/api/handlers/__init__.py,sha256=MI6OGDf1rM8jf_uCKK_JYeOGMts62CNy10BwwNlG0Tk,200
23
24
  edri/api/handlers/base_handler.py,sha256=Lte9SEprQcIYv1nC4Zj2eMmHcvNcCGJQS4TWaP8XlrI,13156
24
25
  edri/api/handlers/html_handler.py,sha256=OprcTg1IQDI7eBK-_oHqA60P1H30LA9xIQpD7iV-Neg,7464
25
- edri/api/handlers/http_handler.py,sha256=jjOyx2Wu756K99x6zvJoybgM2Bj0eWXkjEElLYkCDZ0,36338
26
+ edri/api/handlers/http_handler.py,sha256=nN6YVeiDzhOPsb7D0xqcIVR_FVWugOgjouFGZblOhdM,43256
26
27
  edri/api/handlers/rest_handler.py,sha256=GAG5lVTsRMCf9IUmYb_pokxyPcOfbnKZ2p3jxfy_-Dw,3300
27
28
  edri/api/handlers/websocket_handler.py,sha256=Dh2XannDuW0eFj5CEzf3owlGc1VTyQ8ehjpxYRrCYW8,8144
28
29
  edri/api/static_pages/documentation.j2,sha256=Fe7KLsbqp9P-pQYqG2z8rbhhGVDDFf3m6SQ2mc3PFG4,8934
@@ -41,7 +42,7 @@ edri/dataclass/response.py,sha256=VBMmVdna1IOKC5YGBXor6AayYOoiEYb9xx_RZ3bpKnw,38
41
42
  edri/dataclass/directive/__init__.py,sha256=nfvsh1BmxhACW7Q8gnwy7y3l3_cI1P0k2WP0jV5RJhI,608
42
43
  edri/dataclass/directive/base.py,sha256=2ghQpv1bGcNHYEMA0nyWGumIplXBzj9cPQ34aJ7uVr0,296
43
44
  edri/dataclass/directive/html.py,sha256=UCuwksxt_Q9b1wha1DjEygJWAyq2Hdnir5zG9lGi8as,946
44
- edri/dataclass/directive/http.py,sha256=FRiuU92TvkAOW0FKHgUIujcqD8zqfZcCs2Dj1kCgwqU,2811
45
+ edri/dataclass/directive/http.py,sha256=iah8pVZll6ccf-gJKxwX0sAzzltFZ7WH3gY-yuv_Ex4,2900
45
46
  edri/events/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
46
47
  edri/events/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
47
48
  edri/events/api/client/__init__.py,sha256=6q7CJ4eLMAuz_EFIs7us-xDXudy0Z5DIHd0YCVtTeuo,170
@@ -157,7 +158,7 @@ tests/utility/test_validation.py,sha256=wZcXjLrj3JheVLKnYKkkYfyC8CCpHVAw9Jn_uDnu
157
158
  tests/utility/manager/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
158
159
  tests/utility/manager/test_scheduler.py,sha256=sROffYvSOaWsYQxQGTy6l9Mn_qeNPRmJoXLVPKU3XNY,9153
159
160
  tests/utility/manager/test_store.py,sha256=xlo1JUsPLIhPJyQn7AXldAgWDo_O8ba2ns25TEaaGdQ,2821
160
- edri-2025.12.3rc1.dist-info/METADATA,sha256=OBAQ4p4GB6h8PesxFiCHbrlM1_JkrmsdOG-5oCk5QUQ,8374
161
- edri-2025.12.3rc1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
162
- edri-2025.12.3rc1.dist-info/top_level.txt,sha256=himES6JgPlx4Zt8aDrQEj2fxAd7IDD6MBOsiNZkzKHQ,11
163
- edri-2025.12.3rc1.dist-info/RECORD,,
161
+ edri-2025.12.3rc2.dist-info/METADATA,sha256=ApA6oZH9FenWEefx2CegCab_RVMUECQx-N9-DxF4gGA,8374
162
+ edri-2025.12.3rc2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
163
+ edri-2025.12.3rc2.dist-info/top_level.txt,sha256=himES6JgPlx4Zt8aDrQEj2fxAd7IDD6MBOsiNZkzKHQ,11
164
+ edri-2025.12.3rc2.dist-info/RECORD,,