Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
62508cf
feat(schema): generate user-facing write/read schema models [INFP-234]
dgarros Jul 3, 2026
01db54d
feat(schema): offline write-contract validation + generated-model dri…
dgarros Jul 3, 2026
a5bb130
fix(schema): gate schema extensions through the write contract
dgarros Jul 3, 2026
7fe67ef
refactor(schema): name generated models InfrahubSchema{Write,Read} + …
dgarros Jul 5, 2026
a4a2ede
refactor(schema): validate against InfrahubSchemaWrite root instead o…
dgarros Jul 5, 2026
abd5eaa
feat(schema): generate a typed ComputedAttribute model instead of dic…
dgarros Jul 5, 2026
e8e0cde
feat(schema): emit typed models for choices, parameters, and computed…
dgarros Jul 5, 2026
262bb38
feat(schema): model generated attribute as a kind-discriminated union
dgarros Jul 5, 2026
cc34ae7
feat(schema): add extensions to write model and profile/template read…
dgarros Jul 6, 2026
d5b8f6a
feat(schema): type generated SDK schema fields with dedicated enums
dgarros Jul 6, 2026
7870629
refactor(schema): back public SDK schema models with generated models…
dgarros Jul 6, 2026
69fbd28
fix(schema): render generated enum-typed defaults as enum members
dgarros Jul 6, 2026
f2b56a7
test(schema): suppress expected ty error for raw-string enum coercion…
dgarros Jul 7, 2026
7fbda1c
refactor(schema): move kind/hash onto generated read models [INFP-234]
dgarros Jul 7, 2026
d9a0f8d
feat(schema): expose kind as a property on write schema nodes [INFP-234]
dgarros Jul 8, 2026
0680e1a
chore(schema): regenerate SDK models and protocols after develop reba…
dgarros Jul 14, 2026
072a3bf
fix(schema): normalize schema.load/check payloads to the write contra…
dgarros Jul 16, 2026
87af0dd
Revert "normalize schema.load/check payloads to the write contract" […
dgarros Jul 16, 2026
fe7062e
refactor(schema): address PR review on SDK schema models and tests [I…
dgarros Jul 22, 2026
6feec36
refactor(schema): tolerate and drop extra fields on write/read models…
dgarros Jul 22, 2026
baf4715
docs(changelog): drop breaking label from SDK schema-models note [INF…
dgarros Jul 22, 2026
da61253
test(schema): skip IPAddress node tests pending backend attribute kin…
dgarros Jul 23, 2026
3262c61
fix(schema): require version on the generated write root [INFP-234]
dgarros Jul 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ dist/*

# Generated files
generated/
# Committed, generated schema models (write/read variants) must be version-controlled.
!infrahub_sdk/schema/generated/
!infrahub_sdk/schema/generated/*.py
sandbox/

# hatch-vcs version file (written at build time; must not be tracked)
Expand Down
7 changes: 7 additions & 0 deletions changelog/+infp-234-sdk-schema-models.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
The hand-maintained schema models in `infrahub_sdk.schema` are now backed by the generated write/read contract (`infrahub_sdk.schema.generated`). Public names, import paths, and behavior methods are unchanged, but a few defaults and constraints now match the server contract:

- `AttributeKind.STRING` has been removed. It was deprecated and `kind="String"` was already rejected server-side; use `AttributeKind.TEXT` instead.
- Write and read models drop unknown fields silently (`extra="ignore"`). A submitted field that is not part of the write contract — read-level, internal, or a typo — is dropped rather than rejected, and a read model tolerates additional fields returned by a newer server.
- Write-model defaults now match the server contract: relationship `min_count`/`max_count` default to `0` (was `None`), node `branch` defaults to `"aware"`, `generate_profile` defaults to `True`, and `generate_template` defaults to `False`. This changes the round-trip output of programmatically-built schemas.

Constructing `AttributeSchema(name=..., kind=AttributeKind.TEXT, ...)`, `NodeSchema`, `GenericSchema`, `RelationshipSchema`, `SchemaRoot`, and the read-side `*API` models continues to work unchanged.
14 changes: 14 additions & 0 deletions infrahub_sdk/protocols.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ class CoreTransformation(CoreNode):
query: RelatedNode
repository: RelatedNode
tags: RelationshipManager
artifact_definitions: RelationshipManager


class CoreTriggerRule(CoreNode):
Expand Down Expand Up @@ -314,6 +315,8 @@ class CoreArtifactDefinition(CoreTaskTarget):
fingerprint: StringOptional
targets: RelatedNode
transformation: RelatedNode
artifacts: RelationshipManager
validators: RelationshipManager


class CoreArtifactThread(CoreThread):
Expand Down Expand Up @@ -345,6 +348,7 @@ class CoreCheckDefinition(CoreTaskTarget):
query: RelatedNode
targets: RelatedNode
tags: RelationshipManager
validators: RelationshipManager


class CoreCustomWebhook(CoreWebhook, CoreTaskTarget):
Expand Down Expand Up @@ -405,6 +409,8 @@ class CoreGeneratorDefinition(CoreTaskTarget):
query: RelatedNode
repository: RelatedNode
targets: RelatedNode
instances: RelationshipManager
validators: RelationshipManager


class CoreGeneratorGroup(CoreGroup):
Expand Down Expand Up @@ -439,6 +445,7 @@ class CoreGraphQLQuery(CoreNode):
height: IntegerOptional
repository: RelatedNode
tags: RelationshipManager
query_groups: RelationshipManager


class CoreGraphQLQueryGroup(CoreGroup):
Expand Down Expand Up @@ -820,6 +827,7 @@ class CoreTransformationSync(CoreNodeSync):
query: RelatedNodeSync
repository: RelatedNodeSync
tags: RelationshipManagerSync
artifact_definitions: RelationshipManagerSync


class CoreTriggerRuleSync(CoreNodeSync):
Expand Down Expand Up @@ -912,6 +920,8 @@ class CoreArtifactDefinitionSync(CoreTaskTargetSync):
fingerprint: StringOptional
targets: RelatedNodeSync
transformation: RelatedNodeSync
artifacts: RelationshipManagerSync
validators: RelationshipManagerSync


class CoreArtifactThreadSync(CoreThreadSync):
Expand Down Expand Up @@ -943,6 +953,7 @@ class CoreCheckDefinitionSync(CoreTaskTargetSync):
query: RelatedNodeSync
targets: RelatedNodeSync
tags: RelationshipManagerSync
validators: RelationshipManagerSync


class CoreCustomWebhookSync(CoreWebhookSync, CoreTaskTargetSync):
Expand Down Expand Up @@ -1003,6 +1014,8 @@ class CoreGeneratorDefinitionSync(CoreTaskTargetSync):
query: RelatedNodeSync
repository: RelatedNodeSync
targets: RelatedNodeSync
instances: RelationshipManagerSync
validators: RelationshipManagerSync


class CoreGeneratorGroupSync(CoreGroupSync):
Expand Down Expand Up @@ -1037,6 +1050,7 @@ class CoreGraphQLQuerySync(CoreNodeSync):
height: IntegerOptional
repository: RelatedNodeSync
tags: RelationshipManagerSync
query_groups: RelationshipManagerSync


class CoreGraphQLQueryGroupSync(CoreGroupSync):
Expand Down
4 changes: 2 additions & 2 deletions infrahub_sdk/protocols_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from .context import RequestContext
from .node.metadata import NodeMetadata
from .schema import MainSchemaTypes
from .schema import MainSchemaTypesAPI


@runtime_checkable
Expand Down Expand Up @@ -181,7 +181,7 @@ class AnyAttributeOptional(Attribute):


class CoreNodeBase:
_schema: MainSchemaTypes
_schema: MainSchemaTypesAPI
_internal_id: str
id: str # NOTE this is incorrect, should be str | None
display_label: str | None
Expand Down
28 changes: 23 additions & 5 deletions infrahub_sdk/schema/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
from ..protocols_base import CoreNodeBase
from ..queries import SCHEMA_HASH_SYNC_STATUS
from .export import RESTRICTED_NAMESPACES, NamespaceExport, SchemaExport, schema_to_export_dict
from .generated.read import InfrahubSchemaRead
from .generated.write import InfrahubSchemaWrite
from .main import (
AttributeSchema,
AttributeSchemaAPI,
Expand All @@ -42,6 +44,7 @@
SchemaRootAPI,
TemplateSchemaAPI,
)
from .validate import SchemaValidationErrorDetail, SchemaValidationResult, validate_schema

if TYPE_CHECKING:
from ..client import InfrahubClient, InfrahubClientSync, SchemaType, SchemaTypeSync
Expand All @@ -56,6 +59,8 @@
"BranchSupportType",
"GenericSchema",
"GenericSchemaAPI",
"InfrahubSchemaRead",
"InfrahubSchemaWrite",
"NamespaceExport",
"NodeSchema",
"NodeSchemaAPI",
Expand All @@ -67,8 +72,11 @@
"SchemaExport",
"SchemaRoot",
"SchemaRootAPI",
"SchemaValidationErrorDetail",
"SchemaValidationResult",
"TemplateSchemaAPI",
"schema_to_export_dict",
"validate_schema",
]


Expand Down Expand Up @@ -166,7 +174,9 @@ def _build_export_schemas(
return SchemaExport(namespaces=ns_map)

def validate(self, data: dict[str, Any]) -> None:
SchemaRoot(**data)
# Validate against the generated write contract so this matches what /api/schema/load
# enforces (unknown keys rejected, attribute kinds discriminated, extensions understood).
InfrahubSchemaWrite.model_validate(data)

def validate_data_against_schema(self, schema: MainSchemaTypesAPI, data: dict) -> None:
for key in data:
Expand Down Expand Up @@ -362,7 +372,9 @@ async def load(
branch = branch or self.client.default_branch
url = f"{self.client.address}/api/schema/load?branch={branch}"
response = await self.client._post(
url=url, timeout=max(120, self.client.default_timeout), payload={"schemas": schemas}
url=url,
timeout=max(120, self.client.default_timeout),
payload={"schemas": schemas},
)

if wait_until_converged:
Expand Down Expand Up @@ -394,7 +406,9 @@ async def check(self, schemas: list[dict], branch: str | None = None) -> tuple[b
branch = branch or self.client.default_branch
url = f"{self.client.address}/api/schema/check?branch={branch}"
response = await self.client._post(
url=url, timeout=max(120, self.client.default_timeout), payload={"schemas": schemas}
url=url,
timeout=max(120, self.client.default_timeout),
payload={"schemas": schemas},
)

if response.status_code == httpx.codes.ACCEPTED:
Expand Down Expand Up @@ -892,7 +906,9 @@ def load(
branch = branch or self.client.default_branch
url = f"{self.client.address}/api/schema/load?branch={branch}"
response = self.client._post(
url=url, timeout=max(120, self.client.default_timeout), payload={"schemas": schemas}
url=url,
timeout=max(120, self.client.default_timeout),
payload={"schemas": schemas},
)

if wait_until_converged:
Expand Down Expand Up @@ -924,7 +940,9 @@ def check(self, schemas: list[dict], branch: str | None = None) -> tuple[bool, d
branch = branch or self.client.default_branch
url = f"{self.client.address}/api/schema/check?branch={branch}"
response = self.client._post(
url=url, timeout=max(120, self.client.default_timeout), payload={"schemas": schemas}
url=url,
timeout=max(120, self.client.default_timeout),
payload={"schemas": schemas},
)

if response.status_code == httpx.codes.ACCEPTED:
Expand Down
4 changes: 4 additions & 0 deletions infrahub_sdk/schema/generated/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Generated by "invoke backend.generate", do not edit directly
from . import enums, read, write

__all__ = ["enums", "read", "write"]
84 changes: 84 additions & 0 deletions infrahub_sdk/schema/generated/enums.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Generated by "invoke backend.generate", do not edit directly

from __future__ import annotations

from enum import Enum


class BranchSupportType(str, Enum):
AWARE = "aware"
AGNOSTIC = "agnostic"
LOCAL = "local"


class RelationshipKind(str, Enum):
GENERIC = "Generic"
ATTRIBUTE = "Attribute"
COMPONENT = "Component"
PARENT = "Parent"
GROUP = "Group"
HIERARCHY = "Hierarchy"
PROFILE = "Profile"
TEMPLATE = "Template"


class RelationshipCardinality(str, Enum):
ONE = "one"
MANY = "many"


class RelationshipDirection(str, Enum):
BIDIR = "bidirectional"
OUTBOUND = "outbound"
INBOUND = "inbound"


class RelationshipDeleteBehavior(str, Enum):
NO_ACTION = "no-action"
CASCADE = "cascade"


class AllowOverrideType(str, Enum):
NONE = "none"
ANY = "any"


class SchemaState(str, Enum):
PRESENT = "present"
ABSENT = "absent"


class SchemaAttributeDisplay(str, Enum):
DEFAULT = "default"
EXTRA = "extra"


class ComputedAttributeKind(str, Enum):
USER = "User"
JINJA2 = "Jinja2"
TRANSFORM_PYTHON = "TransformPython"


class AttributeKind(str, Enum):
ID = "ID"
DROPDOWN = "Dropdown"
TEXT = "Text"
TEXTAREA = "TextArea"
DATETIME = "DateTime"
EMAIL = "Email"
PASSWORD = "Password"
HASHEDPASSWORD = "HashedPassword"
URL = "URL"
FILE = "File"
MAC_ADDRESS = "MacAddress"
COLOR = "Color"
NUMBER = "Number"
NUMBERPOOL = "NumberPool"
BANDWIDTH = "Bandwidth"
IPHOST = "IPHost"
IPNETWORK = "IPNetwork"
BOOLEAN = "Boolean"
CHECKBOX = "Checkbox"
LIST = "List"
JSON = "JSON"
ANY = "Any"
Loading