acex-devkit 2.1.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.
Files changed (62) hide show
  1. {acex_devkit-2.1.0 → acex_devkit-2.1.1}/PKG-INFO +11 -6
  2. {acex_devkit-2.1.0 → acex_devkit-2.1.1}/README.md +9 -5
  3. {acex_devkit-2.1.0 → acex_devkit-2.1.1}/pyproject.toml +2 -1
  4. acex_devkit-2.1.1/src/acex_devkit/configdiffer/__init__.py +4 -0
  5. {acex_devkit-2.1.0 → acex_devkit-2.1.1}/src/acex_devkit/configdiffer/command.py +2 -3
  6. {acex_devkit-2.1.0 → acex_devkit-2.1.1}/src/acex_devkit/configdiffer/configdiffer.py +38 -25
  7. {acex_devkit-2.1.0 → acex_devkit-2.1.1}/src/acex_devkit/configdiffer/diff.py +33 -36
  8. {acex_devkit-2.1.0 → acex_devkit-2.1.1}/src/acex_devkit/configdiffer/old_configdiffer.py +86 -100
  9. {acex_devkit-2.1.0 → acex_devkit-2.1.1}/src/acex_devkit/configdiffer/old_diff.py +27 -22
  10. {acex_devkit-2.1.0 → acex_devkit-2.1.1}/src/acex_devkit/drivers/base.py +13 -15
  11. {acex_devkit-2.1.0 → acex_devkit-2.1.1}/src/acex_devkit/drivers/base_driver.py +12 -7
  12. {acex_devkit-2.1.0 → acex_devkit-2.1.1}/src/acex_devkit/exceptions/__init__.py +10 -0
  13. acex_devkit-2.1.1/src/acex_devkit/models/__init__.py +254 -0
  14. {acex_devkit-2.1.0 → acex_devkit-2.1.1}/src/acex_devkit/models/acl_model.py +52 -37
  15. acex_devkit-2.1.1/src/acex_devkit/models/agent_manifest.py +36 -0
  16. acex_devkit-2.1.1/src/acex_devkit/models/ai_ops.py +24 -0
  17. {acex_devkit-2.1.0 → acex_devkit-2.1.1}/src/acex_devkit/models/asset.py +21 -7
  18. acex_devkit-2.1.1/src/acex_devkit/models/assignment.py +23 -0
  19. {acex_devkit-2.1.0 → acex_devkit-2.1.1}/src/acex_devkit/models/attribute_value.py +12 -11
  20. {acex_devkit-2.1.0 → acex_devkit-2.1.1}/src/acex_devkit/models/augment.py +5 -3
  21. acex_devkit-2.1.1/src/acex_devkit/models/auth_config.py +12 -0
  22. acex_devkit-2.1.1/src/acex_devkit/models/base.py +6 -0
  23. acex_devkit-2.1.1/src/acex_devkit/models/capability.py +11 -0
  24. {acex_devkit-2.1.0 → acex_devkit-2.1.1}/src/acex_devkit/models/collection_agent.py +21 -14
  25. acex_devkit-2.1.1/src/acex_devkit/models/compliance.py +21 -0
  26. acex_devkit-2.1.1/src/acex_devkit/models/composed_configuration.py +1141 -0
  27. acex_devkit-2.1.1/src/acex_devkit/models/config_components.py +78 -0
  28. acex_devkit-2.1.1/src/acex_devkit/models/config_snapshot.py +88 -0
  29. acex_devkit-2.1.1/src/acex_devkit/models/contact.py +31 -0
  30. {acex_devkit-2.1.0 → acex_devkit-2.1.1}/src/acex_devkit/models/container_entry.py +1 -5
  31. {acex_devkit-2.1.0 → acex_devkit-2.1.1}/src/acex_devkit/models/credential.py +11 -12
  32. {acex_devkit-2.1.0 → acex_devkit-2.1.1}/src/acex_devkit/models/external_value.py +15 -14
  33. {acex_devkit-2.1.0 → acex_devkit-2.1.1}/src/acex_devkit/models/lldp_neighbor.py +3 -3
  34. acex_devkit-2.1.1/src/acex_devkit/models/lldp_topology.py +48 -0
  35. acex_devkit-2.1.1/src/acex_devkit/models/logging.py +100 -0
  36. {acex_devkit-2.1.0 → acex_devkit-2.1.1}/src/acex_devkit/models/logical_node.py +12 -4
  37. {acex_devkit-2.1.0 → acex_devkit-2.1.1}/src/acex_devkit/models/management_connection.py +14 -4
  38. acex_devkit-2.1.1/src/acex_devkit/models/node_response.py +72 -0
  39. acex_devkit-2.1.1/src/acex_devkit/models/pagination.py +12 -0
  40. {acex_devkit-2.1.0 → acex_devkit-2.1.1}/src/acex_devkit/models/reference.py +7 -6
  41. acex_devkit-2.1.1/src/acex_devkit/models/region.py +32 -0
  42. acex_devkit-2.1.1/src/acex_devkit/models/site.py +34 -0
  43. acex_devkit-2.1.1/src/acex_devkit/models/spanning_tree.py +85 -0
  44. acex_devkit-2.1.1/src/acex_devkit/models/telemetry_agent.py +90 -0
  45. {acex_devkit-2.1.0 → acex_devkit-2.1.1}/src/acex_devkit/normalizer/base.py +5 -7
  46. {acex_devkit-2.1.0 → acex_devkit-2.1.1}/src/acex_devkit/normalizer/engine.py +9 -11
  47. {acex_devkit-2.1.0 → acex_devkit-2.1.1}/src/acex_devkit/types/__init__.py +8 -8
  48. acex_devkit-2.1.0/src/acex_devkit/configdiffer/__init__.py +0 -3
  49. acex_devkit-2.1.0/src/acex_devkit/models/__init__.py +0 -87
  50. acex_devkit-2.1.0/src/acex_devkit/models/base.py +0 -5
  51. acex_devkit-2.1.0/src/acex_devkit/models/composed_configuration.py +0 -1039
  52. acex_devkit-2.1.0/src/acex_devkit/models/contact.py +0 -18
  53. acex_devkit-2.1.0/src/acex_devkit/models/logging.py +0 -90
  54. acex_devkit-2.1.0/src/acex_devkit/models/node_response.py +0 -57
  55. acex_devkit-2.1.0/src/acex_devkit/models/pagination.py +0 -11
  56. acex_devkit-2.1.0/src/acex_devkit/models/region.py +0 -23
  57. acex_devkit-2.1.0/src/acex_devkit/models/site.py +0 -20
  58. acex_devkit-2.1.0/src/acex_devkit/models/spanning_tree.py +0 -72
  59. {acex_devkit-2.1.0 → acex_devkit-2.1.1}/src/acex_devkit/__init__.py +2 -2
  60. {acex_devkit-2.1.0 → acex_devkit-2.1.1}/src/acex_devkit/drivers/__init__.py +2 -2
  61. {acex_devkit-2.1.0 → acex_devkit-2.1.1}/src/acex_devkit/models/ned.py +0 -0
  62. {acex_devkit-2.1.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.1.0
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.1.0"
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"
@@ -0,0 +1,4 @@
1
+ from .configdiffer import ConfigDiffer
2
+ from .diff import AttributeChange, ComponentChange, ComponentDiffOp, Diff
3
+
4
+ __all__ = ["ConfigDiffer", "AttributeChange", "ComponentChange", "ComponentDiffOp", "Diff"]
@@ -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: Tuple[str, ...] = Field(default_factory=tuple)
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.models.composed_configuration import ComposedConfiguration
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 = ()) -> Dict[tuple, BaseModel]:
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(self, desired: Dict[tuple, BaseModel], observed: Dict[tuple, BaseModel]) -> List[ComponentChange]:
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: Dict[tuple, BaseModel], observed: Dict[tuple, BaseModel]) -> List[ComponentChange]:
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: Dict[tuple, Dict[str, BaseModel]] = defaultdict(dict)
122
- observed_by_container: Dict[tuple, Dict[str, BaseModel]] = defaultdict(dict)
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: Dict[tuple, tuple[str, BaseModel]] = {}
137
- o_by_key: Dict[str, BaseModel] = dict(o_components)
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(self._make_change(
163
- ComponentDiffOp.CHANGE, container_path + (d_key,),
164
- before=o_model, after=d_model,
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(self._make_change(
168
- ComponentDiffOp.ADD, container_path + (d_key,),
169
- after=d_model,
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(self._make_change(
176
- ComponentDiffOp.REMOVE, container_path + (o_key,),
177
- before=o_model,
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 pydantic import BaseModel, Field, field_serializer, computed_field
10
- from typing import Any, Dict, List, Optional, Type
11
- from enum import Enum
9
+ from enum import StrEnum
10
+ from typing import Any
12
11
 
13
- from acex_devkit.models.attribute_value import AttributeValue
12
+ from pydantic import BaseModel, Field, computed_field, field_serializer
14
13
 
15
14
 
16
- class ComponentDiffOp(str, Enum):
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: Optional[Any] = None
29
- after: Optional[Any] = None
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: List[str] # e.g., ['interfaces', 'GigabitEthernet0-0-1']
47
- component_type: Type[Any] # e.g., 'FrontpanelPort', 'Loopback', 'Vlan'
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: Optional[Any] = None
52
- after: Optional[Any] = None
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: Optional[Dict[str, Any]] = None
56
- after_dict: Optional[Dict[str, Any]] = None
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: List[AttributeChange] = Field(default_factory=list)
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: Type[Any]) -> str:
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
- added: List[ComponentChange] = Field(default_factory=list)
85
- removed: List[ComponentChange] = Field(default_factory=list)
86
- changed: List[ComponentChange] = Field(default_factory=list)
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) -> Dict[str, Any]:
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) -> List[ComponentChange]:
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) -> List[ComponentChange]:
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: List[str]) -> List[ComponentChange]:
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]