diff --git a/docs/dev/code_reference/jinja_filters.md b/docs/dev/code_reference/jinja_filters.md new file mode 100644 index 00000000..f72a0592 --- /dev/null +++ b/docs/dev/code_reference/jinja_filters.md @@ -0,0 +1 @@ +::: nautobot_design_builder.jinja_filters diff --git a/mkdocs.yml b/mkdocs.yml index 55de54a7..c242ef06 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -119,7 +119,11 @@ nav: - "admin/release_notes/index.md" - v1.0: "admin/release_notes/version_1.0.md" - v1.1: "admin/release_notes/version_1.1.md" + - v1.2: "admin/release_notes/version_1.2.md" + - v1.3: "admin/release_notes/version_1.3.md" + - v1.4: "admin/release_notes/version_1.4.md" - v2.0: "admin/release_notes/version_2.0.md" + - v2.1: "admin/release_notes/version_2.1.md" - Developer Guide: - Extending the App: "dev/extending.md" - Contributing to the App: "dev/contributing.md" @@ -132,6 +136,7 @@ nav: - Field Descriptors: "dev/code_reference/fields.md" - Design Job: "dev/code_reference/design_job.md" - Jinja Rendering: "dev/code_reference/jinja2.md" + - Jinja Filters: "dev/code_reference/jinja_filters.md" - Template Extensions: "dev/code_reference/ext.md" - Util: "dev/code_reference/util.md" - Nautobot Docs Home ↗︎: "https://docs.nautobot.com" diff --git a/nautobot_design_builder/contrib/ext.py b/nautobot_design_builder/contrib/ext.py index ffcf5fa5..242fd12d 100644 --- a/nautobot_design_builder/contrib/ext.py +++ b/nautobot_design_builder/contrib/ext.py @@ -17,7 +17,7 @@ from nautobot_design_builder.errors import DesignImplementationError, MultipleObjectsReturnedError, DoesNotExistError from nautobot_design_builder.ext import AttributeExtension -from nautobot_design_builder.jinja2 import network_offset +from nautobot_design_builder.jinja_filters import network_offset class LookupMixin: diff --git a/nautobot_design_builder/jinja2.py b/nautobot_design_builder/jinja2.py index 0bc940f1..717f1fdc 100644 --- a/nautobot_design_builder/jinja2.py +++ b/nautobot_design_builder/jinja2.py @@ -1,9 +1,5 @@ """Jinja2 related filters and environment methods.""" -import json -from typing import TYPE_CHECKING -import yaml - from django.template import engines from jinja2 import Environment, FileSystemLoader, StrictUndefined @@ -11,113 +7,6 @@ from jinja2.nativetypes import NativeEnvironment from jinja2.utils import missing -from netaddr import AddrFormatError, IPNetwork - -if TYPE_CHECKING: - from nautobot_design_builder.context import ContextNodeMixin - - -def network_string(network: IPNetwork, attr="") -> str: - """Jinja2 filter to convert the IPNetwork object to a string. - - If an attribute is supplied, first lookup the attribute on the IPNetwork - object, then convert the returned value to a string. - - Args: - network (IPNetwork): Object to convert to string - attr (str, optional): Optional attribute to retrieve from the IPNetwork prior - to converting to a string. Defaults to "". - - Example: - ```jinja - {{ "1.2.3.4/24" | ip_network | network_string("ip") }} - ``` - - Returns: - str: Converted object - """ - if attr: - return str(getattr(network, attr)) - - return str(network) - - -def ip_network(input_str: str) -> IPNetwork: - """Jinja2 filter to convert a string to an IPNetwork object. - - Args: - input_str (str): String correctly formatted as an IP Address - - Returns: - IPNetwork: object that represents the input string - """ - return IPNetwork(input_str) - - -def network_offset(prefix: str, offset: str) -> IPNetwork: - """Jinja2 filter to compute an IPNetwork based off of a prefix and offset. - - Example: - >>> from design_builder.jinja2 import network_offset - >>> network_offset("1.1.0.0/16", "0.0.1.1") - IPNetwork('1.1.1.1/16') - - >>> from design_builder.jinja2 import network_offset - >>> network_offset("1.1.0.0/16", "0.0.1.0/24") - IPNetwork('1.1.1.0/24') - - Args: - prefix (str): Prefix string in the form x.x.x.x/x - offset (str): Prefix string in the form x.x.x.x/x - - Returns: - IPNetwork: Returns an IPNetwork that is the result of prefix + offset. The - returned network object's prefix will be set to the longer prefix length - between the two inputs. - """ - try: - prefix = IPNetwork(prefix) - except AddrFormatError: - # pylint: disable=raise-missing-from - raise AddrFormatError(f"Invalid prefix {prefix}") - - try: - offset = IPNetwork(offset) - except AddrFormatError: - # pylint: disable=raise-missing-from - raise AddrFormatError(f"Invalid offset {offset}") - - # netaddr overloads the + operator to sum - # each octet of a pair of addresses. For instance, - # 1.1.1.1 + 1.2.3.4 = 2.3.4.5 - # The result of the expression is a netaddr.IPAddress - new_prefix = IPNetwork(prefix.ip + offset.ip) - if prefix.prefixlen > offset.prefixlen: - new_prefix.prefixlen = prefix.prefixlen - else: - new_prefix.prefixlen = offset.prefixlen - return new_prefix - - -def _json_default(value): - try: - return value.data - except AttributeError: - # pylint: disable=raise-missing-from - raise TypeError(f"Object of type {value.__class__.__name__} is not JSON serializable") - - -def to_json(value: "ContextNodeMixin"): - """Convert a context node to JSON.""" - return json.dumps(value, default=_json_default) - - -def to_yaml(value: "ContextNodeMixin", *args, **kwargs): - """Convert a context node to YAML.""" - default_flow_style = kwargs.pop("default_flow_style", False) - - return yaml.dump(json.loads(to_json(value)), allow_unicode=True, default_flow_style=default_flow_style, **kwargs) - def new_template_environment(root_context, base_dir=None, native_environment=False) -> NativeEnvironment: """Create a new template environment that will resolve identifiers using the supplied root_context. @@ -175,9 +64,5 @@ def context_class(*args, **kwargs): # Register standard Nautobot filters in the environment env.filters[name] = func - env.filters["to_yaml"] = to_yaml - env.filters["ip_network"] = ip_network - env.filters["network_string"] = network_string - env.filters["network_offset"] = network_offset env.context_class = context_class return env diff --git a/nautobot_design_builder/jinja_filters.py b/nautobot_design_builder/jinja_filters.py new file mode 100644 index 00000000..0635dedb --- /dev/null +++ b/nautobot_design_builder/jinja_filters.py @@ -0,0 +1,141 @@ +"""Useful jinja2 filters for designs.""" + +import json +from typing import Any +from django_jinja import library +from netaddr import AddrFormatError, IPNetwork +import yaml + + +@library.filter +def network_string(network: IPNetwork, attr="") -> str: + """Jinja2 filter to convert the IPNetwork object to a string. + + If an attribute is supplied, first lookup the attribute on the IPNetwork + object, then convert the returned value to a string. + + Args: + network (IPNetwork): Object to convert to string + attr (str, optional): Optional attribute to retrieve from the IPNetwork prior + to converting to a string. Defaults to "". + + Example: + ```jinja + {{ "1.2.3.4/24" | ip_network | network_string("ip") }} + ``` + + Returns: + str: Converted object + """ + if attr: + return str(getattr(network, attr)) + + return str(network) + + +@library.filter +def ip_network(input_str: str) -> IPNetwork: + """Jinja2 filter to convert a string to an IPNetwork object. + + Args: + input_str (str): String correctly formatted as an IP Address + + Returns: + IPNetwork: object that represents the input string + """ + return IPNetwork(input_str) + + +@library.filter +def network_offset(prefix: str, offset: str) -> IPNetwork: + """Jinja2 filter to compute an IPNetwork based off of a prefix and offset. + + Example: + >>> from design_builder.jinja2 import network_offset + >>> network_offset("1.1.0.0/16", "0.0.1.1") + IPNetwork('1.1.1.1/16') + + >>> from design_builder.jinja2 import network_offset + >>> network_offset("1.1.0.0/16", "0.0.1.0/24") + IPNetwork('1.1.1.0/24') + + Args: + prefix (str): Prefix string in the form x.x.x.x/x + offset (str): Prefix string in the form x.x.x.x/x + + Returns: + IPNetwork: Returns an IPNetwork that is the result of prefix + offset. The + returned network object's prefix will be set to the longer prefix length + between the two inputs. + """ + try: + prefix = IPNetwork(prefix) + except AddrFormatError: + # pylint: disable=raise-missing-from + raise AddrFormatError(f"Invalid prefix {prefix}") + + try: + offset = IPNetwork(offset) + except AddrFormatError: + # pylint: disable=raise-missing-from + raise AddrFormatError(f"Invalid offset {offset}") + + # netaddr overloads the + operator to sum + # each octet of a pair of addresses. For instance, + # 1.1.1.1 + 1.2.3.4 = 2.3.4.5 + # The result of the expression is a netaddr.IPAddress + new_prefix = IPNetwork(prefix.ip + offset.ip) + if prefix.prefixlen > offset.prefixlen: + new_prefix.prefixlen = prefix.prefixlen + else: + new_prefix.prefixlen = offset.prefixlen + return new_prefix + + +def _json_default(value: Any): + try: + return value.data + except AttributeError: + # pylint: disable=raise-missing-from + raise TypeError(f"Object of type {value.__class__.__name__} is not JSON serializable") + + +@library.filter +def to_json(value: Any) -> str: + """Jinja2 filter to render a value to properly formatted JSON. + + This method will render a value to proper JSON. If the value is part of a Design + Builder render context, then the correct type encoding (dictionary, list, etc) is + used. The Nautobot `render_json` method does not handle `UserDict` or `UserList` + which are the primary collection types for Design Builder contexts. This implementation + will unwrap those types and render the contained data. + + Args: + value (Any): The value to be encoded as JSON. + + Returns: + str: JSON encoded value. + """ + return json.dumps(value, default=_json_default) + + +@library.filter +def to_yaml(value: Any, **kwargs) -> str: + """Jinja2 filter to render a value to properly formatted YAML. + + This method will render a value to proper YAML. If the value is part of a Design + Builder render context, then the correct type encoding (dictionary, list, etc) is + used. The Nautobot `render_yaml` method does not handle `UserDict` or `UserList` + which are the primary collection types for Design Builder contexts. This implementation + will unwrap those types and render the contained data. + + Args: + value (Any): The value to be rendered as YAML. + kwargs (Any): Any additional options to pass to the yaml.dump method. + + Returns: + str: YAML encoded value. + """ + default_flow_style = kwargs.pop("default_flow_style", False) + + return yaml.dump(json.loads(to_json(value)), allow_unicode=True, default_flow_style=default_flow_style, **kwargs)