haiway 0.10.14__py3-none-any.whl → 0.10.16__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.
Files changed (43) hide show
  1. haiway/__init__.py +111 -0
  2. haiway/context/__init__.py +27 -0
  3. haiway/context/access.py +615 -0
  4. haiway/context/disposables.py +78 -0
  5. haiway/context/identifier.py +92 -0
  6. haiway/context/logging.py +176 -0
  7. haiway/context/metrics.py +165 -0
  8. haiway/context/state.py +113 -0
  9. haiway/context/tasks.py +64 -0
  10. haiway/context/types.py +12 -0
  11. haiway/helpers/__init__.py +21 -0
  12. haiway/helpers/asynchrony.py +225 -0
  13. haiway/helpers/caching.py +326 -0
  14. haiway/helpers/metrics.py +459 -0
  15. haiway/helpers/retries.py +223 -0
  16. haiway/helpers/throttling.py +133 -0
  17. haiway/helpers/timeouted.py +112 -0
  18. haiway/helpers/tracing.py +137 -0
  19. haiway/py.typed +0 -0
  20. haiway/state/__init__.py +12 -0
  21. haiway/state/attributes.py +747 -0
  22. haiway/state/path.py +524 -0
  23. haiway/state/requirement.py +229 -0
  24. haiway/state/structure.py +414 -0
  25. haiway/state/validation.py +468 -0
  26. haiway/types/__init__.py +14 -0
  27. haiway/types/default.py +108 -0
  28. haiway/types/frozen.py +5 -0
  29. haiway/types/missing.py +95 -0
  30. haiway/utils/__init__.py +28 -0
  31. haiway/utils/always.py +61 -0
  32. haiway/utils/collections.py +185 -0
  33. haiway/utils/env.py +230 -0
  34. haiway/utils/freezing.py +28 -0
  35. haiway/utils/logs.py +57 -0
  36. haiway/utils/mimic.py +77 -0
  37. haiway/utils/noop.py +24 -0
  38. haiway/utils/queue.py +82 -0
  39. {haiway-0.10.14.dist-info → haiway-0.10.16.dist-info}/METADATA +1 -1
  40. haiway-0.10.16.dist-info/RECORD +42 -0
  41. haiway-0.10.14.dist-info/RECORD +0 -4
  42. {haiway-0.10.14.dist-info → haiway-0.10.16.dist-info}/WHEEL +0 -0
  43. {haiway-0.10.14.dist-info → haiway-0.10.16.dist-info}/licenses/LICENSE +0 -0
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
@@ -0,0 +1,185 @@
1
+ from collections.abc import Mapping, Sequence, Set
2
+ from typing import overload
3
+
4
+ __all__ = [
5
+ "as_dict",
6
+ "as_list",
7
+ "as_set",
8
+ "as_tuple",
9
+ ]
10
+
11
+
12
+ @overload
13
+ def as_list[T](
14
+ collection: Sequence[T],
15
+ /,
16
+ ) -> list[T]: ...
17
+
18
+
19
+ @overload
20
+ def as_list[T](
21
+ collection: Sequence[T] | None,
22
+ /,
23
+ ) -> list[T] | None: ...
24
+
25
+
26
+ def as_list[T](
27
+ collection: Sequence[T] | None,
28
+ /,
29
+ ) -> list[T] | None:
30
+ """
31
+ Converts any given Sequence into a list.
32
+
33
+ Parameters
34
+ ----------
35
+ collection : Sequence[T] | None
36
+ The input collection to be converted.
37
+
38
+ Returns
39
+ -------
40
+ list[T] | None
41
+ A new list containing all elements of the input collection,\
42
+ or the original list if it was already one.
43
+ None if no value was provided.
44
+ """
45
+
46
+ if collection is None:
47
+ return None
48
+
49
+ if isinstance(collection, list):
50
+ return collection
51
+
52
+ else:
53
+ return list(collection)
54
+
55
+
56
+ @overload
57
+ def as_tuple[T](
58
+ collection: Sequence[T],
59
+ /,
60
+ ) -> tuple[T, ...]: ...
61
+
62
+
63
+ @overload
64
+ def as_tuple[T](
65
+ collection: Sequence[T] | None,
66
+ /,
67
+ ) -> tuple[T, ...] | None: ...
68
+
69
+
70
+ def as_tuple[T](
71
+ collection: Sequence[T] | None,
72
+ /,
73
+ ) -> tuple[T, ...] | None:
74
+ """
75
+ Converts any given Sequence into a tuple.
76
+
77
+ Parameters
78
+ ----------
79
+ collection : Sequence[T] | None
80
+ The input collection to be converted.
81
+
82
+ Returns
83
+ -------
84
+ tuple[T] | None
85
+ A new tuple containing all elements of the input collection,\
86
+ or the original tuple if it was already one.
87
+ None if no value was provided.
88
+ """
89
+
90
+ if collection is None:
91
+ return None
92
+
93
+ if isinstance(collection, tuple):
94
+ return collection
95
+
96
+ else:
97
+ return tuple(collection)
98
+
99
+
100
+ @overload
101
+ def as_set[T](
102
+ collection: Set[T],
103
+ /,
104
+ ) -> set[T]: ...
105
+
106
+
107
+ @overload
108
+ def as_set[T](
109
+ collection: Set[T] | None,
110
+ /,
111
+ ) -> set[T] | None: ...
112
+
113
+
114
+ def as_set[T](
115
+ collection: Set[T] | None,
116
+ /,
117
+ ) -> set[T] | None:
118
+ """
119
+ Converts any given Set into a set.
120
+
121
+ Parameters
122
+ ----------
123
+ collection : Set[T]
124
+ The input collection to be converted.
125
+
126
+ Returns
127
+ -------
128
+ set[T]
129
+ A new set containing all elements of the input collection,\
130
+ or the original set if it was already one.
131
+ None if no value was provided.
132
+ """
133
+
134
+ if collection is None:
135
+ return None
136
+
137
+ if isinstance(collection, set):
138
+ return collection
139
+
140
+ else:
141
+ return set(collection)
142
+
143
+
144
+ @overload
145
+ def as_dict[K, V](
146
+ collection: Mapping[K, V],
147
+ /,
148
+ ) -> dict[K, V]: ...
149
+
150
+
151
+ @overload
152
+ def as_dict[K, V](
153
+ collection: Mapping[K, V] | None,
154
+ /,
155
+ ) -> dict[K, V] | None: ...
156
+
157
+
158
+ def as_dict[K, V](
159
+ collection: Mapping[K, V] | None,
160
+ /,
161
+ ) -> dict[K, V] | None:
162
+ """
163
+ Converts any given Mapping into a dict.
164
+
165
+ Parameters
166
+ ----------
167
+ collection : Mapping[K, V]
168
+ The input collection to be converted.
169
+
170
+ Returns
171
+ -------
172
+ dict[K, V]
173
+ A new dict containing all elements of the input collection,\
174
+ or the original dict if it was already one.
175
+ None if no value was provided.
176
+ """
177
+
178
+ if collection is None:
179
+ return None
180
+
181
+ if isinstance(collection, dict):
182
+ return collection
183
+
184
+ else:
185
+ return dict(collection)
haiway/utils/env.py ADDED
@@ -0,0 +1,230 @@
1
+ from os import environ, getenv
2
+ from typing import Literal, overload
3
+
4
+ __all__ = [
5
+ "getenv_bool",
6
+ "getenv_float",
7
+ "getenv_int",
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
+ @overload
29
+ def getenv_bool(
30
+ key: str,
31
+ /,
32
+ *,
33
+ required: Literal[True],
34
+ ) -> bool: ...
35
+
36
+
37
+ def getenv_bool(
38
+ key: str,
39
+ /,
40
+ default: bool | None = None,
41
+ *,
42
+ required: bool = False,
43
+ ) -> bool | None:
44
+ if value := getenv(key=key):
45
+ return value.lower() in ("true", "1", "t")
46
+
47
+ elif required and default is None:
48
+ raise ValueError(f"Required environment value `{key}` is missing!")
49
+
50
+ else:
51
+ return default
52
+
53
+
54
+ @overload
55
+ def getenv_int(
56
+ key: str,
57
+ /,
58
+ ) -> int | None: ...
59
+
60
+
61
+ @overload
62
+ def getenv_int(
63
+ key: str,
64
+ /,
65
+ default: int,
66
+ ) -> int: ...
67
+
68
+
69
+ @overload
70
+ def getenv_int(
71
+ key: str,
72
+ /,
73
+ *,
74
+ required: Literal[True],
75
+ ) -> int: ...
76
+
77
+
78
+ def getenv_int(
79
+ key: str,
80
+ /,
81
+ default: int | None = None,
82
+ *,
83
+ required: bool = False,
84
+ ) -> int | None:
85
+ if value := getenv(key=key):
86
+ try:
87
+ return int(value)
88
+
89
+ except Exception as exc:
90
+ raise ValueError(f"Environment value `{key}` is not a valid int!") from exc
91
+
92
+ elif required and default is None:
93
+ raise ValueError(f"Required environment value `{key}` is missing!")
94
+
95
+ else:
96
+ return default
97
+
98
+
99
+ @overload
100
+ def getenv_float(
101
+ key: str,
102
+ /,
103
+ ) -> float | None: ...
104
+
105
+
106
+ @overload
107
+ def getenv_float(
108
+ key: str,
109
+ /,
110
+ default: float,
111
+ ) -> float: ...
112
+
113
+
114
+ @overload
115
+ def getenv_float(
116
+ key: str,
117
+ /,
118
+ *,
119
+ required: Literal[True],
120
+ ) -> float: ...
121
+
122
+
123
+ def getenv_float(
124
+ key: str,
125
+ /,
126
+ default: float | None = None,
127
+ *,
128
+ required: bool = False,
129
+ ) -> float | None:
130
+ if value := getenv(key=key):
131
+ try:
132
+ return float(value)
133
+
134
+ except Exception as exc:
135
+ raise ValueError(f"Environment value `{key}` is not a valid float!") from exc
136
+
137
+ elif required and default is None:
138
+ raise ValueError(f"Required environment value `{key}` is missing!")
139
+
140
+ else:
141
+ return default
142
+
143
+
144
+ @overload
145
+ def getenv_str(
146
+ key: str,
147
+ /,
148
+ ) -> str | None: ...
149
+
150
+
151
+ @overload
152
+ def getenv_str(
153
+ key: str,
154
+ /,
155
+ default: str,
156
+ ) -> str: ...
157
+
158
+
159
+ @overload
160
+ def getenv_str(
161
+ key: str,
162
+ /,
163
+ *,
164
+ required: Literal[True],
165
+ ) -> str: ...
166
+
167
+
168
+ def getenv_str(
169
+ key: str,
170
+ /,
171
+ default: str | None = None,
172
+ *,
173
+ required: bool = False,
174
+ ) -> str | None:
175
+ if value := getenv(key=key):
176
+ return value
177
+
178
+ elif required and default is None:
179
+ raise ValueError(f"Required environment value `{key}` is missing!")
180
+
181
+ else:
182
+ return default
183
+
184
+
185
+ def load_env(
186
+ path: str | None = None,
187
+ override: bool = True,
188
+ ) -> None:
189
+ """\
190
+ Minimalist implementation of environment variables file loader. \
191
+ When the file is not available configuration won't be loaded.
192
+ Allows only subset of formatting:
193
+ - lines starting with '#' are ignored
194
+ - other comments are not allowed
195
+ - each element is in a new line
196
+ - each element must be a `key=value` pair without whitespaces or additional characters
197
+ - keys without values are ignored
198
+
199
+ Parameters
200
+ ----------
201
+ path: str
202
+ custom path to load environment variables, default is '.env'
203
+ override: bool
204
+ override existing variables on conflict if True, otherwise keep existing
205
+ """
206
+
207
+ try:
208
+ with open(file=path or ".env") as file:
209
+ for line in file.readlines():
210
+ if line.startswith("#"):
211
+ continue # ignore commented
212
+
213
+ idx: int # find where key ends
214
+ for element in enumerate(line):
215
+ if element[1] == "=":
216
+ idx: int = element[0]
217
+ break
218
+ else: # ignore keys without assignment
219
+ continue
220
+
221
+ if idx >= len(line):
222
+ continue # ignore keys without values
223
+
224
+ key: str = line[0:idx]
225
+ value: str = line[idx + 1 :].strip()
226
+ if value and (override or key not in environ):
227
+ environ[key] = value
228
+
229
+ except FileNotFoundError:
230
+ 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
+ """