zbxtemplar 0.1.0__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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Aleksei Dorozhkin
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,229 @@
1
+ Metadata-Version: 2.4
2
+ Name: zbxtemplar
3
+ Version: 0.1.0
4
+ Summary: Programmatic Zabbix template generation — Monitoring as Code
5
+ Author: Aleksei Dorozhkin
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 Aleksei Dorozhkin
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://github.com/mithraelle/zbxtemplar
29
+ Project-URL: Bug Tracker, https://github.com/mithraelle/zbxtemplar/issues
30
+ Classifier: Development Status :: 4 - Beta
31
+ Classifier: Intended Audience :: System Administrators
32
+ Classifier: License :: OSI Approved :: MIT License
33
+ Classifier: Programming Language :: Python :: 3
34
+ Classifier: Topic :: System :: Monitoring
35
+ Requires-Python: >=3.10
36
+ Description-Content-Type: text/markdown
37
+ License-File: LICENSE
38
+ Requires-Dist: pyyaml>=6.0
39
+ Dynamic: license-file
40
+
41
+ # zbxtemplar
42
+
43
+ A Pythonic framework for programmatic Zabbix Template generation (Monitoring as Code).
44
+
45
+ The goal is to cover the essential Zabbix configuration primitives — not every possible option. If you need a field that isn't exposed, raw dicts and string expressions give you an escape hatch.
46
+
47
+ ## Installation
48
+
49
+ ```bash
50
+ pip install .
51
+ ```
52
+
53
+ ## Core Architecture
54
+
55
+ The project follows the `src-layout`:
56
+
57
+ - `zbxtemplar.entities` — Domain models: Template, Item, Trigger, Graph, Dashboard.
58
+ - `zbxtemplar.core` — Module contract (`TemplarModule`), loader, serialization, shared types.
59
+ - `zbxtemplar.main` — CLI entry point.
60
+
61
+ ## Module Contract
62
+
63
+ Templates are defined as Python classes that inherit from `TemplarModule`.
64
+ The constructor is the contract — all template logic lives in `__init__`.
65
+
66
+ ```python
67
+ from zbxtemplar.core import TemplarModule
68
+ from zbxtemplar.entities import Template, Item, Trigger, TriggerPriority, Graph
69
+
70
+ class MyTemplate(TemplarModule):
71
+ def __init__(self):
72
+ super().__init__()
73
+
74
+ template = Template(name="My Service")
75
+ template.add_tag("Service", "MyApp")
76
+ template.add_macro("THRESHOLD", 90, "Alert threshold")
77
+
78
+ item = Item("CPU Usage", "system.cpu.util")
79
+ item.add_trigger("High CPU", "last", ">",
80
+ template.get_macro("THRESHOLD"),
81
+ priority=TriggerPriority.HIGH)
82
+ template.add_item(item)
83
+
84
+ self.templates = [template]
85
+ self.triggers = []
86
+ self.graphs = []
87
+ ```
88
+
89
+ A module file can contain multiple `TemplarModule` subclasses.
90
+ The loader discovers all of them by class name.
91
+
92
+ ### Running standalone
93
+
94
+ Add a `__main__` guard to run the module directly:
95
+
96
+ ```python
97
+ if __name__ == "__main__":
98
+ import yaml
99
+ module = MyTemplate()
100
+ print(yaml.dump(module.to_export(), default_flow_style=False, sort_keys=False))
101
+ ```
102
+
103
+ ## CLI
104
+
105
+ ```bash
106
+ # Basic usage
107
+ zbxtemplar module.py output.yml
108
+ python -m zbxtemplar module.py output.yml
109
+
110
+ # With options
111
+ zbxtemplar module.py output.yml \
112
+ --namespace "My Company" \
113
+ --template-group "Custom Templates"
114
+ ```
115
+
116
+ | Argument | Description |
117
+ |---|---|
118
+ | `module` | Path to a `.py` file with `TemplarModule` subclass(es) |
119
+ | `output` | Output YAML file path |
120
+ | `--namespace` | UUID namespace for deterministic ID generation |
121
+ | `--template-group` | Default template group name |
122
+
123
+ ## Programmatic Loading
124
+
125
+ ```python
126
+ from zbxtemplar.core import load_module
127
+
128
+ modules = load_module("path/to/module.py")
129
+ # Returns {"ClassName": <instance>, ...}
130
+
131
+ for name, mod in modules.items():
132
+ export = mod.to_export() # Full zabbix_export dict
133
+ ```
134
+
135
+ ## Entities Reference
136
+
137
+ ### Template
138
+
139
+ ```python
140
+ template = Template(name="My Template", groups=["Custom Group"])
141
+ template.add_tag("Service", "MyApp")
142
+ template.add_macro("TIMEOUT", 30, "Connection timeout")
143
+ template.add_item(item)
144
+ ```
145
+
146
+ ### Item
147
+
148
+ ```python
149
+ item = Item("CPU Usage", "system.cpu.util")
150
+
151
+ # Expression helper — builds Zabbix function strings
152
+ item.expr("last") # last(/host/key)
153
+ item.expr("min", "10m") # min(/host/key,10m)
154
+ item.expr("count", "#10") # count(/host/key,#10)
155
+
156
+ # Single-item trigger shorthand
157
+ item.add_trigger("High CPU", "last", ">", 90,
158
+ priority=TriggerPriority.HIGH,
159
+ description="CPU threshold exceeded")
160
+
161
+ # With function args
162
+ item.add_trigger("Sustained high", "min", ">", 100,
163
+ fn_args=("10m",), priority=TriggerPriority.WARNING)
164
+ ```
165
+
166
+ All enum values (`ItemType`, `ValueType`, `TriggerPriority`, `GraphType`, `DrawType`, `CalcFnc`, etc.) follow the Zabbix export format documentation.
167
+
168
+ ### Trigger
169
+
170
+ ```python
171
+ # Standalone (multi-item) trigger — raw expression string
172
+ Trigger(name="Complex alert",
173
+ expression=item1.expr("last") + ">0 and " + item2.expr("min", "5m") + "<100",
174
+ priority=TriggerPriority.WARNING,
175
+ description="Multi-item condition")
176
+ ```
177
+
178
+ ### Macro
179
+
180
+ Macros work seamlessly in string contexts:
181
+
182
+ ```python
183
+ template.add_macro("MY_MACRO", 1, "Description")
184
+ macro = template.get_macro("MY_MACRO")
185
+
186
+ str(macro) # {$MY_MACRO}
187
+ item.expr("last") + ">" + macro # last(/host/key)>{$MY_MACRO}
188
+ item.add_trigger("Alert", "last", ">", macro) # works as threshold
189
+ ```
190
+
191
+ ### Graph
192
+
193
+ ```python
194
+ graph = Graph("CPU Graph",
195
+ graph_type=GraphType.STACKED,
196
+ y_min_type=YAxisType.FIXED, y_min=0,
197
+ y_max_type=YAxisType.FIXED, y_max=100)
198
+
199
+ graph.add_item(item1, "FF0000")
200
+ graph.add_item(item2, "00FF00",
201
+ drawtype=DrawType.BOLD_LINE,
202
+ calc_fnc=CalcFnc.MAX,
203
+ yaxisside=YAxisSide.RIGHT)
204
+ ```
205
+
206
+ ### Dashboard
207
+
208
+ ```python
209
+ from zbxtemplar.entities import Dashboard, DashboardPage
210
+ from zbxtemplar.entities.DashboardWidget import ClassicGraph
211
+
212
+ page = DashboardPage(name="Overview", display_period=120)
213
+ page.add_widget(ClassicGraph(host=template.name, graph=graph, width=36, height=5))
214
+
215
+ dashboard = Dashboard("My Dashboard", display_period=60, auto_start=YesNo.NO)
216
+ dashboard.add_page(page)
217
+ template.add_dashboard(dashboard)
218
+ ```
219
+
220
+ Widgets are concrete subclasses of `Widget` (abstract). Each widget type lives in `zbxtemplar.entities.DashboardWidget`. Creating custom widgets is straightforward — subclass `Widget`, define `type` and `widget_fields()`.
221
+
222
+ ## Global Configuration
223
+
224
+ ```python
225
+ from zbxtemplar.core.ZbxEntity import set_uuid_namespace, set_template_group
226
+
227
+ set_uuid_namespace("My Company") # Deterministic UUIDs scoped to namespace
228
+ set_template_group("My Templates") # Default group for new templates
229
+ ```
@@ -0,0 +1,189 @@
1
+ # zbxtemplar
2
+
3
+ A Pythonic framework for programmatic Zabbix Template generation (Monitoring as Code).
4
+
5
+ The goal is to cover the essential Zabbix configuration primitives — not every possible option. If you need a field that isn't exposed, raw dicts and string expressions give you an escape hatch.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ pip install .
11
+ ```
12
+
13
+ ## Core Architecture
14
+
15
+ The project follows the `src-layout`:
16
+
17
+ - `zbxtemplar.entities` — Domain models: Template, Item, Trigger, Graph, Dashboard.
18
+ - `zbxtemplar.core` — Module contract (`TemplarModule`), loader, serialization, shared types.
19
+ - `zbxtemplar.main` — CLI entry point.
20
+
21
+ ## Module Contract
22
+
23
+ Templates are defined as Python classes that inherit from `TemplarModule`.
24
+ The constructor is the contract — all template logic lives in `__init__`.
25
+
26
+ ```python
27
+ from zbxtemplar.core import TemplarModule
28
+ from zbxtemplar.entities import Template, Item, Trigger, TriggerPriority, Graph
29
+
30
+ class MyTemplate(TemplarModule):
31
+ def __init__(self):
32
+ super().__init__()
33
+
34
+ template = Template(name="My Service")
35
+ template.add_tag("Service", "MyApp")
36
+ template.add_macro("THRESHOLD", 90, "Alert threshold")
37
+
38
+ item = Item("CPU Usage", "system.cpu.util")
39
+ item.add_trigger("High CPU", "last", ">",
40
+ template.get_macro("THRESHOLD"),
41
+ priority=TriggerPriority.HIGH)
42
+ template.add_item(item)
43
+
44
+ self.templates = [template]
45
+ self.triggers = []
46
+ self.graphs = []
47
+ ```
48
+
49
+ A module file can contain multiple `TemplarModule` subclasses.
50
+ The loader discovers all of them by class name.
51
+
52
+ ### Running standalone
53
+
54
+ Add a `__main__` guard to run the module directly:
55
+
56
+ ```python
57
+ if __name__ == "__main__":
58
+ import yaml
59
+ module = MyTemplate()
60
+ print(yaml.dump(module.to_export(), default_flow_style=False, sort_keys=False))
61
+ ```
62
+
63
+ ## CLI
64
+
65
+ ```bash
66
+ # Basic usage
67
+ zbxtemplar module.py output.yml
68
+ python -m zbxtemplar module.py output.yml
69
+
70
+ # With options
71
+ zbxtemplar module.py output.yml \
72
+ --namespace "My Company" \
73
+ --template-group "Custom Templates"
74
+ ```
75
+
76
+ | Argument | Description |
77
+ |---|---|
78
+ | `module` | Path to a `.py` file with `TemplarModule` subclass(es) |
79
+ | `output` | Output YAML file path |
80
+ | `--namespace` | UUID namespace for deterministic ID generation |
81
+ | `--template-group` | Default template group name |
82
+
83
+ ## Programmatic Loading
84
+
85
+ ```python
86
+ from zbxtemplar.core import load_module
87
+
88
+ modules = load_module("path/to/module.py")
89
+ # Returns {"ClassName": <instance>, ...}
90
+
91
+ for name, mod in modules.items():
92
+ export = mod.to_export() # Full zabbix_export dict
93
+ ```
94
+
95
+ ## Entities Reference
96
+
97
+ ### Template
98
+
99
+ ```python
100
+ template = Template(name="My Template", groups=["Custom Group"])
101
+ template.add_tag("Service", "MyApp")
102
+ template.add_macro("TIMEOUT", 30, "Connection timeout")
103
+ template.add_item(item)
104
+ ```
105
+
106
+ ### Item
107
+
108
+ ```python
109
+ item = Item("CPU Usage", "system.cpu.util")
110
+
111
+ # Expression helper — builds Zabbix function strings
112
+ item.expr("last") # last(/host/key)
113
+ item.expr("min", "10m") # min(/host/key,10m)
114
+ item.expr("count", "#10") # count(/host/key,#10)
115
+
116
+ # Single-item trigger shorthand
117
+ item.add_trigger("High CPU", "last", ">", 90,
118
+ priority=TriggerPriority.HIGH,
119
+ description="CPU threshold exceeded")
120
+
121
+ # With function args
122
+ item.add_trigger("Sustained high", "min", ">", 100,
123
+ fn_args=("10m",), priority=TriggerPriority.WARNING)
124
+ ```
125
+
126
+ All enum values (`ItemType`, `ValueType`, `TriggerPriority`, `GraphType`, `DrawType`, `CalcFnc`, etc.) follow the Zabbix export format documentation.
127
+
128
+ ### Trigger
129
+
130
+ ```python
131
+ # Standalone (multi-item) trigger — raw expression string
132
+ Trigger(name="Complex alert",
133
+ expression=item1.expr("last") + ">0 and " + item2.expr("min", "5m") + "<100",
134
+ priority=TriggerPriority.WARNING,
135
+ description="Multi-item condition")
136
+ ```
137
+
138
+ ### Macro
139
+
140
+ Macros work seamlessly in string contexts:
141
+
142
+ ```python
143
+ template.add_macro("MY_MACRO", 1, "Description")
144
+ macro = template.get_macro("MY_MACRO")
145
+
146
+ str(macro) # {$MY_MACRO}
147
+ item.expr("last") + ">" + macro # last(/host/key)>{$MY_MACRO}
148
+ item.add_trigger("Alert", "last", ">", macro) # works as threshold
149
+ ```
150
+
151
+ ### Graph
152
+
153
+ ```python
154
+ graph = Graph("CPU Graph",
155
+ graph_type=GraphType.STACKED,
156
+ y_min_type=YAxisType.FIXED, y_min=0,
157
+ y_max_type=YAxisType.FIXED, y_max=100)
158
+
159
+ graph.add_item(item1, "FF0000")
160
+ graph.add_item(item2, "00FF00",
161
+ drawtype=DrawType.BOLD_LINE,
162
+ calc_fnc=CalcFnc.MAX,
163
+ yaxisside=YAxisSide.RIGHT)
164
+ ```
165
+
166
+ ### Dashboard
167
+
168
+ ```python
169
+ from zbxtemplar.entities import Dashboard, DashboardPage
170
+ from zbxtemplar.entities.DashboardWidget import ClassicGraph
171
+
172
+ page = DashboardPage(name="Overview", display_period=120)
173
+ page.add_widget(ClassicGraph(host=template.name, graph=graph, width=36, height=5))
174
+
175
+ dashboard = Dashboard("My Dashboard", display_period=60, auto_start=YesNo.NO)
176
+ dashboard.add_page(page)
177
+ template.add_dashboard(dashboard)
178
+ ```
179
+
180
+ Widgets are concrete subclasses of `Widget` (abstract). Each widget type lives in `zbxtemplar.entities.DashboardWidget`. Creating custom widgets is straightforward — subclass `Widget`, define `type` and `widget_fields()`.
181
+
182
+ ## Global Configuration
183
+
184
+ ```python
185
+ from zbxtemplar.core.ZbxEntity import set_uuid_namespace, set_template_group
186
+
187
+ set_uuid_namespace("My Company") # Deterministic UUIDs scoped to namespace
188
+ set_template_group("My Templates") # Default group for new templates
189
+ ```
@@ -0,0 +1,34 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "zbxtemplar"
7
+ version = "0.1.0"
8
+ description = "Programmatic Zabbix template generation — Monitoring as Code"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = {file = "LICENSE"}
12
+ authors = [
13
+ {name = "Aleksei Dorozhkin"}
14
+ ]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Intended Audience :: System Administrators",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Programming Language :: Python :: 3",
20
+ "Topic :: System :: Monitoring",
21
+ ]
22
+ dependencies = [
23
+ "pyyaml>=6.0",
24
+ ]
25
+
26
+ [project.urls]
27
+ "Homepage" = "https://github.com/mithraelle/zbxtemplar"
28
+ "Bug Tracker" = "https://github.com/mithraelle/zbxtemplar/issues"
29
+
30
+ [project.scripts]
31
+ zbxtemplar = "zbxtemplar.main:main"
32
+
33
+ [tool.setuptools.packages.find]
34
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
File without changes
@@ -0,0 +1,3 @@
1
+ from zbxtemplar.main import main
2
+
3
+ raise SystemExit(main())
@@ -0,0 +1,47 @@
1
+ import inspect
2
+ import importlib.util
3
+ import os
4
+
5
+ import zbxtemplar.core.ZbxEntity as _zbx
6
+
7
+
8
+ class TemplarModule:
9
+ def __init__(self):
10
+ self.templates = []
11
+ self.triggers = []
12
+ self.graphs = []
13
+
14
+ def to_export(self, version: str = "7.4") -> dict:
15
+ groups = set()
16
+ for t in self.templates:
17
+ for g in t.groups:
18
+ groups.add(g["name"])
19
+
20
+ export = {
21
+ "zabbix_export": {
22
+ "version": version,
23
+ "template_groups": [
24
+ {"uuid": _zbx._make_uuid(g), "name": g}
25
+ for g in sorted(groups)
26
+ ],
27
+ "templates": [t.to_dict() for t in self.templates],
28
+ }
29
+ }
30
+ if self.triggers:
31
+ export["zabbix_export"]["triggers"] = [t.to_dict() for t in self.triggers]
32
+ if self.graphs:
33
+ export["zabbix_export"]["graphs"] = [g.to_dict() for g in self.graphs]
34
+ return export
35
+
36
+
37
+ def load_module(filename: str) -> dict:
38
+ mod_name = os.path.splitext(os.path.basename(filename))[0]
39
+ spec = importlib.util.spec_from_file_location(mod_name, filename)
40
+ mod = importlib.util.module_from_spec(spec)
41
+ spec.loader.exec_module(mod)
42
+
43
+ result = {}
44
+ for name, obj in inspect.getmembers(mod, inspect.isclass):
45
+ if issubclass(obj, TemplarModule) and obj is not TemplarModule:
46
+ result[name] = obj()
47
+ return result
@@ -0,0 +1,165 @@
1
+ from typing import Any, List, Optional, Dict, Union
2
+ from enum import Enum
3
+ import uuid
4
+
5
+ class YesNo(str, Enum):
6
+ NO = "NO"
7
+ YES = "YES"
8
+
9
+ ZBX_TEMPLAR_NAMESPACE = "Zbx Templar"
10
+ ZBX_TEMPLAR_TEMPLATE_GROUP = "Templar Templates"
11
+ _NAMESPACE_UUID = uuid.uuid5(uuid.NAMESPACE_DNS, ZBX_TEMPLAR_NAMESPACE)
12
+
13
+ def set_uuid_namespace(namespace: str):
14
+ global ZBX_TEMPLAR_NAMESPACE, _NAMESPACE_UUID
15
+ ZBX_TEMPLAR_NAMESPACE = namespace
16
+ _NAMESPACE_UUID = uuid.uuid5(uuid.NAMESPACE_DNS, namespace)
17
+
18
+ def set_template_group(group: str):
19
+ global ZBX_TEMPLAR_TEMPLATE_GROUP
20
+ ZBX_TEMPLAR_TEMPLATE_GROUP = group
21
+
22
+ def _make_uuid(seed: str) -> str:
23
+ """Deterministic UUID from seed, masked as UUIDv4. Zabbix rejects UUID5 on import."""
24
+ h = uuid.uuid5(_NAMESPACE_UUID, seed).hex
25
+ return h[:12] + '4' + h[13:16] + hex(0x8 | (int(h[16], 16) & 0x3))[2:] + h[17:]
26
+
27
+ def _serialize(value):
28
+ if hasattr(value, 'to_dict'):
29
+ return value.to_dict()
30
+ if isinstance(value, dict):
31
+ return {k: _serialize(v) for k, v in value.items()}
32
+ if isinstance(value, list):
33
+ return [_serialize(v) for v in value]
34
+ if isinstance(value, Enum):
35
+ return value.value
36
+ if hasattr(value, '__dict__') and not isinstance(value, (str, int, float, bool)):
37
+ return {k: _serialize(v) for k, v in value.__dict__.items()}
38
+ return value
39
+
40
+ class ZbxEntity:
41
+ def __init__(self, name: str, uuid_seed: str = None):
42
+ super().__init__()
43
+ self.name = name
44
+ self.uuid = _make_uuid(uuid_seed or name)
45
+
46
+ def to_dict(self) -> dict[str, Any]:
47
+ # Attributes starting with _ are internal-only (e.g. _host) and skipped here
48
+ result = {}
49
+ for key, value in self.__dict__.items():
50
+ if key.startswith('_'):
51
+ continue
52
+ if value is None or value == {} or value == []:
53
+ continue
54
+ if key == 'tags' and hasattr(self, 'tags_to_list'):
55
+ result[key] = self.tags_to_list()
56
+ elif key == 'macros' and hasattr(self, 'macros_to_list'):
57
+ result[key] = self.macros_to_list()
58
+ else:
59
+ result[key] = _serialize(value)
60
+ return result
61
+
62
+ class Tag:
63
+ def __init__(self, name: str, value: str = ""):
64
+ self.name = name
65
+ self.value = value
66
+
67
+ def to_dict(self):
68
+ return {"tag": self.name, "value": self.value}
69
+
70
+ class WithTags():
71
+ def __init__(self):
72
+ super().__init__()
73
+ self.tags: Dict[str, Tag] = {}
74
+
75
+ def add_tag(self, tag: str, value: str = ""):
76
+ self.tags[tag] = Tag(name=tag, value=value)
77
+ return self
78
+
79
+ def tags_to_list(self):
80
+ return [t.to_dict() for t in self.tags.values()]
81
+
82
+ def load_tags(self, tags: Dict[str, str]):
83
+ """Load tags from a dict.
84
+
85
+ Args:
86
+ tags: Mapping of tag name to value.
87
+ Example: ``{"Application": "CPU", "Component": "OS"}``
88
+ """
89
+ for tag, value in tags.items():
90
+ self.add_tag(tag, value)
91
+ return self
92
+
93
+ class Macro():
94
+ def __init__(self, name: str, value: str, description: Optional[str] = None):
95
+ self.name = name
96
+ self.value = value
97
+ self.description = description
98
+
99
+ @property
100
+ def full_name(self) -> str:
101
+ return f"{{${self.name}}}"
102
+
103
+ def __str__(self):
104
+ return self.full_name
105
+
106
+ def __add__(self, other):
107
+ return str(self) + other
108
+
109
+ def __radd__(self, other):
110
+ return other + str(self)
111
+
112
+ def to_dict(self):
113
+ result = {"macro": self.full_name, "value": self.value}
114
+ if self.description is not None:
115
+ result["description"] = self.description
116
+ return result
117
+
118
+ class WithMacros():
119
+ def __init__(self):
120
+ super().__init__()
121
+ self.macros: Dict[str, Macro] = {}
122
+
123
+ def add_macro(self, name: str, value: Union[str, int], description: Union[str, None] = None):
124
+ clean_name = name.replace("{$", "").replace("}", "")
125
+ self.macros[clean_name] = Macro(name=clean_name, value=str(value), description=description)
126
+ return self
127
+
128
+ def load_macros(self, macros: Union[Dict[str, Union[str, tuple]], List[dict]]):
129
+ """Load macros from a dict or a list of dicts.
130
+
131
+ Accepts two formats:
132
+
133
+ **Dict** — for programmatic use::
134
+
135
+ load_macros({"TIMEOUT": "30"})
136
+ load_macros({"TIMEOUT": ("30", "Connection timeout")})
137
+
138
+ **List of dicts** — for data loaded from YAML/XML::
139
+
140
+ load_macros([
141
+ {"name": "TIMEOUT", "value": "30", "description": "Connection timeout"}
142
+ ])
143
+
144
+ Args:
145
+ macros: Either a ``{name: value}`` / ``{name: (value, description)}`` dict,
146
+ or a list of dicts with ``name``, ``value``, and optional ``description`` keys.
147
+ """
148
+ if isinstance(macros, dict):
149
+ for name, value in macros.items():
150
+ if isinstance(value, tuple):
151
+ self.add_macro(name, value[0], value[1] if len(value) > 1 else None)
152
+ else:
153
+ self.add_macro(name, value)
154
+ else:
155
+ for entry in macros:
156
+ self.add_macro(entry["name"], entry["value"], entry.get("description"))
157
+ return self
158
+
159
+ def macros_to_list(self):
160
+ return [m.to_dict() for m in self.macros.values()]
161
+
162
+ def get_macro(self, name: str) -> Optional[Macro]:
163
+ clean_name = name.replace("{$", "").replace("}", "")
164
+ return self.macros.get(clean_name)
165
+