modified: netbox_services/engine/__init__.py

modified:   netbox_services/engine/allocators.py
modified:   netbox_services/engine/context.py
new file:   netbox_services/engine/mappers.py
modified:   netbox_services/engine/resolver.py
new file:   netbox_services/engine/templating.py
modified:   netbox_services/forms.py
modified:   netbox_services/models.py
modified:   netbox_services/tests/test_allocators.py
new file:   netbox_services/tests/test_mappers.py
modified:   netbox_services/tests/test_resolver.py
new file:   netbox_services/tests/test_templating.py
Simplify layout to template and service instances
This commit is contained in:
sven committed 2026-08-17 15:12:42 +02:00
1 parent 4ff9eac274
commit b1c9c7a40b
12 files changed
+1133 -33

No files matched your search

+80
View File
@@ -0,0 +1,80 @@
"""Jinja2-based attribute templating with ``..`` / ``...`` parent access.
Syntax inside ``{{ }}`` expressions:
- ``..`` → immediate parent (alias ``p1``)
- ``...`` → parent of parent (alias ``p2``)
- ``....`` → great-grandparent (alias ``p3``)
- ``service`` → the Service instance
- ``index`` → 0-based index within a ``quantity`` loop
Example::
attrs:
name: "rtr-{{ service.tenant.slug }}-{{ index + 1 }}"
site: "{{ ..site.pk }}" # parent device's site PK
notes: "{{ ...region.slug }}" # grandparent region slug
"""
import re
from jinja2.sandbox import SandboxedEnvironment
_DOT_RE = re.compile(r"(?<![.\w])\.{2,}")
_env = SandboxedEnvironment(
variable_start_string="{{",
variable_end_string="}}",
autoescape=False,
)
def preprocess_dots(s):
"""Replace ``..`` → ``p1.``, ``...`` → ``p2.``, etc.
Runs of 2+ dots not preceded by a word char or dot are replaced.
This preserves single dots (attribute access, IP addresses like
``10.0.0.1``) and literal strings without ``{{ }}``.
"""
def replacer(m):
depth = len(m.group(0)) - 1
return f"p{depth}."
return _DOT_RE.sub(replacer, s)
def _build_jinja_ctx(service, ancestors, index):
ctx = {"service": service, "index": index}
for i, obj in enumerate(ancestors):
ctx[f"p{i + 1}"] = obj
return ctx
def render_value(value, service, ancestors, index):
"""Render a single attribute value.
Strings are preprocessed (``..`` → ``p1.``) then Jinja2-rendered.
Non-strings (int, bool, list, dict) are returned as-is.
"""
if not isinstance(value, str):
return value
if "{{" not in value:
return value
processed = preprocess_dots(value)
template = _env.from_string(processed)
return template.render(**_build_jinja_ctx(service, ancestors, index))
def render_attrs(attrs, service, ancestors, index):
"""Render all values in an attrs dict.
*ancestors* is a list where ``ancestors[0]`` is the immediate parent,
``ancestors[1]`` is the grandparent, etc.
"""
if not attrs:
return {}
return {
key: render_value(val, service, ancestors, index)
for key, val in attrs.items()
}