edri 2025.11.1rc3__tar.gz → 2025.12.1__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.
Files changed (168) hide show
  1. {edri-2025.11.1rc3 → edri-2025.12.1}/PKG-INFO +2 -1
  2. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/abstract/__init__.py +10 -3
  3. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/abstract/manager/manager_base.py +42 -6
  4. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/api/broker.py +8 -8
  5. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/api/dataclass/api_event.py +2 -2
  6. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/api/handlers/base_handler.py +0 -1
  7. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/api/handlers/http_handler.py +29 -4
  8. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/api/listener.py +3 -1
  9. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/config/setting.py +12 -4
  10. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/dataclass/directive/http.py +10 -0
  11. edri-2025.12.1/edri/utility/cache.py +19 -0
  12. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/utility/validation.py +15 -15
  13. {edri-2025.11.1rc3 → edri-2025.12.1}/edri.egg-info/PKG-INFO +2 -1
  14. {edri-2025.11.1rc3 → edri-2025.12.1}/edri.egg-info/SOURCES.txt +1 -0
  15. {edri-2025.11.1rc3 → edri-2025.12.1}/edri.egg-info/requires.txt +1 -0
  16. {edri-2025.11.1rc3 → edri-2025.12.1}/setup.py +3 -2
  17. {edri-2025.11.1rc3 → edri-2025.12.1}/tests/abstract/manager/test_manager_base.py +1 -0
  18. {edri-2025.11.1rc3 → edri-2025.12.1}/tests/utility/test_validation.py +46 -46
  19. {edri-2025.11.1rc3 → edri-2025.12.1}/README.md +0 -0
  20. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/__init__.py +0 -0
  21. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/abstract/manager/__init__.py +0 -0
  22. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/abstract/manager/manager_priority_base.py +0 -0
  23. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/abstract/manager/worker.py +0 -0
  24. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/abstract/worker/__init__.py +0 -0
  25. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/abstract/worker/worker.py +0 -0
  26. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/abstract/worker/worker_process.py +0 -0
  27. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/abstract/worker/worker_thread.py +0 -0
  28. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/api/__init__.py +0 -0
  29. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/api/dataclass/__init__.py +0 -0
  30. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/api/dataclass/client.py +0 -0
  31. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/api/dataclass/file.py +0 -0
  32. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/api/extensions/__init__.py +0 -0
  33. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/api/extensions/url_extension.py +0 -0
  34. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/api/extensions/url_prefix.py +0 -0
  35. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/api/handlers/__init__.py +0 -0
  36. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/api/handlers/html_handler.py +0 -0
  37. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/api/handlers/rest_handler.py +0 -0
  38. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/api/handlers/websocket_handler.py +0 -0
  39. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/api/middleware.py +0 -0
  40. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/api/static_pages/documentation.j2 +0 -0
  41. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/api/static_pages/health_check_status.j2 +0 -0
  42. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/api/static_pages/status_300.j2 +0 -0
  43. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/api/static_pages/status_400.j2 +0 -0
  44. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/api/static_pages/status_500.j2 +0 -0
  45. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/config/__init__.py +0 -0
  46. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/config/constant.py +0 -0
  47. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/dataclass/__init__.py +0 -0
  48. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/dataclass/directive/__init__.py +0 -0
  49. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/dataclass/directive/base.py +0 -0
  50. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/dataclass/directive/html.py +0 -0
  51. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/dataclass/event.py +0 -0
  52. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/dataclass/health_checker.py +0 -0
  53. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/dataclass/injection.py +0 -0
  54. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/dataclass/response.py +0 -0
  55. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/events/__init__.py +0 -0
  56. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/events/api/__init__.py +0 -0
  57. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/events/api/client/__init__.py +0 -0
  58. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/events/api/client/documentation.py +0 -0
  59. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/events/api/client/register.py +0 -0
  60. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/events/api/client/unregister.py +0 -0
  61. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/events/api/group/__init__.py +0 -0
  62. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/events/api/group/client.py +0 -0
  63. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/events/api/group/manage.py +0 -0
  64. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/events/api/manage/__init__.py +0 -0
  65. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/events/api/manage/list_registered.py +0 -0
  66. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/events/api/manage/register.py +0 -0
  67. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/events/api/manage/unregister.py +0 -0
  68. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/events/api/manage/unregister_all.py +0 -0
  69. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/events/edri/__init__.py +0 -0
  70. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/events/edri/group/__init__.py +0 -0
  71. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/events/edri/group/manager.py +0 -0
  72. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/events/edri/group/router.py +0 -0
  73. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/events/edri/group/scheduler.py +0 -0
  74. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/events/edri/group/store.py +0 -0
  75. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/events/edri/group/switch.py +0 -0
  76. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/events/edri/group/test.py +0 -0
  77. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/events/edri/manager/__init__.py +0 -0
  78. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/events/edri/manager/restart.py +0 -0
  79. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/events/edri/manager/stream_close.py +0 -0
  80. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/events/edri/manager/stream_create.py +0 -0
  81. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/events/edri/manager/stream_message.py +0 -0
  82. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/events/edri/manager/worker_quit.py +0 -0
  83. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/events/edri/router/__init__.py +0 -0
  84. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/events/edri/router/demands.py +0 -0
  85. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/events/edri/router/health_check.py +0 -0
  86. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/events/edri/router/last_events.py +0 -0
  87. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/events/edri/router/send_from.py +0 -0
  88. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/events/edri/router/subscribe.py +0 -0
  89. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/events/edri/router/subscribe_connector.py +0 -0
  90. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/events/edri/router/subscribed_external.py +0 -0
  91. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/events/edri/router/subscribed_new.py +0 -0
  92. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/events/edri/router/unsubscribe.py +0 -0
  93. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/events/edri/router/unsubscribe_all.py +0 -0
  94. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/events/edri/scheduler/__init__.py +0 -0
  95. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/events/edri/scheduler/cancel.py +0 -0
  96. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/events/edri/scheduler/set.py +0 -0
  97. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/events/edri/scheduler/set_or_update.py +0 -0
  98. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/events/edri/scheduler/update.py +0 -0
  99. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/events/edri/store/__init__.py +0 -0
  100. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/events/edri/store/delete.py +0 -0
  101. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/events/edri/store/get.py +0 -0
  102. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/events/edri/store/get_callback.py +0 -0
  103. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/events/edri/store/set.py +0 -0
  104. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/router/__init__.py +0 -0
  105. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/router/cache.py +0 -0
  106. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/router/connector/__init__.py +0 -0
  107. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/router/connector/connector.py +0 -0
  108. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/router/connector/socket.py +0 -0
  109. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/router/health_checker.py +0 -0
  110. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/router/router.py +0 -0
  111. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/switch/__init__.py +0 -0
  112. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/switch/connection.py +0 -0
  113. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/switch/forwarder.py +0 -0
  114. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/switch/receiver.py +0 -0
  115. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/switch/sender.py +0 -0
  116. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/switch/switch.py +0 -0
  117. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/utility/__init__.py +0 -0
  118. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/utility/function.py +0 -0
  119. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/utility/json_encoder.py +0 -0
  120. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/utility/manager/__init__.py +0 -0
  121. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/utility/manager/scheduler.py +0 -0
  122. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/utility/manager/store.py +0 -0
  123. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/utility/normalized_default_dict.py +0 -0
  124. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/utility/queue.py +0 -0
  125. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/utility/shared_memory_pipe.py +0 -0
  126. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/utility/storage.py +0 -0
  127. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/utility/transformation.py +0 -0
  128. {edri-2025.11.1rc3 → edri-2025.12.1}/edri/utility/watcher.py +0 -0
  129. {edri-2025.11.1rc3 → edri-2025.12.1}/edri.egg-info/dependency_links.txt +0 -0
  130. {edri-2025.11.1rc3 → edri-2025.12.1}/edri.egg-info/top_level.txt +0 -0
  131. {edri-2025.11.1rc3 → edri-2025.12.1}/setup.cfg +0 -0
  132. {edri-2025.11.1rc3 → edri-2025.12.1}/tests/__init__.py +0 -0
  133. {edri-2025.11.1rc3 → edri-2025.12.1}/tests/abstract/__init__.py +0 -0
  134. {edri-2025.11.1rc3 → edri-2025.12.1}/tests/abstract/manager/__init__.py +0 -0
  135. {edri-2025.11.1rc3 → edri-2025.12.1}/tests/abstract/manager/test_manager_base_priority.py +0 -0
  136. {edri-2025.11.1rc3 → edri-2025.12.1}/tests/abstract/worker/__init__.py +0 -0
  137. {edri-2025.11.1rc3 → edri-2025.12.1}/tests/abstract/worker/test_worker.py +0 -0
  138. {edri-2025.11.1rc3 → edri-2025.12.1}/tests/api/__init__.py +0 -0
  139. {edri-2025.11.1rc3 → edri-2025.12.1}/tests/api/handlers/__init__.py +0 -0
  140. {edri-2025.11.1rc3 → edri-2025.12.1}/tests/api/handlers/test_base_handler.py +0 -0
  141. {edri-2025.11.1rc3 → edri-2025.12.1}/tests/api/handlers/test_html_handler.py +0 -0
  142. {edri-2025.11.1rc3 → edri-2025.12.1}/tests/api/handlers/test_http_handler.py +0 -0
  143. {edri-2025.11.1rc3 → edri-2025.12.1}/tests/api/test_broker.py +0 -0
  144. {edri-2025.11.1rc3 → edri-2025.12.1}/tests/dataclass/__init__.py +0 -0
  145. {edri-2025.11.1rc3 → edri-2025.12.1}/tests/dataclass/event/__init__.py +0 -0
  146. {edri-2025.11.1rc3 → edri-2025.12.1}/tests/dataclass/event/test_event.py +0 -0
  147. {edri-2025.11.1rc3 → edri-2025.12.1}/tests/dataclass/event/test_event_init.py +0 -0
  148. {edri-2025.11.1rc3 → edri-2025.12.1}/tests/dataclass/event/test_response.py +0 -0
  149. {edri-2025.11.1rc3 → edri-2025.12.1}/tests/events/__init__.py +0 -0
  150. {edri-2025.11.1rc3 → edri-2025.12.1}/tests/events/test/__init__.py +0 -0
  151. {edri-2025.11.1rc3 → edri-2025.12.1}/tests/events/test/event_request.py +0 -0
  152. {edri-2025.11.1rc3 → edri-2025.12.1}/tests/events/test/ping.py +0 -0
  153. {edri-2025.11.1rc3 → edri-2025.12.1}/tests/events/test/ping2.py +0 -0
  154. {edri-2025.11.1rc3 → edri-2025.12.1}/tests/router/__init__.py +0 -0
  155. {edri-2025.11.1rc3 → edri-2025.12.1}/tests/router/test_cache.py +0 -0
  156. {edri-2025.11.1rc3 → edri-2025.12.1}/tests/router/test_health_checker.py +0 -0
  157. {edri-2025.11.1rc3 → edri-2025.12.1}/tests/router/test_router.py +0 -0
  158. {edri-2025.11.1rc3 → edri-2025.12.1}/tests/test_edri_init.py +0 -0
  159. {edri-2025.11.1rc3 → edri-2025.12.1}/tests/utility/__init__.py +0 -0
  160. {edri-2025.11.1rc3 → edri-2025.12.1}/tests/utility/manager/__init__.py +0 -0
  161. {edri-2025.11.1rc3 → edri-2025.12.1}/tests/utility/manager/test_scheduler.py +0 -0
  162. {edri-2025.11.1rc3 → edri-2025.12.1}/tests/utility/manager/test_store.py +0 -0
  163. {edri-2025.11.1rc3 → edri-2025.12.1}/tests/utility/test_function.py +0 -0
  164. {edri-2025.11.1rc3 → edri-2025.12.1}/tests/utility/test_json_encoder.py +0 -0
  165. {edri-2025.11.1rc3 → edri-2025.12.1}/tests/utility/test_normalized_default_dict.py +0 -0
  166. {edri-2025.11.1rc3 → edri-2025.12.1}/tests/utility/test_shared_memory_pipe.py +0 -0
  167. {edri-2025.11.1rc3 → edri-2025.12.1}/tests/utility/test_storage.py +0 -0
  168. {edri-2025.11.1rc3 → edri-2025.12.1}/tests/utility/test_transformation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: edri
3
- Version: 2025.11.1rc3
3
+ Version: 2025.12.1
4
4
  Summary: Event Driven Routing Infrastructure
5
5
  Author: Marek Olšan
6
6
  Author-email: marek.olsan@gmail.com
@@ -26,6 +26,7 @@ Requires-Dist: watchdog>=6
26
26
  Requires-Dist: websockets>=14
27
27
  Requires-Dist: posix-ipc>=1.2.0
28
28
  Requires-Dist: markdown>=3.0
29
+ Requires-Dist: pytz>=2024.1
29
30
  Provides-Extra: uvicorn
30
31
  Requires-Dist: uvicorn[standard]>=0.32.0; extra == "uvicorn"
31
32
  Provides-Extra: hypercorn
@@ -2,9 +2,16 @@ from .manager.manager_base import ManagerBase
2
2
  from .manager.manager_priority_base import ManagerPriorityBase
3
3
 
4
4
 
5
- def request(func):
6
- func.__purpose__ = "request"
7
- return func
5
+ def request(func=None, /, *, cache: str = None):
6
+ def wrapper(func):
7
+ func.__purpose__ = "request"
8
+ func.__cache__ = cache
9
+ return func
10
+
11
+ if func is None:
12
+ return wrapper
13
+
14
+ return wrapper(func)
8
15
 
9
16
 
10
17
  def response(func):
@@ -1,7 +1,7 @@
1
1
  from abc import ABC, ABCMeta
2
2
  from copy import deepcopy
3
- from dataclasses import dataclass
4
3
  from datetime import datetime
4
+ from http import HTTPMethod
5
5
  from importlib import invalidate_caches, import_module, reload
6
6
  from inspect import signature, Signature, ismethod, isfunction
7
7
  from logging import getLogger, Logger
@@ -12,16 +12,21 @@ from random import randint
12
12
  from time import sleep
13
13
  from traceback import format_exc
14
14
  from types import UnionType
15
- from typing import Optional, Type, Tuple, Callable, Union, Generic, TypeVar, Never, get_args, Iterable, get_origin
15
+ from typing import Optional, Type, Tuple, Callable, Union, TypeVar, Never, get_args, Iterable, get_origin
16
16
 
17
17
  from edri.abstract.manager.worker import Worker
18
- from edri.abstract.worker import WorkerThread, WorkerProcess
18
+ from edri.abstract.worker import WorkerProcess
19
+ from edri.api.dataclass.api_event import api_events
20
+ from edri.config.setting import API_CACHE_CONTROL, API_CACHE_HEADERS
21
+ from edri.dataclass.directive.http import NotModifiedResponseDirective, HeaderResponseDirective
19
22
  from edri.dataclass.event import Event
20
23
  from edri.dataclass.health_checker import Status
24
+ from edri.dataclass.response import ResponseStatus
21
25
  from edri.events.edri.group import Manager
22
26
  from edri.events.edri.manager import StreamCreate, StreamMessage, StreamClose, WorkerQuit, Restart
23
27
  from edri.events.edri.router import Subscribe, HealthCheck, UnsubscribeAll
24
28
  from edri.events.edri.store import Get
29
+ from edri.utility.cache import Cache
25
30
  from edri.utility.storage import Storage
26
31
 
27
32
  T = TypeVar("T", bound=Event)
@@ -30,11 +35,23 @@ T = TypeVar("T", bound=Event)
30
35
  class ManagerBaseMeta(ABCMeta):
31
36
 
32
37
  def __new__(mcls, name, bases, namespace, /, **kwargs):
38
+ namespace["_cache_keys"] = dict()
39
+ namespace["_cache_methods"] = dict()
33
40
  for attr_name, attr_value in list(namespace.items()):
34
- if callable(attr_value) and (
35
- purpose := getattr(attr_value, "__purpose__", None)): # Check for the decorator's marker
41
+ if not callable(attr_value):
42
+ continue
43
+ if purpose := getattr(attr_value, "__purpose__", None): # Check for the decorator's marker
36
44
  if purpose == "request":
37
45
  new_name = f"solve_req_{attr_name}"
46
+
47
+ if cache := getattr(attr_value, "__cache__", None):
48
+ namespace["_cache_keys"][attr_name] = cache
49
+ event = signature(attr_value).parameters["event"].annotation
50
+ for api_event in api_events:
51
+ if api_event.event == event:
52
+ break
53
+ namespace["_cache_methods"][event] = api_event.method
54
+
38
55
  elif purpose == "response":
39
56
  new_name = f"solve_res_{attr_name}"
40
57
  else:
@@ -109,6 +126,10 @@ class ManagerBase(ABC, Process, metaclass=ManagerBaseMeta):
109
126
  self._from_time = from_time
110
127
  self._store_get: Optional[Callable] = None
111
128
  self._exceptions: list[tuple[str, dict, Exception, str]] = []
129
+ self._cache: Cache
130
+ self._cache_vary = "Accept,Accept-Encoding"
131
+ if API_CACHE_HEADERS:
132
+ self._cache_vary += f",{API_CACHE_HEADERS}"
112
133
 
113
134
  def _subscribe(self) -> None:
114
135
  """
@@ -609,10 +630,25 @@ class ManagerBase(ABC, Process, metaclass=ManagerBaseMeta):
609
630
  self.resolve_unknown(event)
610
631
  else:
611
632
  had_response = event.has_response()
633
+ cache_key = self._cache_keys.get(resolver.__name__, None)
634
+ if cache_key:
635
+ etag = self._cache.tag(cache_key)
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:
638
+ event.response.add_directive(NotModifiedResponseDirective())
639
+ self.router_queue.put(event)
640
+ return
612
641
  resolver(event)
613
642
  if not had_response and event.has_response() and event.response._changed:
614
643
  if event._switch:
615
644
  event._switch.received = False
645
+ if cache_key and etag:
646
+ if event.response.get_status() == ResponseStatus.OK and method in (HTTPMethod.POST, HTTPMethod.PUT, HTTPMethod.PATCH, HTTPMethod.DELETE):
647
+ self._cache.renew(cache_key)
648
+ else:
649
+ event.response.add_directive(HeaderResponseDirective(name="ETag", value=etag))
650
+ event.response.add_directive(HeaderResponseDirective(name="Cache-Control", value=API_CACHE_CONTROL))
651
+ event.response.add_directive(HeaderResponseDirective(name="Vary", value=self._cache_vary))
616
652
  self.router_queue.put(event)
617
653
 
618
654
  def get_pipes(self) -> set[Connection]:
@@ -828,7 +864,7 @@ class ManagerBase(ABC, Process, metaclass=ManagerBaseMeta):
828
864
  """
829
865
  Hook method called after the manager process starts. Can be overridden to perform initialization tasks.
830
866
  """
831
- pass
867
+ self._cache = Cache()
832
868
 
833
869
  def quit(self) -> None:
834
870
  self.router_queue.close()
@@ -21,8 +21,8 @@ from edri.dataclass.response import ResponseStatus, Response
21
21
  from edri.events.api import group, manage, client
22
22
  from edri.events.edri.router import HealthCheck, Subscribe
23
23
  from edri.utility import Storage
24
- from edri.utility.validation import StringValidation, IntegerValidation, FloatValidation, DateValidation, \
25
- TimeValidation, DateTimeValidation
24
+ from edri.utility.validation import StringValidator, IntegerValidator, FloatValidator, DateValidator, \
25
+ TimeValidator, DateTimeValidator
26
26
 
27
27
 
28
28
  class TrieNode:
@@ -391,12 +391,12 @@ class Broker(ManagerBase):
391
391
 
392
392
  def extract_parameters_response(event_cls: Type[Event]) -> tuple[dict[str, Any], dict[str, Any]]:
393
393
  VALIDATION_MAP: dict[type, tuple[str, type]] = {
394
- StringValidation: ("string", StringValidation.__bases__[0]),
395
- IntegerValidation: ("integer", IntegerValidation.__bases__[0]),
396
- FloatValidation: ("float", FloatValidation.__bases__[0]),
397
- DateValidation: ("date", DateValidation.__bases__[0]),
398
- TimeValidation: ("time", TimeValidation.__bases__[0]),
399
- DateTimeValidation: ("datetime", DateTimeValidation.__bases__[0]),
394
+ StringValidator: ("string", StringValidator.__bases__[0]),
395
+ IntegerValidator: ("integer", IntegerValidator.__bases__[0]),
396
+ FloatValidator: ("float", FloatValidator.__bases__[0]),
397
+ DateValidator: ("date", DateValidator.__bases__[0]),
398
+ TimeValidator: ("time", TimeValidator.__bases__[0]),
399
+ DateTimeValidator: ("datetime", DateTimeValidator.__bases__[0]),
400
400
  }
401
401
 
402
402
  def resolve_validation(field_type: Injection) -> tuple[str | None, dict[str, Any]]:
@@ -16,7 +16,7 @@ from edri.config.constant import ApiType
16
16
  from edri.dataclass.event import EventHandlingType, _event, Event
17
17
  from edri.dataclass.injection import Injection
18
18
  from edri.utility.function import camel2snake
19
- from edri.utility.validation import ListValidation
19
+ from edri.utility.validation import ListValidator
20
20
 
21
21
 
22
22
  @dataclass
@@ -113,7 +113,7 @@ def api(cls=None, /, *, init=True, repr=True, eq=True, order=False,
113
113
  raise TypeError(f"{field.type} cannot be used as a type for API event")
114
114
  elif isinstance(field.type, Injection):
115
115
  for validator in field.type.classes:
116
- if validator == ListValidation:
116
+ if validator == ListValidator:
117
117
  raise TypeError(
118
118
  "ListValidation must be used as ListValidation[T], "
119
119
  "e.g. ListValidation[Any] or ListValidation[inject(...)]."
@@ -72,7 +72,6 @@ class BaseHandler[T: ResponseDirective](ABC):
72
72
  def create_event(self, event_constructor: Type[Event]) -> Event:
73
73
  self.insert_default_parameters(event_constructor)
74
74
  self.check_parameters(event_constructor)
75
- # noinspection PyArgumentList
76
75
  event = event_constructor(**self.parameters)
77
76
  event._timing.stamp(self.__class__.__name__, "Created")
78
77
  return event
@@ -29,14 +29,16 @@ from edri.config.setting import MAX_BODY_SIZE, ASSETS_PATH, UPLOAD_FILES_PREFIX,
29
29
  CORS_MAX_AGE, UPLOAD_FILES_PATH
30
30
  from edri.dataclass.directive import HTTPResponseDirective, ResponseDirective
31
31
  from edri.dataclass.directive.base import InternalServerErrorResponseDirective, UnauthorizedResponseDirective
32
- from edri.dataclass.directive.http import CookieResponseDirective, AccessDeniedResponseDirective, NotFoundResponseDirective, \
33
- ConflictResponseDirective, HeaderResponseDirective, UnprocessableContentResponseDirective, BadRequestResponseDirective
32
+ from edri.dataclass.directive.http import CookieResponseDirective, AccessDeniedResponseDirective, \
33
+ NotFoundResponseDirective, \
34
+ ConflictResponseDirective, HeaderResponseDirective, UnprocessableContentResponseDirective, \
35
+ BadRequestResponseDirective, NotModifiedResponseDirective, ServiceUnavailableResponseDirective
34
36
  from edri.dataclass.event import Event
35
37
  from edri.dataclass.injection import Injection
36
38
  from edri.utility import NormalizedDefaultDict
37
39
  from edri.utility.function import camel2snake
38
40
  from edri.utility.shared_memory_pipe import SharedMemoryPipe
39
- from edri.utility.validation import StringValidation
41
+ from edri.utility.validation import StringValidator
40
42
 
41
43
 
42
44
  class EventTypesExtensionsDict(TypedDict):
@@ -194,7 +196,7 @@ class URLNode:
194
196
  raise TypeError("All classes in 'inject' must have the same base class when used as URL parameters.")
195
197
  break # Only check the first valid base class regex
196
198
 
197
- if vot is StringValidation:
199
+ if vot is StringValidator:
198
200
  regex = type_.parameters.get("regex", None)
199
201
  if regex is not None:
200
202
  regex_function = lambda name, _: rf"^(?P<{escape(name)}>{sub(r'^\^(.*)\$$', r'\1', regex.pattern)})$"
@@ -271,6 +273,12 @@ class HTTPHandler[T: HTTPResponseDirective](BaseHandler, ABC):
271
273
  },
272
274
  BadRequestResponseDirective: {
273
275
  "status": HTTPStatus.BAD_REQUEST,
276
+ },
277
+ NotModifiedResponseDirective: {
278
+ "status": HTTPStatus.NOT_MODIFIED,
279
+ },
280
+ ServiceUnavailableResponseDirective: {
281
+ "status": HTTPStatus.SERVICE_UNAVAILABLE,
274
282
  }
275
283
  }
276
284
 
@@ -716,6 +724,23 @@ class HTTPHandler[T: HTTPResponseDirective](BaseHandler, ABC):
716
724
  })
717
725
  except Exception as e:
718
726
  self.logger.error(e, exc_info=e)
727
+
728
+ async def response_headers(self, status: HTTPStatus, *args, **kwargs: Unpack[ResponseKW]):
729
+ headers = kwargs["headers"]
730
+ if headers is None:
731
+ headers = NormalizedDefaultDict(list)
732
+ await self.send({
733
+ 'type': 'http.response.start',
734
+ 'status': status,
735
+ 'headers': self.get_headers_binary(headers),
736
+ })
737
+
738
+ await self.send({
739
+ 'type': 'http.response.body',
740
+ 'body': b"",
741
+ 'more_body': False
742
+ })
743
+
719
744
  def get_event_constructors(self) -> tuple[dict[HTTPMethod, Type[Event]], dict[str, Any]]:
720
745
  return self.url_root.find_methods(self.scope["path"].lower())
721
746
 
@@ -147,7 +147,7 @@ class Listener(Process):
147
147
  if isinstance(handler, HTMLHandler):
148
148
  if await handler.response_assets(scope["path"]):
149
149
  return
150
- self.logger.debug("Unknown url %s", scope["path"])
150
+ self.logger.warning("Unknown url %s", scope["path"])
151
151
  await handler.response_error(HTTPStatus.NOT_FOUND, {
152
152
  "reasons": [{
153
153
  "status_code": HTTPStatus.NOT_FOUND,
@@ -270,6 +270,8 @@ class Listener(Process):
270
270
  await handler.response_file(event_response, headers=headers)
271
271
  else:
272
272
  await handler.response(status, event_response, headers=headers)
273
+ elif status.is_redirection:
274
+ await handler.response_headers(status, headers=headers)
273
275
  else:
274
276
  await handler.response_error(status, event_response, headers=headers)
275
277
  self.unregister(pipe, event._api)
@@ -1,6 +1,8 @@
1
1
  from os import getenv
2
2
  from typing import Literal
3
3
 
4
+ from pytz import timezone
5
+
4
6
 
5
7
  def getenv_bool(name: str, default: bool) -> bool:
6
8
  """Read an environment variable as a boolean, fallback to default if unset."""
@@ -13,9 +15,18 @@ ENVIRONMENT: Literal["development", "production"] = "production" if getenv("ENVI
13
15
 
14
16
  HEALTH_CHECK_TIMEOUT = int(getenv("EDRI_HEALTH_CHECK_TIMEOUT", 10))
15
17
  HEALTH_CHECK_FAILURE_LIMIT = int(getenv("EDRI_HEALTH_CHECK_FAILURE_LIMIT", 3))
18
+
19
+ CORS_ORIGINS = getenv("EDRI_CORS_ORIGINS")
20
+ CORS_HEADERS = getenv("EDRI_CORS_HEADERS")
21
+ CORS_CREDENTIALS = bool(getenv("EDRI_CORS_CREDENTIALS", False))
22
+ CORS_MAX_AGE = getenv("EDRI_CORS_MAX_AGE", None)
23
+
24
+ TIMEZONE = timezone(getenv("EDRI_TIMEZONE", "UTC"))
16
25
  API_RESPONSE_TIMEOUT = int(getenv("EDRI_API_RESPONSE_TIMEOUT", 60))
17
26
  API_RESPONSE_WRAPPED = getenv_bool("EDRI_API_RESPONSE_WRAPPED", True)
18
27
  API_RESPONSE_TIMING = getenv_bool("EDRI_API_RESPONSE_TIMING", ENVIRONMENT == 'development')
28
+ API_CACHE_CONTROL = getenv("EDRI_API_CACHE_CONTROL", "max-age=0, must-revalidate")
29
+ API_CACHE_HEADERS = getenv("EDRI_API_CACHE_HEADERS", CORS_HEADERS)
19
30
 
20
31
  SWITCH_KEY_LENGTH = int(getenv("EDRI_SWITCH_KEY_LENGTH ", 8))
21
32
  SWITCH_HOST = getenv("EDRI_SWITCH_HOST", "localhost")
@@ -37,7 +48,4 @@ ASSETS_PATH = getenv("EDRI_ASSETS_PATH", "assets")
37
48
  MAX_BODY_SIZE = int(getenv("EDRI_MAX_BODY_SIZE", 4096 * 1024))
38
49
  CHUNK_SIZE = int(getenv("EDRI_CHUNK_SIZE", 256 * 1024))
39
50
 
40
- CORS_ORIGINS = getenv("EDRI_CORS_ORIGINS")
41
- CORS_HEADERS = getenv("EDRI_CORS_HEADERS")
42
- CORS_CREDENTIALS = bool(getenv("EDRI_CORS_CREDENTIALS", False))
43
- CORS_MAX_AGE = getenv("EDRI_CORS_MAX_AGE", None)
51
+
@@ -65,3 +65,13 @@ class UnprocessableContentResponseDirective(HTTPResponseDirective):
65
65
  @dataclass
66
66
  class BadRequestResponseDirective(HTTPResponseDirective):
67
67
  message: str | None = None
68
+
69
+
70
+ @dataclass
71
+ class NotModifiedResponseDirective(HTTPResponseDirective):
72
+ pass
73
+
74
+
75
+ @dataclass
76
+ class ServiceUnavailableResponseDirective(HTTPResponseDirective):
77
+ message: str | None = None
@@ -0,0 +1,19 @@
1
+ from collections import defaultdict
2
+ from datetime import datetime
3
+ from pytz import timezone
4
+
5
+
6
+ class Cache:
7
+ timezone = timezone("Europe/Prague")
8
+
9
+ def __init__(self):
10
+ self.last_modified = defaultdict(lambda: datetime.now(tz=self.timezone))
11
+
12
+ def last_change(self, key: str) -> datetime:
13
+ return self.last_modified[key]
14
+
15
+ def tag(self, key: str) -> str:
16
+ return f"{key}-{int(self.last_change(key).timestamp())}"
17
+
18
+ def renew(self, key: str) -> None:
19
+ self.last_modified[key] = datetime.now(tz=self.timezone)
@@ -3,7 +3,7 @@ from re import Pattern
3
3
  from typing import Self, Iterable, Any
4
4
 
5
5
 
6
- class StringValidation(str):
6
+ class StringValidator(str):
7
7
  """
8
8
  A string type that performs validation on initialization.
9
9
 
@@ -21,7 +21,7 @@ class StringValidation(str):
21
21
  or its length is outside the allowed bounds.
22
22
 
23
23
  Example:
24
- >>> StringValidation("hello", minimum_length=3, maximum_length=10)
24
+ >>> StringValidator("hello", minimum_length=3, maximum_length=10)
25
25
  'hello'
26
26
  """
27
27
 
@@ -49,7 +49,7 @@ class StringValidation(str):
49
49
  return (v[:max_len] + "…") if len(v) > max_len else v
50
50
 
51
51
 
52
- class IntegerValidation(int):
52
+ class IntegerValidator(int):
53
53
  """
54
54
  An integer type that performs validation on initialization.
55
55
 
@@ -65,7 +65,7 @@ class IntegerValidation(int):
65
65
  ValueError: If the value is less than `minimum` or greater than `maximum`.
66
66
 
67
67
  Example:
68
- >>> IntegerValidation(5, minimum=1, maximum=10)
68
+ >>> IntegerValidator(5, minimum=1, maximum=10)
69
69
  5
70
70
  """
71
71
 
@@ -78,7 +78,7 @@ class IntegerValidation(int):
78
78
  return value
79
79
 
80
80
 
81
- class FloatValidation(float):
81
+ class FloatValidator(float):
82
82
  """
83
83
  A float type that performs validation on initialization.
84
84
 
@@ -94,7 +94,7 @@ class FloatValidation(float):
94
94
  ValueError: If the value is less than `minimum` or greater than `maximum`.
95
95
 
96
96
  Example:
97
- >>> FloatValidation(3.14, minimum=1.0, maximum=5.0)
97
+ >>> FloatValidator(3.14, minimum=1.0, maximum=5.0)
98
98
  3.14
99
99
  """
100
100
 
@@ -107,7 +107,7 @@ class FloatValidation(float):
107
107
  return value
108
108
 
109
109
 
110
- class DateValidation(date):
110
+ class DateValidator(date):
111
111
  """
112
112
  A date type that performs validation on initialization.
113
113
 
@@ -126,7 +126,7 @@ class DateValidation(date):
126
126
  ValueError: If the date is outside the allowed bounds.
127
127
 
128
128
  Example:
129
- >>> DateValidation(2024, 3, 28, minimum_date=date(2024, 1, 1))
129
+ >>> DateValidator(2024, 3, 28, minimum_date=date(2024, 1, 1))
130
130
  datetime.date(2024, 3, 28)
131
131
  """
132
132
 
@@ -144,7 +144,7 @@ class DateValidation(date):
144
144
  return value
145
145
 
146
146
 
147
- class TimeValidation(time):
147
+ class TimeValidator(time):
148
148
  """
149
149
  A time type that performs validation on initialization.
150
150
 
@@ -164,7 +164,7 @@ class TimeValidation(time):
164
164
  ValueError: If the time is outside the allowed bounds.
165
165
 
166
166
  Example:
167
- >>> TimeValidation(12, 30, maximum_time=time(20, 0))
167
+ >>> TimeValidator(12, 30, maximum_time=time(20, 0))
168
168
  datetime.time(12, 30)
169
169
  """
170
170
 
@@ -182,7 +182,7 @@ class TimeValidation(time):
182
182
  return value
183
183
 
184
184
 
185
- class DateTimeValidation(datetime):
185
+ class DateTimeValidator(datetime):
186
186
  """
187
187
  A datetime type that performs validation on initialization.
188
188
 
@@ -205,7 +205,7 @@ class DateTimeValidation(datetime):
205
205
  ValueError: If the datetime is outside the allowed bounds.
206
206
 
207
207
  Example:
208
- >>> DateTimeValidation(2024, 3, 28, 15, 45,
208
+ >>> DateTimeValidator(2024, 3, 28, 15, 45,
209
209
  ... minimum_datetime=datetime(2024, 3, 1))
210
210
  datetime.datetime(2024, 3, 28, 15, 45)
211
211
  """
@@ -224,7 +224,7 @@ class DateTimeValidation(datetime):
224
224
  return instance
225
225
 
226
226
 
227
- class ListValidation(list):
227
+ class ListValidator(list):
228
228
  """
229
229
  A list type that performs validation on initialization.
230
230
 
@@ -241,9 +241,9 @@ class ListValidation(list):
241
241
  ValueError: If the list length is outside the allowed bounds.
242
242
 
243
243
  Example:
244
- >>> ListValidation([1, 2, 3], minimum_length=2)
244
+ >>> ListValidator([1, 2, 3], minimum_length=2)
245
245
  [1, 2, 3]
246
- >>> ListValidation([1, 2, 3], maximum_length=2)
246
+ >>> ListValidator([1, 2, 3], maximum_length=2)
247
247
  ValueError: List length '3' is greater than maximum allowed '2'
248
248
  """
249
249
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: edri
3
- Version: 2025.11.1rc3
3
+ Version: 2025.12.1
4
4
  Summary: Event Driven Routing Infrastructure
5
5
  Author: Marek Olšan
6
6
  Author-email: marek.olsan@gmail.com
@@ -26,6 +26,7 @@ Requires-Dist: watchdog>=6
26
26
  Requires-Dist: websockets>=14
27
27
  Requires-Dist: posix-ipc>=1.2.0
28
28
  Requires-Dist: markdown>=3.0
29
+ Requires-Dist: pytz>=2024.1
29
30
  Provides-Extra: uvicorn
30
31
  Requires-Dist: uvicorn[standard]>=0.32.0; extra == "uvicorn"
31
32
  Provides-Extra: hypercorn
@@ -112,6 +112,7 @@ edri/switch/receiver.py
112
112
  edri/switch/sender.py
113
113
  edri/switch/switch.py
114
114
  edri/utility/__init__.py
115
+ edri/utility/cache.py
115
116
  edri/utility/function.py
116
117
  edri/utility/json_encoder.py
117
118
  edri/utility/normalized_default_dict.py
@@ -8,6 +8,7 @@ watchdog>=6
8
8
  websockets>=14
9
9
  posix-ipc>=1.2.0
10
10
  markdown>=3.0
11
+ pytz>=2024.1
11
12
 
12
13
  [dev]
13
14
  uvicorn>=0.32.0
@@ -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.11.01rc3',
125
+ version='2025.12.01',
126
126
  packages=find_packages(),
127
127
  description='Event Driven Routing Infrastructure',
128
128
  long_description=long_description,
@@ -140,7 +140,8 @@ setup(
140
140
  "watchdog>=6",
141
141
  "websockets>=14",
142
142
  "posix-ipc>=1.2.0",
143
- "markdown>=3.0"
143
+ "markdown>=3.0",
144
+ "pytz>=2024.1",
144
145
  ],
145
146
  extras_require={
146
147
  "uvicorn": ["uvicorn[standard]>=0.32.0"],
@@ -229,6 +229,7 @@ class TestManagerBase(unittest.TestCase):
229
229
  event_response = MagicMock(spec=Event)
230
230
  event_response._stream = None
231
231
  resolver = MagicMock(side_effect=add_response)
232
+ resolver.__name__ = "resolver"
232
233
 
233
234
  self.manager._requests[event.__class__] = resolver
234
235
  event.has_response.return_value = False