Skip to content

Commit 698d2e4

Browse files
authored
Merge pull request #95 from opsmill/copilot/fix-94
Add support for List attribute kind in schema generator
2 parents 61006e6 + c7dec89 commit 698d2e4

File tree

4 files changed

+53
-10
lines changed

4 files changed

+53
-10
lines changed

infrahub_sync/__init__.py

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66

77
import pydantic
88
from diffsync.enum import DiffSyncFlags
9-
from jinja2 import Template
9+
from jinja2 import StrictUndefined
10+
from jinja2.nativetypes import NativeEnvironment
1011
from netutils.ip import is_ip_within as netutils_is_ip_within
1112
from packaging import version
1213

@@ -170,17 +171,29 @@ def apply_filters(cls, item: dict[str, Any], filters: list[SchemaMappingFilter])
170171

171172
@classmethod
172173
def apply_transform(cls, item: dict[str, Any], transform_expr: str, field: str) -> None:
173-
"""Apply a transformation expression using Jinja2 to a specified field in the item."""
174-
try:
175-
# Create a Jinja2 template from the transformation expression
176-
template = Template(transform_expr)
174+
"""Apply a transformation expression using Jinja2 to a specified field in the item.
177175
178-
# Render the template using the item's context
176+
Uses Jinja's NativeEnvironment so expressions return native Python types
177+
(list/dict/bool/int/str) instead of always strings.
178+
"""
179+
try:
180+
native_env = NativeEnvironment(
181+
undefined=StrictUndefined, # fail fast on missing keys
182+
autoescape=False,
183+
trim_blocks=True,
184+
lstrip_blocks=True,
185+
)
186+
# Compile the template with the native env
187+
template = native_env.from_string(transform_expr)
188+
189+
# Render with the item as context → returns a native Python value
179190
transformed_value = template.render(**item)
180191

181-
# Assign the result back to the item if not empty
182-
if transformed_value:
192+
# Always assign the result, even if it's an empty list/dict/False/0.
193+
# Only skip if the result is literally None (meaning "don't set").
194+
if transformed_value is not None:
183195
item[field] = transformed_value
196+
184197
except Exception as exc:
185198
msg = f"Failed to transform '{field}' with '{transform_expr}': {exc}"
186199
raise ValueError(msg) from exc

infrahub_sync/adapters/infrahub.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import copy
4+
import ipaddress
45
import os
56
from typing import TYPE_CHECKING, Any
67

@@ -294,6 +295,14 @@ def infrahub_node_to_diffsync(self, node: InfrahubNodeSync) -> dict[str, Any]:
294295
# got a ValidationError from pydantic while trying to get the model(**data)
295296
# for IPHost and IPInterface
296297
data[attr_name] = str(attr.value) if attr.value and not isinstance(attr.value, str) else attr.value
298+
val = attr.value
299+
if isinstance(
300+
val,
301+
(ipaddress.IPv4Interface, ipaddress.IPv6Interface, ipaddress.IPv4Network, ipaddress.IPv6Network),
302+
):
303+
data[attr_name] = str(val)
304+
else:
305+
data[attr_name] = val
297306

298307
for rel_schema in node._schema.relationships:
299308
if not has_field(config=self.config, name=node._schema.kind, field=rel_schema.name):

infrahub_sync/adapters/ipfabricsync.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from typing import Self
77
except ImportError:
88
from typing_extensions import Self
9+
import json
910

1011
try:
1112
from ipfabric import IPFClient
@@ -81,7 +82,9 @@ def model_loader(self, model_name: str, model: IpfabricsyncModel) -> None:
8182
transformed_objs = table
8283

8384
for obj in transformed_objs:
85+
print(f"Object to load: {obj}")
8486
data = self.ipfabric_dict_to_diffsync(obj=obj, mapping=element, model=model)
87+
print(f"Data to load: {data}")
8588
item = model(**data)
8689
self.update_or_add_model_instance(item)
8790

@@ -102,8 +105,25 @@ def ipfabric_dict_to_diffsync(self, obj: dict, mapping: SchemaMappingModel, mode
102105
else:
103106
data[field.name] = value
104107
elif field_is_list and field.mapping and not field.reference:
105-
msg = "it's not supported yet to have an attribute of type list with a simple mapping"
106-
raise NotImplementedError(msg)
108+
# Handle list data for attributes like ntp_servers
109+
list_value = obj.get(field.mapping)
110+
if list_value is not None:
111+
# Ensure we end up with a real Python list.
112+
if isinstance(list_value, str):
113+
# Try to parse as JSON first.
114+
s = list_value.strip()
115+
try:
116+
parsed = json.loads(s)
117+
except (json.JSONDecodeError, TypeError):
118+
# Fallbacks: comma-separated -> list; otherwise singleton list
119+
parsed = [part.strip() for part in s.split(",")] if "," in s else [s]
120+
list_value = parsed
121+
# If it's not a list yet, wrap it.
122+
if not isinstance(list_value, list):
123+
list_value = [list_value]
124+
data[field.name] = list_value
125+
else:
126+
data[field.name] = []
107127

108128
elif field.mapping and field.reference:
109129
all_nodes_for_reference = self.store.get_all(model=field.reference)

infrahub_sync/generator/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"Integer": "int",
2626
"Boolean": "bool",
2727
"Checkbox": "bool",
28+
"List": "list[Any]",
2829
}
2930

3031

0 commit comments

Comments
 (0)