mvx-common 0.2.2__tar.gz → 0.2.3__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 (76) hide show
  1. {mvx_common-0.2.2 → mvx_common-0.2.3}/PKG-INFO +1 -1
  2. {mvx_common-0.2.2 → mvx_common-0.2.3}/pyproject.toml +1 -1
  3. {mvx_common-0.2.2 → mvx_common-0.2.3}/src/mvx/common/logger/__init__.py +13 -0
  4. mvx_common-0.2.3/src/mvx/common/logger/pattern_event_policy/__init__.py +15 -0
  5. mvx_common-0.2.3/src/mvx/common/logger/pattern_event_policy/pattern_event_policy.py +338 -0
  6. mvx_common-0.2.3/tests/test_logger/pattern_event_policy/test_pattern_event_policy.py +830 -0
  7. {mvx_common-0.2.2 → mvx_common-0.2.3}/.gitignore +0 -0
  8. {mvx_common-0.2.2 → mvx_common-0.2.3}/LICENSE +0 -0
  9. {mvx_common-0.2.2 → mvx_common-0.2.3}/NOTICE +0 -0
  10. {mvx_common-0.2.2 → mvx_common-0.2.3}/README.md +0 -0
  11. {mvx_common-0.2.2 → mvx_common-0.2.3}/scripts/check.sh +0 -0
  12. {mvx_common-0.2.2 → mvx_common-0.2.3}/scripts/format.sh +0 -0
  13. {mvx_common-0.2.2 → mvx_common-0.2.3}/src/mvx/common/__init__.py +0 -0
  14. {mvx_common-0.2.2 → mvx_common-0.2.3}/src/mvx/common/errors/__init__.py +0 -0
  15. {mvx_common-0.2.2 → mvx_common-0.2.3}/src/mvx/common/errors/invalid_function_argument_error.py +0 -0
  16. {mvx_common-0.2.2 → mvx_common-0.2.3}/src/mvx/common/errors/reasoned_error.py +0 -0
  17. {mvx_common-0.2.2 → mvx_common-0.2.3}/src/mvx/common/errors/runtime_errors.py +0 -0
  18. {mvx_common-0.2.2 → mvx_common-0.2.3}/src/mvx/common/errors/structured_error.py +0 -0
  19. {mvx_common-0.2.2 → mvx_common-0.2.3}/src/mvx/common/helpers/__init__.py +0 -0
  20. {mvx_common-0.2.2 → mvx_common-0.2.3}/src/mvx/common/helpers/api_error_processor.py +0 -0
  21. {mvx_common-0.2.2 → mvx_common-0.2.3}/src/mvx/common/helpers/document_enum.py +0 -0
  22. {mvx_common-0.2.2 → mvx_common-0.2.3}/src/mvx/common/helpers/introspection.py +0 -0
  23. {mvx_common-0.2.2 → mvx_common-0.2.3}/src/mvx/common/helpers/run_with_cancellation_policy.py +0 -0
  24. {mvx_common-0.2.2 → mvx_common-0.2.3}/src/mvx/common/logger/adapter_logging/__init__.py +0 -0
  25. {mvx_common-0.2.2 → mvx_common-0.2.3}/src/mvx/common/logger/adapter_logging/log_record_factory.py +0 -0
  26. {mvx_common-0.2.2 → mvx_common-0.2.3}/src/mvx/common/logger/adapter_logging/logging_configs.py +0 -0
  27. {mvx_common-0.2.2 → mvx_common-0.2.3}/src/mvx/common/logger/adapter_logging/logging_file_sink.py +0 -0
  28. {mvx_common-0.2.2 → mvx_common-0.2.3}/src/mvx/common/logger/adapter_logging/logging_stream_sink.py +0 -0
  29. {mvx_common-0.2.2 → mvx_common-0.2.3}/src/mvx/common/logger/asyncio_log_sink/__init__.py +0 -0
  30. {mvx_common-0.2.2 → mvx_common-0.2.3}/src/mvx/common/logger/asyncio_log_sink/common.py +0 -0
  31. {mvx_common-0.2.2 → mvx_common-0.2.3}/src/mvx/common/logger/asyncio_log_sink/errors.py +0 -0
  32. {mvx_common-0.2.2 → mvx_common-0.2.3}/src/mvx/common/logger/asyncio_log_sink/log_sink.py +0 -0
  33. {mvx_common-0.2.2 → mvx_common-0.2.3}/src/mvx/common/logger/errors.py +0 -0
  34. {mvx_common-0.2.2 → mvx_common-0.2.3}/src/mvx/common/logger/helpers.py +0 -0
  35. {mvx_common-0.2.2 → mvx_common-0.2.3}/src/mvx/common/logger/log_components/__init__.py +0 -0
  36. {mvx_common-0.2.2 → mvx_common-0.2.3}/src/mvx/common/logger/log_components/log_invocation.py +0 -0
  37. {mvx_common-0.2.2 → mvx_common-0.2.3}/src/mvx/common/logger/log_components/protocols.py +0 -0
  38. {mvx_common-0.2.2 → mvx_common-0.2.3}/src/mvx/common/logger/log_context/__init__.py +0 -0
  39. {mvx_common-0.2.2 → mvx_common-0.2.3}/src/mvx/common/logger/log_context/log_context.py +0 -0
  40. {mvx_common-0.2.2 → mvx_common-0.2.3}/src/mvx/common/logger/log_payload_processor/__init__.py +0 -0
  41. {mvx_common-0.2.2 → mvx_common-0.2.3}/src/mvx/common/logger/log_payload_processor/log_payload_processor.py +0 -0
  42. {mvx_common-0.2.2 → mvx_common-0.2.3}/src/mvx/common/logger/log_payload_processor/types.py +0 -0
  43. {mvx_common-0.2.2 → mvx_common-0.2.3}/src/mvx/common/logger/models.py +0 -0
  44. {mvx_common-0.2.2 → mvx_common-0.2.3}/src/mvx/common/py.typed +0 -0
  45. {mvx_common-0.2.2 → mvx_common-0.2.3}/tests/test_errors/test_invalid_function_argument_error.py +0 -0
  46. {mvx_common-0.2.2 → mvx_common-0.2.3}/tests/test_errors/test_reasoned_error.py +0 -0
  47. {mvx_common-0.2.2 → mvx_common-0.2.3}/tests/test_errors/test_runtime_errors.py +0 -0
  48. {mvx_common-0.2.2 → mvx_common-0.2.3}/tests/test_errors/test_structured_error.py +0 -0
  49. {mvx_common-0.2.2 → mvx_common-0.2.3}/tests/test_helpers/test_api_error_processor.py +0 -0
  50. {mvx_common-0.2.2 → mvx_common-0.2.3}/tests/test_helpers/test_run_with_cancellation_policy.py +0 -0
  51. {mvx_common-0.2.2 → mvx_common-0.2.3}/tests/test_logger/adapter_logging/test_log_record_factory.py +0 -0
  52. {mvx_common-0.2.2 → mvx_common-0.2.3}/tests/test_logger/adapter_logging/test_logging_configs.py +0 -0
  53. {mvx_common-0.2.2 → mvx_common-0.2.3}/tests/test_logger/adapter_logging/test_logging_file_sink.py +0 -0
  54. {mvx_common-0.2.2 → mvx_common-0.2.3}/tests/test_logger/adapter_logging/test_logging_stream_sink.py +0 -0
  55. {mvx_common-0.2.2 → mvx_common-0.2.3}/tests/test_logger/asyncio_log_sink/test_asyncio_log_sink.py +0 -0
  56. {mvx_common-0.2.2 → mvx_common-0.2.3}/tests/test_logger/log_components/log_invocation/doc_examples/conftest.py +0 -0
  57. {mvx_common-0.2.2 → mvx_common-0.2.3}/tests/test_logger/log_components/log_invocation/doc_examples/test_awaitable_result.py +0 -0
  58. {mvx_common-0.2.2 → mvx_common-0.2.3}/tests/test_logger/log_components/log_invocation/doc_examples/test_cancellation.py +0 -0
  59. {mvx_common-0.2.2 → mvx_common-0.2.3}/tests/test_logger/log_components/log_invocation/doc_examples/test_context_fields.py +0 -0
  60. {mvx_common-0.2.2 → mvx_common-0.2.3}/tests/test_logger/log_components/log_invocation/doc_examples/test_context_formatter.py +0 -0
  61. {mvx_common-0.2.2 → mvx_common-0.2.3}/tests/test_logger/log_components/log_invocation/doc_examples/test_error_policy.py +0 -0
  62. {mvx_common-0.2.2 → mvx_common-0.2.3}/tests/test_logger/log_components/log_invocation/doc_examples/test_invoke_kwargs.py +0 -0
  63. {mvx_common-0.2.2 → mvx_common-0.2.3}/tests/test_logger/log_components/log_invocation/doc_examples/test_public_api_method.py +0 -0
  64. {mvx_common-0.2.2 → mvx_common-0.2.3}/tests/test_logger/log_components/log_invocation/doc_examples/test_result_logging.py +0 -0
  65. {mvx_common-0.2.2 → mvx_common-0.2.3}/tests/test_logger/log_components/log_invocation/doc_examples/test_standalone_function.py +0 -0
  66. {mvx_common-0.2.2 → mvx_common-0.2.3}/tests/test_logger/log_components/log_invocation/doc_examples/test_verbosity.py +0 -0
  67. {mvx_common-0.2.2 → mvx_common-0.2.3}/tests/test_logger/log_components/log_invocation/test_log_invocation.py +0 -0
  68. {mvx_common-0.2.2 → mvx_common-0.2.3}/tests/test_logger/log_context/test_log_context.py +0 -0
  69. {mvx_common-0.2.2 → mvx_common-0.2.3}/tests/test_logger/log_payload_processor/test_log_payload_processor.py +0 -0
  70. {mvx_common-0.2.2 → mvx_common-0.2.3}/tests/test_logger/test_errors.py +0 -0
  71. {mvx_common-0.2.2 → mvx_common-0.2.3}/tests/test_logger/test_manual_log_context_wiring.py +0 -0
  72. {mvx_common-0.2.2 → mvx_common-0.2.3}/tests/test_logger/test_package_bootstrap.py +0 -0
  73. {mvx_common-0.2.2 → mvx_common-0.2.3}/tests/test_logger/test_package_file_sink_smoke.py +0 -0
  74. {mvx_common-0.2.2 → mvx_common-0.2.3}/tests/test_logger/test_package_internals.py +0 -0
  75. {mvx_common-0.2.2 → mvx_common-0.2.3}/tests/test_logger/test_package_log_invocation_integration.py +0 -0
  76. {mvx_common-0.2.2 → mvx_common-0.2.3}/tests/test_logger/test_package_public_api.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mvx-common
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: Common utilities for MVX Python packages.
5
5
  Project-URL: Documentation, https://mvx-lib.readthedocs.io/en/latest/
6
6
  Project-URL: Source, https://github.com/makarovvvdream-dev/mvx-lib
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mvx-common"
3
- version = "0.2.2"
3
+ version = "0.2.3"
4
4
  description = "Common utilities for MVX Python packages."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -40,6 +40,14 @@ from .log_context import (
40
40
  LogErrorHandlingPolicy,
41
41
  )
42
42
 
43
+ from .pattern_event_policy import (
44
+ PatternLogEventPolicyAction,
45
+ PatternLogEventPolicyRuleConfig,
46
+ PatternLogEventPolicyConfig,
47
+ PatternLogEventPolicy,
48
+ )
49
+
50
+
43
51
  from .log_payload_processor import (
44
52
  LogPayloadProcessor,
45
53
  LogVerbosityLevel,
@@ -106,6 +114,11 @@ __all__ = (
106
114
  # from .log_context
107
115
  "LogContext",
108
116
  "LogErrorHandlingPolicy",
117
+ # from .pattern_event_policy
118
+ "PatternLogEventPolicyAction",
119
+ "PatternLogEventPolicyRuleConfig",
120
+ "PatternLogEventPolicyConfig",
121
+ "PatternLogEventPolicy",
109
122
  # from .log_payload_processor
110
123
  "LogPayloadProcessor",
111
124
  "LogVerbosityLevel",
@@ -0,0 +1,15 @@
1
+ # src/mvx/common/logger/pattern_event_policy/__init__.py
2
+
3
+ from .pattern_event_policy import (
4
+ PatternLogEventPolicyAction,
5
+ PatternLogEventPolicyRuleConfig,
6
+ PatternLogEventPolicyConfig,
7
+ PatternLogEventPolicy,
8
+ )
9
+
10
+ __all__ = (
11
+ "PatternLogEventPolicyAction",
12
+ "PatternLogEventPolicyRuleConfig",
13
+ "PatternLogEventPolicyConfig",
14
+ "PatternLogEventPolicy",
15
+ )
@@ -0,0 +1,338 @@
1
+ # src/mvx/common/logger/pattern_event_policy/pattern_event_policy.py
2
+
3
+ from __future__ import annotations
4
+ from mvx.common.helpers.document_enum import document_enum
5
+
6
+ from dataclasses import dataclass, field
7
+ from enum import StrEnum
8
+ from fnmatch import fnmatchcase
9
+ from typing import Any, Mapping, Sequence
10
+
11
+ from mvx.common.logger import LogEventMeta
12
+
13
+ __all__ = (
14
+ "PatternLogEventPolicyAction",
15
+ "PatternLogEventPolicyRuleConfig",
16
+ "PatternLogEventPolicyConfig",
17
+ "PatternLogEventPolicy",
18
+ )
19
+
20
+
21
+ @document_enum
22
+ class PatternLogEventPolicyAction(StrEnum):
23
+ """Action returned by a matching pattern event policy rule."""
24
+
25
+ #: Allow the matching event
26
+ ALLOW = "allow"
27
+
28
+ #: Deny the matching event
29
+ DENY = "deny"
30
+
31
+
32
+ @dataclass(frozen=True, slots=True)
33
+ class PatternLogEventPolicyRuleConfig:
34
+ """Configuration of one ordered pattern event policy rule.
35
+
36
+ A rule contains an action and optional pattern groups used to match
37
+ ``LogEventMeta`` fields.
38
+
39
+ Pattern groups inside one rule are combined as logical ``and``: every
40
+ non-empty group must match the event metadata. Patterns inside one group
41
+ are alternatives: at least one pattern in that group must match.
42
+
43
+ Empty pattern groups do not restrict the rule. A rule without any pattern
44
+ groups matches every event.
45
+
46
+ Rules are evaluated by ``PatternLogEventPolicy`` in configuration order.
47
+ The first matching rule decides whether the event is enabled.
48
+
49
+ :param action: action returned when the rule matches.
50
+ :param events: shell-style patterns matched against the composed event key.
51
+ :param event_namespaces: shell-style patterns matched against event namespace.
52
+ :param event_names: shell-style patterns matched against event name.
53
+ :param entity_ids: shell-style patterns matched against entity id.
54
+ :param source_paths: shell-style patterns matched against source path.
55
+ :param source_funcs: shell-style patterns matched against source function.
56
+ """
57
+
58
+ action: PatternLogEventPolicyAction
59
+
60
+ events: tuple[str, ...] = field(default_factory=tuple)
61
+ event_namespaces: tuple[str, ...] = field(default_factory=tuple)
62
+ event_names: tuple[str, ...] = field(default_factory=tuple)
63
+ entity_ids: tuple[str, ...] = field(default_factory=tuple)
64
+ source_paths: tuple[str, ...] = field(default_factory=tuple)
65
+ source_funcs: tuple[str, ...] = field(default_factory=tuple)
66
+
67
+ @classmethod
68
+ def from_mapping(cls, data: Mapping[str, Any]) -> PatternLogEventPolicyRuleConfig:
69
+ """Build rule configuration from a mapping.
70
+
71
+ The mapping must contain the ``action`` field with value ``"allow"``
72
+ or ``"deny"``. Pattern fields are optional. Missing pattern fields and
73
+ fields explicitly set to ``None`` are treated as empty pattern groups.
74
+
75
+ Supported pattern fields are:
76
+
77
+ * ``events``;
78
+ * ``event_namespaces``;
79
+ * ``event_names``;
80
+ * ``entity_ids``;
81
+ * ``source_paths``;
82
+ * ``source_funcs``.
83
+
84
+ Pattern fields must be sequences of non-empty strings. String values
85
+ are stripped before they are stored in the resulting configuration.
86
+
87
+ :param data: mapping containing rule configuration.
88
+ :return: parsed rule configuration.
89
+ :raises TypeError: if a field has an invalid type.
90
+ :raises ValueError: if ``action`` is empty, unknown, or a pattern field
91
+ contains an empty string.
92
+ """
93
+ action_raw = _get_required_str(data, "action")
94
+
95
+ try:
96
+ action = PatternLogEventPolicyAction(action_raw)
97
+ except ValueError as exc:
98
+ raise ValueError(
99
+ "field 'action' must be one of: " f"{_enum_values(PatternLogEventPolicyAction)}"
100
+ ) from exc
101
+
102
+ return cls(
103
+ action=action,
104
+ events=_get_str_tuple(data, "events"),
105
+ event_namespaces=_get_str_tuple(data, "event_namespaces"),
106
+ event_names=_get_str_tuple(data, "event_names"),
107
+ entity_ids=_get_str_tuple(data, "entity_ids"),
108
+ source_paths=_get_str_tuple(data, "source_paths"),
109
+ source_funcs=_get_str_tuple(data, "source_funcs"),
110
+ )
111
+
112
+
113
+ @dataclass(frozen=True, slots=True)
114
+ class PatternLogEventPolicyConfig:
115
+ """Configuration of ``PatternLogEventPolicy``.
116
+
117
+ The configuration defines a fallback decision and an ordered rule list.
118
+
119
+ ``default_enabled`` is returned when no rule matches. ``rules`` are checked
120
+ from first to last. The first matching rule wins.
121
+
122
+ The configuration is immutable and can be safely shared as read-only policy
123
+ configuration.
124
+
125
+ :param default_enabled: fallback decision used when no rule matches.
126
+ :param rules: ordered sequence of pattern policy rules.
127
+ """
128
+
129
+ default_enabled: bool = True
130
+ rules: tuple[PatternLogEventPolicyRuleConfig, ...] = field(default_factory=tuple)
131
+
132
+ @classmethod
133
+ def from_mapping(cls, data: Mapping[str, Any]) -> PatternLogEventPolicyConfig:
134
+ """Build pattern policy configuration from a mapping.
135
+
136
+ The mapping may contain:
137
+
138
+ * ``default_enabled``: boolean fallback decision, defaults to ``True``;
139
+ * ``rules``: sequence of rule mappings, defaults to an empty sequence.
140
+
141
+ Rule mappings are parsed by
142
+ ``PatternLogEventPolicyRuleConfig.from_mapping``.
143
+
144
+ :param data: mapping containing policy configuration.
145
+ :return: parsed policy configuration.
146
+ :raises TypeError: if a field has an invalid type.
147
+ :raises ValueError: if a nested rule contains an invalid value.
148
+ """
149
+ return cls(
150
+ default_enabled=_get_bool(data, "default_enabled", default=True),
151
+ rules=_get_rule_tuple(data, "rules"),
152
+ )
153
+
154
+
155
+ class PatternLogEventPolicy:
156
+ """Ready-to-use event policy based on ordered metadata pattern rules.
157
+
158
+ The policy implements the event policy contract used by ``LogContext``.
159
+ It receives ``LogEventMeta`` and returns whether the event is enabled.
160
+
161
+ Matching is based only on event metadata. The policy does not inspect
162
+ payload, normalized payload, logging level, event type, timestamp, sink, or
163
+ destination.
164
+
165
+ Rules are evaluated in the order defined by
166
+ ``PatternLogEventPolicyConfig.rules``. The first matching rule decides the
167
+ result. If no rule matches, ``PatternLogEventPolicyConfig.default_enabled``
168
+ is returned.
169
+ """
170
+
171
+ def __init__(self, config: PatternLogEventPolicyConfig) -> None:
172
+ """Initialize pattern event policy.
173
+
174
+ :param config: immutable pattern policy configuration.
175
+ :raises TypeError: if ``config`` is not a
176
+ ``PatternLogEventPolicyConfig`` instance.
177
+ """
178
+ if not isinstance(config, PatternLogEventPolicyConfig):
179
+ raise TypeError(
180
+ "argument 'config' must be an instance of 'PatternLogEventPolicyConfig'"
181
+ )
182
+
183
+ self._config = config
184
+
185
+ def is_event_enabled(self, event: LogEventMeta) -> bool:
186
+ """Return whether the event is enabled by this policy.
187
+
188
+ The method checks configured rules in order. The first matching rule
189
+ returns its action as a boolean decision. If no rule matches, the
190
+ configured default decision is returned.
191
+
192
+ :param event: event metadata to check.
193
+ :return: ``True`` if the event is enabled, ``False`` otherwise.
194
+ """
195
+ for rule in self._config.rules:
196
+ if _rule_matches(rule, event):
197
+ return rule.action is PatternLogEventPolicyAction.ALLOW
198
+
199
+ return self._config.default_enabled
200
+
201
+
202
+ def _rule_matches(rule: PatternLogEventPolicyRuleConfig, event: LogEventMeta) -> bool:
203
+ event_key = _event_key(event)
204
+
205
+ if rule.events and not _matches_optional_any(event_key, rule.events):
206
+ return False
207
+
208
+ if rule.event_namespaces and not _matches_optional_any(
209
+ event.event_namespace,
210
+ rule.event_namespaces,
211
+ ):
212
+ return False
213
+
214
+ if rule.event_names and not _matches_optional_any(
215
+ event.event_name,
216
+ rule.event_names,
217
+ ):
218
+ return False
219
+
220
+ if rule.entity_ids and not _matches_optional_any(
221
+ event.entity_id,
222
+ rule.entity_ids,
223
+ ):
224
+ return False
225
+
226
+ if rule.source_paths and not _matches_optional_any(
227
+ event.source_path,
228
+ rule.source_paths,
229
+ ):
230
+ return False
231
+
232
+ if rule.source_funcs and not _matches_optional_any(
233
+ event.source_func,
234
+ rule.source_funcs,
235
+ ):
236
+ return False
237
+
238
+ return True
239
+
240
+
241
+ def _event_key(event: LogEventMeta) -> str | None:
242
+ event_name = event.event_name
243
+
244
+ if event_name is None:
245
+ return None
246
+
247
+ event_namespace = event.event_namespace
248
+
249
+ if event_namespace:
250
+ return f"{event_namespace}.{event_name}"
251
+
252
+ return event_name
253
+
254
+
255
+ def _matches_optional_any(value: str | None, patterns: Sequence[str]) -> bool:
256
+ if value is None:
257
+ return False
258
+
259
+ return _matches_any(value, patterns)
260
+
261
+
262
+ def _matches_any(value: str, patterns: Sequence[str]) -> bool:
263
+ return any(fnmatchcase(value, pattern) for pattern in patterns)
264
+
265
+
266
+ def _get_bool(data: Mapping[str, Any], key: str, *, default: bool) -> bool:
267
+ value = data.get(key, default)
268
+
269
+ if not isinstance(value, bool):
270
+ raise TypeError(f"field '{key}' must be bool")
271
+
272
+ return value
273
+
274
+
275
+ def _get_required_str(data: Mapping[str, Any], key: str) -> str:
276
+ value = data.get(key)
277
+
278
+ if not isinstance(value, str):
279
+ raise TypeError(f"field '{key}' must be string")
280
+
281
+ value = value.strip()
282
+
283
+ if not value:
284
+ raise ValueError(f"field '{key}' must not be empty")
285
+
286
+ return value
287
+
288
+
289
+ def _get_str_tuple(data: Mapping[str, Any], key: str) -> tuple[str, ...]:
290
+ value = data.get(key, ())
291
+
292
+ if value is None:
293
+ return ()
294
+
295
+ if isinstance(value, (str, bytes, bytearray)) or not isinstance(value, Sequence):
296
+ raise TypeError(f"field '{key}' must be a sequence of strings")
297
+
298
+ result: list[str] = []
299
+
300
+ for item in value:
301
+ if not isinstance(item, str):
302
+ raise TypeError(f"field '{key}' must contain only strings")
303
+
304
+ item = item.strip()
305
+
306
+ if not item:
307
+ raise ValueError(f"field '{key}' must not contain empty strings")
308
+
309
+ result.append(item)
310
+
311
+ return tuple(result)
312
+
313
+
314
+ def _get_rule_tuple(
315
+ data: Mapping[str, Any],
316
+ key: str,
317
+ ) -> tuple[PatternLogEventPolicyRuleConfig, ...]:
318
+ value = data.get(key, ())
319
+
320
+ if value is None:
321
+ return ()
322
+
323
+ if isinstance(value, (str, bytes, bytearray)) or not isinstance(value, Sequence):
324
+ raise TypeError(f"field '{key}' must be a sequence of mappings")
325
+
326
+ result: list[PatternLogEventPolicyRuleConfig] = []
327
+
328
+ for item in value:
329
+ if not isinstance(item, Mapping):
330
+ raise TypeError(f"field '{key}' must contain only mappings")
331
+
332
+ result.append(PatternLogEventPolicyRuleConfig.from_mapping(item))
333
+
334
+ return tuple(result)
335
+
336
+
337
+ def _enum_values(enum_type: type[StrEnum]) -> str:
338
+ return ", ".join(repr(item.value) for item in enum_type)