haiway 0.1.0__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.
@@ -0,0 +1,125 @@
1
+ import types
2
+ import typing
3
+ from collections.abc import Callable, Sequence
4
+ from typing import Any
5
+
6
+ from haiway import types as _types
7
+ from haiway.state.attributes import AttributeAnnotation
8
+
9
+ __all__ = [
10
+ "attribute_type_validator",
11
+ ]
12
+
13
+
14
+ def attribute_type_validator(
15
+ annotation: AttributeAnnotation,
16
+ /,
17
+ ) -> Callable[[Any], Any]:
18
+ match annotation.origin:
19
+ case types.NoneType:
20
+ return _none_validator
21
+
22
+ case _types.Missing:
23
+ return _missing_validator
24
+
25
+ case types.UnionType:
26
+ return _prepare_union_validator(annotation.arguments)
27
+
28
+ case typing.Literal:
29
+ return _prepare_literal_validator(annotation.arguments)
30
+
31
+ case typing.Any:
32
+ return _any_validator
33
+
34
+ case type() as other_type:
35
+ return _prepare_type_validator(other_type)
36
+
37
+ case other:
38
+ raise TypeError(f"Unsupported type annotation: {other}")
39
+
40
+
41
+ def _none_validator(
42
+ value: Any,
43
+ ) -> Any:
44
+ match value:
45
+ case None:
46
+ return None
47
+
48
+ case _:
49
+ raise TypeError(f"Type '{type(value)}' is not matching expected type 'None'")
50
+
51
+
52
+ def _missing_validator(
53
+ value: Any,
54
+ ) -> Any:
55
+ match value:
56
+ case _types.Missing():
57
+ return _types.MISSING
58
+
59
+ case _:
60
+ raise TypeError(f"Type '{type(value)}' is not matching expected type 'Missing'")
61
+
62
+
63
+ def _any_validator(
64
+ value: Any,
65
+ ) -> Any:
66
+ return value # any is always valid
67
+
68
+
69
+ def _prepare_union_validator(
70
+ elements: Sequence[AttributeAnnotation],
71
+ /,
72
+ ) -> Callable[[Any], Any]:
73
+ validators: list[Callable[[Any], Any]] = [
74
+ attribute_type_validator(alternative) for alternative in elements
75
+ ]
76
+
77
+ def union_validator(
78
+ value: Any,
79
+ ) -> Any:
80
+ errors: list[Exception] = []
81
+ for validator in validators:
82
+ try:
83
+ return validator(value)
84
+
85
+ except Exception as exc:
86
+ errors.append(exc)
87
+
88
+ raise ExceptionGroup("Multiple errors", errors)
89
+
90
+ return union_validator
91
+
92
+
93
+ def _prepare_literal_validator(
94
+ elements: Sequence[Any],
95
+ /,
96
+ ) -> Callable[[Any], Any]:
97
+ def literal_validator(
98
+ value: Any,
99
+ ) -> Any:
100
+ if value in elements:
101
+ return value
102
+
103
+ else:
104
+ raise ValueError(f"Value '{value}' is not matching expected '{elements}'")
105
+
106
+ return literal_validator
107
+
108
+
109
+ def _prepare_type_validator(
110
+ validated_type: type[Any],
111
+ /,
112
+ ) -> Callable[[Any], Any]:
113
+ def type_validator(
114
+ value: Any,
115
+ ) -> Any:
116
+ match value:
117
+ case value if isinstance(value, validated_type):
118
+ return value
119
+
120
+ case _:
121
+ raise TypeError(
122
+ f"Type '{type(value)}' is not matching expected type '{validated_type}'"
123
+ )
124
+
125
+ return type_validator
@@ -0,0 +1,11 @@
1
+ from haiway.types.frozen import frozenlist
2
+ from haiway.types.missing import MISSING, Missing, is_missing, not_missing, when_missing
3
+
4
+ __all__ = [
5
+ "frozenlist",
6
+ "is_missing",
7
+ "Missing",
8
+ "MISSING",
9
+ "not_missing",
10
+ "when_missing",
11
+ ]
haiway/types/frozen.py ADDED
@@ -0,0 +1,5 @@
1
+ __all__ = [
2
+ "frozenlist",
3
+ ]
4
+
5
+ type frozenlist[Value] = tuple[Value, ...]
@@ -0,0 +1,91 @@
1
+ from typing import Any, Final, TypeGuard, cast, final
2
+
3
+ __all__ = [
4
+ "MISSING",
5
+ "Missing",
6
+ "is_missing",
7
+ "not_missing",
8
+ "when_missing",
9
+ ]
10
+
11
+
12
+ class MissingType(type):
13
+ _instance: Any = None
14
+
15
+ def __call__(cls) -> Any:
16
+ if cls._instance is None:
17
+ cls._instance = super().__call__()
18
+ return cls._instance
19
+
20
+ else:
21
+ return cls._instance
22
+
23
+
24
+ @final
25
+ class Missing(metaclass=MissingType):
26
+ """
27
+ Type representing absence of a value. Use MISSING constant for its value.
28
+ """
29
+
30
+ def __bool__(self) -> bool:
31
+ return False
32
+
33
+ def __eq__(
34
+ self,
35
+ value: object,
36
+ ) -> bool:
37
+ return value is MISSING
38
+
39
+ def __str__(self) -> str:
40
+ return "MISSING"
41
+
42
+ def __repr__(self) -> str:
43
+ return "MISSING"
44
+
45
+ def __getattribute__(
46
+ self,
47
+ name: str,
48
+ ) -> Any:
49
+ raise RuntimeError("Missing has no attributes")
50
+
51
+ def __setattr__(
52
+ self,
53
+ __name: str,
54
+ __value: Any,
55
+ ) -> None:
56
+ raise RuntimeError("Missing can't be modified")
57
+
58
+ def __delattr__(
59
+ self,
60
+ __name: str,
61
+ ) -> None:
62
+ raise RuntimeError("Missing can't be modified")
63
+
64
+
65
+ MISSING: Final[Missing] = Missing()
66
+
67
+
68
+ def is_missing(
69
+ check: Any | Missing,
70
+ /,
71
+ ) -> TypeGuard[Missing]:
72
+ return check is MISSING
73
+
74
+
75
+ def not_missing[Value](
76
+ check: Value | Missing,
77
+ /,
78
+ ) -> TypeGuard[Value]:
79
+ return check is not MISSING
80
+
81
+
82
+ def when_missing[Value](
83
+ check: Value | Missing,
84
+ /,
85
+ value: Value,
86
+ ) -> Value:
87
+ if check is MISSING:
88
+ return value
89
+
90
+ else:
91
+ return cast(Value, check)
@@ -0,0 +1,23 @@
1
+ from haiway.utils.always import always, async_always
2
+ from haiway.utils.env import getenv_bool, getenv_float, getenv_int, getenv_str, load_env
3
+ from haiway.utils.immutable import freeze
4
+ from haiway.utils.logs import setup_logging
5
+ from haiway.utils.mimic import mimic_function
6
+ from haiway.utils.noop import async_noop, noop
7
+ from haiway.utils.queue import AsyncQueue
8
+
9
+ __all__ = [
10
+ "always",
11
+ "async_always",
12
+ "async_noop",
13
+ "AsyncQueue",
14
+ "freeze",
15
+ "getenv_bool",
16
+ "getenv_float",
17
+ "getenv_int",
18
+ "getenv_str",
19
+ "load_env",
20
+ "mimic_function",
21
+ "noop",
22
+ "setup_logging",
23
+ ]
haiway/utils/always.py ADDED
@@ -0,0 +1,61 @@
1
+ from collections.abc import Callable, Coroutine
2
+ from typing import Any
3
+
4
+ __all__ = [
5
+ "always",
6
+ "async_always",
7
+ ]
8
+
9
+
10
+ def always[Value](
11
+ value: Value,
12
+ /,
13
+ ) -> Callable[..., Value]:
14
+ """
15
+ Factory method creating functions returning always the same value.
16
+
17
+ Parameters
18
+ ----------
19
+ value: Value
20
+ value to be always returned from prepared function
21
+
22
+ Returns
23
+ -------
24
+ Callable[..., Value]
25
+ function ignoring arguments and always returning the provided value.
26
+ """
27
+
28
+ def always_value(
29
+ *args: Any,
30
+ **kwargs: Any,
31
+ ) -> Value:
32
+ return value
33
+
34
+ return always_value
35
+
36
+
37
+ def async_always[Value](
38
+ value: Value,
39
+ /,
40
+ ) -> Callable[..., Coroutine[None, None, Value]]:
41
+ """
42
+ Factory method creating async functions returning always the same value.
43
+
44
+ Parameters
45
+ ----------
46
+ value: Value
47
+ value to be always returned from prepared function
48
+
49
+ Returns
50
+ -------
51
+ Callable[..., Coroutine[None, None, Value]]
52
+ async function ignoring arguments and always returning the provided value.
53
+ """
54
+
55
+ async def always_value(
56
+ *args: Any,
57
+ **kwargs: Any,
58
+ ) -> Value:
59
+ return value
60
+
61
+ return always_value
haiway/utils/env.py ADDED
@@ -0,0 +1,164 @@
1
+ from os import environ, getenv
2
+ from typing import overload
3
+
4
+ __all__ = [
5
+ "getenv_bool",
6
+ "getenv_int",
7
+ "getenv_float",
8
+ "getenv_str",
9
+ "load_env",
10
+ ]
11
+
12
+
13
+ @overload
14
+ def getenv_bool(
15
+ key: str,
16
+ /,
17
+ ) -> bool | None: ...
18
+
19
+
20
+ @overload
21
+ def getenv_bool(
22
+ key: str,
23
+ /,
24
+ default: bool,
25
+ ) -> bool: ...
26
+
27
+
28
+ def getenv_bool(
29
+ key: str,
30
+ /,
31
+ default: bool | None = None,
32
+ ) -> bool | None:
33
+ if value := getenv(key=key):
34
+ return value.lower() in ("true", "1", "t")
35
+ else:
36
+ return default
37
+
38
+
39
+ @overload
40
+ def getenv_int(
41
+ key: str,
42
+ /,
43
+ ) -> int | None: ...
44
+
45
+
46
+ @overload
47
+ def getenv_int(
48
+ key: str,
49
+ /,
50
+ default: int,
51
+ ) -> int: ...
52
+
53
+
54
+ def getenv_int(
55
+ key: str,
56
+ /,
57
+ default: int | None = None,
58
+ ) -> int | None:
59
+ if value := getenv(key=key):
60
+ return int(value)
61
+
62
+ else:
63
+ return default
64
+
65
+
66
+ @overload
67
+ def getenv_float(
68
+ key: str,
69
+ /,
70
+ ) -> float | None: ...
71
+
72
+
73
+ @overload
74
+ def getenv_float(
75
+ key: str,
76
+ /,
77
+ default: float,
78
+ ) -> float: ...
79
+
80
+
81
+ def getenv_float(
82
+ key: str,
83
+ /,
84
+ default: float | None = None,
85
+ ) -> float | None:
86
+ if value := getenv(key=key):
87
+ return float(value)
88
+
89
+ else:
90
+ return default
91
+
92
+
93
+ @overload
94
+ def getenv_str(
95
+ key: str,
96
+ /,
97
+ ) -> str | None: ...
98
+
99
+
100
+ @overload
101
+ def getenv_str(
102
+ key: str,
103
+ /,
104
+ default: str,
105
+ ) -> str: ...
106
+
107
+
108
+ def getenv_str(
109
+ key: str,
110
+ /,
111
+ default: str | None = None,
112
+ ) -> str | None:
113
+ if value := getenv(key=key):
114
+ return value
115
+ else:
116
+ return default
117
+
118
+
119
+ def load_env(
120
+ path: str | None = None,
121
+ override: bool = True,
122
+ ) -> None:
123
+ """\
124
+ Minimalist implementation of environment variables file loader. \
125
+ When the file is not available configuration won't be loaded.
126
+ Allows only subset of formatting:
127
+ - lines starting with '#' are ignored
128
+ - other comments are not allowed
129
+ - each element is in a new line
130
+ - each element must be a `key=value` pair without whitespaces or additional characters
131
+ - keys without values are ignored
132
+
133
+ Parameters
134
+ ----------
135
+ path: str
136
+ custom path to load environment variables, default is '.env'
137
+ override: bool
138
+ override existing variables on conflict if True, otherwise keep existing
139
+ """
140
+
141
+ try:
142
+ with open(file=path or ".env") as file:
143
+ for line in file.readlines():
144
+ if line.startswith("#"):
145
+ continue # ignore commented
146
+
147
+ idx: int # find where key ends
148
+ for element in enumerate(line):
149
+ if element[1] == "=":
150
+ idx: int = element[0]
151
+ break
152
+ else: # ignore keys without assignment
153
+ continue
154
+
155
+ if idx >= len(line):
156
+ continue # ignore keys without values
157
+
158
+ key: str = line[0:idx]
159
+ value: str = line[idx + 1 :].strip()
160
+ if value and (override or key not in environ):
161
+ environ[key] = value
162
+
163
+ except FileNotFoundError:
164
+ pass # ignore loading if no .env available
@@ -0,0 +1,28 @@
1
+ from typing import Any
2
+
3
+ __all__ = [
4
+ "freeze",
5
+ ]
6
+
7
+
8
+ def freeze(
9
+ instance: object,
10
+ /,
11
+ ) -> None:
12
+ """
13
+ Freeze object instance by replacing __delattr__ and __setattr__ to raising Exceptions.
14
+ """
15
+
16
+ def frozen_set(
17
+ __name: str,
18
+ __value: Any,
19
+ ) -> None:
20
+ raise RuntimeError(f"{instance.__class__.__qualname__} is frozen and can't be modified")
21
+
22
+ def frozen_del(
23
+ __name: str,
24
+ ) -> None:
25
+ raise RuntimeError(f"{instance.__class__.__qualname__} is frozen and can't be modified")
26
+
27
+ instance.__delattr__ = frozen_del
28
+ instance.__setattr__ = frozen_set
haiway/utils/logs.py ADDED
@@ -0,0 +1,57 @@
1
+ from logging.config import dictConfig
2
+
3
+ from haiway.utils.env import getenv_bool
4
+
5
+ __all__ = [
6
+ "setup_logging",
7
+ ]
8
+
9
+
10
+ def setup_logging(
11
+ *loggers: str,
12
+ debug: bool = getenv_bool("DEBUG_LOGGING", __debug__),
13
+ ) -> None:
14
+ """\
15
+ Setup logging configuration and prepare specified loggers.
16
+
17
+ Parameters
18
+ ----------
19
+ *loggers: str
20
+ names of additional loggers to configure.
21
+
22
+ NOTE: this function should be run only once on application start
23
+ """
24
+
25
+ dictConfig(
26
+ config={
27
+ "version": 1,
28
+ "disable_existing_loggers": True,
29
+ "formatters": {
30
+ "standard": {
31
+ "format": "%(asctime)s [%(levelname)-4s] [%(name)s] %(message)s",
32
+ "datefmt": "%d/%b/%Y:%H:%M:%S +0000",
33
+ },
34
+ },
35
+ "handlers": {
36
+ "console": {
37
+ "level": "DEBUG" if debug else "INFO",
38
+ "formatter": "standard",
39
+ "class": "logging.StreamHandler",
40
+ "stream": "ext://sys.stdout",
41
+ },
42
+ },
43
+ "loggers": {
44
+ name: {
45
+ "handlers": ["console"],
46
+ "level": "DEBUG" if debug else "INFO",
47
+ "propagate": False,
48
+ }
49
+ for name in loggers
50
+ },
51
+ "root": { # root logger
52
+ "handlers": ["console"],
53
+ "level": "DEBUG" if debug else "INFO",
54
+ "propagate": False,
55
+ },
56
+ },
57
+ )
haiway/utils/mimic.py ADDED
@@ -0,0 +1,77 @@
1
+ from collections.abc import Callable
2
+ from typing import Any, cast, overload
3
+
4
+ __all__ = [
5
+ "mimic_function",
6
+ ]
7
+
8
+
9
+ @overload
10
+ def mimic_function[**Args, Result](
11
+ function: Callable[Args, Result],
12
+ /,
13
+ within: Callable[..., Any],
14
+ ) -> Callable[Args, Result]: ...
15
+
16
+
17
+ @overload
18
+ def mimic_function[**Args, Result](
19
+ function: Callable[Args, Result],
20
+ /,
21
+ ) -> Callable[[Callable[..., Any]], Callable[Args, Result]]: ...
22
+
23
+
24
+ def mimic_function[**Args, Result](
25
+ function: Callable[Args, Result],
26
+ /,
27
+ within: Callable[..., Result] | None = None,
28
+ ) -> Callable[[Callable[..., Result]], Callable[Args, Result]] | Callable[Args, Result]:
29
+ def mimic(
30
+ target: Callable[..., Result],
31
+ ) -> Callable[Args, Result]:
32
+ # mimic function attributes if able
33
+ for attribute in (
34
+ "__module__",
35
+ "__name__",
36
+ "__qualname__",
37
+ "__doc__",
38
+ "__annotations__",
39
+ "__type_params__",
40
+ "__defaults__",
41
+ "__kwdefaults__",
42
+ "__globals__",
43
+ ):
44
+ try:
45
+ setattr(
46
+ target,
47
+ attribute,
48
+ getattr(
49
+ function,
50
+ attribute,
51
+ ),
52
+ )
53
+
54
+ except AttributeError:
55
+ pass
56
+ try:
57
+ target.__dict__.update(function.__dict__)
58
+
59
+ except AttributeError:
60
+ pass
61
+
62
+ setattr( # noqa: B010 - mimic functools.wraps behavior for correct signature checks
63
+ target,
64
+ "__wrapped__",
65
+ function,
66
+ )
67
+
68
+ return cast(
69
+ Callable[Args, Result],
70
+ target,
71
+ )
72
+
73
+ if target := within:
74
+ return mimic(target)
75
+
76
+ else:
77
+ return mimic
haiway/utils/noop.py ADDED
@@ -0,0 +1,24 @@
1
+ from typing import Any
2
+
3
+ __all__ = [
4
+ "async_noop",
5
+ "noop",
6
+ ]
7
+
8
+
9
+ def noop(
10
+ *args: Any,
11
+ **kwargs: Any,
12
+ ) -> None:
13
+ """
14
+ Placeholder function doing nothing (no operation).
15
+ """
16
+
17
+
18
+ async def async_noop(
19
+ *args: Any,
20
+ **kwargs: Any,
21
+ ) -> None:
22
+ """
23
+ Placeholder async function doing nothing (no operation).
24
+ """