acex-devkit 2.0.0__tar.gz → 2.1.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.
- {acex_devkit-2.0.0 → acex_devkit-2.1.1}/PKG-INFO +11 -6
- {acex_devkit-2.0.0 → acex_devkit-2.1.1}/README.md +9 -5
- {acex_devkit-2.0.0 → acex_devkit-2.1.1}/pyproject.toml +2 -1
- acex_devkit-2.1.1/src/acex_devkit/configdiffer/__init__.py +4 -0
- {acex_devkit-2.0.0 → acex_devkit-2.1.1}/src/acex_devkit/configdiffer/command.py +2 -3
- {acex_devkit-2.0.0 → acex_devkit-2.1.1}/src/acex_devkit/configdiffer/configdiffer.py +38 -25
- {acex_devkit-2.0.0 → acex_devkit-2.1.1}/src/acex_devkit/configdiffer/diff.py +33 -36
- {acex_devkit-2.0.0 → acex_devkit-2.1.1}/src/acex_devkit/configdiffer/old_configdiffer.py +86 -100
- {acex_devkit-2.0.0 → acex_devkit-2.1.1}/src/acex_devkit/configdiffer/old_diff.py +27 -22
- {acex_devkit-2.0.0 → acex_devkit-2.1.1}/src/acex_devkit/drivers/base.py +13 -15
- {acex_devkit-2.0.0 → acex_devkit-2.1.1}/src/acex_devkit/drivers/base_driver.py +12 -7
- {acex_devkit-2.0.0 → acex_devkit-2.1.1}/src/acex_devkit/exceptions/__init__.py +10 -0
- acex_devkit-2.1.1/src/acex_devkit/models/__init__.py +254 -0
- {acex_devkit-2.0.0 → acex_devkit-2.1.1}/src/acex_devkit/models/acl_model.py +52 -37
- acex_devkit-2.1.1/src/acex_devkit/models/agent_manifest.py +36 -0
- acex_devkit-2.1.1/src/acex_devkit/models/ai_ops.py +24 -0
- {acex_devkit-2.0.0 → acex_devkit-2.1.1}/src/acex_devkit/models/asset.py +21 -7
- acex_devkit-2.1.1/src/acex_devkit/models/assignment.py +23 -0
- {acex_devkit-2.0.0 → acex_devkit-2.1.1}/src/acex_devkit/models/attribute_value.py +12 -11
- {acex_devkit-2.0.0 → acex_devkit-2.1.1}/src/acex_devkit/models/augment.py +5 -3
- acex_devkit-2.1.1/src/acex_devkit/models/auth_config.py +12 -0
- acex_devkit-2.1.1/src/acex_devkit/models/base.py +6 -0
- acex_devkit-2.1.1/src/acex_devkit/models/capability.py +11 -0
- {acex_devkit-2.0.0 → acex_devkit-2.1.1}/src/acex_devkit/models/collection_agent.py +21 -14
- acex_devkit-2.1.1/src/acex_devkit/models/compliance.py +21 -0
- acex_devkit-2.1.1/src/acex_devkit/models/composed_configuration.py +1141 -0
- acex_devkit-2.1.1/src/acex_devkit/models/config_components.py +78 -0
- acex_devkit-2.1.1/src/acex_devkit/models/config_snapshot.py +88 -0
- acex_devkit-2.1.1/src/acex_devkit/models/contact.py +31 -0
- {acex_devkit-2.0.0 → acex_devkit-2.1.1}/src/acex_devkit/models/container_entry.py +1 -5
- {acex_devkit-2.0.0 → acex_devkit-2.1.1}/src/acex_devkit/models/credential.py +11 -12
- {acex_devkit-2.0.0 → acex_devkit-2.1.1}/src/acex_devkit/models/external_value.py +15 -14
- {acex_devkit-2.0.0 → acex_devkit-2.1.1}/src/acex_devkit/models/lldp_neighbor.py +3 -3
- acex_devkit-2.1.1/src/acex_devkit/models/lldp_topology.py +48 -0
- acex_devkit-2.1.1/src/acex_devkit/models/logging.py +100 -0
- {acex_devkit-2.0.0 → acex_devkit-2.1.1}/src/acex_devkit/models/logical_node.py +12 -4
- {acex_devkit-2.0.0 → acex_devkit-2.1.1}/src/acex_devkit/models/management_connection.py +14 -4
- acex_devkit-2.1.1/src/acex_devkit/models/node_response.py +72 -0
- acex_devkit-2.1.1/src/acex_devkit/models/pagination.py +12 -0
- {acex_devkit-2.0.0 → acex_devkit-2.1.1}/src/acex_devkit/models/reference.py +7 -6
- acex_devkit-2.1.1/src/acex_devkit/models/region.py +32 -0
- acex_devkit-2.1.1/src/acex_devkit/models/site.py +34 -0
- acex_devkit-2.1.1/src/acex_devkit/models/spanning_tree.py +85 -0
- acex_devkit-2.1.1/src/acex_devkit/models/telemetry_agent.py +90 -0
- {acex_devkit-2.0.0 → acex_devkit-2.1.1}/src/acex_devkit/normalizer/base.py +5 -7
- {acex_devkit-2.0.0 → acex_devkit-2.1.1}/src/acex_devkit/normalizer/engine.py +9 -11
- {acex_devkit-2.0.0 → acex_devkit-2.1.1}/src/acex_devkit/types/__init__.py +8 -8
- acex_devkit-2.0.0/src/acex_devkit/configdiffer/__init__.py +0 -3
- acex_devkit-2.0.0/src/acex_devkit/models/__init__.py +0 -87
- acex_devkit-2.0.0/src/acex_devkit/models/base.py +0 -5
- acex_devkit-2.0.0/src/acex_devkit/models/composed_configuration.py +0 -1015
- acex_devkit-2.0.0/src/acex_devkit/models/contact.py +0 -18
- acex_devkit-2.0.0/src/acex_devkit/models/logging.py +0 -89
- acex_devkit-2.0.0/src/acex_devkit/models/node_response.py +0 -57
- acex_devkit-2.0.0/src/acex_devkit/models/pagination.py +0 -11
- acex_devkit-2.0.0/src/acex_devkit/models/region.py +0 -23
- acex_devkit-2.0.0/src/acex_devkit/models/site.py +0 -20
- acex_devkit-2.0.0/src/acex_devkit/models/spanning_tree.py +0 -72
- {acex_devkit-2.0.0 → acex_devkit-2.1.1}/src/acex_devkit/__init__.py +2 -2
- {acex_devkit-2.0.0 → acex_devkit-2.1.1}/src/acex_devkit/drivers/__init__.py +2 -2
- {acex_devkit-2.0.0 → acex_devkit-2.1.1}/src/acex_devkit/models/ned.py +0 -0
- {acex_devkit-2.0.0 → acex_devkit-2.1.1}/src/acex_devkit/normalizer/__init__.py +5 -5
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: acex-devkit
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.1.1
|
|
4
4
|
Summary: ACE-X DevKit - Development kit for building ACE-X drivers and plugins
|
|
5
5
|
License: AGPL-3.0
|
|
6
6
|
Keywords: automation,devkit,sdk,drivers,plugins
|
|
@@ -14,6 +14,7 @@ Classifier: Programming Language :: Python :: 3.14
|
|
|
14
14
|
Requires-Dist: deepdiff (>=8.6.1,<9.0.0)
|
|
15
15
|
Requires-Dist: ipaddress (>=1.0.23,<2.0.0)
|
|
16
16
|
Requires-Dist: pydantic (>=2.12.5,<3.0.0)
|
|
17
|
+
Requires-Dist: sqlmodel (>=0.0.27,<0.0.28)
|
|
17
18
|
Requires-Dist: typing-extensions (>=4.0.0,<5.0.0)
|
|
18
19
|
Project-URL: Homepage, https://github.com/acex-labs/acex
|
|
19
20
|
Project-URL: Repository, https://github.com/acex-labs/acex
|
|
@@ -37,41 +38,45 @@ pip install acex-devkit
|
|
|
37
38
|
from acex_devkit.drivers import NetworkElementDriver, TransportBase, RendererBase, ParserBase
|
|
38
39
|
from acex_devkit.models import ComposedConfiguration
|
|
39
40
|
|
|
41
|
+
|
|
40
42
|
class MyRenderer(RendererBase):
|
|
41
43
|
def render(self, model: dict) -> str:
|
|
42
44
|
# Implement your rendering logic
|
|
43
45
|
pass
|
|
44
46
|
|
|
47
|
+
|
|
45
48
|
class MyTransport(TransportBase):
|
|
46
49
|
def connect(self) -> None:
|
|
47
50
|
# Implement connection logic
|
|
48
51
|
pass
|
|
49
|
-
|
|
52
|
+
|
|
50
53
|
def send(self, payload: Any) -> None:
|
|
51
54
|
# Implement send logic
|
|
52
55
|
pass
|
|
53
|
-
|
|
56
|
+
|
|
54
57
|
def verify(self) -> bool:
|
|
55
58
|
# Implement verification logic
|
|
56
59
|
return True
|
|
57
|
-
|
|
60
|
+
|
|
58
61
|
def rollback(self) -> None:
|
|
59
62
|
# Implement rollback logic
|
|
60
63
|
pass
|
|
61
64
|
|
|
65
|
+
|
|
62
66
|
class MyParser(ParserBase):
|
|
63
67
|
def parse(self, configuration: str) -> ComposedConfiguration:
|
|
64
68
|
# Implement parsing logic
|
|
65
69
|
pass
|
|
66
70
|
|
|
71
|
+
|
|
67
72
|
class MyDriver(NetworkElementDriver):
|
|
68
73
|
renderer_class = MyRenderer
|
|
69
74
|
transport_class = MyTransport
|
|
70
75
|
parser_class = MyParser
|
|
71
|
-
|
|
76
|
+
|
|
72
77
|
def render(self, configuration: ComposedConfiguration, asset):
|
|
73
78
|
return self.renderer.render(configuration, asset)
|
|
74
|
-
|
|
79
|
+
|
|
75
80
|
def parse(self, configuration: str) -> ComposedConfiguration:
|
|
76
81
|
return self.parser.parse(configuration)
|
|
77
82
|
```
|
|
@@ -16,41 +16,45 @@ pip install acex-devkit
|
|
|
16
16
|
from acex_devkit.drivers import NetworkElementDriver, TransportBase, RendererBase, ParserBase
|
|
17
17
|
from acex_devkit.models import ComposedConfiguration
|
|
18
18
|
|
|
19
|
+
|
|
19
20
|
class MyRenderer(RendererBase):
|
|
20
21
|
def render(self, model: dict) -> str:
|
|
21
22
|
# Implement your rendering logic
|
|
22
23
|
pass
|
|
23
24
|
|
|
25
|
+
|
|
24
26
|
class MyTransport(TransportBase):
|
|
25
27
|
def connect(self) -> None:
|
|
26
28
|
# Implement connection logic
|
|
27
29
|
pass
|
|
28
|
-
|
|
30
|
+
|
|
29
31
|
def send(self, payload: Any) -> None:
|
|
30
32
|
# Implement send logic
|
|
31
33
|
pass
|
|
32
|
-
|
|
34
|
+
|
|
33
35
|
def verify(self) -> bool:
|
|
34
36
|
# Implement verification logic
|
|
35
37
|
return True
|
|
36
|
-
|
|
38
|
+
|
|
37
39
|
def rollback(self) -> None:
|
|
38
40
|
# Implement rollback logic
|
|
39
41
|
pass
|
|
40
42
|
|
|
43
|
+
|
|
41
44
|
class MyParser(ParserBase):
|
|
42
45
|
def parse(self, configuration: str) -> ComposedConfiguration:
|
|
43
46
|
# Implement parsing logic
|
|
44
47
|
pass
|
|
45
48
|
|
|
49
|
+
|
|
46
50
|
class MyDriver(NetworkElementDriver):
|
|
47
51
|
renderer_class = MyRenderer
|
|
48
52
|
transport_class = MyTransport
|
|
49
53
|
parser_class = MyParser
|
|
50
|
-
|
|
54
|
+
|
|
51
55
|
def render(self, configuration: ComposedConfiguration, asset):
|
|
52
56
|
return self.renderer.render(configuration, asset)
|
|
53
|
-
|
|
57
|
+
|
|
54
58
|
def parse(self, configuration: str) -> ComposedConfiguration:
|
|
55
59
|
return self.parser.parse(configuration)
|
|
56
60
|
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "acex-devkit"
|
|
3
|
-
version = "2.
|
|
3
|
+
version = "2.1.1"
|
|
4
4
|
description = "ACE-X DevKit - Development kit for building ACE-X drivers and plugins"
|
|
5
5
|
authors = ["Johan Lahti <johan.lahti@acebit.se>"]
|
|
6
6
|
readme = "README.md"
|
|
@@ -18,6 +18,7 @@ pydantic = "^2.12.5"
|
|
|
18
18
|
typing-extensions = "^4.0.0"
|
|
19
19
|
deepdiff = "^8.6.1"
|
|
20
20
|
ipaddress = "^1.0.23"
|
|
21
|
+
sqlmodel = "^0.0.27"
|
|
21
22
|
|
|
22
23
|
[tool.poetry.group.dev.dependencies]
|
|
23
24
|
pytest = "^8.0.0"
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
from pydantic import BaseModel, Field
|
|
2
|
-
from typing import Optional, Tuple
|
|
3
2
|
|
|
4
3
|
|
|
5
4
|
class Context(BaseModel):
|
|
6
|
-
path:
|
|
5
|
+
path: tuple[str, ...] = Field(default_factory=tuple)
|
|
7
6
|
# priority: int = 100 även här??
|
|
8
7
|
|
|
9
8
|
model_config = {
|
|
10
9
|
"frozen": True, # immutable
|
|
11
10
|
}
|
|
12
11
|
|
|
12
|
+
|
|
13
13
|
class Command(BaseModel):
|
|
14
14
|
context: Context
|
|
15
15
|
command: str
|
|
@@ -18,4 +18,3 @@ class Command(BaseModel):
|
|
|
18
18
|
model_config = {
|
|
19
19
|
"frozen": True,
|
|
20
20
|
}
|
|
21
|
-
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
from collections import defaultdict
|
|
2
|
+
from typing import get_args
|
|
3
|
+
|
|
2
4
|
from pydantic import BaseModel
|
|
3
|
-
from typing import Any, Dict, List, get_args
|
|
4
5
|
|
|
5
|
-
from acex_devkit.
|
|
6
|
+
from acex_devkit.configdiffer.diff import AttributeChange, ComponentChange, ComponentDiffOp, Diff
|
|
6
7
|
from acex_devkit.models.attribute_value import AttributeValue
|
|
8
|
+
from acex_devkit.models.composed_configuration import ComposedConfiguration
|
|
7
9
|
from acex_devkit.models.container_entry import ContainerEntry
|
|
8
|
-
from acex_devkit.configdiffer.diff import Diff, ComponentChange, AttributeChange, ComponentDiffOp
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
class ConfigDiffer:
|
|
12
|
-
|
|
13
13
|
def _is_leaf(self, model: BaseModel) -> bool:
|
|
14
14
|
"""A model is a leaf if it has at least one direct AttributeValue field."""
|
|
15
15
|
for field_info in model.model_fields.values():
|
|
@@ -21,13 +21,15 @@ class ConfigDiffer:
|
|
|
21
21
|
return True
|
|
22
22
|
return False
|
|
23
23
|
|
|
24
|
-
def _flatten(self, model: BaseModel, path: tuple = ()) ->
|
|
24
|
+
def _flatten(self, model: BaseModel, path: tuple = ()) -> dict[tuple, BaseModel]:
|
|
25
25
|
"""
|
|
26
26
|
Recursively walk a model tree, collecting leaf nodes.
|
|
27
27
|
A leaf is any BaseModel that has direct AttributeValue fields.
|
|
28
28
|
A model can be both a leaf (stored) and have children (recursed into).
|
|
29
29
|
Returns a flat mapping of path → leaf instance.
|
|
30
30
|
"""
|
|
31
|
+
if model is None:
|
|
32
|
+
return {}
|
|
31
33
|
result = {}
|
|
32
34
|
|
|
33
35
|
# Store this model if it has AttributeValue fields (e.g. SystemConfig, NtpServer, NetworkInstance)
|
|
@@ -57,8 +59,7 @@ class ConfigDiffer:
|
|
|
57
59
|
|
|
58
60
|
return result
|
|
59
61
|
|
|
60
|
-
|
|
61
|
-
def _attribute_changes(self, before: BaseModel, after: BaseModel) -> List[AttributeChange]:
|
|
62
|
+
def _attribute_changes(self, before: BaseModel, after: BaseModel) -> list[AttributeChange]:
|
|
62
63
|
"""Compare two component instances and return a list of changed attributes."""
|
|
63
64
|
changes = []
|
|
64
65
|
all_fields = set(before.model_fields.keys()) | set(after.model_fields.keys())
|
|
@@ -97,7 +98,9 @@ class ConfigDiffer:
|
|
|
97
98
|
changed_attributes=self._attribute_changes(before, after) if (before and after) else [],
|
|
98
99
|
)
|
|
99
100
|
|
|
100
|
-
def _diff_singletons(
|
|
101
|
+
def _diff_singletons(
|
|
102
|
+
self, desired: dict[tuple, BaseModel], observed: dict[tuple, BaseModel]
|
|
103
|
+
) -> list[ComponentChange]:
|
|
101
104
|
"""Diff leaves that are not ContainerEntry — matched by exact path."""
|
|
102
105
|
changes = []
|
|
103
106
|
all_paths = set(desired) | set(observed)
|
|
@@ -113,13 +116,13 @@ class ConfigDiffer:
|
|
|
113
116
|
changes.append(self._make_change(ComponentDiffOp.REMOVE, path, before=o))
|
|
114
117
|
return changes
|
|
115
118
|
|
|
116
|
-
def _diff_entries(self, desired:
|
|
119
|
+
def _diff_entries(self, desired: dict[tuple, BaseModel], observed: dict[tuple, BaseModel]) -> list[ComponentChange]:
|
|
117
120
|
"""Diff ContainerEntry leaves — matched by identity_fields within each container scope."""
|
|
118
121
|
changes = []
|
|
119
122
|
|
|
120
123
|
# Group by container path (parent = path[:-1]) so identity matching is scoped
|
|
121
|
-
desired_by_container:
|
|
122
|
-
observed_by_container:
|
|
124
|
+
desired_by_container: dict[tuple, dict[str, BaseModel]] = defaultdict(dict)
|
|
125
|
+
observed_by_container: dict[tuple, dict[str, BaseModel]] = defaultdict(dict)
|
|
123
126
|
|
|
124
127
|
for path, model in desired.items():
|
|
125
128
|
desired_by_container[path[:-1]][path[-1]] = model
|
|
@@ -133,8 +136,8 @@ class ConfigDiffer:
|
|
|
133
136
|
o_components = observed_by_container.get(container_path, {})
|
|
134
137
|
|
|
135
138
|
# Build identity → (key, model) lookup for observed
|
|
136
|
-
o_by_identity:
|
|
137
|
-
o_by_key:
|
|
139
|
+
o_by_identity: dict[tuple, tuple[str, BaseModel]] = {}
|
|
140
|
+
o_by_key: dict[str, BaseModel] = dict(o_components)
|
|
138
141
|
for key, model in o_components.items():
|
|
139
142
|
identity = self._identity_key(model)
|
|
140
143
|
if identity: # non-empty identity_fields — match by field values
|
|
@@ -159,23 +162,33 @@ class ConfigDiffer:
|
|
|
159
162
|
if o_model:
|
|
160
163
|
matched_o_keys.add(o_key)
|
|
161
164
|
if d_model != o_model:
|
|
162
|
-
changes.append(
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
165
|
+
changes.append(
|
|
166
|
+
self._make_change(
|
|
167
|
+
ComponentDiffOp.CHANGE,
|
|
168
|
+
container_path + (d_key,),
|
|
169
|
+
before=o_model,
|
|
170
|
+
after=d_model,
|
|
171
|
+
)
|
|
172
|
+
)
|
|
166
173
|
else:
|
|
167
|
-
changes.append(
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
174
|
+
changes.append(
|
|
175
|
+
self._make_change(
|
|
176
|
+
ComponentDiffOp.ADD,
|
|
177
|
+
container_path + (d_key,),
|
|
178
|
+
after=d_model,
|
|
179
|
+
)
|
|
180
|
+
)
|
|
171
181
|
|
|
172
182
|
# Remaining observed entries that weren't matched → removed
|
|
173
183
|
for o_key, o_model in o_components.items():
|
|
174
184
|
if o_key not in matched_o_keys:
|
|
175
|
-
changes.append(
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
185
|
+
changes.append(
|
|
186
|
+
self._make_change(
|
|
187
|
+
ComponentDiffOp.REMOVE,
|
|
188
|
+
container_path + (o_key,),
|
|
189
|
+
before=o_model,
|
|
190
|
+
)
|
|
191
|
+
)
|
|
179
192
|
|
|
180
193
|
return changes
|
|
181
194
|
|
|
@@ -6,15 +6,15 @@ added, removed, or changed when comparing two configurations. This enables drive
|
|
|
6
6
|
to render device-specific command patches based on the component changes.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
-
from
|
|
10
|
-
from typing import Any
|
|
11
|
-
from enum import Enum
|
|
9
|
+
from enum import StrEnum
|
|
10
|
+
from typing import Any
|
|
12
11
|
|
|
13
|
-
from
|
|
12
|
+
from pydantic import BaseModel, Field, computed_field, field_serializer
|
|
14
13
|
|
|
15
14
|
|
|
16
|
-
class ComponentDiffOp(
|
|
15
|
+
class ComponentDiffOp(StrEnum):
|
|
17
16
|
"""Operation type for a component change"""
|
|
17
|
+
|
|
18
18
|
ADD = "add"
|
|
19
19
|
REMOVE = "remove"
|
|
20
20
|
CHANGE = "change"
|
|
@@ -22,17 +22,18 @@ class ComponentDiffOp(str, Enum):
|
|
|
22
22
|
|
|
23
23
|
class AttributeChange(BaseModel):
|
|
24
24
|
"""Represents a change to a single attribute within a component"""
|
|
25
|
+
|
|
25
26
|
model_config = {"arbitrary_types_allowed": True}
|
|
26
27
|
|
|
27
28
|
attribute_name: str
|
|
28
|
-
before:
|
|
29
|
-
after:
|
|
29
|
+
before: Any | None = None
|
|
30
|
+
after: Any | None = None
|
|
30
31
|
|
|
31
32
|
|
|
32
33
|
class ComponentChange(BaseModel):
|
|
33
34
|
"""
|
|
34
35
|
Represents a change to a ConfigComponent.
|
|
35
|
-
|
|
36
|
+
|
|
36
37
|
This is the core structure that drivers will use to render patches.
|
|
37
38
|
It contains:
|
|
38
39
|
- The component path (e.g., ['interfaces', 'GigabitEthernet0-0-1'])
|
|
@@ -42,26 +43,27 @@ class ComponentChange(BaseModel):
|
|
|
42
43
|
- The actual ConfigComponent objects (for driver access)
|
|
43
44
|
- Individual attribute changes (for CHANGE operations)
|
|
44
45
|
"""
|
|
46
|
+
|
|
45
47
|
op: ComponentDiffOp
|
|
46
|
-
path:
|
|
47
|
-
component_type:
|
|
48
|
+
path: list[str] # e.g., ['interfaces', 'GigabitEthernet0-0-1']
|
|
49
|
+
component_type: type[Any] # e.g., 'FrontpanelPort', 'Loopback', 'Vlan'
|
|
48
50
|
component_name: str # e.g., 'GigabitEthernet0-0-1', 'Lo0', '100'
|
|
49
|
-
|
|
51
|
+
|
|
50
52
|
# Actual ConfigComponent objects
|
|
51
|
-
before:
|
|
52
|
-
after:
|
|
53
|
+
before: Any | None = None
|
|
54
|
+
after: Any | None = None
|
|
53
55
|
|
|
54
56
|
# Raw dict representations (for formatters and generic processing)
|
|
55
|
-
before_dict:
|
|
56
|
-
after_dict:
|
|
57
|
-
|
|
57
|
+
before_dict: dict[str, Any] | None = None
|
|
58
|
+
after_dict: dict[str, Any] | None = None
|
|
59
|
+
|
|
58
60
|
# For CHANGE operations: list of changed attributes
|
|
59
|
-
changed_attributes:
|
|
60
|
-
|
|
61
|
+
changed_attributes: list[AttributeChange] = Field(default_factory=list)
|
|
62
|
+
|
|
61
63
|
model_config = {"arbitrary_types_allowed": True}
|
|
62
64
|
|
|
63
65
|
@field_serializer("component_type")
|
|
64
|
-
def serialize_component_type(self, v:
|
|
66
|
+
def serialize_component_type(self, v: type[Any]) -> str:
|
|
65
67
|
return v.__name__
|
|
66
68
|
|
|
67
69
|
@property
|
|
@@ -77,13 +79,14 @@ class ComponentChange(BaseModel):
|
|
|
77
79
|
class Diff(BaseModel):
|
|
78
80
|
"""
|
|
79
81
|
Result of comparing two ComposedConfiguration objects.
|
|
80
|
-
|
|
82
|
+
|
|
81
83
|
This provides a component-centric view of what changed, suitable for
|
|
82
84
|
rendering device-specific patches.
|
|
83
85
|
"""
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
86
|
+
|
|
87
|
+
added: list[ComponentChange] = Field(default_factory=list)
|
|
88
|
+
removed: list[ComponentChange] = Field(default_factory=list)
|
|
89
|
+
changed: list[ComponentChange] = Field(default_factory=list)
|
|
87
90
|
|
|
88
91
|
# Total number of config points in desired/observed configs — populated by ConfigDiffer
|
|
89
92
|
total_desired: int = 0
|
|
@@ -114,7 +117,7 @@ class Diff(BaseModel):
|
|
|
114
117
|
"""Check if there are no changes"""
|
|
115
118
|
return not (self.added or self.removed or self.changed)
|
|
116
119
|
|
|
117
|
-
def summary(self) ->
|
|
120
|
+
def summary(self) -> dict[str, Any]:
|
|
118
121
|
"""Get a summary of changes and compliance metadata"""
|
|
119
122
|
return {
|
|
120
123
|
"added": len(self.added),
|
|
@@ -127,26 +130,20 @@ class Diff(BaseModel):
|
|
|
127
130
|
"compliance_percentage": self.compliance_percentage,
|
|
128
131
|
}
|
|
129
132
|
|
|
130
|
-
def get_all_changes(self) ->
|
|
133
|
+
def get_all_changes(self) -> list[ComponentChange]:
|
|
131
134
|
"""Get all changes in a single list"""
|
|
132
135
|
return self.added + self.removed + self.changed
|
|
133
136
|
|
|
134
|
-
def get_changes_by_type(self, component_type: str) ->
|
|
137
|
+
def get_changes_by_type(self, component_type: str) -> list[ComponentChange]:
|
|
135
138
|
"""Get all changes for a specific component type (e.g., 'Loopback')"""
|
|
136
|
-
return [
|
|
137
|
-
change for change in self.get_all_changes()
|
|
138
|
-
if change.component_type.__name__ == component_type
|
|
139
|
-
]
|
|
139
|
+
return [change for change in self.get_all_changes() if change.component_type.__name__ == component_type]
|
|
140
140
|
|
|
141
|
-
def get_changes_by_path_prefix(self, path_prefix:
|
|
141
|
+
def get_changes_by_path_prefix(self, path_prefix: list[str]) -> list[ComponentChange]:
|
|
142
142
|
"""
|
|
143
143
|
Get all changes under a specific path prefix.
|
|
144
|
-
|
|
144
|
+
|
|
145
145
|
Example:
|
|
146
146
|
get_changes_by_path_prefix(['interfaces']) -> all interface changes
|
|
147
147
|
get_changes_by_path_prefix(['network_instances', 'default']) -> changes in default VRF
|
|
148
148
|
"""
|
|
149
|
-
return [
|
|
150
|
-
change for change in self.get_all_changes()
|
|
151
|
-
if change.path[:len(path_prefix)] == path_prefix
|
|
152
|
-
]
|
|
149
|
+
return [change for change in self.get_all_changes() if change.path[: len(path_prefix)] == path_prefix]
|