Skip to content

Commit aa74912

Browse files
dgarrosclaude
andcommitted
feat(schema): model generated attribute as a kind-discriminated union
Replace the flat attribute write/read models, which typed parameters as a plain union of every parameters shape, with a discriminated union on kind. A shared AttributeSchemaBase carries every field except parameters, and each variant narrows kind to the kinds sharing one parameters shape and carries that parameters model, so a Text attribute no longer validates NumberPool parameters. The public AttributeSchema{Write,Read} name becomes the union alias. Route offline validation of a single item through pydantic.TypeAdapter so a union alias (which has no model_validate) validates like a plain model. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 65edacd commit aa74912

5 files changed

Lines changed: 314 additions & 54 deletions

File tree

infrahub_sdk/schema/generated/read.py

Lines changed: 90 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -121,21 +121,7 @@ class ComputedAttributeTransformPythonRead(BaseModel):
121121
)
122122

123123

124-
AttributeParametersUnionRead = (
125-
NumberPoolParametersRead
126-
| NumberAttributeParametersRead
127-
| TextAttributeParametersRead
128-
| ListAttributeParametersRead
129-
| AttributeParametersRead
130-
)
131-
132-
ComputedAttributeRead = Annotated[
133-
ComputedAttributeUserRead | ComputedAttributeJinja2Read | ComputedAttributeTransformPythonRead,
134-
Field(discriminator="kind"),
135-
]
136-
137-
138-
class AttributeSchemaRead(BaseModel):
124+
class AttributeSchemaBaseRead(BaseModel):
139125
model_config = ConfigDict()
140126
id: str | None = Field(
141127
default=None,
@@ -245,10 +231,6 @@ class AttributeSchemaRead(BaseModel):
245231
default="any",
246232
description="Type of allowed override for the attribute.",
247233
)
248-
parameters: AttributeParametersUnionRead | None = Field(
249-
default=None,
250-
description="Extra parameters specific to this kind of attribute",
251-
)
252234
deprecation: str | None = Field(
253235
default=None,
254236
description="Mark attribute as deprecated and provide a user-friendly message to display",
@@ -260,6 +242,95 @@ class AttributeSchemaRead(BaseModel):
260242
)
261243

262244

245+
class TextAttributeRead(AttributeSchemaBaseRead):
246+
model_config = ConfigDict()
247+
kind: Literal["Text", "TextArea"] = Field(
248+
...,
249+
description="Defines the type of the attribute.",
250+
)
251+
parameters: TextAttributeParametersRead | None = Field(
252+
default=None,
253+
description="Extra parameters specific to this kind of attribute",
254+
)
255+
256+
257+
class NumberAttributeRead(AttributeSchemaBaseRead):
258+
model_config = ConfigDict()
259+
kind: Literal["Number"] = Field(
260+
...,
261+
description="Defines the type of the attribute.",
262+
)
263+
parameters: NumberAttributeParametersRead | None = Field(
264+
default=None,
265+
description="Extra parameters specific to this kind of attribute",
266+
)
267+
268+
269+
class ListAttributeRead(AttributeSchemaBaseRead):
270+
model_config = ConfigDict()
271+
kind: Literal["List"] = Field(
272+
...,
273+
description="Defines the type of the attribute.",
274+
)
275+
parameters: ListAttributeParametersRead | None = Field(
276+
default=None,
277+
description="Extra parameters specific to this kind of attribute",
278+
)
279+
280+
281+
class NumberPoolAttributeRead(AttributeSchemaBaseRead):
282+
model_config = ConfigDict()
283+
kind: Literal["NumberPool"] = Field(
284+
...,
285+
description="Defines the type of the attribute.",
286+
)
287+
parameters: NumberPoolParametersRead | None = Field(
288+
default=None,
289+
description="Extra parameters specific to this kind of attribute",
290+
)
291+
292+
293+
class GenericAttributeRead(AttributeSchemaBaseRead):
294+
model_config = ConfigDict()
295+
kind: Literal[
296+
"ID",
297+
"Dropdown",
298+
"DateTime",
299+
"Email",
300+
"Password",
301+
"HashedPassword",
302+
"URL",
303+
"File",
304+
"MacAddress",
305+
"Color",
306+
"Bandwidth",
307+
"IPHost",
308+
"IPNetwork",
309+
"Boolean",
310+
"Checkbox",
311+
"JSON",
312+
"Any",
313+
] = Field(
314+
...,
315+
description="Defines the type of the attribute.",
316+
)
317+
parameters: AttributeParametersRead | None = Field(
318+
default=None,
319+
description="Extra parameters specific to this kind of attribute",
320+
)
321+
322+
323+
ComputedAttributeRead = Annotated[
324+
ComputedAttributeUserRead | ComputedAttributeJinja2Read | ComputedAttributeTransformPythonRead,
325+
Field(discriminator="kind"),
326+
]
327+
328+
AttributeSchemaRead = Annotated[
329+
TextAttributeRead | NumberAttributeRead | ListAttributeRead | NumberPoolAttributeRead | GenericAttributeRead,
330+
Field(discriminator="kind"),
331+
]
332+
333+
263334
class RelationshipSchemaRead(BaseModel):
264335
model_config = ConfigDict()
265336
id: str | None = Field(

infrahub_sdk/schema/generated/write.py

Lines changed: 90 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -121,21 +121,7 @@ class ComputedAttributeTransformPythonWrite(BaseModel):
121121
)
122122

123123

124-
AttributeParametersUnionWrite = (
125-
NumberPoolParametersWrite
126-
| NumberAttributeParametersWrite
127-
| TextAttributeParametersWrite
128-
| ListAttributeParametersWrite
129-
| AttributeParametersWrite
130-
)
131-
132-
ComputedAttributeWrite = Annotated[
133-
ComputedAttributeUserWrite | ComputedAttributeJinja2Write | ComputedAttributeTransformPythonWrite,
134-
Field(discriminator="kind"),
135-
]
136-
137-
138-
class AttributeSchemaWrite(BaseModel):
124+
class AttributeSchemaBaseWrite(BaseModel):
139125
model_config = ConfigDict(extra="forbid")
140126
id: str | None = Field(
141127
default=None,
@@ -241,10 +227,6 @@ class AttributeSchemaWrite(BaseModel):
241227
default="any",
242228
description="Type of allowed override for the attribute.",
243229
)
244-
parameters: AttributeParametersUnionWrite | None = Field(
245-
default=None,
246-
description="Extra parameters specific to this kind of attribute",
247-
)
248230
deprecation: str | None = Field(
249231
default=None,
250232
description="Mark attribute as deprecated and provide a user-friendly message to display",
@@ -256,6 +238,95 @@ class AttributeSchemaWrite(BaseModel):
256238
)
257239

258240

241+
class TextAttributeWrite(AttributeSchemaBaseWrite):
242+
model_config = ConfigDict(extra="forbid")
243+
kind: Literal["Text", "TextArea"] = Field(
244+
...,
245+
description="Defines the type of the attribute.",
246+
)
247+
parameters: TextAttributeParametersWrite | None = Field(
248+
default=None,
249+
description="Extra parameters specific to this kind of attribute",
250+
)
251+
252+
253+
class NumberAttributeWrite(AttributeSchemaBaseWrite):
254+
model_config = ConfigDict(extra="forbid")
255+
kind: Literal["Number"] = Field(
256+
...,
257+
description="Defines the type of the attribute.",
258+
)
259+
parameters: NumberAttributeParametersWrite | None = Field(
260+
default=None,
261+
description="Extra parameters specific to this kind of attribute",
262+
)
263+
264+
265+
class ListAttributeWrite(AttributeSchemaBaseWrite):
266+
model_config = ConfigDict(extra="forbid")
267+
kind: Literal["List"] = Field(
268+
...,
269+
description="Defines the type of the attribute.",
270+
)
271+
parameters: ListAttributeParametersWrite | None = Field(
272+
default=None,
273+
description="Extra parameters specific to this kind of attribute",
274+
)
275+
276+
277+
class NumberPoolAttributeWrite(AttributeSchemaBaseWrite):
278+
model_config = ConfigDict(extra="forbid")
279+
kind: Literal["NumberPool"] = Field(
280+
...,
281+
description="Defines the type of the attribute.",
282+
)
283+
parameters: NumberPoolParametersWrite | None = Field(
284+
default=None,
285+
description="Extra parameters specific to this kind of attribute",
286+
)
287+
288+
289+
class GenericAttributeWrite(AttributeSchemaBaseWrite):
290+
model_config = ConfigDict(extra="forbid")
291+
kind: Literal[
292+
"ID",
293+
"Dropdown",
294+
"DateTime",
295+
"Email",
296+
"Password",
297+
"HashedPassword",
298+
"URL",
299+
"File",
300+
"MacAddress",
301+
"Color",
302+
"Bandwidth",
303+
"IPHost",
304+
"IPNetwork",
305+
"Boolean",
306+
"Checkbox",
307+
"JSON",
308+
"Any",
309+
] = Field(
310+
...,
311+
description="Defines the type of the attribute.",
312+
)
313+
parameters: AttributeParametersWrite | None = Field(
314+
default=None,
315+
description="Extra parameters specific to this kind of attribute",
316+
)
317+
318+
319+
ComputedAttributeWrite = Annotated[
320+
ComputedAttributeUserWrite | ComputedAttributeJinja2Write | ComputedAttributeTransformPythonWrite,
321+
Field(discriminator="kind"),
322+
]
323+
324+
AttributeSchemaWrite = Annotated[
325+
TextAttributeWrite | NumberAttributeWrite | ListAttributeWrite | NumberPoolAttributeWrite | GenericAttributeWrite,
326+
Field(discriminator="kind"),
327+
]
328+
329+
259330
class RelationshipSchemaWrite(BaseModel):
260331
model_config = ConfigDict(extra="forbid")
261332
id: str | None = Field(

infrahub_sdk/schema/validate.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
from typing import Any
1414

15-
from pydantic import BaseModel, Field
15+
from pydantic import BaseModel, Field, TypeAdapter
1616
from pydantic import ValidationError as PydanticValidationError
1717

1818
from .generated.write import (
@@ -83,12 +83,16 @@ def _collect_validation_errors(
8383
errors.append(SchemaValidationErrorDetail(field=location, message=message))
8484

8585

86-
def _validate_item(model: type[BaseModel], item: Any, prefix: str, errors: list[SchemaValidationErrorDetail]) -> None:
87-
"""Validate a single mapping against a write model, appending field-level errors under ``prefix``."""
86+
def _validate_item(model: Any, item: Any, prefix: str, errors: list[SchemaValidationErrorDetail]) -> None:
87+
"""Validate a single mapping against a write model, appending field-level errors under ``prefix``.
88+
89+
``model`` may be a plain model class or a discriminated-union alias (which has no
90+
``model_validate``), so validation goes through a ``TypeAdapter`` that handles both.
91+
"""
8892
if not isinstance(item, dict):
8993
return
9094
try:
91-
model.model_validate(item)
95+
TypeAdapter(model).validate_python(item)
9296
except PydanticValidationError as exc:
9397
_collect_validation_errors(exc=exc, errors=errors, prefix=prefix)
9498

0 commit comments

Comments
 (0)