From 574ca5eb772fc422642f80d2b9033ea7f1eb21df Mon Sep 17 00:00:00 2001 From: lou lecrivain Date: Mon, 17 Aug 2026 15:40:11 +0200 Subject: [PATCH 1/5] vendor ansible version_compare j2 filter --- .../ansible/module_utils/compat/__init__.py | 0 .../ansible/module_utils/compat/version.py | 342 ++++++++++++++++++ .../lib/ansible/plugins/test/__init__.py | 0 .../ansible/lib/ansible/plugins/test/core.py | 102 ++++++ .../ansible/lib/ansible/utils/__init__.py | 0 .../ansible/lib/ansible/utils/version.py | 276 ++++++++++++++ 6 files changed, 720 insertions(+) create mode 100644 cosmo/_vendor/ansible/lib/ansible/module_utils/compat/__init__.py create mode 100644 cosmo/_vendor/ansible/lib/ansible/module_utils/compat/version.py create mode 100644 cosmo/_vendor/ansible/lib/ansible/plugins/test/__init__.py create mode 100644 cosmo/_vendor/ansible/lib/ansible/plugins/test/core.py create mode 100644 cosmo/_vendor/ansible/lib/ansible/utils/__init__.py create mode 100644 cosmo/_vendor/ansible/lib/ansible/utils/version.py diff --git a/cosmo/_vendor/ansible/lib/ansible/module_utils/compat/__init__.py b/cosmo/_vendor/ansible/lib/ansible/module_utils/compat/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cosmo/_vendor/ansible/lib/ansible/module_utils/compat/version.py b/cosmo/_vendor/ansible/lib/ansible/module_utils/compat/version.py new file mode 100644 index 00000000..e07696b6 --- /dev/null +++ b/cosmo/_vendor/ansible/lib/ansible/module_utils/compat/version.py @@ -0,0 +1,342 @@ +# Vendored copy of distutils/version.py from CPython 3.9.5 +# +# Implements multiple version numbering conventions for the +# Python Module Distribution Utilities. +# +# PSF License (see licenses/PSF-license.txt or https://opensource.org/licenses/Python-2.0) +# + +"""Provides classes to represent module version numbers (one class for +each style of version numbering). There are currently two such classes +implemented: StrictVersion and LooseVersion. + +Every version number class implements the following interface: + * the 'parse' method takes a string and parses it to some internal + representation; if the string is an invalid version number, + 'parse' raises a ValueError exception + * the class constructor takes an optional string argument which, + if supplied, is passed to 'parse' + * __str__ reconstructs the string that was passed to 'parse' (or + an equivalent string -- ie. one that will generate an equivalent + version number instance) + * __repr__ generates Python code to recreate the version number instance + * _cmp compares the current instance with either another instance + of the same class or a string (which will be parsed to an instance + of the same class, thus must follow the same rules) +""" + +from __future__ import annotations + +import re + +try: + RE_FLAGS = re.VERBOSE | re.ASCII # type: ignore[attr-defined] +except AttributeError: + RE_FLAGS = re.VERBOSE + + +class Version: + """Abstract base class for version numbering classes. Just provides + constructor (__init__) and reproducer (__repr__), because those + seem to be the same for all version numbering classes; and route + rich comparisons to _cmp. + """ + + def __init__(self, vstring=None): + if vstring: + self.parse(vstring) + + def __repr__(self): + return "%s ('%s')" % (self.__class__.__name__, str(self)) + + def __eq__(self, other): + c = self._cmp(other) + if c is NotImplemented: + return c + return c == 0 + + def __lt__(self, other): + c = self._cmp(other) + if c is NotImplemented: + return c + return c < 0 + + def __le__(self, other): + c = self._cmp(other) + if c is NotImplemented: + return c + return c <= 0 + + def __gt__(self, other): + c = self._cmp(other) + if c is NotImplemented: + return c + return c > 0 + + def __ge__(self, other): + c = self._cmp(other) + if c is NotImplemented: + return c + return c >= 0 + + +# Interface for version-number classes -- must be implemented +# by the following classes (the concrete ones -- Version should +# be treated as an abstract class). +# __init__ (string) - create and take same action as 'parse' +# (string parameter is optional) +# parse (string) - convert a string representation to whatever +# internal representation is appropriate for +# this style of version numbering +# __str__ (self) - convert back to a string; should be very similar +# (if not identical to) the string supplied to parse +# __repr__ (self) - generate Python code to recreate +# the instance +# _cmp (self, other) - compare two version numbers ('other' may +# be an unparsed version string, or another +# instance of your version class) + + +class StrictVersion(Version): + """Version numbering for anal retentives and software idealists. + Implements the standard interface for version number classes as + described above. A version number consists of two or three + dot-separated numeric components, with an optional "pre-release" tag + on the end. The pre-release tag consists of the letter 'a' or 'b' + followed by a number. If the numeric components of two version + numbers are equal, then one with a pre-release tag will always + be deemed earlier (lesser) than one without. + + The following are valid version numbers (shown in the order that + would be obtained by sorting according to the supplied cmp function): + + 0.4 0.4.0 (these two are equivalent) + 0.4.1 + 0.5a1 + 0.5b3 + 0.5 + 0.9.6 + 1.0 + 1.0.4a3 + 1.0.4b1 + 1.0.4 + + The following are examples of invalid version numbers: + + 1 + 2.7.2.2 + 1.3.a4 + 1.3pl1 + 1.3c4 + + The rationale for this version numbering system will be explained + in the distutils documentation. + """ + + version_re = re.compile(r"^(\d+) \. (\d+) (\. (\d+))? ([ab](\d+))?$", RE_FLAGS) + + def parse(self, vstring): + match = self.version_re.match(vstring) + if not match: + raise ValueError("invalid version number '%s'" % vstring) + + major, minor, patch, prerelease, prerelease_num = match.group(1, 2, 4, 5, 6) + + if patch: + self.version = tuple(map(int, [major, minor, patch])) + else: + self.version = tuple(map(int, [major, minor])) + (0,) + + if prerelease: + self.prerelease = (prerelease[0], int(prerelease_num)) + else: + self.prerelease = None + + def __str__(self): + if self.version[2] == 0: + vstring = ".".join(map(str, self.version[0:2])) + else: + vstring = ".".join(map(str, self.version)) + + if self.prerelease: + vstring = vstring + self.prerelease[0] + str(self.prerelease[1]) + + return vstring + + def _cmp(self, other): + if isinstance(other, str): + other = StrictVersion(other) + elif not isinstance(other, StrictVersion): + return NotImplemented + + if self.version != other.version: + # numeric versions don't match + # prerelease stuff doesn't matter + if self.version < other.version: + return -1 + else: + return 1 + + # have to compare prerelease + # case 1: neither has prerelease; they're equal + # case 2: self has prerelease, other doesn't; other is greater + # case 3: self doesn't have prerelease, other does: self is greater + # case 4: both have prerelease: must compare them! + + if not self.prerelease and not other.prerelease: + return 0 + elif self.prerelease and not other.prerelease: + return -1 + elif not self.prerelease and other.prerelease: + return 1 + elif self.prerelease and other.prerelease: + if self.prerelease == other.prerelease: + return 0 + elif self.prerelease < other.prerelease: + return -1 + else: + return 1 + else: + raise AssertionError("never get here") + + +# end class StrictVersion + +# The rules according to Greg Stein: +# 1) a version number has 1 or more numbers separated by a period or by +# sequences of letters. If only periods, then these are compared +# left-to-right to determine an ordering. +# 2) sequences of letters are part of the tuple for comparison and are +# compared lexicographically +# 3) recognize the numeric components may have leading zeroes +# +# The LooseVersion class below implements these rules: a version number +# string is split up into a tuple of integer and string components, and +# comparison is a simple tuple comparison. This means that version +# numbers behave in a predictable and obvious way, but a way that might +# not necessarily be how people *want* version numbers to behave. There +# wouldn't be a problem if people could stick to purely numeric version +# numbers: just split on period and compare the numbers as tuples. +# However, people insist on putting letters into their version numbers; +# the most common purpose seems to be: +# - indicating a "pre-release" version +# ('alpha', 'beta', 'a', 'b', 'pre', 'p') +# - indicating a post-release patch ('p', 'pl', 'patch') +# but of course this can't cover all version number schemes, and there's +# no way to know what a programmer means without asking them. +# +# The problem is what to do with letters (and other non-numeric +# characters) in a version number. The current implementation does the +# obvious and predictable thing: keep them as strings and compare +# lexically within a tuple comparison. This has the desired effect if +# an appended letter sequence implies something "post-release": +# eg. "0.99" < "0.99pl14" < "1.0", and "5.001" < "5.001m" < "5.002". +# +# However, if letters in a version number imply a pre-release version, +# the "obvious" thing isn't correct. Eg. you would expect that +# "1.5.1" < "1.5.2a2" < "1.5.2", but under the tuple/lexical comparison +# implemented here, this just isn't so. +# +# Two possible solutions come to mind. The first is to tie the +# comparison algorithm to a particular set of semantic rules, as has +# been done in the StrictVersion class above. This works great as long +# as everyone can go along with bondage and discipline. Hopefully a +# (large) subset of Python module programmers will agree that the +# particular flavour of bondage and discipline provided by StrictVersion +# provides enough benefit to be worth using, and will submit their +# version numbering scheme to its domination. The free-thinking +# anarchists in the lot will never give in, though, and something needs +# to be done to accommodate them. +# +# Perhaps a "moderately strict" version class could be implemented that +# lets almost anything slide (syntactically), and makes some heuristic +# assumptions about non-digits in version number strings. This could +# sink into special-case-hell, though; if I was as talented and +# idiosyncratic as Larry Wall, I'd go ahead and implement a class that +# somehow knows that "1.2.1" < "1.2.2a2" < "1.2.2" < "1.2.2pl3", and is +# just as happy dealing with things like "2g6" and "1.13++". I don't +# think I'm smart enough to do it right though. +# +# In any case, I've coded the test suite for this module (see +# ../test/test_version.py) specifically to fail on things like comparing +# "1.2a2" and "1.2". That's not because the *code* is doing anything +# wrong, it's because the simple, obvious design doesn't match my +# complicated, hairy expectations for real-world version numbers. It +# would be a snap to fix the test suite to say, "Yep, LooseVersion does +# the Right Thing" (ie. the code matches the conception). But I'd rather +# have a conception that matches common notions about version numbers. + + +class LooseVersion(Version): + """Version numbering for anarchists and software realists. + Implements the standard interface for version number classes as + described above. A version number consists of a series of numbers, + separated by either periods or strings of letters. When comparing + version numbers, the numeric components will be compared + numerically, and the alphabetic components lexically. The following + are all valid version numbers, in no particular order: + + 1.5.1 + 1.5.2b2 + 161 + 3.10a + 8.02 + 3.4j + 1996.07.12 + 3.2.pl0 + 3.1.1.6 + 2g6 + 11g + 0.960923 + 2.2beta29 + 1.13++ + 5.5.kw + 2.0b1pl0 + + In fact, there is no such thing as an invalid version number under + this scheme; the rules for comparison are simple and predictable, + but may not always give the results you want (for some definition + of "want"). + """ + + component_re = re.compile(r"(\d+ | [a-z]+ | \.)", re.VERBOSE) + + def __init__(self, vstring=None): + if vstring: + self.parse(vstring) + + def parse(self, vstring): + # I've given up on thinking I can reconstruct the version string + # from the parsed tuple -- so I just store the string here for + # use by __str__ + self.vstring = vstring + components = [x for x in self.component_re.split(vstring) if x and x != "."] + for i, obj in enumerate(components): + try: + components[i] = int(obj) + except ValueError: + pass + + self.version = components + + def __str__(self): + return self.vstring + + def __repr__(self): + return "LooseVersion ('%s')" % str(self) + + def _cmp(self, other): + if isinstance(other, str): + other = LooseVersion(other) + elif not isinstance(other, LooseVersion): + return NotImplemented + + if self.version == other.version: + return 0 + if self.version < other.version: + return -1 + if self.version > other.version: + return 1 + + +# end class LooseVersion diff --git a/cosmo/_vendor/ansible/lib/ansible/plugins/test/__init__.py b/cosmo/_vendor/ansible/lib/ansible/plugins/test/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cosmo/_vendor/ansible/lib/ansible/plugins/test/core.py b/cosmo/_vendor/ansible/lib/ansible/plugins/test/core.py new file mode 100644 index 00000000..7008dbba --- /dev/null +++ b/cosmo/_vendor/ansible/lib/ansible/plugins/test/core.py @@ -0,0 +1,102 @@ +# (c) 2012, Jeroen Hoekx +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +from __future__ import annotations + +import operator as py_operator + +from cosmo._vendor.ansible.lib.ansible.module_utils.compat.version import ( + LooseVersion, + StrictVersion, +) + +from cosmo._vendor.ansible.lib.ansible.utils.version import SemanticVersion + +try: + from packaging.version import Version as PEP440Version + + HAS_PACKAGING = True +except ImportError: + HAS_PACKAGING = False + + +def version_compare(value, version, operator="eq", strict=None, version_type=None): + """Perform a version comparison on a value""" + op_map = { + "==": "eq", + "=": "eq", + "eq": "eq", + "<": "lt", + "lt": "lt", + "<=": "le", + "le": "le", + ">": "gt", + "gt": "gt", + ">=": "ge", + "ge": "ge", + "!=": "ne", + "<>": "ne", + "ne": "ne", + } + + type_map = { + "loose": LooseVersion, + "strict": StrictVersion, + "semver": SemanticVersion, + "semantic": SemanticVersion, + "pep440": PEP440Version, + } + + if strict is not None and version_type is not None: + raise Exception("Cannot specify both 'strict' and 'version_type'") + + if not value: + raise Exception("Input version value cannot be empty") + + if not version: + raise Exception("Version parameter to compare against cannot be empty") + + if version_type == "pep440" and not HAS_PACKAGING: + raise Exception( + "The pep440 version_type requires the Python 'packaging' library" + ) + + Version = LooseVersion + if strict: + Version = StrictVersion + elif version_type: + try: + Version = type_map[version_type] + except KeyError: + raise Exception( + "Invalid version type (%s). Must be one of %s" + % (version_type, ", ".join(map(repr, type_map))) + ) + + if operator in op_map: + operator = op_map[operator] + else: + raise Exception( + "Invalid operator type (%s). Must be one of %s" + % (operator, ", ".join(map(repr, op_map))) + ) + + try: + method = getattr(py_operator, operator) + return method(Version(str(value)), Version(str(version))) + except Exception as e: + raise Exception("Version comparison failed: %s" % str(e)) diff --git a/cosmo/_vendor/ansible/lib/ansible/utils/__init__.py b/cosmo/_vendor/ansible/lib/ansible/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cosmo/_vendor/ansible/lib/ansible/utils/version.py b/cosmo/_vendor/ansible/lib/ansible/utils/version.py new file mode 100644 index 00000000..4ba5e253 --- /dev/null +++ b/cosmo/_vendor/ansible/lib/ansible/utils/version.py @@ -0,0 +1,276 @@ +# Copyright (c) 2020 Matt Martz +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import annotations + +import re + +from cosmo._vendor.ansible.lib.ansible.module_utils.compat.version import ( + LooseVersion, + Version, +) + +# Regular expression taken from +# https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string +SEMVER_RE = re.compile( + r""" + ^ + (?P0|[1-9]\d*) + \. + (?P0|[1-9]\d*) + \. + (?P0|[1-9]\d*) + (?: + - + (?P + (?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*) + (?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))* + ) + )? + (?: + \+ + (?P[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*) + )? + $ + """, + flags=re.X, +) + + +class _Alpha: + """Class to easily allow comparing strings + + Largely this exists to make comparing an integer and a string on py3 + so that it works like py2. + """ + + def __init__(self, specifier): + self.specifier = specifier + + def __repr__(self): + return repr(self.specifier) + + def __eq__(self, other): + if isinstance(other, _Alpha): + return self.specifier == other.specifier + elif isinstance(other, str): + return self.specifier == other + + return False + + def __ne__(self, other): + return not self.__eq__(other) + + def __lt__(self, other): + if isinstance(other, _Alpha): + return self.specifier < other.specifier + elif isinstance(other, str): + return self.specifier < other + elif isinstance(other, _Numeric): + return False + + raise ValueError + + def __le__(self, other): + return self.__lt__(other) or self.__eq__(other) + + def __gt__(self, other): + return not self.__le__(other) + + def __ge__(self, other): + return not self.__lt__(other) + + +class _Numeric: + """Class to easily allow comparing numbers + + Largely this exists to make comparing an integer and a string on py3 + so that it works like py2. + """ + + def __init__(self, specifier): + self.specifier = int(specifier) + + def __repr__(self): + return repr(self.specifier) + + def __eq__(self, other): + if isinstance(other, _Numeric): + return self.specifier == other.specifier + elif isinstance(other, int): + return self.specifier == other + + return False + + def __ne__(self, other): + return not self.__eq__(other) + + def __lt__(self, other): + if isinstance(other, _Numeric): + return self.specifier < other.specifier + elif isinstance(other, int): + return self.specifier < other + elif isinstance(other, _Alpha): + return True + + raise ValueError + + def __le__(self, other): + return self.__lt__(other) or self.__eq__(other) + + def __gt__(self, other): + return not self.__le__(other) + + def __ge__(self, other): + return not self.__lt__(other) + + +class SemanticVersion(Version): + """Version comparison class that implements Semantic Versioning 2.0.0 + + Based off of ``distutils.version.Version`` + """ + + version_re = SEMVER_RE + + def __init__(self, vstring=None): + self.vstring = vstring + self.major = None + self.minor = None + self.patch = None + self.prerelease = () + self.buildmetadata = () + + if vstring: + self.parse(vstring) + + def __repr__(self): + return "SemanticVersion(%r)" % self.vstring + + @staticmethod + def from_loose_version(loose_version): + """This method is designed to take a ``LooseVersion`` + and attempt to construct a ``SemanticVersion`` from it + + This is useful where you want to do simple version math + without requiring users to provide a compliant semver. + """ + if not isinstance(loose_version, LooseVersion): + raise ValueError("%r is not a LooseVersion" % loose_version) + + try: + version = loose_version.version[:] + except AttributeError: + raise ValueError("%r is not a LooseVersion" % loose_version) + + extra_idx = 3 + for marker in ("-", "+"): + try: + idx = version.index(marker) + except ValueError: + continue + else: + if idx < extra_idx: + extra_idx = idx + version = version[:extra_idx] + + if version and set(type(v) for v in version) != set((int,)): + raise ValueError("Non integer values in %r" % loose_version) + + # Extra is everything to the right of the core version + extra = re.search("[+-].+$", loose_version.vstring) + + version = version + [0] * (3 - len(version)) + return SemanticVersion( + "%s%s" + % (".".join(str(v) for v in version), extra.group(0) if extra else "") + ) + + def parse(self, vstring): + match = SEMVER_RE.match(vstring) + if not match: + raise ValueError("invalid semantic version '%s'" % vstring) + + major, minor, patch, prerelease, buildmetadata = match.group(1, 2, 3, 4, 5) + self.vstring = vstring + self.major = int(major) + self.minor = int(minor) + self.patch = int(patch) + + if prerelease: + self.prerelease = tuple( + _Numeric(x) if x.isdigit() else _Alpha(x) for x in prerelease.split(".") + ) + if buildmetadata: + self.buildmetadata = tuple( + _Numeric(x) if x.isdigit() else _Alpha(x) + for x in buildmetadata.split(".") + ) + + @property + def core(self): + return self.major, self.minor, self.patch + + @property + def is_prerelease(self): + return bool(self.prerelease) + + @property + def is_stable(self): + # Major version zero (0.y.z) is for initial development. Anything MAY change at any time. + # The public API SHOULD NOT be considered stable. + # https://semver.org/#spec-item-4 + return not (self.major == 0 or self.is_prerelease) + + def _cmp(self, other): + if isinstance(other, str): + other = SemanticVersion(other) + + if self.core != other.core: + # if the core version doesn't match + # prerelease and buildmetadata doesn't matter + if self.core < other.core: + return -1 + else: + return 1 + + if not any((self.prerelease, other.prerelease)): + return 0 + + if self.prerelease and not other.prerelease: + return -1 + elif not self.prerelease and other.prerelease: + return 1 + else: + if self.prerelease < other.prerelease: + return -1 + elif self.prerelease > other.prerelease: + return 1 + + # Build metadata MUST be ignored when determining version precedence + # https://semver.org/#spec-item-10 + # With the above in mind it is ignored here + + # If we have made it here, things should be equal + return 0 + + # The Py2 and Py3 implementations of distutils.version.Version + # are quite different, this makes the Py2 and Py3 implementations + # the same + def __eq__(self, other): + return self._cmp(other) == 0 + + def __ne__(self, other): + return not self.__eq__(other) + + def __lt__(self, other): + return self._cmp(other) < 0 + + def __le__(self, other): + return self._cmp(other) <= 0 + + def __gt__(self, other): + return self._cmp(other) > 0 + + def __ge__(self, other): + return self._cmp(other) >= 0 From e42f8398e1b3dd34c233883880c787fe06c9fb77 Mon Sep 17 00:00:00 2001 From: lou lecrivain Date: Mon, 17 Aug 2026 16:27:49 +0200 Subject: [PATCH 2/5] exclude vendoring directory from coverage --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ed4ead09..3064d690 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,8 @@ cosmo = "cosmo.__main__:main" [tool.coverage.run] omit = [ - "cosmo/tests/*" + "cosmo/tests/*", + "cosmo/_vendor/*", ] [tool.poetry.dependencies] From 4d07afdd54bf0be9d2f7b7c3f5e61cbb8bfbb420 Mon Sep 17 00:00:00 2001 From: lou lecrivain Date: Mon, 17 Aug 2026 16:08:21 +0200 Subject: [PATCH 3/5] add jinja2 as runtime dependency --- poetry.lock | 121 ++++++++++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 1 + 2 files changed, 120 insertions(+), 2 deletions(-) diff --git a/poetry.lock b/poetry.lock index fc9fca47..58aac2d3 100644 --- a/poetry.lock +++ b/poetry.lock @@ -188,7 +188,7 @@ description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" groups = ["dev"] -markers = "platform_system == \"Windows\" or sys_platform == \"win32\"" +markers = "sys_platform == \"win32\" or platform_system == \"Windows\"" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, @@ -359,6 +359,24 @@ files = [ {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, ] +[[package]] +name = "jinja2" +version = "3.1.6" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, + {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + [[package]] name = "jsonschema" version = "4.25.0" @@ -396,6 +414,105 @@ files = [ [package.dependencies] referencing = ">=0.31.0" +[[package]] +name = "markupsafe" +version = "3.0.3" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559"}, + {file = "markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419"}, + {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695"}, + {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591"}, + {file = "markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c"}, + {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f"}, + {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6"}, + {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1"}, + {file = "markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa"}, + {file = "markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8"}, + {file = "markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1"}, + {file = "markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad"}, + {file = "markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a"}, + {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50"}, + {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf"}, + {file = "markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f"}, + {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a"}, + {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115"}, + {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a"}, + {file = "markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19"}, + {file = "markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01"}, + {file = "markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c"}, + {file = "markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e"}, + {file = "markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce"}, + {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d"}, + {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d"}, + {file = "markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a"}, + {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b"}, + {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f"}, + {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b"}, + {file = "markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d"}, + {file = "markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c"}, + {file = "markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f"}, + {file = "markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795"}, + {file = "markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219"}, + {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6"}, + {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676"}, + {file = "markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9"}, + {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1"}, + {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc"}, + {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12"}, + {file = "markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed"}, + {file = "markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5"}, + {file = "markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485"}, + {file = "markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73"}, + {file = "markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37"}, + {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19"}, + {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025"}, + {file = "markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6"}, + {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f"}, + {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb"}, + {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009"}, + {file = "markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354"}, + {file = "markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218"}, + {file = "markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287"}, + {file = "markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe"}, + {file = "markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026"}, + {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737"}, + {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97"}, + {file = "markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d"}, + {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda"}, + {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf"}, + {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe"}, + {file = "markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9"}, + {file = "markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581"}, + {file = "markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4"}, + {file = "markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab"}, + {file = "markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175"}, + {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634"}, + {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50"}, + {file = "markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e"}, + {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5"}, + {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523"}, + {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc"}, + {file = "markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d"}, + {file = "markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9"}, + {file = "markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa"}, + {file = "markupsafe-3.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26"}, + {file = "markupsafe-3.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc"}, + {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c"}, + {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42"}, + {file = "markupsafe-3.0.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b"}, + {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758"}, + {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2"}, + {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d"}, + {file = "markupsafe-3.0.3-cp39-cp39-win32.whl", hash = "sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7"}, + {file = "markupsafe-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e"}, + {file = "markupsafe-3.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8"}, + {file = "markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698"}, +] + [[package]] name = "multimethod" version = "2.0" @@ -1034,4 +1151,4 @@ zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] [metadata] lock-version = "2.1" python-versions = ">=3.10,<4.0" -content-hash = "db6d360fc17670d71278e4e318982f051f5672065b543508ceeb81c1ea565284" +content-hash = "60200f99667ab20a325f82af25aae2ba6cef849cf6b8f245ebea3a3159d5dfd6" diff --git a/pyproject.toml b/pyproject.toml index 3064d690..6ac87b6b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,6 +23,7 @@ deepmerge = "^2.0" termcolor = "^3.1.0" multimethod = "^2.0" jsonschema = "4.25.0" +jinja2 = "^3.1.6" [tool.poetry.dev-dependencies] From c701ea1f6271d08f082b50402a33775ce2c964ff Mon Sep 17 00:00:00 2001 From: lou lecrivain Date: Mon, 17 Aug 2026 15:50:12 +0200 Subject: [PATCH 4/5] switch to j2 templating for connected_devices + fix tests --- cosmo/clients/netbox.py | 3 + cosmo/clients/netbox_v4.py | 78 +++++++++++++++---- ...s.graphql => connected_devices.graphql.j2} | 12 ++- cosmo/tests/utils.py | 16 +++- 4 files changed, 90 insertions(+), 19 deletions(-) rename cosmo/clients/queries/{connected_devices.graphql => connected_devices.graphql.j2} (50%) diff --git a/cosmo/clients/netbox.py b/cosmo/clients/netbox.py index 6571875a..4c5d9fe3 100644 --- a/cosmo/clients/netbox.py +++ b/cosmo/clients/netbox.py @@ -27,6 +27,7 @@ def __init__(self, url, token, verify_certs=True): multiple_mac_addresses=True, netbox_43_query_syntax=True, feature_flags=feature_flags, + netbox_version=self.base_version, verify_certs=self.verify_certs, ) elif self.base_version > Version("4.2.0"): @@ -37,6 +38,7 @@ def __init__(self, url, token, verify_certs=True): multiple_mac_addresses=True, netbox_43_query_syntax=False, feature_flags=feature_flags, + netbox_version=self.base_version, verify_certs=self.verify_certs, ) elif self.base_version > Version("4.0.0"): @@ -47,6 +49,7 @@ def __init__(self, url, token, verify_certs=True): multiple_mac_addresses=False, netbox_43_query_syntax=False, feature_flags=feature_flags, + netbox_version=self.base_version, verify_certs=self.verify_certs, ) else: diff --git a/cosmo/clients/netbox_v4.py b/cosmo/clients/netbox_v4.py index 0ac09136..97188634 100644 --- a/cosmo/clients/netbox_v4.py +++ b/cosmo/clients/netbox_v4.py @@ -4,7 +4,10 @@ from multiprocessing import Manager from os import PathLike from pathlib import Path +from packaging.version import Version +from jinja2 import Environment, PackageLoader +from cosmo._vendor.ansible.lib.ansible.plugins.test.core import version_compare from cosmo.clients import get_client_mp_context from cosmo.clients.netbox_client import NetboxAPIClient from cosmo.common import FileTemplate, clip @@ -13,16 +16,25 @@ class ParallelQuery(ABC): - def __init__(self, client: NetboxAPIClient, **kwargs): + def __init__( + self, client: NetboxAPIClient, *args, netbox_version: Version, **kwargs + ): self.client = client + self.netbox_version = netbox_version self.data_promise = None self.kwargs = kwargs + self.j2env = Environment(loader=PackageLoader("cosmo.clients", "queries")) + self.j2env.filters["version_compare"] = version_compare + @staticmethod def file_template(relpath: str | PathLike): return FileTemplate(Path(__file__).parent.joinpath(Path(relpath))) + def j2_template(self, relpath: str): + return self.j2env.get_template(relpath) + def fetch_data(self, pool): return pool.apply_async(self._fetch_data, args=(self.kwargs, pool)) @@ -45,15 +57,11 @@ def __init__(self, *args, netbox_43_query_syntax=False, **kwargs): self.netbox_43_query_syntax = netbox_43_query_syntax def _fetch_data(self, kwargs, pool): - tag_filter = ( - 'tags: { name: { exact: "bgp:cpe" }}' - if self.netbox_43_query_syntax - else 'tag: "bgp_cpe"' - ) - query_template = self.file_template("queries/connected_devices.graphql") + query_template = self.j2_template("connected_devices.graphql.j2") return self.client.query( - query_template.substitute(tag_filter=tag_filter), "connected_devices_query" + query_template.render(netbox_version=str(self.netbox_version)), + "connected_devices_query", )["data"] def _merge_into(self, data: dict, query_data): @@ -326,6 +334,7 @@ def __init__( multiple_mac_addresses, netbox_43_query_syntax, feature_flags, + netbox_version, verify_certs=True, ): self.url = url @@ -333,6 +342,7 @@ def __init__( self.multiple_mac_addresses = multiple_mac_addresses self.netbox_43_query_syntax = netbox_43_query_syntax self.feature_flags = feature_flags + self.netbox_version = netbox_version self.verify_certs = verify_certs def worker_amount(self, n_queries: int): @@ -361,38 +371,72 @@ def get_data(self, device_config): client, device=d, multiple_mac_addresses=self.multiple_mac_addresses, + netbox_version=self.netbox_version, ), ( - TobagoLineMembersDataQuery(client, device=d) + TobagoLineMembersDataQuery( + client, device=d, netbox_version=self.netbox_version + ) if self.feature_flags["tobago"] and ( features.featureIsEnabled("interface-auto-descriptions") or features.featureIsEnabled("new-bgp-cpe-group-naming") ) - else TobagoLineMemberDataDummyQuery(client, device=d) + else TobagoLineMemberDataDummyQuery( + client, device=d, netbox_version=self.netbox_version + ) ), ] ) queries.extend( [ - L2VPNDataQuery(client, device_list=device_list), + L2VPNDataQuery( + client, + device_list=device_list, + netbox_version=self.netbox_version, + ), ( - StaticRouteQuery(client, device_list=device_list) + StaticRouteQuery( + client, + device_list=device_list, + netbox_version=self.netbox_version, + ) if self.feature_flags["routing"] - else StaticRouteDummyQuery(client, device_list=device_list) + else StaticRouteDummyQuery( + client, + device_list=device_list, + netbox_version=self.netbox_version, + ) + ), + DeviceMACQuery( + client, + device_list=device_list, + netbox_version=self.netbox_version, ), - DeviceMACQuery(client, device_list=device_list), ConnectedDevicesDataQuery( client, device_list=device_list, netbox_43_query_syntax=self.netbox_43_query_syntax, + netbox_version=self.netbox_version, + ), + LoopbackDataQuery( + client, + device_list=device_list, + netbox_version=self.netbox_version, ), - LoopbackDataQuery(client, device_list=device_list), ( - IPPoolDataQuery(client, device_list=device_list) + IPPoolDataQuery( + client, + device_list=device_list, + netbox_version=self.netbox_version, + ) if self.feature_flags["ippools"] - else IPPoolDataDummyQuery(client, device_list=device_list) + else IPPoolDataDummyQuery( + client, + device_list=device_list, + netbox_version=self.netbox_version, + ) ), ] ) diff --git a/cosmo/clients/queries/connected_devices.graphql b/cosmo/clients/queries/connected_devices.graphql.j2 similarity index 50% rename from cosmo/clients/queries/connected_devices.graphql rename to cosmo/clients/queries/connected_devices.graphql.j2 index b141975b..5aa3b79e 100644 --- a/cosmo/clients/queries/connected_devices.graphql +++ b/cosmo/clients/queries/connected_devices.graphql.j2 @@ -1,5 +1,15 @@ query { - interface_list(filters: { $tag_filter }) { + interface_list(filters: { + {% if netbox_version | version_compare("4.6.99999", "<=") and netbox_version | version_compare("4.3.0", ">") %} + tags: { name: { exact: "bgp:cpe" } } + {% elif netbox_version | version_compare("4.3.99999", "<=") and netbox_version | version_compare("4.2.0", ">") %} + tags: { name: { exact: "bgp_cpe" } } + {% elif netbox_version | version_compare("4.2.99999", "<=") and netbox_version | version_compare("4.0.0", ">=") %} + tag: "bgp_cpe" + {% else %} + COSMO_NETBOX_VERSION_FAILURE: YES + {% endif %} + }) { __typename id, parent { diff --git a/cosmo/tests/utils.py b/cosmo/tests/utils.py index 555f812e..c59c895d 100644 --- a/cosmo/tests/utils.py +++ b/cosmo/tests/utils.py @@ -1,5 +1,7 @@ import json import multiprocessing +import os +import stat def override_get_client_mp_context(method=None): @@ -148,6 +150,18 @@ def _openReplacement(self, file, *args, **kwargs): else: return open(file, *args, **kwargs) + def _isFileReplacement(self, file, *args, **kwargs): + if file == self.requestedPath: + return True + else: + # just cloning the small and unlikely to change isfile function from cpython's Lib/genericpath.py in order + # to avoid recursion / monkeypatching loop. + try: + st = os.stat(file) + except (OSError, ValueError): + return False + return stat.S_ISREG(st.st_mode) + def _patch(self): self.patches.append( self.mocker.patch(self.namespace + ".open", self._openReplacement) @@ -155,7 +169,7 @@ def _patch(self): self.patches.append( self.mocker.patch( self.namespace + ".os.path.isfile", - lambda f: True if f == self.requestedPath else False, + self._isFileReplacement, ) ) From 730a7b78d881b5f2e9fd03d24c3364dce5cdb0c8 Mon Sep 17 00:00:00 2001 From: lou lecrivain Date: Mon, 17 Aug 2026 16:42:50 +0200 Subject: [PATCH 5/5] factorize template renderering stateful code --- cosmo/clients/netbox_v4.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/cosmo/clients/netbox_v4.py b/cosmo/clients/netbox_v4.py index 97188634..d306d27f 100644 --- a/cosmo/clients/netbox_v4.py +++ b/cosmo/clients/netbox_v4.py @@ -5,7 +5,7 @@ from os import PathLike from pathlib import Path from packaging.version import Version -from jinja2 import Environment, PackageLoader +from jinja2 import Environment, PackageLoader, Template from cosmo._vendor.ansible.lib.ansible.plugins.test.core import version_compare from cosmo.clients import get_client_mp_context @@ -35,6 +35,12 @@ def file_template(relpath: str | PathLike): def j2_template(self, relpath: str): return self.j2env.get_template(relpath) + def render(self, template: Template): + return template.render( + netbox_version=str(self.netbox_version), + device=json.dumps(self.kwargs.get("device")), + ) + def fetch_data(self, pool): return pool.apply_async(self._fetch_data, args=(self.kwargs, pool)) @@ -60,7 +66,7 @@ def _fetch_data(self, kwargs, pool): query_template = self.j2_template("connected_devices.graphql.j2") return self.client.query( - query_template.render(netbox_version=str(self.netbox_version)), + self.render(query_template), "connected_devices_query", )["data"]