edri 2025.11.1rc3__py3-none-any.whl → 2025.12.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- edri/abstract/__init__.py +10 -3
- edri/abstract/manager/manager_base.py +42 -6
- edri/api/broker.py +8 -8
- edri/api/dataclass/api_event.py +2 -2
- edri/api/handlers/base_handler.py +0 -1
- edri/api/handlers/http_handler.py +29 -4
- edri/api/listener.py +3 -1
- edri/config/setting.py +12 -4
- edri/dataclass/directive/http.py +10 -0
- edri/utility/cache.py +19 -0
- edri/utility/validation.py +15 -15
- {edri-2025.11.1rc3.dist-info → edri-2025.12.1.dist-info}/METADATA +2 -1
- {edri-2025.11.1rc3.dist-info → edri-2025.12.1.dist-info}/RECORD +17 -16
- tests/abstract/manager/test_manager_base.py +1 -0
- tests/utility/test_validation.py +46 -46
- {edri-2025.11.1rc3.dist-info → edri-2025.12.1.dist-info}/WHEEL +0 -0
- {edri-2025.11.1rc3.dist-info → edri-2025.12.1.dist-info}/top_level.txt +0 -0
edri/abstract/__init__.py
CHANGED
|
@@ -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
|
|
7
|
-
|
|
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,
|
|
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
|
|
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)
|
|
35
|
-
|
|
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
|
-
|
|
867
|
+
self._cache = Cache()
|
|
832
868
|
|
|
833
869
|
def quit(self) -> None:
|
|
834
870
|
self.router_queue.close()
|
edri/api/broker.py
CHANGED
|
@@ -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
|
|
25
|
-
|
|
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
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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]]:
|
edri/api/dataclass/api_event.py
CHANGED
|
@@ -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
|
|
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 ==
|
|
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,
|
|
33
|
-
|
|
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
|
|
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
|
|
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
|
|
edri/api/listener.py
CHANGED
|
@@ -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.
|
|
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)
|
edri/config/setting.py
CHANGED
|
@@ -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
|
-
|
|
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
|
+
|
edri/dataclass/directive/http.py
CHANGED
|
@@ -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
|
edri/utility/cache.py
ADDED
|
@@ -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)
|
edri/utility/validation.py
CHANGED
|
@@ -3,7 +3,7 @@ from re import Pattern
|
|
|
3
3
|
from typing import Self, Iterable, Any
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
class
|
|
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
|
-
>>>
|
|
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
|
|
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
|
-
>>>
|
|
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
|
|
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
|
-
>>>
|
|
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
|
|
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
|
-
>>>
|
|
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
|
|
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
|
-
>>>
|
|
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
|
|
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
|
-
>>>
|
|
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
|
|
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
|
-
>>>
|
|
244
|
+
>>> ListValidator([1, 2, 3], minimum_length=2)
|
|
245
245
|
[1, 2, 3]
|
|
246
|
-
>>>
|
|
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.
|
|
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
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
edri/__init__.py,sha256=bBVs4ynkUzMRudKZyuRScpPmUZTIL1LDfIytZ_L7oKE,6257
|
|
2
|
-
edri/abstract/__init__.py,sha256=
|
|
2
|
+
edri/abstract/__init__.py,sha256=C6ew041GNVcQlfkE77kHeZ9Ts1rIaWRqqh3bKz7Kb1E,488
|
|
3
3
|
edri/abstract/manager/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
-
edri/abstract/manager/manager_base.py,sha256=
|
|
4
|
+
edri/abstract/manager/manager_base.py,sha256=mguHncpNnx-5RSAPdkzzl74NpOghpl2pI76H0biyZEo,42373
|
|
5
5
|
edri/abstract/manager/manager_priority_base.py,sha256=1bUGsIr6MUrCNsJsLNF_tYaiFDX5t8vK2794vdZumQo,8219
|
|
6
6
|
edri/abstract/manager/worker.py,sha256=xMJMDQtcH-j8s37cUoMsF_ZrWS4elbU7vnjdKfAv0gM,403
|
|
7
7
|
edri/abstract/worker/__init__.py,sha256=qcCF2MnReil9G9ojrr7I3hjMks-1tsap4yrYH5Vr07Q,164
|
|
@@ -9,20 +9,20 @@ edri/abstract/worker/worker.py,sha256=JSTItljxBiCm_ljXctzTiKwb90Yk8Cy6EzZJTchtv9
|
|
|
9
9
|
edri/abstract/worker/worker_process.py,sha256=QiNxOuwkMds0sV2MBLyp7bjrovm5xColC7I7TNcSO4s,1957
|
|
10
10
|
edri/abstract/worker/worker_thread.py,sha256=xoMPuDn-hAkWk6kFY3Xf8mxOVP__5t7-x7f-b396-8M,2176
|
|
11
11
|
edri/api/__init__.py,sha256=ZDxCpHKFGajJ1RwDpV7CzxLDUaKpozJRfOCv1OPv5ZY,142
|
|
12
|
-
edri/api/broker.py,sha256=
|
|
13
|
-
edri/api/listener.py,sha256=
|
|
12
|
+
edri/api/broker.py,sha256=I3z_bKbcTDnKXk82yteGEQmuxpqHgp5KrhQaJmk3US0,37258
|
|
13
|
+
edri/api/listener.py,sha256=8ffX8ntJ5XbHAqT5hM38teMRsd1cBjcJ7PJAMa4Ez7c,20403
|
|
14
14
|
edri/api/middleware.py,sha256=6_x55swthVDczT-fu_1ufY1cDsHTZ04jMx6J6xfjbsM,5483
|
|
15
15
|
edri/api/dataclass/__init__.py,sha256=8Y-zcaJtzMdALnNG7M9jsCaB1qAJKM8Ld3h9MDajYjA,292
|
|
16
|
-
edri/api/dataclass/api_event.py,sha256=
|
|
16
|
+
edri/api/dataclass/api_event.py,sha256=08edshexI9FxdebPIgTQMSQ4fEtGpAa3K_VYCmN1jFs,6587
|
|
17
17
|
edri/api/dataclass/client.py,sha256=ctc2G4mXJR2wUSujANudT3LqxW7qxk_YkpM_TEXD0tM,216
|
|
18
18
|
edri/api/dataclass/file.py,sha256=OJfJlrCTjSnzCF8yFVnxr8rGeL0l08WVMsXJx00S4qc,225
|
|
19
19
|
edri/api/extensions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
20
20
|
edri/api/extensions/url_extension.py,sha256=rZKumjR7J6pDTiSLIZf8IzxGgDZP7p2g0Kgs0USug_U,1971
|
|
21
21
|
edri/api/extensions/url_prefix.py,sha256=kNI6g5ZlW0w-J_IMacYLco1EQvmTtMJyEkN6-SK1wC0,491
|
|
22
22
|
edri/api/handlers/__init__.py,sha256=MI6OGDf1rM8jf_uCKK_JYeOGMts62CNy10BwwNlG0Tk,200
|
|
23
|
-
edri/api/handlers/base_handler.py,sha256=
|
|
23
|
+
edri/api/handlers/base_handler.py,sha256=aZN95tWX7hkmJ3D401c-JPfF2azjH0t1jJy_zsjPc_4,13113
|
|
24
24
|
edri/api/handlers/html_handler.py,sha256=OprcTg1IQDI7eBK-_oHqA60P1H30LA9xIQpD7iV-Neg,7464
|
|
25
|
-
edri/api/handlers/http_handler.py,sha256=
|
|
25
|
+
edri/api/handlers/http_handler.py,sha256=AC-8oi4ez5LcNfCd-wrkGSc9fLo2fBNw51yEg8MDiPs,36368
|
|
26
26
|
edri/api/handlers/rest_handler.py,sha256=GAG5lVTsRMCf9IUmYb_pokxyPcOfbnKZ2p3jxfy_-Dw,3300
|
|
27
27
|
edri/api/handlers/websocket_handler.py,sha256=Dh2XannDuW0eFj5CEzf3owlGc1VTyQ8ehjpxYRrCYW8,8144
|
|
28
28
|
edri/api/static_pages/documentation.j2,sha256=Fe7KLsbqp9P-pQYqG2z8rbhhGVDDFf3m6SQ2mc3PFG4,8934
|
|
@@ -32,7 +32,7 @@ edri/api/static_pages/status_400.j2,sha256=ArSvsNy9GG-Gbqt6fbRSqETmGV4aTJa3Zgwwt
|
|
|
32
32
|
edri/api/static_pages/status_500.j2,sha256=39T6VeU_7m-6-RJyVsS48dD56Hp3ZcOeVC397T8GlsY,1468
|
|
33
33
|
edri/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
34
34
|
edri/config/constant.py,sha256=5angk0yL3LlMWShzt0D7TflK5d2yLj2HDWoVqCqSLbY,783
|
|
35
|
-
edri/config/setting.py,sha256
|
|
35
|
+
edri/config/setting.py,sha256=kzii_wmx4C8-uE_TNfBvRlJMkpJrvVLXvZam-2pm3eE,2065
|
|
36
36
|
edri/dataclass/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
37
37
|
edri/dataclass/event.py,sha256=3XwbS_8Nst0V5D6vQ0FYhrX5rx6KfLGd3-9ba71xUMQ,9866
|
|
38
38
|
edri/dataclass/health_checker.py,sha256=62H5wGUtOhql3acPwFtMhpGKPUTmFwWQ4hlqIn6tjfo,1784
|
|
@@ -41,7 +41,7 @@ edri/dataclass/response.py,sha256=VBMmVdna1IOKC5YGBXor6AayYOoiEYb9xx_RZ3bpKnw,38
|
|
|
41
41
|
edri/dataclass/directive/__init__.py,sha256=nfvsh1BmxhACW7Q8gnwy7y3l3_cI1P0k2WP0jV5RJhI,608
|
|
42
42
|
edri/dataclass/directive/base.py,sha256=2ghQpv1bGcNHYEMA0nyWGumIplXBzj9cPQ34aJ7uVr0,296
|
|
43
43
|
edri/dataclass/directive/html.py,sha256=UCuwksxt_Q9b1wha1DjEygJWAyq2Hdnir5zG9lGi8as,946
|
|
44
|
-
edri/dataclass/directive/http.py,sha256=
|
|
44
|
+
edri/dataclass/directive/http.py,sha256=_kxof6aSZV1IK3awbK2KKG12gWM4d4l_GDzWt_TZTqs,2727
|
|
45
45
|
edri/events/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
46
46
|
edri/events/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
47
47
|
edri/events/api/client/__init__.py,sha256=6q7CJ4eLMAuz_EFIs7us-xDXudy0Z5DIHd0YCVtTeuo,170
|
|
@@ -105,6 +105,7 @@ edri/switch/receiver.py,sha256=e2ugVM_DHWspVzHT5HBWajwo0YTexKeisqkXXEYscQ8,4240
|
|
|
105
105
|
edri/switch/sender.py,sha256=xNXL6PeP9UMmMFr_Ol-cXJ5aYRVEWzx3GX1OchRbq-k,1173
|
|
106
106
|
edri/switch/switch.py,sha256=ODxhUU5JQvdQRnw8idZIIW0EJc-DUzfdLdZGY_mwNdI,3388
|
|
107
107
|
edri/utility/__init__.py,sha256=3ZpwOHlERrIru5wOGNtuhKb3kwvUFv5ALzP2CRyV6lg,136
|
|
108
|
+
edri/utility/cache.py,sha256=fcKa-RDOq3_pMV0HO0s2iV8R2knjiyjNdFeu9N5lOpA,549
|
|
108
109
|
edri/utility/function.py,sha256=xoW3SUjKbSgDvcArJskHh3wq_17kwMu3X362zbVU15c,6549
|
|
109
110
|
edri/utility/json_encoder.py,sha256=2VZrVgUKFbkBRCX0V7lZEWiS1DzpFRn5LtJKx36zPaY,3952
|
|
110
111
|
edri/utility/normalized_default_dict.py,sha256=EgzO-cdpekH3IkldGiD7nwj_2aTYIdGrlXvheEu6uNc,5011
|
|
@@ -112,7 +113,7 @@ edri/utility/queue.py,sha256=xBbeu1DT3Krdxni0YABk7gDZ5fLQL9eX-H3U-1jSqag,3628
|
|
|
112
113
|
edri/utility/shared_memory_pipe.py,sha256=kmtd-1999s-cUVThxXVtw4N-rp_WgrHtl-h4hhEliXA,6396
|
|
113
114
|
edri/utility/storage.py,sha256=AbZwtj8py0OBy3dM5C0fJ97uV88TERZO79heEmyE9Yk,3781
|
|
114
115
|
edri/utility/transformation.py,sha256=4FeRNav-ifxuqgwq9ys3G5WtMzUAC3_2B3tnFhMENho,1450
|
|
115
|
-
edri/utility/validation.py,sha256=
|
|
116
|
+
edri/utility/validation.py,sha256=wDQb55CBB-I-KaevucmSWlevYAGxMJ8Pok0Ya3S8cLk,9609
|
|
116
117
|
edri/utility/watcher.py,sha256=9nwU-h6B_QCd02-z-2-Hvf6huro8B9yVcZAepoFtXQ4,4623
|
|
117
118
|
edri/utility/manager/__init__.py,sha256=bNyqET60wyq-QFmNwk52UKRweK5lYTDH_TF2UgS6enk,73
|
|
118
119
|
edri/utility/manager/scheduler.py,sha256=3wRPph-FGNrVMN3TG7SvZ_PDW8mNK7UdM3PnjI_QTH8,11624
|
|
@@ -121,7 +122,7 @@ tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
|
121
122
|
tests/test_edri_init.py,sha256=CME8WwikVgbz3qyjalLauShMcCjERyDBjlFD98xZIgs,4209
|
|
122
123
|
tests/abstract/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
123
124
|
tests/abstract/manager/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
124
|
-
tests/abstract/manager/test_manager_base.py,sha256=
|
|
125
|
+
tests/abstract/manager/test_manager_base.py,sha256=btKW00EmllyL_LFN7cp4I68vho8wNTEG-sF79ocksnc,16484
|
|
125
126
|
tests/abstract/manager/test_manager_base_priority.py,sha256=-z4Y1zkOGXNk1ytGla1UeLQutrfXjeWaSNBBsN5gqQk,5869
|
|
126
127
|
tests/abstract/worker/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
127
128
|
tests/abstract/worker/test_worker.py,sha256=4i2MuLSBaQh_wBMK5DEbx-k1F-rs15oRWzvQ_wktN8I,5298
|
|
@@ -152,11 +153,11 @@ tests/utility/test_normalized_default_dict.py,sha256=Utd4EhN6uW6m3eBxz1hhUwRCOWq
|
|
|
152
153
|
tests/utility/test_shared_memory_pipe.py,sha256=JVzr-h8rjjlB6Uk_OnUBAlRwD2X4e74VKHBKCOE8KFM,7308
|
|
153
154
|
tests/utility/test_storage.py,sha256=DD7H-xk3kTuz3x2zuJuJyHDV4eJMYy5iEsHoPAAfjuQ,1349
|
|
154
155
|
tests/utility/test_transformation.py,sha256=4IJFuP3tp3Bd4vH3ohwxLb8yy4m1teK-e7UJbxJzevc,2840
|
|
155
|
-
tests/utility/test_validation.py,sha256=
|
|
156
|
+
tests/utility/test_validation.py,sha256=wZcXjLrj3JheVLKnYKkkYfyC8CCpHVAw9Jn_uDnuEfk,9103
|
|
156
157
|
tests/utility/manager/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
157
158
|
tests/utility/manager/test_scheduler.py,sha256=sROffYvSOaWsYQxQGTy6l9Mn_qeNPRmJoXLVPKU3XNY,9153
|
|
158
159
|
tests/utility/manager/test_store.py,sha256=xlo1JUsPLIhPJyQn7AXldAgWDo_O8ba2ns25TEaaGdQ,2821
|
|
159
|
-
edri-2025.
|
|
160
|
-
edri-2025.
|
|
161
|
-
edri-2025.
|
|
162
|
-
edri-2025.
|
|
160
|
+
edri-2025.12.1.dist-info/METADATA,sha256=4Z-LtT8w_wnSd7I64CpMEAz00c0ya2fgv0kiobSD37w,8371
|
|
161
|
+
edri-2025.12.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
162
|
+
edri-2025.12.1.dist-info/top_level.txt,sha256=himES6JgPlx4Zt8aDrQEj2fxAd7IDD6MBOsiNZkzKHQ,11
|
|
163
|
+
edri-2025.12.1.dist-info/RECORD,,
|
|
@@ -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
|
tests/utility/test_validation.py
CHANGED
|
@@ -2,44 +2,44 @@ import unittest
|
|
|
2
2
|
from datetime import date, time, datetime
|
|
3
3
|
from re import compile
|
|
4
4
|
|
|
5
|
-
from edri.utility.validation import
|
|
5
|
+
from edri.utility.validation import StringValidator, IntegerValidator, FloatValidator, DateValidator, TimeValidator, DateTimeValidator
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class TestStringValidation(unittest.TestCase):
|
|
9
9
|
def test_valid_string(self):
|
|
10
|
-
value =
|
|
10
|
+
value = StringValidator('hello', minimum_length=3, maximum_length=10)
|
|
11
11
|
self.assertEqual(value, 'hello')
|
|
12
12
|
|
|
13
13
|
def test_too_short_string(self):
|
|
14
14
|
with self.assertRaises(ValueError) as cm:
|
|
15
|
-
|
|
15
|
+
StringValidator('hi', minimum_length=3)
|
|
16
16
|
self.assertIn("too short", str(cm.exception))
|
|
17
17
|
|
|
18
18
|
def test_too_long_string(self):
|
|
19
19
|
with self.assertRaises(ValueError) as cm:
|
|
20
|
-
|
|
20
|
+
StringValidator('this is a long string', maximum_length=10)
|
|
21
21
|
self.assertIn("too long", str(cm.exception))
|
|
22
22
|
|
|
23
23
|
def test_regex_match(self):
|
|
24
24
|
pattern = compile(r'^[a-z]+$')
|
|
25
|
-
value =
|
|
25
|
+
value = StringValidator('hello', regex=pattern)
|
|
26
26
|
self.assertEqual(value, 'hello')
|
|
27
27
|
|
|
28
28
|
def test_regex_no_match(self):
|
|
29
29
|
pattern = compile(r'^[a-z]+$')
|
|
30
30
|
with self.assertRaises(ValueError) as cm:
|
|
31
|
-
|
|
31
|
+
StringValidator('Hello123', regex=pattern)
|
|
32
32
|
self.assertIn("not match", str(cm.exception))
|
|
33
33
|
|
|
34
34
|
def test_all_constraints_pass(self):
|
|
35
35
|
pattern = compile(r'^[a-z]+$')
|
|
36
|
-
value =
|
|
36
|
+
value = StringValidator('hello', minimum_length=3, maximum_length=10, regex=pattern)
|
|
37
37
|
self.assertEqual(value, 'hello')
|
|
38
38
|
|
|
39
39
|
def test_all_constraints_fail(self):
|
|
40
40
|
pattern = compile(r'^[a-z]+$')
|
|
41
41
|
with self.assertRaises(ValueError) as cm:
|
|
42
|
-
|
|
42
|
+
StringValidator('Hi', minimum_length=3, maximum_length=4, regex=pattern)
|
|
43
43
|
self.assertTrue(
|
|
44
44
|
any(msg in str(cm.exception) for msg in ["not match", "too short", "too long"])
|
|
45
45
|
)
|
|
@@ -48,142 +48,142 @@ class TestStringValidation(unittest.TestCase):
|
|
|
48
48
|
class TestIntegerValidation(unittest.TestCase):
|
|
49
49
|
def test_valid_integer_no_constraints(self):
|
|
50
50
|
# No constraints applied should return the number as-is.
|
|
51
|
-
value =
|
|
51
|
+
value = IntegerValidator(10)
|
|
52
52
|
self.assertEqual(value, 10)
|
|
53
53
|
|
|
54
54
|
def test_valid_integer_with_constraints(self):
|
|
55
55
|
# Valid integer within the specified range.
|
|
56
|
-
value =
|
|
56
|
+
value = IntegerValidator(5, minimum=1, maximum=10)
|
|
57
57
|
self.assertEqual(value, 5)
|
|
58
58
|
|
|
59
59
|
def test_integer_below_minimum(self):
|
|
60
60
|
# Should raise a ValueError because 0 is below the minimum of 1.
|
|
61
61
|
with self.assertRaises(ValueError) as cm:
|
|
62
|
-
|
|
62
|
+
IntegerValidator(0, minimum=1)
|
|
63
63
|
self.assertIn("too small", str(cm.exception))
|
|
64
64
|
|
|
65
65
|
def test_integer_above_maximum(self):
|
|
66
66
|
# Should raise a ValueError because 11 is above the maximum of 10.
|
|
67
67
|
with self.assertRaises(ValueError) as cm:
|
|
68
|
-
|
|
68
|
+
IntegerValidator(11, maximum=10)
|
|
69
69
|
self.assertIn("too big", str(cm.exception))
|
|
70
70
|
|
|
71
71
|
|
|
72
72
|
class TestFloatValidation(unittest.TestCase):
|
|
73
73
|
def test_valid_float_no_constraints(self):
|
|
74
74
|
# No constraints applied should return the float as-is.
|
|
75
|
-
value =
|
|
75
|
+
value = FloatValidator(3.14)
|
|
76
76
|
self.assertEqual(value, 3.14)
|
|
77
77
|
|
|
78
78
|
def test_valid_float_with_constraints(self):
|
|
79
79
|
# Valid float within the specified range.
|
|
80
|
-
value =
|
|
80
|
+
value = FloatValidator(3.14, minimum=1.0, maximum=5.0)
|
|
81
81
|
self.assertEqual(value, 3.14)
|
|
82
82
|
|
|
83
83
|
def test_float_below_minimum(self):
|
|
84
84
|
# Should raise a ValueError because 0.5 is below the minimum of 1.0.
|
|
85
85
|
with self.assertRaises(ValueError) as cm:
|
|
86
|
-
|
|
86
|
+
FloatValidator(0.5, minimum=1.0)
|
|
87
87
|
self.assertIn("too small", str(cm.exception))
|
|
88
88
|
|
|
89
89
|
def test_float_above_maximum(self):
|
|
90
90
|
# Should raise a ValueError because 6.0 is above the maximum of 5.0.
|
|
91
91
|
with self.assertRaises(ValueError) as cm:
|
|
92
|
-
|
|
92
|
+
FloatValidator(6.0, maximum=5.0)
|
|
93
93
|
self.assertIn("too big", str(cm.exception))
|
|
94
94
|
|
|
95
95
|
|
|
96
96
|
class TestDateValidation(unittest.TestCase):
|
|
97
97
|
|
|
98
98
|
def test_valid_date_no_constraints(self):
|
|
99
|
-
d =
|
|
99
|
+
d = DateValidator(2024, 3, 28)
|
|
100
100
|
self.assertEqual(d, date(2024, 3, 28))
|
|
101
101
|
|
|
102
102
|
def test_valid_date_within_constraints(self):
|
|
103
|
-
d =
|
|
104
|
-
|
|
105
|
-
|
|
103
|
+
d = DateValidator(2024, 3, 28,
|
|
104
|
+
minimum_date=date(2024, 1, 1),
|
|
105
|
+
maximum_date=date(2024, 12, 31))
|
|
106
106
|
self.assertEqual(d, date(2024, 3, 28))
|
|
107
107
|
|
|
108
108
|
def test_date_equal_to_minimum(self):
|
|
109
|
-
d =
|
|
109
|
+
d = DateValidator(2024, 1, 1, minimum_date=date(2024, 1, 1))
|
|
110
110
|
self.assertEqual(d, date(2024, 1, 1))
|
|
111
111
|
|
|
112
112
|
def test_date_equal_to_maximum(self):
|
|
113
|
-
d =
|
|
113
|
+
d = DateValidator(2024, 12, 31, maximum_date=date(2024, 12, 31))
|
|
114
114
|
self.assertEqual(d, date(2024, 12, 31))
|
|
115
115
|
|
|
116
116
|
def test_date_below_minimum_raises(self):
|
|
117
117
|
with self.assertRaises(ValueError) as context:
|
|
118
|
-
|
|
118
|
+
DateValidator(2023, 12, 31, minimum_date=date(2024, 1, 1))
|
|
119
119
|
self.assertIn("earlier than minimum allowed", str(context.exception))
|
|
120
120
|
|
|
121
121
|
def test_date_above_maximum_raises(self):
|
|
122
122
|
with self.assertRaises(ValueError) as context:
|
|
123
|
-
|
|
123
|
+
DateValidator(2025, 1, 1, maximum_date=date(2024, 12, 31))
|
|
124
124
|
self.assertIn("later than maximum allowed", str(context.exception))
|
|
125
125
|
|
|
126
126
|
def test_invalid_date_raises(self):
|
|
127
127
|
with self.assertRaises(ValueError):
|
|
128
|
-
|
|
128
|
+
DateValidator(2024, 2, 30) # Invalid day in February
|
|
129
129
|
|
|
130
130
|
|
|
131
131
|
class TestTimeValidation(unittest.TestCase):
|
|
132
132
|
|
|
133
133
|
def test_valid_time_no_constraints(self):
|
|
134
|
-
t =
|
|
134
|
+
t = TimeValidator(12, 30)
|
|
135
135
|
self.assertEqual(t, time(12, 30))
|
|
136
136
|
|
|
137
137
|
def test_valid_time_within_constraints(self):
|
|
138
|
-
t =
|
|
139
|
-
|
|
140
|
-
|
|
138
|
+
t = TimeValidator(14, 45,
|
|
139
|
+
minimum_time=time(12, 0),
|
|
140
|
+
maximum_time=time(20, 0))
|
|
141
141
|
self.assertEqual(t, time(14, 45))
|
|
142
142
|
|
|
143
143
|
def test_time_equal_to_minimum(self):
|
|
144
|
-
t =
|
|
144
|
+
t = TimeValidator(8, 0, minimum_time=time(8, 0))
|
|
145
145
|
self.assertEqual(t, time(8, 0))
|
|
146
146
|
|
|
147
147
|
def test_time_equal_to_maximum(self):
|
|
148
|
-
t =
|
|
148
|
+
t = TimeValidator(22, 0, maximum_time=time(22, 0))
|
|
149
149
|
self.assertEqual(t, time(22, 0))
|
|
150
150
|
|
|
151
151
|
def test_time_below_minimum_raises(self):
|
|
152
152
|
with self.assertRaises(ValueError) as context:
|
|
153
|
-
|
|
153
|
+
TimeValidator(6, 59, minimum_time=time(7, 0))
|
|
154
154
|
self.assertIn("earlier than minimum allowed", str(context.exception))
|
|
155
155
|
|
|
156
156
|
def test_time_above_maximum_raises(self):
|
|
157
157
|
with self.assertRaises(ValueError) as context:
|
|
158
|
-
|
|
158
|
+
TimeValidator(23, 1, maximum_time=time(23, 0))
|
|
159
159
|
self.assertIn("later than maximum allowed", str(context.exception))
|
|
160
160
|
|
|
161
161
|
def test_time_with_seconds_and_microseconds(self):
|
|
162
|
-
t =
|
|
162
|
+
t = TimeValidator(10, 15, 30, 500000)
|
|
163
163
|
self.assertEqual(t, time(10, 15, 30, 500000))
|
|
164
164
|
|
|
165
165
|
def test_time_with_timezone(self):
|
|
166
166
|
tz = time(0, 0).tzinfo # no tzinfo set, just testing the arg
|
|
167
|
-
t =
|
|
167
|
+
t = TimeValidator(10, 0, 0, 0, tz)
|
|
168
168
|
self.assertEqual(t, time(10, 0))
|
|
169
169
|
|
|
170
170
|
def test_invalid_hour_raises(self):
|
|
171
171
|
with self.assertRaises(ValueError):
|
|
172
|
-
|
|
172
|
+
TimeValidator(25, 0) # hour out of range
|
|
173
173
|
|
|
174
174
|
def test_invalid_minute_raises(self):
|
|
175
175
|
with self.assertRaises(ValueError):
|
|
176
|
-
|
|
176
|
+
TimeValidator(12, 60) # minute out of range
|
|
177
177
|
|
|
178
178
|
|
|
179
179
|
class TestDateTimeValidation(unittest.TestCase):
|
|
180
180
|
|
|
181
181
|
def test_valid_datetime_no_constraints(self):
|
|
182
|
-
dt =
|
|
182
|
+
dt = DateTimeValidator(2024, 3, 28, 15, 30)
|
|
183
183
|
self.assertEqual(dt, datetime(2024, 3, 28, 15, 30))
|
|
184
184
|
|
|
185
185
|
def test_valid_datetime_within_constraints(self):
|
|
186
|
-
dt =
|
|
186
|
+
dt = DateTimeValidator(
|
|
187
187
|
2024, 3, 28, 12, 0,
|
|
188
188
|
minimum_datetime=datetime(2024, 3, 1, 0, 0),
|
|
189
189
|
maximum_datetime=datetime(2024, 12, 31, 23, 59)
|
|
@@ -191,14 +191,14 @@ class TestDateTimeValidation(unittest.TestCase):
|
|
|
191
191
|
self.assertEqual(dt, datetime(2024, 3, 28, 12, 0))
|
|
192
192
|
|
|
193
193
|
def test_datetime_equal_to_minimum(self):
|
|
194
|
-
dt =
|
|
194
|
+
dt = DateTimeValidator(
|
|
195
195
|
2024, 3, 1, 0, 0,
|
|
196
196
|
minimum_datetime=datetime(2024, 3, 1, 0, 0)
|
|
197
197
|
)
|
|
198
198
|
self.assertEqual(dt, datetime(2024, 3, 1, 0, 0))
|
|
199
199
|
|
|
200
200
|
def test_datetime_equal_to_maximum(self):
|
|
201
|
-
dt =
|
|
201
|
+
dt = DateTimeValidator(
|
|
202
202
|
2024, 12, 31, 23, 59,
|
|
203
203
|
maximum_datetime=datetime(2024, 12, 31, 23, 59)
|
|
204
204
|
)
|
|
@@ -206,7 +206,7 @@ class TestDateTimeValidation(unittest.TestCase):
|
|
|
206
206
|
|
|
207
207
|
def test_datetime_below_minimum_raises(self):
|
|
208
208
|
with self.assertRaises(ValueError) as context:
|
|
209
|
-
|
|
209
|
+
DateTimeValidator(
|
|
210
210
|
2024, 2, 29, 23, 59,
|
|
211
211
|
minimum_datetime=datetime(2024, 3, 1)
|
|
212
212
|
)
|
|
@@ -214,21 +214,21 @@ class TestDateTimeValidation(unittest.TestCase):
|
|
|
214
214
|
|
|
215
215
|
def test_datetime_above_maximum_raises(self):
|
|
216
216
|
with self.assertRaises(ValueError) as context:
|
|
217
|
-
|
|
217
|
+
DateTimeValidator(
|
|
218
218
|
2025, 1, 1, 0, 0,
|
|
219
219
|
maximum_datetime=datetime(2024, 12, 31, 23, 59)
|
|
220
220
|
)
|
|
221
221
|
self.assertIn("later than maximum allowed", str(context.exception))
|
|
222
222
|
|
|
223
223
|
def test_datetime_with_seconds_and_microseconds(self):
|
|
224
|
-
dt =
|
|
224
|
+
dt = DateTimeValidator(2024, 3, 28, 12, 45, 30, 999999)
|
|
225
225
|
self.assertEqual(dt, datetime(2024, 3, 28, 12, 45, 30, 999999))
|
|
226
226
|
|
|
227
227
|
def test_invalid_datetime_raises(self):
|
|
228
228
|
with self.assertRaises(ValueError):
|
|
229
|
-
|
|
229
|
+
DateTimeValidator(2024, 2, 30, 12, 0) # Invalid date
|
|
230
230
|
|
|
231
231
|
def test_timezone_argument_is_applied(self):
|
|
232
232
|
tz = datetime.now().astimezone().tzinfo
|
|
233
|
-
dt =
|
|
233
|
+
dt = DateTimeValidator(2024, 3, 28, 10, 0, 0, 0, tz)
|
|
234
234
|
self.assertEqual(dt.tzinfo, tz)
|
|
File without changes
|
|
File without changes
|