edri 2025.12.3__tar.gz → 2025.12.3rc1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {edri-2025.12.3 → edri-2025.12.3rc1}/PKG-INFO +1 -1
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/abstract/manager/manager_base.py +2 -2
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/api/dataclass/__init__.py +0 -1
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/api/dataclass/file.py +0 -1
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/api/handlers/http_handler.py +16 -194
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/api/listener.py +1 -1
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/dataclass/directive/http.py +0 -5
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri.egg-info/PKG-INFO +1 -1
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri.egg-info/SOURCES.txt +0 -1
- {edri-2025.12.3 → edri-2025.12.3rc1}/setup.py +1 -1
- edri-2025.12.3/edri/api/dataclass/range.py +0 -71
- {edri-2025.12.3 → edri-2025.12.3rc1}/README.md +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/__init__.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/abstract/__init__.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/abstract/manager/__init__.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/abstract/manager/manager_priority_base.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/abstract/manager/worker.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/abstract/worker/__init__.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/abstract/worker/worker.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/abstract/worker/worker_process.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/abstract/worker/worker_thread.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/api/__init__.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/api/broker.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/api/dataclass/api_event.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/api/dataclass/client.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/api/extensions/__init__.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/api/extensions/url_extension.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/api/extensions/url_prefix.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/api/handlers/__init__.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/api/handlers/base_handler.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/api/handlers/html_handler.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/api/handlers/rest_handler.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/api/handlers/websocket_handler.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/api/middleware.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/api/static_pages/documentation.j2 +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/api/static_pages/health_check_status.j2 +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/api/static_pages/status_300.j2 +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/api/static_pages/status_400.j2 +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/api/static_pages/status_500.j2 +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/config/__init__.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/config/constant.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/config/setting.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/dataclass/__init__.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/dataclass/directive/__init__.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/dataclass/directive/base.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/dataclass/directive/html.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/dataclass/event.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/dataclass/health_checker.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/dataclass/injection.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/dataclass/response.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/events/__init__.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/events/api/__init__.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/events/api/client/__init__.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/events/api/client/documentation.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/events/api/client/register.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/events/api/client/unregister.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/events/api/group/__init__.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/events/api/group/client.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/events/api/group/manage.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/events/api/manage/__init__.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/events/api/manage/list_registered.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/events/api/manage/register.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/events/api/manage/unregister.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/events/api/manage/unregister_all.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/events/edri/__init__.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/events/edri/group/__init__.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/events/edri/group/manager.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/events/edri/group/router.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/events/edri/group/scheduler.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/events/edri/group/store.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/events/edri/group/switch.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/events/edri/group/test.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/events/edri/manager/__init__.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/events/edri/manager/restart.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/events/edri/manager/stream_close.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/events/edri/manager/stream_create.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/events/edri/manager/stream_message.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/events/edri/manager/worker_quit.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/events/edri/router/__init__.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/events/edri/router/demands.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/events/edri/router/health_check.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/events/edri/router/last_events.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/events/edri/router/send_from.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/events/edri/router/subscribe.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/events/edri/router/subscribe_connector.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/events/edri/router/subscribed_external.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/events/edri/router/subscribed_new.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/events/edri/router/unsubscribe.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/events/edri/router/unsubscribe_all.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/events/edri/scheduler/__init__.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/events/edri/scheduler/cancel.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/events/edri/scheduler/set.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/events/edri/scheduler/set_or_update.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/events/edri/scheduler/update.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/events/edri/store/__init__.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/events/edri/store/delete.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/events/edri/store/get.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/events/edri/store/get_callback.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/events/edri/store/set.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/router/__init__.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/router/cache.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/router/connector/__init__.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/router/connector/connector.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/router/connector/socket.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/router/health_checker.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/router/router.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/switch/__init__.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/switch/connection.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/switch/forwarder.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/switch/receiver.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/switch/sender.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/switch/switch.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/utility/__init__.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/utility/cache.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/utility/function.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/utility/json_encoder.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/utility/manager/__init__.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/utility/manager/scheduler.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/utility/manager/store.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/utility/normalized_default_dict.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/utility/queue.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/utility/shared_memory_pipe.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/utility/storage.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/utility/transformation.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/utility/validation.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri/utility/watcher.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri.egg-info/dependency_links.txt +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri.egg-info/requires.txt +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/edri.egg-info/top_level.txt +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/setup.cfg +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/tests/__init__.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/tests/abstract/__init__.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/tests/abstract/manager/__init__.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/tests/abstract/manager/test_manager_base.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/tests/abstract/manager/test_manager_base_priority.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/tests/abstract/worker/__init__.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/tests/abstract/worker/test_worker.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/tests/api/__init__.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/tests/api/handlers/__init__.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/tests/api/handlers/test_base_handler.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/tests/api/handlers/test_html_handler.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/tests/api/handlers/test_http_handler.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/tests/api/test_broker.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/tests/dataclass/__init__.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/tests/dataclass/event/__init__.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/tests/dataclass/event/test_event.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/tests/dataclass/event/test_event_init.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/tests/dataclass/event/test_response.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/tests/events/__init__.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/tests/events/test/__init__.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/tests/events/test/event_request.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/tests/events/test/ping.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/tests/events/test/ping2.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/tests/router/__init__.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/tests/router/test_cache.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/tests/router/test_health_checker.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/tests/router/test_router.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/tests/test_edri_init.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/tests/utility/__init__.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/tests/utility/manager/__init__.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/tests/utility/manager/test_scheduler.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/tests/utility/manager/test_store.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/tests/utility/test_function.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/tests/utility/test_json_encoder.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/tests/utility/test_normalized_default_dict.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/tests/utility/test_shared_memory_pipe.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/tests/utility/test_storage.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/tests/utility/test_transformation.py +0 -0
- {edri-2025.12.3 → edri-2025.12.3rc1}/tests/utility/test_validation.py +0 -0
|
@@ -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
|
|
637
|
+
if etag and hasattr(event, "etag") and event.etag and 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=
|
|
649
|
+
event.response.add_directive(HeaderResponseDirective(name="ETag", value=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)
|
|
@@ -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
|
|
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
|
|
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,14 +30,15 @@ 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
|
-
|
|
35
|
-
|
|
33
|
+
NotFoundResponseDirective, \
|
|
34
|
+
ConflictResponseDirective, HeaderResponseDirective, UnprocessableContentResponseDirective, \
|
|
35
|
+
BadRequestResponseDirective, NotModifiedResponseDirective, ServiceUnavailableResponseDirective
|
|
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
|
|
41
42
|
|
|
42
43
|
|
|
43
44
|
class EventTypesExtensionsDict(TypedDict):
|
|
@@ -235,23 +236,6 @@ class URLNode:
|
|
|
235
236
|
|
|
236
237
|
return repr_str
|
|
237
238
|
|
|
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
|
-
|
|
255
239
|
|
|
256
240
|
class HTTPHandler[T: HTTPResponseDirective](BaseHandler, ABC):
|
|
257
241
|
_directive_handlers: dict[Type[T], HTTPDirectiveHandlerDict[T]] = {
|
|
@@ -292,12 +276,6 @@ class HTTPHandler[T: HTTPResponseDirective](BaseHandler, ABC):
|
|
|
292
276
|
},
|
|
293
277
|
ServiceUnavailableResponseDirective: {
|
|
294
278
|
"status": HTTPStatus.SERVICE_UNAVAILABLE,
|
|
295
|
-
},
|
|
296
|
-
PartialContentResponseDirective: {
|
|
297
|
-
"status": HTTPStatus.PARTIAL_CONTENT,
|
|
298
|
-
},
|
|
299
|
-
RangeNotSatisfiableResponseDirective: {
|
|
300
|
-
"status": HTTPStatus.REQUESTED_RANGE_NOT_SATISFIABLE,
|
|
301
279
|
}
|
|
302
280
|
}
|
|
303
281
|
|
|
@@ -640,39 +618,16 @@ class HTTPHandler[T: HTTPResponseDirective](BaseHandler, ABC):
|
|
|
640
618
|
|
|
641
619
|
async def response_file(self, event: Event, *args, **kwargs: Unpack[ResponseKW]):
|
|
642
620
|
headers = kwargs["headers"]
|
|
643
|
-
request_headers = kwargs["request_headers"]
|
|
644
621
|
file = event.response.file
|
|
645
622
|
|
|
646
623
|
headers["Content-Type"].append(file.mime_type)
|
|
647
624
|
headers["Content-Disposition"].append(f"attachment;filename*=UTF-8''{quote(file.file_name, encoding='utf-8')}")
|
|
648
625
|
|
|
649
626
|
if isinstance(file.path, Path):
|
|
650
|
-
|
|
651
|
-
headers["Accept-Ranges"].append("bytes")
|
|
652
|
-
if file.fingerprint:
|
|
653
|
-
headers["ETag"].append(f"\"{file.fingerprint}\"")
|
|
627
|
+
headers["Content-Length"].append(str(file.path.lstat().st_size))
|
|
654
628
|
try:
|
|
655
629
|
with file.path.open("rb") as data:
|
|
656
|
-
|
|
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
|
-
|
|
630
|
+
await self.response_file_path(data, headers)
|
|
676
631
|
except FileNotFoundError as e:
|
|
677
632
|
await self.response_error(HTTPStatus.INTERNAL_SERVER_ERROR, {
|
|
678
633
|
"reasons": [{
|
|
@@ -725,60 +680,22 @@ class HTTPHandler[T: HTTPResponseDirective](BaseHandler, ABC):
|
|
|
725
680
|
}]
|
|
726
681
|
})
|
|
727
682
|
|
|
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
|
-
):
|
|
683
|
+
async def response_file_path(self, file: BinaryIO, headers: NormalizedDefaultDict[str, Headers]):
|
|
736
684
|
try:
|
|
737
685
|
await self.send({
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
686
|
+
'type': 'http.response.start',
|
|
687
|
+
'status': HTTPStatus.OK,
|
|
688
|
+
'headers': self.get_headers_binary(headers),
|
|
741
689
|
})
|
|
742
|
-
|
|
743
690
|
chunk_size = 1024 * 1024
|
|
744
|
-
|
|
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())
|
|
691
|
+
while data := file.read(chunk_size):
|
|
769
692
|
await self.send({
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
693
|
+
'type': 'http.response.body',
|
|
694
|
+
'body': data,
|
|
695
|
+
'more_body': len(data) == chunk_size
|
|
773
696
|
})
|
|
774
|
-
data = next_data
|
|
775
|
-
|
|
776
|
-
if not sent_any:
|
|
777
|
-
await self.send({"type": "http.response.body", "body": b"", "more_body": False})
|
|
778
|
-
|
|
779
697
|
except Exception as e:
|
|
780
698
|
self.logger.error(e, exc_info=e)
|
|
781
|
-
raise
|
|
782
699
|
|
|
783
700
|
async def response_file_shared_memory_pipe(self, smp: SharedMemoryPipe, headers: NormalizedDefaultDict[str, Headers]):
|
|
784
701
|
try:
|
|
@@ -904,98 +821,3 @@ class HTTPHandler[T: HTTPResponseDirective](BaseHandler, ABC):
|
|
|
904
821
|
return super().convert_type(value.value, annotation)
|
|
905
822
|
|
|
906
823
|
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
|
|
@@ -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)
|
|
271
271
|
else:
|
|
272
272
|
await handler.response(status, event_response, headers=headers)
|
|
273
273
|
elif status.is_redirection:
|
|
@@ -23,7 +23,6 @@ edri/api/dataclass/__init__.py
|
|
|
23
23
|
edri/api/dataclass/api_event.py
|
|
24
24
|
edri/api/dataclass/client.py
|
|
25
25
|
edri/api/dataclass/file.py
|
|
26
|
-
edri/api/dataclass/range.py
|
|
27
26
|
edri/api/extensions/__init__.py
|
|
28
27
|
edri/api/extensions/url_extension.py
|
|
29
28
|
edri/api/extensions/url_prefix.py
|
|
@@ -122,7 +122,7 @@ Whether you're building a real-time data processing system, a distributed servic
|
|
|
122
122
|
|
|
123
123
|
setup(
|
|
124
124
|
name='edri',
|
|
125
|
-
version='2025.12.
|
|
125
|
+
version='2025.12.03rc1',
|
|
126
126
|
packages=find_packages(),
|
|
127
127
|
description='Event Driven Routing Infrastructure',
|
|
128
128
|
long_description=long_description,
|
|
@@ -1,71 +0,0 @@
|
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|