Skip to content

Commit 65edacd

Browse files
dgarrosclaude
andcommitted
feat(schema): emit typed models for choices, parameters, and computed_attribute
Replace the opaque dict[str, Any] sub-blocks in the generated write/read schema models with typed models: DropdownChoice, a plain union of the five attribute parameter shapes, and a kind-discriminated union for computed_attribute that enforces the jinja2_template/transform requirement natively. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 17e653b commit 65edacd

3 files changed

Lines changed: 321 additions & 37 deletions

File tree

infrahub_sdk/schema/generated/read.py

Lines changed: 124 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,139 @@
22

33
from __future__ import annotations
44

5-
from typing import Any, Literal
5+
from typing import Annotated, Any, Literal
66

77
from pydantic import BaseModel, ConfigDict, Field
88

99

10-
class ComputedAttributeRead(BaseModel):
10+
class AttributeParametersRead(BaseModel):
1111
model_config = ConfigDict()
12-
kind: Literal["User", "Jinja2", "TransformPython"] = Field(
12+
13+
14+
class ListAttributeParametersRead(AttributeParametersRead):
15+
model_config = ConfigDict()
16+
regex: str | None = Field(
17+
default=None,
18+
description="Regular expression that each list item value must match if defined",
19+
)
20+
21+
22+
class TextAttributeParametersRead(AttributeParametersRead):
23+
model_config = ConfigDict()
24+
regex: str | None = Field(
25+
default=None,
26+
description="Regular expression that attribute value must match if defined",
27+
)
28+
min_length: int | None = Field(
29+
default=None,
30+
description="Set a minimum number of characters allowed.",
31+
)
32+
max_length: int | None = Field(
33+
default=None,
34+
description="Set a maximum number of characters allowed.",
35+
)
36+
37+
38+
class NumberAttributeParametersRead(AttributeParametersRead):
39+
model_config = ConfigDict()
40+
min_value: int | None = Field(
41+
default=None,
42+
description="Set a minimum value allowed.",
43+
)
44+
max_value: int | None = Field(
45+
default=None,
46+
description="Set a maximum value allowed.",
47+
)
48+
excluded_values: str | None = Field(
49+
default=None,
50+
description="List of values or range of values not allowed for the attribute, format is: '100,150-200,280,300-400'",
51+
pattern=r"^(\d+(?:-\d+)?)(?:,\d+(?:-\d+)?)*$",
52+
)
53+
54+
55+
class NumberPoolParametersRead(AttributeParametersRead):
56+
model_config = ConfigDict()
57+
end_range: int = Field(
58+
default=9223372036854775807,
59+
description="End range for numbers for the associated NumberPool",
60+
)
61+
start_range: int = Field(
62+
default=1,
63+
description="Start range for numbers for the associated NumberPool",
64+
)
65+
number_pool_id: str | None = Field(
66+
default=None,
67+
description="The ID of the numberpool associated with this attribute. Only set after the number pool has been provisioned.",
68+
)
69+
70+
71+
class DropdownChoiceRead(BaseModel):
72+
model_config = ConfigDict()
73+
name: str = Field(
1374
...,
14-
description="Defines how the value of the attribute is computed.",
75+
description="Name of the choice, must be unique within the dropdown.",
1576
)
16-
jinja2_template: str | None = Field(
77+
description: str | None = Field(
1778
default=None,
18-
description="Jinja2 template used to compute the value, required when kind is Jinja2.",
79+
description="Description of the choice.",
80+
)
81+
color: str | None = Field(
82+
default=None,
83+
description="Color of the choice, must be a valid HTML color code.",
84+
pattern=r"#[0-9a-fA-F]{6}\b",
1985
)
20-
transform: str | None = Field(
86+
label: str | None = Field(
2187
default=None,
88+
description="Human friendly representation of the choice.",
89+
)
90+
91+
92+
class ComputedAttributeUserRead(BaseModel):
93+
model_config = ConfigDict()
94+
kind: Literal["User"] = Field(
95+
...,
96+
description="Defines how the value of the attribute is computed.",
97+
)
98+
99+
100+
class ComputedAttributeJinja2Read(BaseModel):
101+
model_config = ConfigDict()
102+
kind: Literal["Jinja2"] = Field(
103+
...,
104+
description="Defines how the value of the attribute is computed.",
105+
)
106+
jinja2_template: str = Field(
107+
...,
108+
description="Jinja2 template used to compute the value, required when kind is Jinja2.",
109+
)
110+
111+
112+
class ComputedAttributeTransformPythonRead(BaseModel):
113+
model_config = ConfigDict()
114+
kind: Literal["TransformPython"] = Field(
115+
...,
116+
description="Defines how the value of the attribute is computed.",
117+
)
118+
transform: str = Field(
119+
...,
22120
description="Python transform name or ID, required when kind is TransformPython.",
23121
)
24122

25123

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+
26138
class AttributeSchemaRead(BaseModel):
27139
model_config = ConfigDict()
28140
id: str | None = Field(
@@ -71,7 +183,7 @@ class AttributeSchemaRead(BaseModel):
71183
default=None,
72184
description="Defines how the value of this attribute will be populated.",
73185
)
74-
choices: list[dict[str, Any]] | None = Field(
186+
choices: list[DropdownChoiceRead] | None = Field(
75187
default=None,
76188
description="Define a list of valid choices for a dropdown attribute.",
77189
)
@@ -133,7 +245,7 @@ class AttributeSchemaRead(BaseModel):
133245
default="any",
134246
description="Type of allowed override for the attribute.",
135247
)
136-
parameters: dict[str, Any] | None = Field(
248+
parameters: AttributeParametersUnionRead | None = Field(
137249
default=None,
138250
description="Extra parameters specific to this kind of attribute",
139251
)
@@ -164,7 +276,7 @@ class RelationshipSchemaRead(BaseModel):
164276
peer: str = Field(
165277
...,
166278
description="Type (kind) of objects supported on the other end of the relationship.",
167-
pattern="^[A-Z][a-zA-Z0-9]+$",
279+
pattern=r"^[A-Z][a-zA-Z0-9]+$",
168280
)
169281
kind: Literal["Generic", "Attribute", "Component", "Parent", "Group", "Hierarchy", "Profile", "Template"] = Field(
170282
default="Generic",
@@ -266,14 +378,14 @@ class BaseNodeSchemaRead(BaseModel):
266378
name: str = Field(
267379
...,
268380
description="Node name, must be unique within a namespace and must start with an uppercase letter.",
269-
pattern="^[A-Z][a-zA-Z0-9]+$",
381+
pattern=r"^[A-Z][a-zA-Z0-9]+$",
270382
min_length=2,
271383
max_length=32,
272384
)
273385
namespace: str = Field(
274386
...,
275387
description="Node Namespace, Namespaces are used to organize models into logical groups and to prevent name collisions.",
276-
pattern="^[A-Z][a-z0-9]+$",
388+
pattern=r"^[A-Z][a-z0-9]+$",
277389
min_length=3,
278390
max_length=64,
279391
)

infrahub_sdk/schema/generated/write.py

Lines changed: 124 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,139 @@
22

33
from __future__ import annotations
44

5-
from typing import Any, Literal
5+
from typing import Annotated, Any, Literal
66

77
from pydantic import BaseModel, ConfigDict, Field
88

99

10-
class ComputedAttributeWrite(BaseModel):
10+
class AttributeParametersWrite(BaseModel):
1111
model_config = ConfigDict(extra="forbid")
12-
kind: Literal["User", "Jinja2", "TransformPython"] = Field(
12+
13+
14+
class ListAttributeParametersWrite(AttributeParametersWrite):
15+
model_config = ConfigDict(extra="forbid")
16+
regex: str | None = Field(
17+
default=None,
18+
description="Regular expression that each list item value must match if defined",
19+
)
20+
21+
22+
class TextAttributeParametersWrite(AttributeParametersWrite):
23+
model_config = ConfigDict(extra="forbid")
24+
regex: str | None = Field(
25+
default=None,
26+
description="Regular expression that attribute value must match if defined",
27+
)
28+
min_length: int | None = Field(
29+
default=None,
30+
description="Set a minimum number of characters allowed.",
31+
)
32+
max_length: int | None = Field(
33+
default=None,
34+
description="Set a maximum number of characters allowed.",
35+
)
36+
37+
38+
class NumberAttributeParametersWrite(AttributeParametersWrite):
39+
model_config = ConfigDict(extra="forbid")
40+
min_value: int | None = Field(
41+
default=None,
42+
description="Set a minimum value allowed.",
43+
)
44+
max_value: int | None = Field(
45+
default=None,
46+
description="Set a maximum value allowed.",
47+
)
48+
excluded_values: str | None = Field(
49+
default=None,
50+
description="List of values or range of values not allowed for the attribute, format is: '100,150-200,280,300-400'",
51+
pattern=r"^(\d+(?:-\d+)?)(?:,\d+(?:-\d+)?)*$",
52+
)
53+
54+
55+
class NumberPoolParametersWrite(AttributeParametersWrite):
56+
model_config = ConfigDict(extra="forbid")
57+
end_range: int = Field(
58+
default=9223372036854775807,
59+
description="End range for numbers for the associated NumberPool",
60+
)
61+
start_range: int = Field(
62+
default=1,
63+
description="Start range for numbers for the associated NumberPool",
64+
)
65+
number_pool_id: str | None = Field(
66+
default=None,
67+
description="The ID of the numberpool associated with this attribute. Only set after the number pool has been provisioned.",
68+
)
69+
70+
71+
class DropdownChoiceWrite(BaseModel):
72+
model_config = ConfigDict(extra="forbid")
73+
name: str = Field(
1374
...,
14-
description="Defines how the value of the attribute is computed.",
75+
description="Name of the choice, must be unique within the dropdown.",
1576
)
16-
jinja2_template: str | None = Field(
77+
description: str | None = Field(
1778
default=None,
18-
description="Jinja2 template used to compute the value, required when kind is Jinja2.",
79+
description="Description of the choice.",
80+
)
81+
color: str | None = Field(
82+
default=None,
83+
description="Color of the choice, must be a valid HTML color code.",
84+
pattern=r"#[0-9a-fA-F]{6}\b",
1985
)
20-
transform: str | None = Field(
86+
label: str | None = Field(
2187
default=None,
88+
description="Human friendly representation of the choice.",
89+
)
90+
91+
92+
class ComputedAttributeUserWrite(BaseModel):
93+
model_config = ConfigDict(extra="forbid")
94+
kind: Literal["User"] = Field(
95+
...,
96+
description="Defines how the value of the attribute is computed.",
97+
)
98+
99+
100+
class ComputedAttributeJinja2Write(BaseModel):
101+
model_config = ConfigDict(extra="forbid")
102+
kind: Literal["Jinja2"] = Field(
103+
...,
104+
description="Defines how the value of the attribute is computed.",
105+
)
106+
jinja2_template: str = Field(
107+
...,
108+
description="Jinja2 template used to compute the value, required when kind is Jinja2.",
109+
)
110+
111+
112+
class ComputedAttributeTransformPythonWrite(BaseModel):
113+
model_config = ConfigDict(extra="forbid")
114+
kind: Literal["TransformPython"] = Field(
115+
...,
116+
description="Defines how the value of the attribute is computed.",
117+
)
118+
transform: str = Field(
119+
...,
22120
description="Python transform name or ID, required when kind is TransformPython.",
23121
)
24122

25123

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+
26138
class AttributeSchemaWrite(BaseModel):
27139
model_config = ConfigDict(extra="forbid")
28140
id: str | None = Field(
@@ -71,7 +183,7 @@ class AttributeSchemaWrite(BaseModel):
71183
default=None,
72184
description="Defines how the value of this attribute will be populated.",
73185
)
74-
choices: list[dict[str, Any]] | None = Field(
186+
choices: list[DropdownChoiceWrite] | None = Field(
75187
default=None,
76188
description="Define a list of valid choices for a dropdown attribute.",
77189
)
@@ -129,7 +241,7 @@ class AttributeSchemaWrite(BaseModel):
129241
default="any",
130242
description="Type of allowed override for the attribute.",
131243
)
132-
parameters: dict[str, Any] | None = Field(
244+
parameters: AttributeParametersUnionWrite | None = Field(
133245
default=None,
134246
description="Extra parameters specific to this kind of attribute",
135247
)
@@ -160,7 +272,7 @@ class RelationshipSchemaWrite(BaseModel):
160272
peer: str = Field(
161273
...,
162274
description="Type (kind) of objects supported on the other end of the relationship.",
163-
pattern="^[A-Z][a-zA-Z0-9]+$",
275+
pattern=r"^[A-Z][a-zA-Z0-9]+$",
164276
)
165277
kind: Literal["Generic", "Attribute", "Component", "Parent", "Group", "Hierarchy", "Profile", "Template"] = Field(
166278
default="Generic",
@@ -254,14 +366,14 @@ class BaseNodeSchemaWrite(BaseModel):
254366
name: str = Field(
255367
...,
256368
description="Node name, must be unique within a namespace and must start with an uppercase letter.",
257-
pattern="^[A-Z][a-zA-Z0-9]+$",
369+
pattern=r"^[A-Z][a-zA-Z0-9]+$",
258370
min_length=2,
259371
max_length=32,
260372
)
261373
namespace: str = Field(
262374
...,
263375
description="Node Namespace, Namespaces are used to organize models into logical groups and to prevent name collisions.",
264-
pattern="^[A-Z][a-z0-9]+$",
376+
pattern=r"^[A-Z][a-z0-9]+$",
265377
min_length=3,
266378
max_length=64,
267379
)

0 commit comments

Comments
 (0)