diff --git a/changelog/+protocols-hierarchical-duplicates.fixed.md b/changelog/+protocols-hierarchical-duplicates.fixed.md new file mode 100644 index 000000000..c329d4e24 --- /dev/null +++ b/changelog/+protocols-hierarchical-duplicates.fixed.md @@ -0,0 +1 @@ +Protocols generated for a hierarchical kind no longer declare `parent` and `children` twice. When the schema already exposes them as relationships, which is the case for any schema read from the API, they were emitted once from the relationship list and once more from the hierarchy, and the second pair overrode the first. Regenerate with `infrahubctl protocols` to drop the duplicates. diff --git a/infrahub_sdk/protocols_generator/constants.py b/infrahub_sdk/protocols_generator/constants.py index 282217948..2c873645a 100644 --- a/infrahub_sdk/protocols_generator/constants.py +++ b/infrahub_sdk/protocols_generator/constants.py @@ -1,4 +1,5 @@ TEMPLATE_FILE_NAME = "template.j2" +HEADER_FILE_NAME = "header.j2" ATTRIBUTE_KIND_MAP = { "ID": "String", diff --git a/infrahub_sdk/protocols_generator/generator.py b/infrahub_sdk/protocols_generator/generator.py index e0a4c61cc..6b5ed4ccc 100644 --- a/infrahub_sdk/protocols_generator/generator.py +++ b/infrahub_sdk/protocols_generator/generator.py @@ -4,6 +4,7 @@ from pathlib import Path import jinja2 +from typing_extensions import assert_never from .. import protocols as sdk_protocols from ..schema import ( @@ -18,11 +19,12 @@ RelationshipSchemaAPI, TemplateSchemaAPI, ) -from .constants import ATTRIBUTE_KIND_MAP, CORE_BASE_CLASS_TO_SYNCIFY, TEMPLATE_FILE_NAME +from .constants import ATTRIBUTE_KIND_MAP, CORE_BASE_CLASS_TO_SYNCIFY, HEADER_FILE_NAME, TEMPLATE_FILE_NAME +from .target import ProtocolTarget -def load_template() -> str: - path = Path(__file__).parent / TEMPLATE_FILE_NAME +def load_template(file_name: str = TEMPLATE_FILE_NAME) -> str: + path = Path(__file__).parent / file_name return path.read_text() @@ -35,7 +37,10 @@ def move_to_end_of_list(lst: list, item: str) -> list: class CodeGenerator: - def __init__(self, schema: dict[str, MainSchemaTypesAll]) -> None: + def __init__( + self, schema: dict[str, MainSchemaTypesAll], target: ProtocolTarget = ProtocolTarget.USER_SCHEMA + ) -> None: + self.target: ProtocolTarget = target self.generics: dict[str, GenericSchemaAPI | GenericSchema] = {} self.nodes: dict[str, NodeSchemaAPI | NodeSchema] = {} self.profiles: dict[str, ProfileSchemaAPI] = {} @@ -51,14 +56,29 @@ def __init__(self, schema: dict[str, MainSchemaTypesAll]) -> None: if isinstance(schema_type, TemplateSchemaAPI): self.templates[name] = schema_type - self.base_protocols = [ - e - for e in dir(sdk_protocols) - if not e.startswith("__") - and not e.endswith("__") - and e - not in {"TYPE_CHECKING", "CoreNode", "Optional", "Protocol", "Union", "annotations", "runtime_checkable"} - ] + match self.target: + case ProtocolTarget.USER_SCHEMA: + self.base_protocols = [ + e + for e in dir(sdk_protocols) + if not e.startswith("__") + and not e.endswith("__") + and e + not in { + "TYPE_CHECKING", + "CoreNode", + "Optional", + "Protocol", + "Union", + "annotations", + "runtime_checkable", + } + ] + case ProtocolTarget.SDK_CORE: + # Nothing can be imported from the module being generated. + self.base_protocols = [] + case _: + assert_never(self.target) self.sorted_generics = self._sort_and_filter_models(self.generics, filters=["CoreNode", *self.base_protocols]) self.sorted_nodes = self._sort_and_filter_models(self.nodes, filters=["CoreNode", *self.base_protocols]) @@ -69,7 +89,29 @@ def __init__(self, schema: dict[str, MainSchemaTypesAll]) -> None: self.templates, filters=["CoreObjectTemplate", *self.base_protocols] ) + # Which referenced names have a Sync counterpart to switch to when rendering sync output. + # The two positions resolve names differently for a user schema: a peer may be any core + # kind, while an inheritance list only ever switches the few base classes. Generating the + # core module makes both the same, since every class in it is local. + match self.target: + case ProtocolTarget.USER_SCHEMA: + self._inherited_sync_names = frozenset(CORE_BASE_CLASS_TO_SYNCIFY) + self._peer_sync_names = frozenset( + name for name in self.base_protocols if f"{name}Sync" in self.base_protocols + ) + case ProtocolTarget.SDK_CORE: + local_names = self._local_class_names() | {"CoreNode"} + self._inherited_sync_names = local_names + self._peer_sync_names = local_names + case _: + assert_never(self.target) + def render(self, sync: bool = True) -> str: + """Render the protocols module. + + ``sync`` selects which variant to render for ``USER_SCHEMA``. ``SDK_CORE`` renders both + variants into a single module, so it does not apply there. + """ jinja2_env = jinja2.Environment( loader=jinja2.BaseLoader(), trim_blocks=True, @@ -80,23 +122,45 @@ def render(self, sync: bool = True) -> str: jinja2_env.filters["render_relationship"] = self._jinja2_filter_render_relationship jinja2_env.filters["syncify"] = self._jinja2_filter_syncify - template = jinja2_env.from_string(load_template()) - return template.render( + header = jinja2_env.from_string(load_template(HEADER_FILE_NAME)) + body = jinja2_env.from_string(load_template(TEMPLATE_FILE_NAME)) + + match self.target: + case ProtocolTarget.USER_SCHEMA: + return header.render(sync=sync, base_protocols=self.base_protocols, core=False) + self._render_body( + body, sync=sync, suffix="" + ) + case ProtocolTarget.SDK_CORE: + return ( + header.render(sync=False, base_protocols=self.base_protocols, core=True) + + self._render_body(body, sync=False, suffix="") + + self._render_body(body, sync=True, suffix="Sync") + ) + case _: + assert_never(self.target) + + def _render_body(self, body: jinja2.Template, sync: bool, suffix: str) -> str: + return body.render( generics=self.sorted_generics, nodes=self.sorted_nodes, profiles=self.sorted_profiles, templates=self.sorted_templates, - base_protocols=self.base_protocols, core_node_name="CoreNodeSync" if sync else "CoreNode", sync=sync, + suffix=suffix, ) - @staticmethod - def _jinja2_filter_syncify(value: str | list, sync: bool = False) -> str | list: + def _local_class_names(self) -> frozenset[str]: + return frozenset( + f"{model.namespace}{model.name}" + for model in (*self.sorted_generics, *self.sorted_nodes, *self.sorted_profiles, *self.sorted_templates) + ) + + def _jinja2_filter_syncify(self, value: str | list, sync: bool = False) -> str | list: """Filter to help with the convertion to sync. If a string is provided, append Sync to the end of the string - If a list is provided, search for CoreNode and replace it with CoreNodeSync + If a list is provided, append Sync to the items that have a Sync counterpart """ if isinstance(value, list): # Order the list based on the CORE_BASE_CLASS_TO_SYNCIFY list to ensure the base classes are always last @@ -110,7 +174,7 @@ def _jinja2_filter_syncify(value: str | list, sync: bool = False) -> str | list: return f"{value}Sync" if isinstance(value, list): - return [f"{item}Sync" if item in CORE_BASE_CLASS_TO_SYNCIFY else item for item in value] + return [f"{item}Sync" if item in self._inherited_sync_names else item for item in value] return value @@ -136,10 +200,9 @@ def _jinja2_filter_render_relationship(self, value: RelationshipSchemaAPI, sync: if sync: type_ += "Sync" - # Core peer protocols expose a dedicated ``*Sync`` variant; reference it in sync - # output. Locally generated node/generic classes keep their name (they already - # inherit the sync base class), so only swap when a ``*Sync`` peer actually exists. - if f"{peer}Sync" in self.base_protocols: + # Peers with a dedicated ``*Sync`` variant are referenced by it in sync output. The + # rest keep their name, because they already are the sync class. + if peer in self._peer_sync_names: peer = f"{peer}Sync" return f"{name}: {type_}[{peer}]" diff --git a/infrahub_sdk/protocols_generator/header.j2 b/infrahub_sdk/protocols_generator/header.j2 new file mode 100644 index 000000000..67eeb2a83 --- /dev/null +++ b/infrahub_sdk/protocols_generator/header.j2 @@ -0,0 +1,92 @@ +{% if core %} +# Generated by "invoke backend.generate", do not edit directly + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from .protocols_base import CoreNode, CoreNodeSync + +if TYPE_CHECKING: + from infrahub_sdk.node import ( + RelationshipAttribute, + RelationshipAttributeSync, + RelationshipManager, + RelationshipManagerSync, + ) + from .protocols_base import ( + AnyAttribute, + AnyAttributeOptional, + String, + StringOptional, + Integer, + IntegerOptional, + Boolean, + BooleanOptional, + DateTime, + DateTimeOptional, + Dropdown, + DropdownOptional, + HashedPassword, + HashedPasswordOptional, + MacAddress, + MacAddressOptional, + IPHost, + IPHostOptional, + IPNetwork, + IPNetworkOptional, + IPAddress, + IPAddressOptional, + JSONAttribute, + JSONAttributeOptional, + ListAttribute, + ListAttributeOptional, + URL, + URLOptional, + ) + +{% else %} +# +# Generated by "infrahubctl protocols" +# + +from __future__ import annotations + +from typing import TYPE_CHECKING, Optional + +from infrahub_sdk.protocols import {{ "CoreNode" | syncify(sync) }}, {{ base_protocols | join(', ') }} + +if TYPE_CHECKING: + from infrahub_sdk.node import {{ "RelatedNode" | syncify(sync) }}, {{ "RelationshipAttribute" | syncify(sync) }}, {{ "RelationshipManager" | syncify(sync) }} + from infrahub_sdk.protocols_base import ( + AnyAttribute, + AnyAttributeOptional, + String, + StringOptional, + Integer, + IntegerOptional, + Boolean, + BooleanOptional, + DateTime, + DateTimeOptional, + Dropdown, + DropdownOptional, + HashedPassword, + HashedPasswordOptional, + MacAddress, + MacAddressOptional, + IPHost, + IPHostOptional, + IPNetwork, + IPNetworkOptional, + IPAddress, + IPAddressOptional, + JSONAttribute, + JSONAttributeOptional, + ListAttribute, + ListAttributeOptional, + URL, + URLOptional, + ) + +{% endif %} diff --git a/infrahub_sdk/protocols_generator/target.py b/infrahub_sdk/protocols_generator/target.py new file mode 100644 index 000000000..7dfcacadd --- /dev/null +++ b/infrahub_sdk/protocols_generator/target.py @@ -0,0 +1,16 @@ +from enum import Enum + + +class ProtocolTarget(str, Enum): + """Which protocols module is being generated. + + ``USER_SCHEMA`` renders protocols for a user's own schema into a standalone module, one file + per async/sync variant, importing the core kinds it references from ``infrahub_sdk.protocols``. + + ``SDK_CORE`` renders ``infrahub_sdk.protocols`` itself. Every kind is local, so nothing can be + imported from that module, both variants share a single file, and the sync classes carry a + ``Sync`` suffix to keep their names distinct. + """ + + USER_SCHEMA = "user-schema" + SDK_CORE = "sdk-core" diff --git a/infrahub_sdk/protocols_generator/template.j2 b/infrahub_sdk/protocols_generator/template.j2 index a1c9fca53..917f5a530 100644 --- a/infrahub_sdk/protocols_generator/template.j2 +++ b/infrahub_sdk/protocols_generator/template.j2 @@ -1,49 +1,6 @@ -# -# Generated by "infrahubctl protocols" -# - -from __future__ import annotations - -from typing import TYPE_CHECKING, Optional - -from infrahub_sdk.protocols import {{ "CoreNode" | syncify(sync) }}, {{ base_protocols | join(', ') }} - -if TYPE_CHECKING: - from infrahub_sdk.node import {{ "RelatedNode" | syncify(sync) }}, {{ "RelationshipAttribute" | syncify(sync) }}, {{ "RelationshipManager" | syncify(sync) }} - from infrahub_sdk.protocols_base import ( - AnyAttribute, - AnyAttributeOptional, - String, - StringOptional, - Integer, - IntegerOptional, - Boolean, - BooleanOptional, - DateTime, - DateTimeOptional, - Dropdown, - DropdownOptional, - HashedPassword, - HashedPasswordOptional, - MacAddress, - MacAddressOptional, - IPHost, - IPHostOptional, - IPNetwork, - IPNetworkOptional, - IPAddress, - IPAddressOptional, - JSONAttribute, - JSONAttributeOptional, - ListAttribute, - ListAttributeOptional, - URL, - URLOptional, - ) - {% for generic in generics %} -class {{ generic.namespace + generic.name }}({{core_node_name}}): +class {{ generic.namespace + generic.name }}{{ suffix }}({{core_node_name}}): {% if not generic.attributes|default([]) and not generic.relationships|default([]) %} pass {% endif %} @@ -53,16 +10,16 @@ class {{ generic.namespace + generic.name }}({{core_node_name}}): {% for relationship in generic.relationships | sort(attribute='name') | default([]) %} {{ relationship | render_relationship(sync) }} {% endfor %} - {% if generic.hierarchical | default(false) %} - parent: {{ "RelationshipAttribute" | syncify(sync) }}[{{ generic.namespace + generic.name }}] - children: {{ "RelationshipManager" | syncify(sync) }}[{{ generic.namespace + generic.name }}] + {% if generic.hierarchical | default(false) and "parent" not in generic.relationships | default([]) | map(attribute="name") %} + parent: {{ "RelationshipAttribute" | syncify(sync) }}[{{ generic.namespace + generic.name }}{{ suffix }}] + children: {{ "RelationshipManager" | syncify(sync) }}[{{ generic.namespace + generic.name }}{{ suffix }}] {% endif %} {% endfor %} {% for node in nodes %} -class {{ node.namespace + node.name }}({{ node.inherit_from | syncify(sync) | join(", ") or core_node_name }}): +class {{ node.namespace + node.name }}{{ suffix }}({{ node.inherit_from | syncify(sync) | join(", ") or core_node_name }}): {% if not node.attributes|default([]) and not node.relationships|default([]) %} pass {% endif %} @@ -72,9 +29,9 @@ class {{ node.namespace + node.name }}({{ node.inherit_from | syncify(sync) | jo {% for relationship in node.relationships | sort(attribute='name') | default([]) %} {{ relationship | render_relationship(sync) }} {% endfor %} - {% if node.hierarchical | default(false) %} - parent: {{ "RelationshipAttribute" | syncify(sync) }}[{{ node.namespace + node.name }}] - children: {{ "RelationshipManager" | syncify(sync) }}[{{ node.namespace + node.name }}] + {% if node.hierarchical | default(false) and "parent" not in node.relationships | default([]) | map(attribute="name") %} + parent: {{ "RelationshipAttribute" | syncify(sync) }}[{{ node.namespace + node.name }}{{ suffix }}] + children: {{ "RelationshipManager" | syncify(sync) }}[{{ node.namespace + node.name }}{{ suffix }}] {% endif %} {% endfor %} @@ -82,7 +39,7 @@ class {{ node.namespace + node.name }}({{ node.inherit_from | syncify(sync) | jo {% for node in profiles %} -class {{ node.namespace + node.name }}({{ node.inherit_from | syncify(sync) | join(", ") or core_node_name }}): +class {{ node.namespace + node.name }}{{ suffix }}({{ node.inherit_from | syncify(sync) | join(", ") or core_node_name }}): {% if not node.attributes|default([]) and not node.relationships|default([]) %} pass {% endif %} @@ -92,9 +49,9 @@ class {{ node.namespace + node.name }}({{ node.inherit_from | syncify(sync) | jo {% for relationship in node.relationships | sort(attribute='name') | default([]) %} {{ relationship | render_relationship(sync) }} {% endfor %} - {% if node.hierarchical | default(false) %} - parent: {{ "RelationshipAttribute" | syncify(sync) }}[{{ node.namespace + node.name }}] - children: {{ "RelationshipManager" | syncify(sync) }}[{{ node.namespace + node.name }}] + {% if node.hierarchical | default(false) and "parent" not in node.relationships | default([]) | map(attribute="name") %} + parent: {{ "RelationshipAttribute" | syncify(sync) }}[{{ node.namespace + node.name }}{{ suffix }}] + children: {{ "RelationshipManager" | syncify(sync) }}[{{ node.namespace + node.name }}{{ suffix }}] {% endif %} {% endfor %} @@ -102,7 +59,7 @@ class {{ node.namespace + node.name }}({{ node.inherit_from | syncify(sync) | jo {% for node in templates %} -class {{ node.namespace + node.name }}({{ node.inherit_from | syncify(sync) | join(", ") or core_node_name }}): +class {{ node.namespace + node.name }}{{ suffix }}({{ node.inherit_from | syncify(sync) | join(", ") or core_node_name }}): {% if not node.attributes|default([]) and not node.relationships|default([]) %} pass {% endif %} diff --git a/tests/fixtures/protocols_generator/user_schema_async.txt b/tests/fixtures/protocols_generator/user_schema_async.txt new file mode 100644 index 000000000..194c08dc0 --- /dev/null +++ b/tests/fixtures/protocols_generator/user_schema_async.txt @@ -0,0 +1,358 @@ +# +# Generated by "infrahubctl protocols" +# + +from __future__ import annotations + +from typing import TYPE_CHECKING, Optional + +from infrahub_sdk.protocols import CoreNode, BuiltinIPAddress, BuiltinIPAddressSync, BuiltinIPNamespace, BuiltinIPNamespaceSync, BuiltinIPPrefix, BuiltinIPPrefixSync, BuiltinTag, BuiltinTagSync, CoreAccount, CoreAccountGroup, CoreAccountGroupSync, CoreAccountRole, CoreAccountRoleSync, CoreAccountSync, CoreAction, CoreActionSync, CoreArtifact, CoreArtifactCheck, CoreArtifactCheckSync, CoreArtifactDefinition, CoreArtifactDefinitionSync, CoreArtifactSync, CoreArtifactTarget, CoreArtifactTargetSync, CoreArtifactThread, CoreArtifactThreadSync, CoreArtifactValidator, CoreArtifactValidatorSync, CoreBasePermission, CoreBasePermissionSync, CoreChangeComment, CoreChangeCommentSync, CoreChangeThread, CoreChangeThreadSync, CoreCheck, CoreCheckDefinition, CoreCheckDefinitionSync, CoreCheckSync, CoreComment, CoreCommentSync, CoreCredential, CoreCredentialSync, CoreCustomWebhook, CoreCustomWebhookSync, CoreDataCheck, CoreDataCheckSync, CoreDataValidator, CoreDataValidatorSync, CoreEnvKeyValue, CoreEnvKeyValueSync, CoreFileCheck, CoreFileCheckSync, CoreFileObject, CoreFileObjectSync, CoreFileThread, CoreFileThreadSync, CoreGeneratorAction, CoreGeneratorActionSync, CoreGeneratorAwareGroup, CoreGeneratorAwareGroupSync, CoreGeneratorCheck, CoreGeneratorCheckSync, CoreGeneratorDefinition, CoreGeneratorDefinitionSync, CoreGeneratorGroup, CoreGeneratorGroupSync, CoreGeneratorInstance, CoreGeneratorInstanceSync, CoreGeneratorValidator, CoreGeneratorValidatorSync, CoreGenericAccount, CoreGenericAccountSync, CoreGenericRepository, CoreGenericRepositorySync, CoreGlobalPermission, CoreGlobalPermissionSync, CoreGraphQLQuery, CoreGraphQLQueryGroup, CoreGraphQLQueryGroupSync, CoreGraphQLQuerySync, CoreGroup, CoreGroupAction, CoreGroupActionSync, CoreGroupSync, CoreGroupTriggerRule, CoreGroupTriggerRuleSync, CoreIPAddressPool, CoreIPAddressPoolSync, CoreIPPool, CoreIPPoolSync, CoreIPPrefixPool, CoreIPPrefixPoolSync, CoreKeyValue, CoreKeyValueSync, CoreMenu, CoreMenuItem, CoreMenuItemSync, CoreMenuSync, CoreNodeSync, CoreNodeTriggerAttributeMatch, CoreNodeTriggerAttributeMatchSync, CoreNodeTriggerMatch, CoreNodeTriggerMatchSync, CoreNodeTriggerRelationshipMatch, CoreNodeTriggerRelationshipMatchSync, CoreNodeTriggerRule, CoreNodeTriggerRuleSync, CoreNumberPool, CoreNumberPoolSync, CoreObjectComponentTemplate, CoreObjectComponentTemplateSync, CoreObjectPermission, CoreObjectPermissionSync, CoreObjectTemplate, CoreObjectTemplateSync, CoreObjectThread, CoreObjectThreadSync, CorePasswordCredential, CorePasswordCredentialSync, CoreProfile, CoreProfileSync, CoreProposedChange, CoreProposedChangeSync, CoreReadOnlyRepository, CoreReadOnlyRepositorySync, CoreRepository, CoreRepositoryGroup, CoreRepositoryGroupSync, CoreRepositorySync, CoreRepositoryValidator, CoreRepositoryValidatorSync, CoreResourcePool, CoreResourcePoolSync, CoreSchemaCheck, CoreSchemaCheckSync, CoreSchemaValidator, CoreSchemaValidatorSync, CoreStandardCheck, CoreStandardCheckSync, CoreStandardGroup, CoreStandardGroupSync, CoreStandardWebhook, CoreStandardWebhookSync, CoreStaticKeyValue, CoreStaticKeyValueSync, CoreTaskTarget, CoreTaskTargetSync, CoreThread, CoreThreadComment, CoreThreadCommentSync, CoreThreadSync, CoreTransformJinja2, CoreTransformJinja2Sync, CoreTransformPython, CoreTransformPythonSync, CoreTransformation, CoreTransformationSync, CoreTriggerRule, CoreTriggerRuleSync, CoreUserValidator, CoreUserValidatorSync, CoreValidator, CoreValidatorSync, CoreWebhook, CoreWebhookSync, CoreWeightedPoolResource, CoreWeightedPoolResourceSync, InternalAccountToken, InternalAccountTokenSync, InternalExternalIdentity, InternalExternalIdentitySync, InternalIPPrefixAvailable, InternalIPPrefixAvailableSync, InternalIPRangeAvailable, InternalIPRangeAvailableSync, InternalRefreshToken, InternalRefreshTokenSync, IpamNamespace, IpamNamespaceSync, LineageOwner, LineageOwnerSync, LineageSource, LineageSourceSync + +if TYPE_CHECKING: + from infrahub_sdk.node import RelatedNode, RelationshipAttribute, RelationshipManager + from infrahub_sdk.protocols_base import ( + AnyAttribute, + AnyAttributeOptional, + String, + StringOptional, + Integer, + IntegerOptional, + Boolean, + BooleanOptional, + DateTime, + DateTimeOptional, + Dropdown, + DropdownOptional, + HashedPassword, + HashedPasswordOptional, + MacAddress, + MacAddressOptional, + IPHost, + IPHostOptional, + IPNetwork, + IPNetworkOptional, + IPAddress, + IPAddressOptional, + JSONAttribute, + JSONAttributeOptional, + ListAttribute, + ListAttributeOptional, + URL, + URLOptional, + ) + + +class LocationGeneric(CoreNode): + description: StringOptional + name: String + shortname: String + children: RelationshipManager[LocationGeneric] + member_of_groups: RelationshipManager[CoreGroup] + parent: RelationshipAttribute[LocationGeneric] + profiles: RelationshipManager[CoreProfile] + servers: RelationshipManager[NetworkManagementServer] + subscriber_of_groups: RelationshipManager[CoreGroup] + tags: RelationshipManager[BuiltinTag] + +class NetworkManagementServer(CoreNode): + description: StringOptional + name: String + status: Dropdown + ip_addresses: RelationshipManager[IpamIPAddress] + location: RelationshipAttribute[LocationGeneric] + member_of_groups: RelationshipManager[CoreGroup] + profiles: RelationshipManager[CoreProfile] + subscriber_of_groups: RelationshipManager[CoreGroup] + + + +class LocationCountry(LocationGeneric): + description: StringOptional + name: String + shortname: String + timezone: StringOptional + children: RelationshipManager[LocationSite] + member_of_groups: RelationshipManager[CoreGroup] + parent: RelationshipAttribute[LocationGeneric] + profiles: RelationshipManager[CoreProfile] + servers: RelationshipManager[NetworkManagementServer] + subscriber_of_groups: RelationshipManager[CoreGroup] + tags: RelationshipManager[BuiltinTag] + + +class InfraDevice(CoreNode): + description: StringOptional + name: String + role: Dropdown + role_id: Integer + status: Dropdown + location: RelationshipAttribute[LocationSite] + member_of_groups: RelationshipManager[CoreGroup] + object_template: RelationshipAttribute[TemplateInfraDevice] + primary_ip: RelationshipAttribute[IpamIPAddress] + profiles: RelationshipManager[CoreProfile] + subscriber_of_groups: RelationshipManager[CoreGroup] + + +class NetworkDhcpServer(NetworkManagementServer): + description: StringOptional + lease_time: String + name: String + status: Dropdown + ip_addresses: RelationshipManager[IpamIPAddress] + location: RelationshipAttribute[LocationGeneric] + member_of_groups: RelationshipManager[CoreGroup] + profiles: RelationshipManager[CoreProfile] + subscriber_of_groups: RelationshipManager[CoreGroup] + + +class IpamIPAddress(BuiltinIPAddress): + address: IPHost + description: StringOptional + fqdn: StringOptional + ip_namespace: RelationshipAttribute[BuiltinIPNamespace] + ip_prefix: RelationshipAttribute[BuiltinIPPrefix] + member_of_groups: RelationshipManager[CoreGroup] + profiles: RelationshipManager[CoreProfile] + subscriber_of_groups: RelationshipManager[CoreGroup] + + +class NetworkNTPServer(NetworkManagementServer): + description: StringOptional + name: String + status: Dropdown + ip_addresses: RelationshipManager[IpamIPAddress] + location: RelationshipAttribute[LocationGeneric] + member_of_groups: RelationshipManager[CoreGroup] + profiles: RelationshipManager[CoreProfile] + subscriber_of_groups: RelationshipManager[CoreGroup] + + +class NetworkNameServer(NetworkManagementServer): + description: StringOptional + name: String + status: Dropdown + ip_addresses: RelationshipManager[IpamIPAddress] + location: RelationshipAttribute[LocationGeneric] + member_of_groups: RelationshipManager[CoreGroup] + profiles: RelationshipManager[CoreProfile] + subscriber_of_groups: RelationshipManager[CoreGroup] + + +class IpamPrefix(BuiltinIPPrefix): + broadcast_address: StringOptional + description: StringOptional + hostmask: StringOptional + is_pool: Boolean + is_top_level: BooleanOptional + member_type: Dropdown + netmask: StringOptional + network_address: StringOptional + prefix: IPNetwork + role: DropdownOptional + status: Dropdown + utilization: IntegerOptional + children: RelationshipManager[BuiltinIPPrefix] + ip_addresses: RelationshipManager[BuiltinIPAddress] + ip_namespace: RelationshipAttribute[BuiltinIPNamespace] + member_of_groups: RelationshipManager[CoreGroup] + parent: RelationshipAttribute[BuiltinIPPrefix] + profiles: RelationshipManager[CoreProfile] + resource_pool: RelationshipManager[CoreIPAddressPool] + subscriber_of_groups: RelationshipManager[CoreGroup] + + +class LocationRack(LocationGeneric): + description: StringOptional + facility_id: StringOptional + height: Integer + name: String + shortname: String + children: RelationshipManager[LocationGeneric] + member_of_groups: RelationshipManager[CoreGroup] + parent: RelationshipAttribute[LocationSite] + profiles: RelationshipManager[CoreProfile] + servers: RelationshipManager[NetworkManagementServer] + subscriber_of_groups: RelationshipManager[CoreGroup] + tags: RelationshipManager[BuiltinTag] + + +class LocationSite(LocationGeneric): + description: StringOptional + facility_id: StringOptional + name: String + physical_address: StringOptional + shortname: String + children: RelationshipManager[LocationRack] + member_of_groups: RelationshipManager[CoreGroup] + parent: RelationshipAttribute[LocationCountry] + profiles: RelationshipManager[CoreProfile] + servers: RelationshipManager[NetworkManagementServer] + subscriber_of_groups: RelationshipManager[CoreGroup] + tags: RelationshipManager[BuiltinTag] + + + + +class ProfileBuiltinIPAddress(LineageSource, CoreProfile, CoreNode): + description: StringOptional + profile_name: String + profile_priority: Integer + member_of_groups: RelationshipManager[CoreGroup] + related_nodes: RelationshipManager[BuiltinIPAddress] + subscriber_of_groups: RelationshipManager[CoreGroup] + + +class ProfileBuiltinIPPrefix(LineageSource, CoreProfile, CoreNode): + description: StringOptional + is_pool: BooleanOptional + member_type: DropdownOptional + profile_name: String + profile_priority: Integer + member_of_groups: RelationshipManager[CoreGroup] + related_nodes: RelationshipManager[BuiltinIPPrefix] + subscriber_of_groups: RelationshipManager[CoreGroup] + + +class ProfileBuiltinTag(LineageSource, CoreProfile, CoreNode): + description: StringOptional + profile_name: String + profile_priority: Integer + member_of_groups: RelationshipManager[CoreGroup] + related_nodes: RelationshipManager[BuiltinTag] + subscriber_of_groups: RelationshipManager[CoreGroup] + + +class ProfileInfraDevice(LineageSource, CoreProfile, CoreNode): + description: StringOptional + profile_name: String + profile_priority: Integer + role: DropdownOptional + status: DropdownOptional + member_of_groups: RelationshipManager[CoreGroup] + related_nodes: RelationshipManager[InfraDevice] + subscriber_of_groups: RelationshipManager[CoreGroup] + + +class ProfileIpamIPAddress(LineageSource, CoreProfile, CoreNode): + description: StringOptional + fqdn: StringOptional + profile_name: String + profile_priority: Integer + member_of_groups: RelationshipManager[CoreGroup] + related_nodes: RelationshipManager[IpamIPAddress] + subscriber_of_groups: RelationshipManager[CoreGroup] + + +class ProfileIpamNamespace(LineageSource, CoreProfile, CoreNode): + description: StringOptional + profile_name: String + profile_priority: Integer + member_of_groups: RelationshipManager[CoreGroup] + related_nodes: RelationshipManager[IpamNamespace] + subscriber_of_groups: RelationshipManager[CoreGroup] + + +class ProfileIpamPrefix(LineageSource, CoreProfile, CoreNode): + description: StringOptional + is_pool: BooleanOptional + member_type: DropdownOptional + profile_name: String + profile_priority: Integer + role: DropdownOptional + member_of_groups: RelationshipManager[CoreGroup] + related_nodes: RelationshipManager[IpamPrefix] + subscriber_of_groups: RelationshipManager[CoreGroup] + + +class ProfileLocationCountry(LineageSource, CoreProfile, CoreNode): + description: StringOptional + profile_name: String + profile_priority: Integer + timezone: StringOptional + member_of_groups: RelationshipManager[CoreGroup] + related_nodes: RelationshipManager[LocationCountry] + subscriber_of_groups: RelationshipManager[CoreGroup] + + +class ProfileLocationGeneric(LineageSource, CoreProfile, CoreNode): + description: StringOptional + profile_name: String + profile_priority: Integer + member_of_groups: RelationshipManager[CoreGroup] + related_nodes: RelationshipManager[LocationGeneric] + subscriber_of_groups: RelationshipManager[CoreGroup] + + +class ProfileLocationRack(LineageSource, CoreProfile, CoreNode): + description: StringOptional + facility_id: StringOptional + height: IntegerOptional + profile_name: String + profile_priority: Integer + member_of_groups: RelationshipManager[CoreGroup] + related_nodes: RelationshipManager[LocationRack] + subscriber_of_groups: RelationshipManager[CoreGroup] + + +class ProfileLocationSite(LineageSource, CoreProfile, CoreNode): + description: StringOptional + facility_id: StringOptional + physical_address: StringOptional + profile_name: String + profile_priority: Integer + member_of_groups: RelationshipManager[CoreGroup] + related_nodes: RelationshipManager[LocationSite] + subscriber_of_groups: RelationshipManager[CoreGroup] + + +class ProfileNetworkDhcpServer(LineageSource, CoreProfile, CoreNode): + description: StringOptional + profile_name: String + profile_priority: Integer + member_of_groups: RelationshipManager[CoreGroup] + related_nodes: RelationshipManager[NetworkDhcpServer] + subscriber_of_groups: RelationshipManager[CoreGroup] + + +class ProfileNetworkManagementServer(LineageSource, CoreProfile, CoreNode): + description: StringOptional + profile_name: String + profile_priority: Integer + member_of_groups: RelationshipManager[CoreGroup] + related_nodes: RelationshipManager[NetworkManagementServer] + subscriber_of_groups: RelationshipManager[CoreGroup] + + +class ProfileNetworkNTPServer(LineageSource, CoreProfile, CoreNode): + description: StringOptional + profile_name: String + profile_priority: Integer + member_of_groups: RelationshipManager[CoreGroup] + related_nodes: RelationshipManager[NetworkNTPServer] + subscriber_of_groups: RelationshipManager[CoreGroup] + + +class ProfileNetworkNameServer(LineageSource, CoreProfile, CoreNode): + description: StringOptional + profile_name: String + profile_priority: Integer + member_of_groups: RelationshipManager[CoreGroup] + related_nodes: RelationshipManager[NetworkNameServer] + subscriber_of_groups: RelationshipManager[CoreGroup] + + + + +class TemplateInfraDevice(LineageSource, CoreObjectTemplate, CoreNode): + description: StringOptional + role: Dropdown + role_id: IntegerOptional + status: Dropdown + template_name: String + location: RelationshipAttribute[LocationSite] + member_of_groups: RelationshipManager[CoreGroup] + primary_ip: RelationshipAttribute[IpamIPAddress] + related_nodes: RelationshipManager[InfraDevice] + subscriber_of_groups: RelationshipManager[CoreGroup] + diff --git a/tests/fixtures/protocols_generator/user_schema_sync.txt b/tests/fixtures/protocols_generator/user_schema_sync.txt new file mode 100644 index 000000000..46c274f0b --- /dev/null +++ b/tests/fixtures/protocols_generator/user_schema_sync.txt @@ -0,0 +1,358 @@ +# +# Generated by "infrahubctl protocols" +# + +from __future__ import annotations + +from typing import TYPE_CHECKING, Optional + +from infrahub_sdk.protocols import CoreNodeSync, BuiltinIPAddress, BuiltinIPAddressSync, BuiltinIPNamespace, BuiltinIPNamespaceSync, BuiltinIPPrefix, BuiltinIPPrefixSync, BuiltinTag, BuiltinTagSync, CoreAccount, CoreAccountGroup, CoreAccountGroupSync, CoreAccountRole, CoreAccountRoleSync, CoreAccountSync, CoreAction, CoreActionSync, CoreArtifact, CoreArtifactCheck, CoreArtifactCheckSync, CoreArtifactDefinition, CoreArtifactDefinitionSync, CoreArtifactSync, CoreArtifactTarget, CoreArtifactTargetSync, CoreArtifactThread, CoreArtifactThreadSync, CoreArtifactValidator, CoreArtifactValidatorSync, CoreBasePermission, CoreBasePermissionSync, CoreChangeComment, CoreChangeCommentSync, CoreChangeThread, CoreChangeThreadSync, CoreCheck, CoreCheckDefinition, CoreCheckDefinitionSync, CoreCheckSync, CoreComment, CoreCommentSync, CoreCredential, CoreCredentialSync, CoreCustomWebhook, CoreCustomWebhookSync, CoreDataCheck, CoreDataCheckSync, CoreDataValidator, CoreDataValidatorSync, CoreEnvKeyValue, CoreEnvKeyValueSync, CoreFileCheck, CoreFileCheckSync, CoreFileObject, CoreFileObjectSync, CoreFileThread, CoreFileThreadSync, CoreGeneratorAction, CoreGeneratorActionSync, CoreGeneratorAwareGroup, CoreGeneratorAwareGroupSync, CoreGeneratorCheck, CoreGeneratorCheckSync, CoreGeneratorDefinition, CoreGeneratorDefinitionSync, CoreGeneratorGroup, CoreGeneratorGroupSync, CoreGeneratorInstance, CoreGeneratorInstanceSync, CoreGeneratorValidator, CoreGeneratorValidatorSync, CoreGenericAccount, CoreGenericAccountSync, CoreGenericRepository, CoreGenericRepositorySync, CoreGlobalPermission, CoreGlobalPermissionSync, CoreGraphQLQuery, CoreGraphQLQueryGroup, CoreGraphQLQueryGroupSync, CoreGraphQLQuerySync, CoreGroup, CoreGroupAction, CoreGroupActionSync, CoreGroupSync, CoreGroupTriggerRule, CoreGroupTriggerRuleSync, CoreIPAddressPool, CoreIPAddressPoolSync, CoreIPPool, CoreIPPoolSync, CoreIPPrefixPool, CoreIPPrefixPoolSync, CoreKeyValue, CoreKeyValueSync, CoreMenu, CoreMenuItem, CoreMenuItemSync, CoreMenuSync, CoreNodeSync, CoreNodeTriggerAttributeMatch, CoreNodeTriggerAttributeMatchSync, CoreNodeTriggerMatch, CoreNodeTriggerMatchSync, CoreNodeTriggerRelationshipMatch, CoreNodeTriggerRelationshipMatchSync, CoreNodeTriggerRule, CoreNodeTriggerRuleSync, CoreNumberPool, CoreNumberPoolSync, CoreObjectComponentTemplate, CoreObjectComponentTemplateSync, CoreObjectPermission, CoreObjectPermissionSync, CoreObjectTemplate, CoreObjectTemplateSync, CoreObjectThread, CoreObjectThreadSync, CorePasswordCredential, CorePasswordCredentialSync, CoreProfile, CoreProfileSync, CoreProposedChange, CoreProposedChangeSync, CoreReadOnlyRepository, CoreReadOnlyRepositorySync, CoreRepository, CoreRepositoryGroup, CoreRepositoryGroupSync, CoreRepositorySync, CoreRepositoryValidator, CoreRepositoryValidatorSync, CoreResourcePool, CoreResourcePoolSync, CoreSchemaCheck, CoreSchemaCheckSync, CoreSchemaValidator, CoreSchemaValidatorSync, CoreStandardCheck, CoreStandardCheckSync, CoreStandardGroup, CoreStandardGroupSync, CoreStandardWebhook, CoreStandardWebhookSync, CoreStaticKeyValue, CoreStaticKeyValueSync, CoreTaskTarget, CoreTaskTargetSync, CoreThread, CoreThreadComment, CoreThreadCommentSync, CoreThreadSync, CoreTransformJinja2, CoreTransformJinja2Sync, CoreTransformPython, CoreTransformPythonSync, CoreTransformation, CoreTransformationSync, CoreTriggerRule, CoreTriggerRuleSync, CoreUserValidator, CoreUserValidatorSync, CoreValidator, CoreValidatorSync, CoreWebhook, CoreWebhookSync, CoreWeightedPoolResource, CoreWeightedPoolResourceSync, InternalAccountToken, InternalAccountTokenSync, InternalExternalIdentity, InternalExternalIdentitySync, InternalIPPrefixAvailable, InternalIPPrefixAvailableSync, InternalIPRangeAvailable, InternalIPRangeAvailableSync, InternalRefreshToken, InternalRefreshTokenSync, IpamNamespace, IpamNamespaceSync, LineageOwner, LineageOwnerSync, LineageSource, LineageSourceSync + +if TYPE_CHECKING: + from infrahub_sdk.node import RelatedNodeSync, RelationshipAttributeSync, RelationshipManagerSync + from infrahub_sdk.protocols_base import ( + AnyAttribute, + AnyAttributeOptional, + String, + StringOptional, + Integer, + IntegerOptional, + Boolean, + BooleanOptional, + DateTime, + DateTimeOptional, + Dropdown, + DropdownOptional, + HashedPassword, + HashedPasswordOptional, + MacAddress, + MacAddressOptional, + IPHost, + IPHostOptional, + IPNetwork, + IPNetworkOptional, + IPAddress, + IPAddressOptional, + JSONAttribute, + JSONAttributeOptional, + ListAttribute, + ListAttributeOptional, + URL, + URLOptional, + ) + + +class LocationGeneric(CoreNodeSync): + description: StringOptional + name: String + shortname: String + children: RelationshipManagerSync[LocationGeneric] + member_of_groups: RelationshipManagerSync[CoreGroupSync] + parent: RelationshipAttributeSync[LocationGeneric] + profiles: RelationshipManagerSync[CoreProfileSync] + servers: RelationshipManagerSync[NetworkManagementServer] + subscriber_of_groups: RelationshipManagerSync[CoreGroupSync] + tags: RelationshipManagerSync[BuiltinTagSync] + +class NetworkManagementServer(CoreNodeSync): + description: StringOptional + name: String + status: Dropdown + ip_addresses: RelationshipManagerSync[IpamIPAddress] + location: RelationshipAttributeSync[LocationGeneric] + member_of_groups: RelationshipManagerSync[CoreGroupSync] + profiles: RelationshipManagerSync[CoreProfileSync] + subscriber_of_groups: RelationshipManagerSync[CoreGroupSync] + + + +class LocationCountry(LocationGeneric): + description: StringOptional + name: String + shortname: String + timezone: StringOptional + children: RelationshipManagerSync[LocationSite] + member_of_groups: RelationshipManagerSync[CoreGroupSync] + parent: RelationshipAttributeSync[LocationGeneric] + profiles: RelationshipManagerSync[CoreProfileSync] + servers: RelationshipManagerSync[NetworkManagementServer] + subscriber_of_groups: RelationshipManagerSync[CoreGroupSync] + tags: RelationshipManagerSync[BuiltinTagSync] + + +class InfraDevice(CoreNodeSync): + description: StringOptional + name: String + role: Dropdown + role_id: Integer + status: Dropdown + location: RelationshipAttributeSync[LocationSite] + member_of_groups: RelationshipManagerSync[CoreGroupSync] + object_template: RelationshipAttributeSync[TemplateInfraDevice] + primary_ip: RelationshipAttributeSync[IpamIPAddress] + profiles: RelationshipManagerSync[CoreProfileSync] + subscriber_of_groups: RelationshipManagerSync[CoreGroupSync] + + +class NetworkDhcpServer(NetworkManagementServer): + description: StringOptional + lease_time: String + name: String + status: Dropdown + ip_addresses: RelationshipManagerSync[IpamIPAddress] + location: RelationshipAttributeSync[LocationGeneric] + member_of_groups: RelationshipManagerSync[CoreGroupSync] + profiles: RelationshipManagerSync[CoreProfileSync] + subscriber_of_groups: RelationshipManagerSync[CoreGroupSync] + + +class IpamIPAddress(BuiltinIPAddress): + address: IPHost + description: StringOptional + fqdn: StringOptional + ip_namespace: RelationshipAttributeSync[BuiltinIPNamespaceSync] + ip_prefix: RelationshipAttributeSync[BuiltinIPPrefixSync] + member_of_groups: RelationshipManagerSync[CoreGroupSync] + profiles: RelationshipManagerSync[CoreProfileSync] + subscriber_of_groups: RelationshipManagerSync[CoreGroupSync] + + +class NetworkNTPServer(NetworkManagementServer): + description: StringOptional + name: String + status: Dropdown + ip_addresses: RelationshipManagerSync[IpamIPAddress] + location: RelationshipAttributeSync[LocationGeneric] + member_of_groups: RelationshipManagerSync[CoreGroupSync] + profiles: RelationshipManagerSync[CoreProfileSync] + subscriber_of_groups: RelationshipManagerSync[CoreGroupSync] + + +class NetworkNameServer(NetworkManagementServer): + description: StringOptional + name: String + status: Dropdown + ip_addresses: RelationshipManagerSync[IpamIPAddress] + location: RelationshipAttributeSync[LocationGeneric] + member_of_groups: RelationshipManagerSync[CoreGroupSync] + profiles: RelationshipManagerSync[CoreProfileSync] + subscriber_of_groups: RelationshipManagerSync[CoreGroupSync] + + +class IpamPrefix(BuiltinIPPrefix): + broadcast_address: StringOptional + description: StringOptional + hostmask: StringOptional + is_pool: Boolean + is_top_level: BooleanOptional + member_type: Dropdown + netmask: StringOptional + network_address: StringOptional + prefix: IPNetwork + role: DropdownOptional + status: Dropdown + utilization: IntegerOptional + children: RelationshipManagerSync[BuiltinIPPrefixSync] + ip_addresses: RelationshipManagerSync[BuiltinIPAddressSync] + ip_namespace: RelationshipAttributeSync[BuiltinIPNamespaceSync] + member_of_groups: RelationshipManagerSync[CoreGroupSync] + parent: RelationshipAttributeSync[BuiltinIPPrefixSync] + profiles: RelationshipManagerSync[CoreProfileSync] + resource_pool: RelationshipManagerSync[CoreIPAddressPoolSync] + subscriber_of_groups: RelationshipManagerSync[CoreGroupSync] + + +class LocationRack(LocationGeneric): + description: StringOptional + facility_id: StringOptional + height: Integer + name: String + shortname: String + children: RelationshipManagerSync[LocationGeneric] + member_of_groups: RelationshipManagerSync[CoreGroupSync] + parent: RelationshipAttributeSync[LocationSite] + profiles: RelationshipManagerSync[CoreProfileSync] + servers: RelationshipManagerSync[NetworkManagementServer] + subscriber_of_groups: RelationshipManagerSync[CoreGroupSync] + tags: RelationshipManagerSync[BuiltinTagSync] + + +class LocationSite(LocationGeneric): + description: StringOptional + facility_id: StringOptional + name: String + physical_address: StringOptional + shortname: String + children: RelationshipManagerSync[LocationRack] + member_of_groups: RelationshipManagerSync[CoreGroupSync] + parent: RelationshipAttributeSync[LocationCountry] + profiles: RelationshipManagerSync[CoreProfileSync] + servers: RelationshipManagerSync[NetworkManagementServer] + subscriber_of_groups: RelationshipManagerSync[CoreGroupSync] + tags: RelationshipManagerSync[BuiltinTagSync] + + + + +class ProfileBuiltinIPAddress(LineageSource, CoreProfileSync, CoreNodeSync): + description: StringOptional + profile_name: String + profile_priority: Integer + member_of_groups: RelationshipManagerSync[CoreGroupSync] + related_nodes: RelationshipManagerSync[BuiltinIPAddressSync] + subscriber_of_groups: RelationshipManagerSync[CoreGroupSync] + + +class ProfileBuiltinIPPrefix(LineageSource, CoreProfileSync, CoreNodeSync): + description: StringOptional + is_pool: BooleanOptional + member_type: DropdownOptional + profile_name: String + profile_priority: Integer + member_of_groups: RelationshipManagerSync[CoreGroupSync] + related_nodes: RelationshipManagerSync[BuiltinIPPrefixSync] + subscriber_of_groups: RelationshipManagerSync[CoreGroupSync] + + +class ProfileBuiltinTag(LineageSource, CoreProfileSync, CoreNodeSync): + description: StringOptional + profile_name: String + profile_priority: Integer + member_of_groups: RelationshipManagerSync[CoreGroupSync] + related_nodes: RelationshipManagerSync[BuiltinTagSync] + subscriber_of_groups: RelationshipManagerSync[CoreGroupSync] + + +class ProfileInfraDevice(LineageSource, CoreProfileSync, CoreNodeSync): + description: StringOptional + profile_name: String + profile_priority: Integer + role: DropdownOptional + status: DropdownOptional + member_of_groups: RelationshipManagerSync[CoreGroupSync] + related_nodes: RelationshipManagerSync[InfraDevice] + subscriber_of_groups: RelationshipManagerSync[CoreGroupSync] + + +class ProfileIpamIPAddress(LineageSource, CoreProfileSync, CoreNodeSync): + description: StringOptional + fqdn: StringOptional + profile_name: String + profile_priority: Integer + member_of_groups: RelationshipManagerSync[CoreGroupSync] + related_nodes: RelationshipManagerSync[IpamIPAddress] + subscriber_of_groups: RelationshipManagerSync[CoreGroupSync] + + +class ProfileIpamNamespace(LineageSource, CoreProfileSync, CoreNodeSync): + description: StringOptional + profile_name: String + profile_priority: Integer + member_of_groups: RelationshipManagerSync[CoreGroupSync] + related_nodes: RelationshipManagerSync[IpamNamespaceSync] + subscriber_of_groups: RelationshipManagerSync[CoreGroupSync] + + +class ProfileIpamPrefix(LineageSource, CoreProfileSync, CoreNodeSync): + description: StringOptional + is_pool: BooleanOptional + member_type: DropdownOptional + profile_name: String + profile_priority: Integer + role: DropdownOptional + member_of_groups: RelationshipManagerSync[CoreGroupSync] + related_nodes: RelationshipManagerSync[IpamPrefix] + subscriber_of_groups: RelationshipManagerSync[CoreGroupSync] + + +class ProfileLocationCountry(LineageSource, CoreProfileSync, CoreNodeSync): + description: StringOptional + profile_name: String + profile_priority: Integer + timezone: StringOptional + member_of_groups: RelationshipManagerSync[CoreGroupSync] + related_nodes: RelationshipManagerSync[LocationCountry] + subscriber_of_groups: RelationshipManagerSync[CoreGroupSync] + + +class ProfileLocationGeneric(LineageSource, CoreProfileSync, CoreNodeSync): + description: StringOptional + profile_name: String + profile_priority: Integer + member_of_groups: RelationshipManagerSync[CoreGroupSync] + related_nodes: RelationshipManagerSync[LocationGeneric] + subscriber_of_groups: RelationshipManagerSync[CoreGroupSync] + + +class ProfileLocationRack(LineageSource, CoreProfileSync, CoreNodeSync): + description: StringOptional + facility_id: StringOptional + height: IntegerOptional + profile_name: String + profile_priority: Integer + member_of_groups: RelationshipManagerSync[CoreGroupSync] + related_nodes: RelationshipManagerSync[LocationRack] + subscriber_of_groups: RelationshipManagerSync[CoreGroupSync] + + +class ProfileLocationSite(LineageSource, CoreProfileSync, CoreNodeSync): + description: StringOptional + facility_id: StringOptional + physical_address: StringOptional + profile_name: String + profile_priority: Integer + member_of_groups: RelationshipManagerSync[CoreGroupSync] + related_nodes: RelationshipManagerSync[LocationSite] + subscriber_of_groups: RelationshipManagerSync[CoreGroupSync] + + +class ProfileNetworkDhcpServer(LineageSource, CoreProfileSync, CoreNodeSync): + description: StringOptional + profile_name: String + profile_priority: Integer + member_of_groups: RelationshipManagerSync[CoreGroupSync] + related_nodes: RelationshipManagerSync[NetworkDhcpServer] + subscriber_of_groups: RelationshipManagerSync[CoreGroupSync] + + +class ProfileNetworkManagementServer(LineageSource, CoreProfileSync, CoreNodeSync): + description: StringOptional + profile_name: String + profile_priority: Integer + member_of_groups: RelationshipManagerSync[CoreGroupSync] + related_nodes: RelationshipManagerSync[NetworkManagementServer] + subscriber_of_groups: RelationshipManagerSync[CoreGroupSync] + + +class ProfileNetworkNTPServer(LineageSource, CoreProfileSync, CoreNodeSync): + description: StringOptional + profile_name: String + profile_priority: Integer + member_of_groups: RelationshipManagerSync[CoreGroupSync] + related_nodes: RelationshipManagerSync[NetworkNTPServer] + subscriber_of_groups: RelationshipManagerSync[CoreGroupSync] + + +class ProfileNetworkNameServer(LineageSource, CoreProfileSync, CoreNodeSync): + description: StringOptional + profile_name: String + profile_priority: Integer + member_of_groups: RelationshipManagerSync[CoreGroupSync] + related_nodes: RelationshipManagerSync[NetworkNameServer] + subscriber_of_groups: RelationshipManagerSync[CoreGroupSync] + + + + +class TemplateInfraDevice(LineageSource, CoreObjectTemplateSync, CoreNodeSync): + description: StringOptional + role: Dropdown + role_id: IntegerOptional + status: Dropdown + template_name: String + location: RelationshipAttributeSync[LocationSite] + member_of_groups: RelationshipManagerSync[CoreGroupSync] + primary_ip: RelationshipAttributeSync[IpamIPAddress] + related_nodes: RelationshipManagerSync[InfraDevice] + subscriber_of_groups: RelationshipManagerSync[CoreGroupSync] + diff --git a/tests/unit/sdk/test_protocols_generator.py b/tests/unit/sdk/test_protocols_generator.py index 531556bcd..6e0c7ab2d 100644 --- a/tests/unit/sdk/test_protocols_generator.py +++ b/tests/unit/sdk/test_protocols_generator.py @@ -5,11 +5,15 @@ from infrahub_sdk import InfrahubClient from infrahub_sdk.protocols_generator.generator import CodeGenerator +from infrahub_sdk.protocols_generator.target import ProtocolTarget from infrahub_sdk.schema import AttributeSchemaAPI +from tests.helpers.fixtures import read_fixture if TYPE_CHECKING: from pytest_httpx import HTTPXMock +GOLDEN_SUBDIR = "protocols_generator" + @dataclass class SyncifyTestCase: @@ -73,6 +77,19 @@ class RenderAttributeTestCase: ] +@dataclass +class GoldenTestCase: + name: str + sync: bool + fixture: str + + +GOLDEN_TEST_CASES = [ + GoldenTestCase(name="async", sync=False, fixture="user_schema_async.txt"), + GoldenTestCase(name="sync", sync=True, fixture="user_schema_sync.txt"), +] + + @pytest.mark.parametrize( "test_case", [pytest.param(tc, id=tc.name) for tc in RENDER_ATTRIBUTE_TEST_CASES], @@ -92,8 +109,10 @@ async def test_filter_render_attribute(test_case: RenderAttributeTestCase) -> No [pytest.param(tc, id=tc.name) for tc in SYNCIFY_TEST_CASES], ) async def test_filter_syncify(test_case: SyncifyTestCase) -> None: - assert CodeGenerator._jinja2_filter_syncify(value=test_case.input, sync=test_case.sync) == test_case.output - assert CodeGenerator._jinja2_filter_syncify(value=test_case.input, sync=test_case.sync) == test_case.output + generator = CodeGenerator(schema={}) + + assert generator._jinja2_filter_syncify(value=test_case.input, sync=test_case.sync) == test_case.output + assert generator._jinja2_filter_syncify(value=test_case.input, sync=test_case.sync) == test_case.output async def test_generator(client: InfrahubClient, mock_schema_query_05: "HTTPXMock") -> None: @@ -128,3 +147,56 @@ class LocationSite(LocationGeneric): assert "class LocationGeneric(CoreNode)" in async_protocols assert "class LocationCountry(LocationGeneric)" in async_protocols assert "class TemplateInfraDevice(LineageSource, CoreObjectTemplate, CoreNode)" in async_protocols + + +@pytest.mark.parametrize( + "test_case", + [pytest.param(tc, id=tc.name) for tc in GOLDEN_TEST_CASES], +) +async def test_render_user_schema_matches_golden( + test_case: GoldenTestCase, client: InfrahubClient, mock_schema_query_05: "HTTPXMock" +) -> None: + """Rendering a user schema produces output byte-identical to the committed reference. + + The output of `infrahubctl protocols` is checked into user repositories and type-checked + there, so any change to it - a renamed annotation, a reordered member, a different import + line - is a change to something users depend on. Comparing the whole file makes that + surface as a diff a reviewer has to accept on purpose, rather than passing unnoticed + because the assertions only sampled a few lines. + """ + schemas = await client.schema.fetch(branch="main") + + rendered = CodeGenerator(schema=schemas).render(sync=test_case.sync) + + assert rendered == read_fixture(test_case.fixture, GOLDEN_SUBDIR) + + +async def test_render_sdk_core_emits_both_variants(client: InfrahubClient, mock_schema_query_05: "HTTPXMock") -> None: + """Generating infrahub_sdk.protocols itself puts both variants in one module. + + Every kind is local there, so the module cannot import the kinds it defines, the sync classes + need a suffix to keep their names distinct, and a peer has to be referenced by its own sync + class rather than by an imported one. + """ + schemas = await client.schema.fetch(branch="main") + + rendered = CodeGenerator(schema=schemas, target=ProtocolTarget.SDK_CORE).render() + + assert "from infrahub_sdk.protocols import" not in rendered + assert "from .protocols_base import CoreNode, CoreNodeSync" in rendered + + assert "class LocationGeneric(CoreNode):" in rendered + assert "class LocationGenericSync(CoreNodeSync):" in rendered + assert "class LocationSite(LocationGeneric):" in rendered + assert "class LocationSiteSync(LocationGenericSync):" in rendered + + assert "parent: RelationshipAttribute[LocationCountry]" in rendered + assert "parent: RelationshipAttributeSync[LocationCountrySync]" in rendered + + +async def test_render_sdk_core_ignores_sync_argument(client: InfrahubClient, mock_schema_query_05: "HTTPXMock") -> None: + """The async/sync choice does not apply when both variants land in the same module.""" + schemas = await client.schema.fetch(branch="main") + generator = CodeGenerator(schema=schemas, target=ProtocolTarget.SDK_CORE) + + assert generator.render(sync=True) == generator.render(sync=False)