model-lib 0.99.1__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.
@@ -0,0 +1,40 @@
1
+ # path-sync copy -n python-template
2
+
3
+ # === DO_NOT_EDIT: path-sync gitignore ===
4
+ # Python bytecode
5
+ __pycache__/
6
+ *.pyc
7
+ *.pyo
8
+
9
+ # Virtual environments
10
+ .venv/
11
+ venv/
12
+
13
+ # Build artifacts
14
+ dist/
15
+ build/
16
+ *.egg-info/
17
+
18
+ # Coverage
19
+ .coverage
20
+ htmlcov/
21
+ coverage.xml
22
+
23
+ # Cache directories
24
+ .ruff_cache/
25
+ .pytest_cache/
26
+ .mypy_cache/
27
+
28
+ # IDE
29
+ .idea/
30
+ *.iml
31
+ .vscode/
32
+
33
+ # pkg-ext dev mode files
34
+ .groups-dev.yaml
35
+ CHANGELOG-dev.md
36
+ *.api-dev.yaml
37
+
38
+ # MkDocs build output
39
+ site/
40
+ # === OK_EDIT: path-sync gitignore ===
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Espen Albert
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,20 @@
1
+ Metadata-Version: 2.4
2
+ Name: model-lib
3
+ Version: 0.99.1
4
+ Summary: Pydantic model utilities for serialization and settings
5
+ Author-email: EspenAlbert <espen.albert1@gmail.com>
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Programming Language :: Python :: 3.13
10
+ Classifier: Programming Language :: Python :: 3.14
11
+ Requires-Python: >=3.13
12
+ Requires-Dist: pydantic-settings>=2.8
13
+ Requires-Dist: pydantic>=2.11
14
+ Requires-Dist: pyyaml>=6.0.2
15
+ Requires-Dist: zero-3rdparty
16
+ Provides-Extra: toml
17
+ Requires-Dist: tomli-w~=1.1.0; extra == 'toml'
18
+ Description-Content-Type: text/markdown
19
+
20
+ # model-lib
@@ -0,0 +1 @@
1
+ # model-lib
@@ -0,0 +1,5 @@
1
+ # Generated by pkg-ext
2
+ # flake8: noqa
3
+
4
+ VERSION = "0.99.1"
5
+ __all__ = []
@@ -0,0 +1,48 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import logging
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+ from typing import Any, Generic, TypeVar
8
+
9
+ from pydantic import Field
10
+ from pydantic.fields import FieldInfo
11
+ from zero_3rdparty.run_env import running_in_container_environment
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ def env_value_as_str(value: Any) -> str | None:
17
+ if value is None:
18
+ return None
19
+ if isinstance(value, Path | bool | float | int):
20
+ value = str(value)
21
+ if isinstance(value, list | dict):
22
+ value = json.dumps(value)
23
+ assert isinstance(value, str), f"env value must be str, was {value!r}"
24
+ return value
25
+
26
+
27
+ T = TypeVar("T")
28
+
29
+
30
+ def container_or_default(container_default: T, cls_default: T) -> FieldInfo:
31
+ return Field(default_factory=DockerOrClsDefDefault(docker_default=container_default, cls_default=cls_default)) # type: ignore
32
+
33
+
34
+ @dataclass
35
+ class DockerOrClsDefDefault(Generic[T]):
36
+ docker_default: T
37
+ cls_default: T
38
+
39
+ def __call__(self) -> T:
40
+ if running_in_container_environment():
41
+ return self.docker_default
42
+ return self.cls_default
43
+
44
+
45
+ def port_info(number: int, *, url_prefix: str, protocol: str) -> int:
46
+ """Used for pants plugin "artifacts" to know which url_prefix and protocol
47
+ to use in Chart."""
48
+ return Field(default=number)
@@ -0,0 +1,30 @@
1
+ from pathlib import Path
2
+ from typing import Callable, TypeVar, Union
3
+
4
+ from zero_3rdparty.enum_utils import StrEnum
5
+
6
+ RegisteredPayloadT = TypeVar("RegisteredPayloadT")
7
+ PayloadT = Union[RegisteredPayloadT, str, bytes, Path, dict, list]
8
+ ModelRawT = Union[dict, list]
9
+ METADATA_DUMP_KEY = "metadata"
10
+ MODEL_DUMP_KEY = "model"
11
+ METADATA_MODEL_NAME_FIELD = "model_name"
12
+ METADATA_MODEL_NAME_BACKUP_FIELD = "model_name_backup"
13
+
14
+
15
+ class FileFormat(StrEnum):
16
+ json = "json"
17
+ pretty_json = "pretty_json"
18
+ json_pretty = "json_pretty"
19
+ yaml = "yaml"
20
+ yml = "yml"
21
+ # only recommended to use with pydantic2
22
+ json_pydantic = "json_pydantic"
23
+ pydantic_json = "pydantic_json"
24
+ # pip install tomlkit
25
+ toml = "toml"
26
+ toml_compact = "toml_compact"
27
+
28
+
29
+ FileFormatT = Union[FileFormat, str]
30
+ PayloadParser = Callable[[RegisteredPayloadT, FileFormatT], ModelRawT]
@@ -0,0 +1,31 @@
1
+ from __future__ import annotations
2
+
3
+ import pydantic
4
+ from pydantic import BaseModel, RootModel, model_serializer
5
+ from zero_3rdparty.iter_utils import ignore_falsy
6
+
7
+ from model_lib.model_dump import register_dumper
8
+
9
+
10
+ def base_model_dumper(model: BaseModel):
11
+ if isinstance(model, RootModel):
12
+ return model.root
13
+ fields = model.model_fields # type: ignore
14
+ return {key: value for key, value in model if key in fields}
15
+
16
+
17
+ class IgnoreFalsy(BaseModel):
18
+ @model_serializer(mode="wrap")
19
+ def _ignore_falsy(
20
+ self,
21
+ nxt: pydantic.SerializerFunctionWrapHandler,
22
+ ):
23
+ serialized = nxt(self)
24
+ no_falsy = ignore_falsy(**serialized) # type: ignore
25
+ return self.dump_dict_modifier(no_falsy)
26
+
27
+ def dump_dict_modifier(self, payload: dict) -> dict:
28
+ return payload
29
+
30
+
31
+ register_dumper(BaseModel, base_model_dumper)
@@ -0,0 +1,63 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Type, TypeVar
4
+
5
+ from zero_3rdparty.error import BaseError
6
+
7
+ from model_lib.constants import FileFormat, PayloadT
8
+
9
+ T = TypeVar("T")
10
+
11
+
12
+ class UnknownModelError(BaseError):
13
+ def __init__(self, model_name: str):
14
+ self.model_name: str = model_name
15
+
16
+
17
+ class ClsNameAlreadyExist(BaseError):
18
+ def __init__(self, new_cls_path: str, old_cls_path: str):
19
+ self.new_cls_path = new_cls_path
20
+ self.old_cls_path = old_cls_path
21
+
22
+
23
+ class NoDumper(Exception):
24
+ def __init__(self, instance_type: Type[T]):
25
+ self.instance_type = instance_type
26
+
27
+
28
+ class DumperExist(Exception):
29
+ def __init__(self, existing_type: Type, new_type: Type):
30
+ self.existing_type = existing_type
31
+ self.new_type = new_type
32
+
33
+
34
+ class EnvVarParsingFailure(BaseError):
35
+ def __init__(self, errors: list[Any]):
36
+ self.errors = errors
37
+
38
+
39
+ class ParseError(BaseError):
40
+ pass
41
+
42
+
43
+ class PayloadParserNotFoundError(ParseError):
44
+ def __init__(self, payload: object, format: FileFormat):
45
+ self.payload = payload
46
+ self.format = format
47
+
48
+
49
+ class PayloadError(ParseError):
50
+ def __init__(self, payload: PayloadT, message: str, metadata: dict | None = None):
51
+ self.message = message
52
+ self.payload = payload
53
+ self.metadata = metadata
54
+
55
+
56
+ class DumpConfigureError(BaseError):
57
+ pass
58
+
59
+
60
+ class PayloadParserAlreadyExistError(DumpConfigureError):
61
+ def __init__(self, old_call_name: str, new_call_name: str):
62
+ self.old_call_name = old_call_name
63
+ self.new_call_name = new_call_name
@@ -0,0 +1,36 @@
1
+ """isort:skip_file."""
2
+
3
+ from model_lib.metadata.metadata import (
4
+ current_metadata,
5
+ metadata_from_context_dict,
6
+ pop_metadata,
7
+ read_metadata,
8
+ read_metadata_or_none,
9
+ set_metadata,
10
+ update_metadata,
11
+ )
12
+ from model_lib.metadata.metadata_dump import (
13
+ add_metadata_dumper,
14
+ dump_metadata,
15
+ get_metadata_dumpers,
16
+ metadata_dumper,
17
+ set_metadata_dumpers,
18
+ )
19
+ from model_lib.metadata.metadata_fields import EventMetadata, iter_tags
20
+
21
+ __all__ = (
22
+ "EventMetadata",
23
+ "add_metadata_dumper",
24
+ "current_metadata",
25
+ "dump_metadata",
26
+ "get_metadata_dumpers",
27
+ "iter_tags",
28
+ "metadata_dumper",
29
+ "metadata_from_context_dict",
30
+ "pop_metadata",
31
+ "read_metadata",
32
+ "read_metadata_or_none",
33
+ "set_metadata",
34
+ "set_metadata_dumpers",
35
+ "update_metadata",
36
+ )
@@ -0,0 +1,205 @@
1
+ """A dictionary with values specific to a thread/task Useful for:
2
+
3
+ - current Request in a http handler
4
+ - metadata during event processing
5
+ - storing unhandled errors
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from asyncio import Task, create_task
11
+ from collections import UserDict
12
+ from contextlib import suppress
13
+ from contextvars import Context, ContextVar
14
+ from dataclasses import dataclass
15
+ from typing import Callable, Coroutine, Protocol, TypeVar, Union
16
+
17
+ from typing_extensions import TypeAlias
18
+ from zero_3rdparty.object_name import as_name
19
+
20
+ T = TypeVar("T")
21
+ KeyT: TypeAlias = Union[type, str]
22
+
23
+
24
+ def identity(value: T) -> T:
25
+ return value
26
+
27
+
28
+ class ParentChildCopy(Protocol):
29
+ def __call__(
30
+ self,
31
+ parent: LocalDict,
32
+ child: LocalDict,
33
+ is_new_thread: bool,
34
+ func_name: str,
35
+ ) -> None: ...
36
+
37
+
38
+ @dataclass
39
+ class CopyConfig:
40
+ """
41
+ >>> c1 = CopyConfig()
42
+ >>> c1.copy_to_thread
43
+ False
44
+ >>> c1.copy_to_task
45
+ True
46
+ """
47
+
48
+ never: bool = False
49
+ thread_copy: bool | Callable[[], bool] = False
50
+ task_copy: bool | Callable[[], bool] = True
51
+ copy_func: Callable[[T], T] = identity # type: ignore
52
+ # is thread
53
+ on_copy_done: ParentChildCopy | None = None
54
+
55
+ @property
56
+ def copy_to_thread(self) -> bool:
57
+ if self.never:
58
+ return False
59
+ thread_copy = self.thread_copy
60
+ if isinstance(thread_copy, bool):
61
+ return thread_copy
62
+ return thread_copy()
63
+
64
+ @property
65
+ def copy_to_task(self) -> bool:
66
+ if self.never:
67
+ return False
68
+ task_copy = self.task_copy
69
+ if isinstance(task_copy, bool):
70
+ return task_copy
71
+ return task_copy()
72
+
73
+
74
+ _copy_config: dict[KeyT, CopyConfig] = {}
75
+ DEFAULT_CONFIG = CopyConfig()
76
+ assert not DEFAULT_CONFIG.copy_to_thread
77
+ assert DEFAULT_CONFIG.copy_to_task
78
+
79
+
80
+ def copy_value(key: KeyT, value: T) -> T:
81
+ config = get_copy_behavior(key) or DEFAULT_CONFIG
82
+ return config.copy_func(value) # type: ignore
83
+
84
+
85
+ def set_copy_behavior(t: KeyT, config: CopyConfig) -> None:
86
+ global _copy_config
87
+ _copy_config[t] = config
88
+
89
+
90
+ def get_copy_behavior(t: KeyT) -> CopyConfig | None:
91
+ return _copy_config.get(t)
92
+
93
+
94
+ class LocalDict(UserDict):
95
+ def set_instance(self, instance: T) -> T | None:
96
+ """Returns the previous instance if set."""
97
+ instance_type = type(instance)
98
+ old = self.get(instance_type, None)
99
+ self[instance_type] = instance
100
+ return old
101
+
102
+ def get_instance(self, instance_type: type[T]) -> T:
103
+ return self[instance_type]
104
+
105
+ def get_instance_or_none(self, instance_type: type[T]) -> T | None:
106
+ return self.get(instance_type)
107
+
108
+ def pop_instance(self, instance_type: type[T]) -> T | None:
109
+ return self.pop(instance_type, None)
110
+
111
+ def copy_to_new_thread(self, thread_name: str) -> LocalDict:
112
+ on_done_calls = []
113
+
114
+ def should_include(t: KeyT) -> bool:
115
+ config = get_copy_behavior(t) or DEFAULT_CONFIG
116
+ if on_done := config.on_copy_done:
117
+ on_done_calls.append(on_done)
118
+ return config.copy_to_thread
119
+
120
+ copy = LocalDict({key: copy_value(key, value) for key, value in self.items() if should_include(key)})
121
+ for call in on_done_calls:
122
+ call(self, copy, True, thread_name)
123
+ return copy
124
+
125
+ def copy_to_new_task(self, task_name: str) -> LocalDict:
126
+ on_done_calls = []
127
+
128
+ def should_include(t: KeyT):
129
+ config = get_copy_behavior(t) or DEFAULT_CONFIG
130
+ if on_done := config.on_copy_done:
131
+ on_done_calls.append(on_done)
132
+ return config.copy_to_task
133
+
134
+ copy = LocalDict({key: copy_value(key, value) for key, value in self.items() if should_include(key)})
135
+ for call in on_done_calls:
136
+ call(self, copy, False, task_name)
137
+ return copy
138
+
139
+
140
+ _context_dict: ContextVar[LocalDict] = ContextVar(f"{__name__}.LocalDict")
141
+
142
+
143
+ def get_context_dict() -> LocalDict:
144
+ try:
145
+ local_dict = _context_dict.get()
146
+ except LookupError:
147
+ local_dict = LocalDict()
148
+ _context_dict.set(local_dict)
149
+ return local_dict
150
+ if local_dict is ...:
151
+ local_dict = LocalDict()
152
+ _context_dict.set(local_dict)
153
+ return local_dict
154
+
155
+
156
+ def set_context_dict(context_dict: LocalDict):
157
+ _context_dict.set(context_dict)
158
+
159
+
160
+ def force_new_context_dict_on_task(task_name: str = "unknown"):
161
+ old = get_context_dict()
162
+ new = old.copy_to_new_task(task_name)
163
+ set_context_dict(new)
164
+
165
+
166
+ def create_task_copy_context(awaitable: Coroutine) -> Task:
167
+ old = get_context_dict()
168
+ task_name = as_name(awaitable)
169
+ new = old.copy_to_new_task(task_name)
170
+
171
+ def start_task():
172
+ set_context_dict(new)
173
+ return create_task(awaitable)
174
+
175
+ return Context().run(start_task)
176
+
177
+
178
+ def clear_context_dict():
179
+ """Ideally we would _context.dict.reset(token) but we have no token But
180
+ this is usually only needed for testing."""
181
+ try:
182
+ old = _context_dict.get()
183
+ except LookupError:
184
+ return
185
+ if old is ...:
186
+ return
187
+ _context_dict.set(...) # type: ignore
188
+
189
+
190
+ def get_context_instance(t: type[T]) -> T:
191
+ return get_context_dict().get_instance(t)
192
+
193
+
194
+ def get_context_instance_or_none(t: type[T]) -> T | None:
195
+ with suppress(KeyError):
196
+ return get_context_instance(t)
197
+ return None
198
+
199
+
200
+ def set_context_instance(t: T) -> T | None:
201
+ return get_context_dict().set_instance(t)
202
+
203
+
204
+ def pop_context_instance(t: type[T]) -> T | None:
205
+ return get_context_dict().pop_instance(t)
@@ -0,0 +1,60 @@
1
+ from __future__ import annotations
2
+
3
+ from contextlib import suppress
4
+ from copy import deepcopy
5
+ from typing import Any, Iterable, Mapping, TypeVar
6
+
7
+ from model_lib.metadata.context_dict import (
8
+ CopyConfig,
9
+ LocalDict,
10
+ get_context_dict,
11
+ set_copy_behavior,
12
+ )
13
+
14
+ _METADATA_LOCAL_KEY = __name__
15
+ T = TypeVar("T")
16
+
17
+
18
+ set_copy_behavior( # type: ignore
19
+ _METADATA_LOCAL_KEY,
20
+ CopyConfig(
21
+ thread_copy=True,
22
+ task_copy=True,
23
+ copy_func=deepcopy,
24
+ ),
25
+ )
26
+
27
+
28
+ def update_metadata(metadata: Mapping[str, Any] | Iterable[tuple[str, Any]]) -> dict:
29
+ metadata_in_context = current_metadata()
30
+ metadata_in_context.update(metadata)
31
+ return metadata_in_context
32
+
33
+
34
+ def current_metadata() -> dict[str, Any]:
35
+ context_dict = get_context_dict()
36
+ return metadata_from_context_dict(context_dict)
37
+
38
+
39
+ def metadata_from_context_dict(context_dict: LocalDict) -> dict:
40
+ return context_dict.setdefault(_METADATA_LOCAL_KEY, {})
41
+
42
+
43
+ def read_metadata(key: str) -> Any:
44
+ """Raises KeyError."""
45
+ return current_metadata()[key]
46
+
47
+
48
+ def read_metadata_or_none(key: str) -> Any:
49
+ with suppress(KeyError):
50
+ return read_metadata(key)
51
+
52
+
53
+ def set_metadata(key: str, value: Any) -> None:
54
+ metadata = current_metadata()
55
+ metadata[key] = value
56
+
57
+
58
+ def pop_metadata(key: str, default: T | None = None) -> T | None:
59
+ metadata = current_metadata()
60
+ return metadata.pop(key, default)
@@ -0,0 +1,97 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from contextlib import suppress
5
+ from copy import deepcopy
6
+ from time import time
7
+ from typing import Any, Callable
8
+ from uuid import uuid4
9
+
10
+ from typing_extensions import TypeAlias
11
+ from zero_3rdparty.object_name import as_name
12
+
13
+ from model_lib.metadata.metadata import current_metadata
14
+ from model_lib.metadata.metadata_fields import EventMetadata
15
+
16
+ logger = logging.getLogger(__name__)
17
+ MetadataDumper = Callable[[dict], Any]
18
+ RemoveDumper: TypeAlias = Callable[[], None]
19
+ _dumpers: list[MetadataDumper] = []
20
+
21
+
22
+ def set_metadata_dumpers(*calls: MetadataDumper) -> None:
23
+ global _dumpers
24
+ _dumpers = list(calls)
25
+
26
+
27
+ def _no_removal():
28
+ return None
29
+
30
+
31
+ def add_metadata_dumper(call: MetadataDumper) -> RemoveDumper:
32
+ global _dumpers
33
+ call_names = {as_name(call): call for call in _dumpers}
34
+ call_name = as_name(call)
35
+ if same_name_call := call_names.get(call_name):
36
+ if same_name_call is call:
37
+ logger.warning(f"metadat_dump_call_already_added={call_name}")
38
+ return _no_removal
39
+ _dumpers.remove(same_name_call)
40
+ _dumpers.append(call)
41
+
42
+ def remove():
43
+ with suppress(ValueError):
44
+ _dumpers.remove(call)
45
+
46
+ return remove
47
+
48
+
49
+ def get_metadata_dumpers() -> list[MetadataDumper]:
50
+ return _dumpers
51
+
52
+
53
+ def dump_metadata(skip_dumpers: bool = False) -> dict:
54
+ metadata = current_metadata()
55
+ dumped_metadata = deepcopy(metadata)
56
+ if not skip_dumpers:
57
+ for dumper in _dumpers:
58
+ dumper(dumped_metadata)
59
+ return dumped_metadata
60
+
61
+
62
+ class metadata_dumper:
63
+ def __init__(
64
+ self,
65
+ static_dict: dict,
66
+ dump_event_id: bool = True,
67
+ dump_time: bool = True,
68
+ extra_calls: list[MetadataDumper] | None = None,
69
+ remove_existing: bool = False,
70
+ ):
71
+ self.calls = calls = [] if remove_existing else list(get_metadata_dumpers())
72
+ if static_dict:
73
+
74
+ def add_static_metadata(metadata: dict):
75
+ metadata |= static_dict
76
+
77
+ calls.append(add_static_metadata)
78
+ if dump_event_id:
79
+
80
+ def add_event_id(metadata: dict):
81
+ metadata[EventMetadata.event_id] = uuid4().hex
82
+
83
+ self.calls.append(add_event_id)
84
+ if dump_time:
85
+
86
+ def add_ts(metadata: dict):
87
+ metadata[EventMetadata.dump_time] = time()
88
+
89
+ self.calls.append(add_ts)
90
+ calls.extend(extra_calls or [])
91
+
92
+ def __enter__(self):
93
+ self.old_dumpers = get_metadata_dumpers()
94
+ set_metadata_dumpers(*self.calls)
95
+
96
+ def __exit__(self, exc_type, exc_val, exc_tb):
97
+ set_metadata_dumpers(*self.old_dumpers)
@@ -0,0 +1,29 @@
1
+ """Example of metadata to use when dumping."""
2
+
3
+ from typing import Any, Iterable
4
+
5
+
6
+ class EventMetadata:
7
+ task_id_parent = "task_id_parent"
8
+ topic = "topic"
9
+ topic_prefix = "topic_prefix"
10
+ tags = "tags" #: a list of [(key, value)] pairs
11
+ app_id = "app_id"
12
+ task_level = "task_level" # see Action.task_level
13
+ task_id = "task_id" # see Action.task_uuid
14
+ task_start = "task_start" # the start of the 1st RootContext
15
+ debug = "debug" # flag for sending messages to a debug exchange
16
+ trace_start = "trace_start" # the start of current RootContext
17
+ dispatch_start_time = "dispatch_start_time" # start of the DispatchContext
18
+ dispatch_name = "dispatch_name" # who dispatched
19
+ dump_time = "dump_time" # dump time
20
+ event_id = "event_id" # unique event_id per Event (useful to avoid duplicates)
21
+ trace_action = "trace_action" # action name of the current trace
22
+ session_id = "session_id" # found from request/previous metadata
23
+ user_id = "user_id" # found from request/previous metadata
24
+ maybe_duplicate = "maybe_duplicate" # e.g. when consuming the same message twice
25
+
26
+
27
+ def iter_tags(metadata: dict) -> Iterable[tuple[str, Any]]:
28
+ #: a list of [(key, value)] pairs
29
+ yield from metadata.get(EventMetadata.tags, [])