edri 2025.11.1rc2__tar.gz → 2025.11.1rc4__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.1rc2 → edri-2025.11.1rc4}/PKG-INFO +1 -1
  2. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/api/broker.py +8 -8
  3. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/api/dataclass/api_event.py +10 -1
  4. edri-2025.11.1rc4/edri/api/handlers/base_handler.py +327 -0
  5. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/api/handlers/http_handler.py +26 -4
  6. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/api/listener.py +2 -0
  7. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/dataclass/directive/http.py +4 -0
  8. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/dataclass/injection.py +6 -2
  9. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/utility/validation.py +54 -13
  10. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri.egg-info/PKG-INFO +1 -1
  11. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/setup.py +1 -1
  12. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/tests/utility/test_validation.py +46 -46
  13. edri-2025.11.1rc2/edri/api/handlers/base_handler.py +0 -172
  14. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/README.md +0 -0
  15. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/__init__.py +0 -0
  16. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/abstract/__init__.py +0 -0
  17. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/abstract/manager/__init__.py +0 -0
  18. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/abstract/manager/manager_base.py +0 -0
  19. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/abstract/manager/manager_priority_base.py +0 -0
  20. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/abstract/manager/worker.py +0 -0
  21. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/abstract/worker/__init__.py +0 -0
  22. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/abstract/worker/worker.py +0 -0
  23. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/abstract/worker/worker_process.py +0 -0
  24. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/abstract/worker/worker_thread.py +0 -0
  25. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/api/__init__.py +0 -0
  26. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/api/dataclass/__init__.py +0 -0
  27. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/api/dataclass/client.py +0 -0
  28. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/api/dataclass/file.py +0 -0
  29. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/api/extensions/__init__.py +0 -0
  30. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/api/extensions/url_extension.py +0 -0
  31. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/api/extensions/url_prefix.py +0 -0
  32. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/api/handlers/__init__.py +0 -0
  33. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/api/handlers/html_handler.py +0 -0
  34. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/api/handlers/rest_handler.py +0 -0
  35. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/api/handlers/websocket_handler.py +0 -0
  36. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/api/middleware.py +0 -0
  37. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/api/static_pages/documentation.j2 +0 -0
  38. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/api/static_pages/health_check_status.j2 +0 -0
  39. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/api/static_pages/status_300.j2 +0 -0
  40. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/api/static_pages/status_400.j2 +0 -0
  41. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/api/static_pages/status_500.j2 +0 -0
  42. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/config/__init__.py +0 -0
  43. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/config/constant.py +0 -0
  44. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/config/setting.py +0 -0
  45. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/dataclass/__init__.py +0 -0
  46. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/dataclass/directive/__init__.py +0 -0
  47. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/dataclass/directive/base.py +0 -0
  48. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/dataclass/directive/html.py +0 -0
  49. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/dataclass/event.py +0 -0
  50. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/dataclass/health_checker.py +0 -0
  51. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/dataclass/response.py +0 -0
  52. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/events/__init__.py +0 -0
  53. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/events/api/__init__.py +0 -0
  54. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/events/api/client/__init__.py +0 -0
  55. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/events/api/client/documentation.py +0 -0
  56. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/events/api/client/register.py +0 -0
  57. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/events/api/client/unregister.py +0 -0
  58. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/events/api/group/__init__.py +0 -0
  59. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/events/api/group/client.py +0 -0
  60. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/events/api/group/manage.py +0 -0
  61. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/events/api/manage/__init__.py +0 -0
  62. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/events/api/manage/list_registered.py +0 -0
  63. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/events/api/manage/register.py +0 -0
  64. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/events/api/manage/unregister.py +0 -0
  65. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/events/api/manage/unregister_all.py +0 -0
  66. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/events/edri/__init__.py +0 -0
  67. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/events/edri/group/__init__.py +0 -0
  68. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/events/edri/group/manager.py +0 -0
  69. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/events/edri/group/router.py +0 -0
  70. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/events/edri/group/scheduler.py +0 -0
  71. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/events/edri/group/store.py +0 -0
  72. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/events/edri/group/switch.py +0 -0
  73. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/events/edri/group/test.py +0 -0
  74. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/events/edri/manager/__init__.py +0 -0
  75. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/events/edri/manager/restart.py +0 -0
  76. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/events/edri/manager/stream_close.py +0 -0
  77. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/events/edri/manager/stream_create.py +0 -0
  78. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/events/edri/manager/stream_message.py +0 -0
  79. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/events/edri/manager/worker_quit.py +0 -0
  80. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/events/edri/router/__init__.py +0 -0
  81. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/events/edri/router/demands.py +0 -0
  82. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/events/edri/router/health_check.py +0 -0
  83. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/events/edri/router/last_events.py +0 -0
  84. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/events/edri/router/send_from.py +0 -0
  85. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/events/edri/router/subscribe.py +0 -0
  86. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/events/edri/router/subscribe_connector.py +0 -0
  87. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/events/edri/router/subscribed_external.py +0 -0
  88. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/events/edri/router/subscribed_new.py +0 -0
  89. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/events/edri/router/unsubscribe.py +0 -0
  90. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/events/edri/router/unsubscribe_all.py +0 -0
  91. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/events/edri/scheduler/__init__.py +0 -0
  92. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/events/edri/scheduler/cancel.py +0 -0
  93. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/events/edri/scheduler/set.py +0 -0
  94. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/events/edri/scheduler/set_or_update.py +0 -0
  95. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/events/edri/scheduler/update.py +0 -0
  96. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/events/edri/store/__init__.py +0 -0
  97. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/events/edri/store/delete.py +0 -0
  98. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/events/edri/store/get.py +0 -0
  99. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/events/edri/store/get_callback.py +0 -0
  100. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/events/edri/store/set.py +0 -0
  101. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/router/__init__.py +0 -0
  102. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/router/cache.py +0 -0
  103. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/router/connector/__init__.py +0 -0
  104. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/router/connector/connector.py +0 -0
  105. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/router/connector/socket.py +0 -0
  106. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/router/health_checker.py +0 -0
  107. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/router/router.py +0 -0
  108. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/switch/__init__.py +0 -0
  109. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/switch/connection.py +0 -0
  110. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/switch/forwarder.py +0 -0
  111. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/switch/receiver.py +0 -0
  112. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/switch/sender.py +0 -0
  113. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/switch/switch.py +0 -0
  114. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/utility/__init__.py +0 -0
  115. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/utility/function.py +0 -0
  116. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/utility/json_encoder.py +0 -0
  117. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/utility/manager/__init__.py +0 -0
  118. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/utility/manager/scheduler.py +0 -0
  119. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/utility/manager/store.py +0 -0
  120. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/utility/normalized_default_dict.py +0 -0
  121. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/utility/queue.py +0 -0
  122. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/utility/shared_memory_pipe.py +0 -0
  123. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/utility/storage.py +0 -0
  124. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/utility/transformation.py +0 -0
  125. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri/utility/watcher.py +0 -0
  126. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri.egg-info/SOURCES.txt +0 -0
  127. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri.egg-info/dependency_links.txt +0 -0
  128. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri.egg-info/requires.txt +0 -0
  129. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/edri.egg-info/top_level.txt +0 -0
  130. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/setup.cfg +0 -0
  131. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/tests/__init__.py +0 -0
  132. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/tests/abstract/__init__.py +0 -0
  133. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/tests/abstract/manager/__init__.py +0 -0
  134. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/tests/abstract/manager/test_manager_base.py +0 -0
  135. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/tests/abstract/manager/test_manager_base_priority.py +0 -0
  136. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/tests/abstract/worker/__init__.py +0 -0
  137. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/tests/abstract/worker/test_worker.py +0 -0
  138. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/tests/api/__init__.py +0 -0
  139. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/tests/api/handlers/__init__.py +0 -0
  140. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/tests/api/handlers/test_base_handler.py +0 -0
  141. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/tests/api/handlers/test_html_handler.py +0 -0
  142. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/tests/api/handlers/test_http_handler.py +0 -0
  143. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/tests/api/test_broker.py +0 -0
  144. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/tests/dataclass/__init__.py +0 -0
  145. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/tests/dataclass/event/__init__.py +0 -0
  146. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/tests/dataclass/event/test_event.py +0 -0
  147. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/tests/dataclass/event/test_event_init.py +0 -0
  148. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/tests/dataclass/event/test_response.py +0 -0
  149. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/tests/events/__init__.py +0 -0
  150. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/tests/events/test/__init__.py +0 -0
  151. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/tests/events/test/event_request.py +0 -0
  152. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/tests/events/test/ping.py +0 -0
  153. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/tests/events/test/ping2.py +0 -0
  154. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/tests/router/__init__.py +0 -0
  155. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/tests/router/test_cache.py +0 -0
  156. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/tests/router/test_health_checker.py +0 -0
  157. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/tests/router/test_router.py +0 -0
  158. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/tests/test_edri_init.py +0 -0
  159. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/tests/utility/__init__.py +0 -0
  160. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/tests/utility/manager/__init__.py +0 -0
  161. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/tests/utility/manager/test_scheduler.py +0 -0
  162. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/tests/utility/manager/test_store.py +0 -0
  163. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/tests/utility/test_function.py +0 -0
  164. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/tests/utility/test_json_encoder.py +0 -0
  165. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/tests/utility/test_normalized_default_dict.py +0 -0
  166. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/tests/utility/test_shared_memory_pipe.py +0 -0
  167. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/tests/utility/test_storage.py +0 -0
  168. {edri-2025.11.1rc2 → edri-2025.11.1rc4}/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.1rc2
3
+ Version: 2025.11.1rc4
4
4
  Summary: Event Driven Routing Infrastructure
5
5
  Author: Marek Olšan
6
6
  Author-email: marek.olsan@gmail.com
@@ -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]]:
@@ -3,7 +3,7 @@ from enum import Enum
3
3
  from http import HTTPMethod
4
4
  from inspect import isclass
5
5
  from logging import getLogger
6
- from types import NoneType, UnionType
6
+ from types import NoneType, UnionType, GenericAlias
7
7
  from typing import Type, get_origin, get_args
8
8
  from uuid import UUID
9
9
 
@@ -14,7 +14,9 @@ from edri.api.dataclass.file import File
14
14
  from edri.api.extensions.url_prefix import PrefixBase
15
15
  from edri.config.constant import ApiType
16
16
  from edri.dataclass.event import EventHandlingType, _event, Event
17
+ from edri.dataclass.injection import Injection
17
18
  from edri.utility.function import camel2snake
19
+ from edri.utility.validation import ListValidator
18
20
 
19
21
 
20
22
  @dataclass
@@ -109,6 +111,13 @@ def api(cls=None, /, *, init=True, repr=True, eq=True, order=False,
109
111
  raise TypeError(f"{item_args[0]} cannot be used as a type for API event")
110
112
  elif item_type not in allowed_types and not hasattr(field.type, "fromisoformat"):
111
113
  raise TypeError(f"{field.type} cannot be used as a type for API event")
114
+ elif isinstance(field.type, Injection):
115
+ for validator in field.type.classes:
116
+ if validator == ListValidator:
117
+ raise TypeError(
118
+ "ListValidation must be used as ListValidation[T], "
119
+ "e.g. ListValidation[Any] or ListValidation[inject(...)]."
120
+ )
112
121
 
113
122
  http_method = method or dataclass.method
114
123
  if http_method is None:
@@ -0,0 +1,327 @@
1
+ from abc import ABC, abstractmethod
2
+ from dataclasses import fields, MISSING
3
+ from inspect import signature
4
+ from logging import getLogger
5
+ from types import UnionType, NoneType, GenericAlias
6
+ from typing import Callable, Type, get_origin, Union, get_args, Any, TypedDict, Literal, TypeAliasType, Iterable
7
+ from urllib.parse import parse_qs, unquote
8
+
9
+ from edri.dataclass.directive import ResponseDirective
10
+ from edri.dataclass.event import Event
11
+ from edri.dataclass.injection import Injection
12
+ from edri.utility.function import camel2snake
13
+
14
+
15
+ class BaseDirectiveHandlerDict[T](TypedDict):
16
+ pass
17
+
18
+
19
+ class BaseHandler[T: ResponseDirective](ABC):
20
+ _directive_handlers: dict[Type[T], BaseDirectiveHandlerDict[T]] = {}
21
+
22
+ def __init__(self,
23
+ scope: dict,
24
+ receive: Callable,
25
+ send: Callable):
26
+ super().__init__()
27
+ self.send = send
28
+ self.scope = scope
29
+ self.receive = receive
30
+ self.scope = scope
31
+ self.logger = getLogger(__name__)
32
+
33
+ self.parameters: dict[str, Any] = self.parse_url_parameters()
34
+
35
+ @classmethod
36
+ def directive_handlers(cls) -> dict[Type[ResponseDirective], BaseDirectiveHandlerDict]:
37
+ handlers = {}
38
+ for class_obj in reversed(cls.mro()):
39
+ if hasattr(class_obj, "_directive_handlers"):
40
+ # noinspection PyProtectedMember
41
+ handlers.update(class_obj._directive_handlers)
42
+ return handlers
43
+
44
+ @abstractmethod
45
+ async def response(self, status: Any, data: Any, *args, **kwargs) -> None:
46
+ pass
47
+
48
+ @abstractmethod
49
+ async def response_error(self, status: Any, response: Any, *args, **kwargs) -> None:
50
+ pass
51
+
52
+ def check_parameters(self, event_constructor: Type[Event]) -> None:
53
+ check_parameters = {}
54
+ for name, annotation in ((f.name, f.type) for f in fields(event_constructor)):
55
+ if name.startswith("_") or name == "method" or name == "response":
56
+ continue
57
+ try:
58
+ value = self.parameters.pop(name)
59
+ except KeyError:
60
+ raise ValueError(f"Missing value for parameter {name}")
61
+ try:
62
+ value = self.convert_type(value, annotation)
63
+ except TypeError:
64
+ raise ValueError(f"Wrong type {type(value)} for {name}:{annotation}")
65
+ except Exception:
66
+ raise ValueError("Unknown error during type checking")
67
+ check_parameters[name] = value
68
+ if self.parameters:
69
+ raise ValueError(f"Unknown parameters: {self.parameters}")
70
+ self.parameters = check_parameters
71
+
72
+ def create_event(self, event_constructor: Type[Event]) -> Event:
73
+ self.insert_default_parameters(event_constructor)
74
+ self.check_parameters(event_constructor)
75
+ event = event_constructor(**self.parameters)
76
+ event._timing.stamp(self.__class__.__name__, "Created")
77
+ return event
78
+
79
+ def convert_type(self, value: Any, annotation: type) -> Any:
80
+ """
81
+ Validates and converts input values to the specified annotation type,
82
+ supporting:
83
+ - basic types
84
+ - Optional / Union / |
85
+ - list / tuple / dict with type args
86
+ - Literal
87
+ - list-like subclasses (e.g. ListValidation[int])
88
+ - Injection of validation classes (e.g. Injection((ListValidation[int],), {...}))
89
+ """
90
+ annotation = self._normalize_annotation(annotation)
91
+
92
+ # Any
93
+ if annotation is Any:
94
+ return value
95
+
96
+ # Injection (chain of validation classes)
97
+ if isinstance(annotation, Injection):
98
+ return self._convert_injection(value, annotation)
99
+
100
+ # Unions / Optional
101
+ if self._is_union(annotation):
102
+ return self._convert_union(value, annotation)
103
+
104
+ origin = get_origin(annotation)
105
+
106
+ # Generics: list, tuple, dict, Literal, ListValidation[int], ...
107
+ if origin is not None:
108
+ return self._convert_generic(value, annotation, origin)
109
+
110
+ # Non-generic simple types (including bare ListValidation, bool, etc.)
111
+ return self._convert_simple(value, annotation)
112
+
113
+ def _normalize_annotation(self, annotation: type) -> type:
114
+ """Unwrap TypeAliasType and other simple normalizations."""
115
+ if isinstance(annotation, TypeAliasType):
116
+ return annotation.__value__
117
+ return annotation
118
+
119
+ def _is_union(self, annotation: type) -> bool:
120
+ """Check if annotation is a Union / Optional / | type."""
121
+ return isinstance(annotation, UnionType) or get_origin(annotation) is Union
122
+
123
+ def _convert_injection(self, value: Any, injection: Injection) -> Any:
124
+ """
125
+ Run all validation classes in an Injection.
126
+
127
+ Each `cls` in `injection.classes` is:
128
+ - either a plain validation class (e.g. ListValidation),
129
+ - or a GenericAlias like ListValidation[int].
130
+
131
+ For list-like generics (e.g. ListValidation[int]) we:
132
+ - convert each element of `value` using the inner type (int here),
133
+ - then instantiate the validation class with filtered params.
134
+ """
135
+ try:
136
+ for cls in injection.classes:
137
+ # Determine underlying class and optional inner type
138
+ item_type = None
139
+
140
+ if isinstance(cls, GenericAlias):
141
+ origin = get_origin(cls) # e.g. ListValidation
142
+ args = get_args(cls) # e.g. (int,)
143
+ target_cls = origin
144
+
145
+ # list-like validation class: ListValidation[int], MyListValidator[str], ...
146
+ if issubclass(origin, list) and args:
147
+ item_type = args[0]
148
+ else:
149
+ target_cls = cls
150
+
151
+ # Filter parameters to those that target_cls.__init__ actually accepts
152
+ sig = signature(target_cls)
153
+ param_names = [
154
+ p.name for p in sig.parameters.values()
155
+ if p.name != "self"
156
+ ]
157
+ filtered_params = {
158
+ k: v for k, v in injection.parameters.items()
159
+ if k in param_names
160
+ }
161
+
162
+ # If this is a list-like validator with an inner type -> convert elements first
163
+ if item_type is not None:
164
+ if (not isinstance(value, Iterable)) or isinstance(value, (str, bytes)):
165
+ raise TypeError(
166
+ f"Value '{value}' is not a valid iterable for validator {cls}"
167
+ )
168
+
169
+ converted_items = [
170
+ self.convert_type(item, item_type) for item in value
171
+ ]
172
+ value = target_cls(converted_items, **filtered_params)
173
+ else:
174
+ # Any other validation class: just feed the (possibly already converted) value
175
+ value = target_cls(value, **filtered_params)
176
+
177
+ return value
178
+
179
+ except ValueError:
180
+ # Your original contract: map validator ValueError -> TypeError
181
+ raise TypeError(f"Value '{value}' cannot be converted to type {injection}")
182
+
183
+ def _convert_union(self, value: Any, annotation: type) -> Any:
184
+ """Handle Union/Optional annotations."""
185
+ annotations = get_args(annotation)
186
+
187
+ # Handle Optional[...] where None is allowed
188
+ if value is None:
189
+ if NoneType in annotations:
190
+ return None
191
+ raise TypeError(f"Value '{value}' cannot be converted to type {annotation}")
192
+
193
+ # Try each type in the Union
194
+ last_error: Exception | None = None
195
+ for ann in annotations:
196
+ try:
197
+ return self.convert_type(value, ann)
198
+ except TypeError as e:
199
+ last_error = e
200
+ continue
201
+
202
+ raise TypeError(f"Value '{value}' cannot be converted to type {annotation}") from last_error
203
+
204
+ def _convert_generic(self, value: Any, annotation: type, origin: type) -> Any:
205
+ """
206
+ Handle generics like:
207
+ - list[T] and list-like subclasses (ListValidation[T])
208
+ - tuple[X, Y, ...]
209
+ - dict[K, V]
210
+ - Literal[...]
211
+ """
212
+ args = get_args(annotation)
213
+
214
+ # list[T] and list-like subclasses (e.g. ListValidation[int])
215
+ if isinstance(origin, type) and issubclass(origin, list):
216
+ if not isinstance(value, Iterable) or isinstance(value, (str, bytes)):
217
+ raise TypeError(f"Value '{value}' is not a valid iterable for type {annotation}")
218
+
219
+ item_type = args[0] if args else Any
220
+ converted_items = [self.convert_type(item, item_type) for item in value]
221
+
222
+ if origin is list:
223
+ return converted_items
224
+
225
+ # Subclass of list, e.g. ListValidation[int]
226
+ try:
227
+ return origin(converted_items)
228
+ except Exception as e:
229
+ raise TypeError(
230
+ f"Value '{value}' cannot be converted to list-like type {annotation}"
231
+ ) from e
232
+
233
+ # ---- tuple[X, Y, ...] ----
234
+ if origin is tuple:
235
+ if not isinstance(value, tuple):
236
+ raise TypeError(f"Value '{value}' is not a tuple for type {annotation}")
237
+ return tuple(self.convert_type(v, a) for v, a in zip(value, args))
238
+
239
+ # ---- dict[K, V] ----
240
+ if origin is dict:
241
+ if not isinstance(value, dict):
242
+ raise TypeError(f"Value '{value}' is not a dict for type {annotation}")
243
+ if len(args) != 2:
244
+ raise TypeError("Key and value types for dict must be specified")
245
+ key_type, value_type = args
246
+ return {
247
+ self.convert_type(k, key_type): self.convert_type(v, value_type)
248
+ for k, v in value.items()
249
+ }
250
+
251
+ # ---- Literal["a", "b", ...] ----
252
+ if origin is Literal:
253
+ literal_values = args
254
+ if value in literal_values:
255
+ return value
256
+ raise TypeError(
257
+ f"Value '{value}' is not one of the allowed Literal values {literal_values}"
258
+ )
259
+
260
+ # Unknown generic
261
+ raise TypeError(f"Value '{value}' cannot be converted to type {annotation}")
262
+
263
+ def _convert_simple(self, value: Any, annotation: type) -> Any:
264
+ """
265
+ Handle non-generic types: bool, dataclasses, custom classes,
266
+ and bare list subclasses like ListValidation.
267
+ """
268
+ # Already correct type
269
+ if isinstance(value, annotation):
270
+ return value
271
+
272
+ # Support bare list-like subclasses (e.g. annotation is ListValidation without [T])
273
+ try:
274
+ is_list_subclass = isinstance(annotation, type) and issubclass(annotation, list)
275
+ except TypeError:
276
+ is_list_subclass = False
277
+
278
+ if is_list_subclass and not isinstance(value, (str, bytes)) and isinstance(value, Iterable):
279
+ try:
280
+ return annotation(value)
281
+ except Exception as e:
282
+ raise TypeError(
283
+ f"Value '{value}' cannot be converted to list-like type {annotation}"
284
+ ) from e
285
+
286
+ # Special case: string "false" -> False for bool
287
+ if isinstance(value, str) and value.lower() == "false" and annotation is bool:
288
+ return False
289
+
290
+ # Normal constructor-based conversion
291
+ try:
292
+ return annotation(value)
293
+ except Exception:
294
+ # Try fromisoformat if available (e.g., datetime, date)
295
+ if hasattr(annotation, "fromisoformat"):
296
+ try:
297
+ return annotation.fromisoformat(value)
298
+ except Exception:
299
+ raise TypeError(
300
+ "Value '%s' cannot be converted from isoformat to type %s"
301
+ % (value, annotation)
302
+ )
303
+ raise TypeError(f"Value '{value}' cannot be converted to type {annotation}")
304
+
305
+ @abstractmethod
306
+ def handle_directives(self, directives: list[ResponseDirective]) -> ...:
307
+ pass
308
+
309
+ def insert_default_parameters(self, event_constructor: Type[Event]) -> None:
310
+ for field in fields(event_constructor):
311
+ if field.name.startswith("_") or field.name in ("response", "method"):
312
+ continue
313
+ if field.name in self.parameters:
314
+ continue
315
+ if field.default is not MISSING:
316
+ self.parameters[field.name] = field.default
317
+ continue
318
+ if field.default_factory is not MISSING:
319
+ self.parameters[field.name] = field.default_factory()
320
+ continue
321
+
322
+ def parse_url_parameters(self) -> dict[str, Any]:
323
+ url_parameters = parse_qs(unquote(self.scope["query_string"].decode()), keep_blank_values=True)
324
+ return {
325
+ camel2snake(key.strip("[]")): value if key.endswith("[]") else value[-1] for key, value in
326
+ url_parameters.items()
327
+ }
@@ -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
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,9 @@ 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,
274
279
  }
275
280
  }
276
281
 
@@ -716,6 +721,23 @@ class HTTPHandler[T: HTTPResponseDirective](BaseHandler, ABC):
716
721
  })
717
722
  except Exception as e:
718
723
  self.logger.error(e, exc_info=e)
724
+
725
+ async def response_headers(self, status: HTTPStatus, *args, **kwargs: Unpack[ResponseKW]):
726
+ headers = kwargs["headers"]
727
+ if headers is None:
728
+ headers = NormalizedDefaultDict(list)
729
+ await self.send({
730
+ 'type': 'http.response.start',
731
+ 'status': status,
732
+ 'headers': self.get_headers_binary(headers),
733
+ })
734
+
735
+ await self.send({
736
+ 'type': 'http.response.body',
737
+ 'body': b"",
738
+ 'more_body': False
739
+ })
740
+
719
741
  def get_event_constructors(self) -> tuple[dict[HTTPMethod, Type[Event]], dict[str, Any]]:
720
742
  return self.url_root.find_methods(self.scope["path"].lower())
721
743
 
@@ -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)
@@ -65,3 +65,7 @@ class UnprocessableContentResponseDirective(HTTPResponseDirective):
65
65
  @dataclass
66
66
  class BadRequestResponseDirective(HTTPResponseDirective):
67
67
  message: str | None = None
68
+
69
+ @dataclass
70
+ class NotModifiedResponseDirective(HTTPResponseDirective):
71
+ pass
@@ -1,5 +1,6 @@
1
1
  from inspect import signature
2
- from typing import Any, Type
2
+ from types import GenericAlias
3
+ from typing import Any, Type, get_origin
3
4
 
4
5
 
5
6
  class Injection:
@@ -32,7 +33,10 @@ class Injection:
32
33
  # Create the callable on demand when iterating
33
34
  for cls in self.classes:
34
35
  # Get the signature of the __init__ method of the class
35
- sig = signature(cls)
36
+ if isinstance(cls, GenericAlias):
37
+ sig = signature(get_origin(cls))
38
+ else:
39
+ sig = signature(cls)
36
40
  # Extract parameter names from the signature
37
41
  param_names: list[str] = [param.name for param in sig.parameters.values() if param.name != 'self']
38
42
 
@@ -1,9 +1,9 @@
1
1
  from datetime import date, datetime, time
2
2
  from re import Pattern
3
- from typing import Self
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
  """
@@ -222,3 +222,44 @@ class DateTimeValidation(datetime):
222
222
  raise ValueError(f"Datetime '{instance}' is later than maximum allowed '{maximum_datetime}'")
223
223
 
224
224
  return instance
225
+
226
+
227
+ class ListValidator(list):
228
+ """
229
+ A list type that performs validation on initialization.
230
+
231
+ This class validates the list against optional constraints:
232
+ - Minimum allowed length.
233
+ - Maximum allowed length.
234
+
235
+ Args:
236
+ iterable (Iterable, optional): Values to initialize the list with.
237
+ minimum_length (int, optional): The smallest allowed list length.
238
+ maximum_length (int, optional): The largest allowed list length.
239
+
240
+ Raises:
241
+ ValueError: If the list length is outside the allowed bounds.
242
+
243
+ Example:
244
+ >>> ListValidator([1, 2, 3], minimum_length=2)
245
+ [1, 2, 3]
246
+ >>> ListValidator([1, 2, 3], maximum_length=2)
247
+ ValueError: List length '3' is greater than maximum allowed '2'
248
+ """
249
+
250
+ def __init__(self, iterable: Iterable[Any] = (), /, *, minimum_length: int | None = None,
251
+ maximum_length: int | None = None):
252
+
253
+ super().__init__(iterable)
254
+
255
+ length = len(self)
256
+
257
+ if minimum_length is not None and length < minimum_length:
258
+ raise ValueError(
259
+ f"List length '{length}' is smaller than minimum allowed '{minimum_length}'"
260
+ )
261
+
262
+ if maximum_length is not None and length > maximum_length:
263
+ raise ValueError(
264
+ f"List length '{length}' is greater than maximum allowed '{maximum_length}'"
265
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: edri
3
- Version: 2025.11.1rc2
3
+ Version: 2025.11.1rc4
4
4
  Summary: Event Driven Routing Infrastructure
5
5
  Author: Marek Olšan
6
6
  Author-email: marek.olsan@gmail.com
@@ -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.01rc2',
125
+ version='2025.11.01rc4',
126
126
  packages=find_packages(),
127
127
  description='Event Driven Routing Infrastructure',
128
128
  long_description=long_description,