feat: generate user-facing write/read schema models + offline validation#1135
feat: generate user-facing write/read schema models + offline validation#1135dgarros wants to merge 18 commits into
Conversation
Deploying infrahub-sdk-python with
|
| Latest commit: |
d15ce11
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://06974a59.infrahub-sdk-python.pages.dev |
| Branch Preview URL: | https://dga-user-schema-infp-234.infrahub-sdk-python.pages.dev |
Codecov Report❌ Patch coverage is
@@ Coverage Diff @@
## infrahub-develop #1135 +/- ##
====================================================
- Coverage 83.03% 77.18% -5.85%
====================================================
Files 139 144 +5
Lines 12365 12693 +328
Branches 1846 1852 +6
====================================================
- Hits 10267 9797 -470
- Misses 1531 2339 +808
+ Partials 567 557 -10
Flags with carried forward coverage won't be shown. Click here to find out more.
... and 29 files with indirect coverage changes 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
2 issues found across 11 files
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
ce6e067 to
aa401e0
Compare
There was a problem hiding this comment.
1 issue found across 5 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
There was a problem hiding this comment.
3 issues found across 20 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="tests/unit/test_schema_generated_models.py">
<violation number="1" location="tests/unit/test_schema_generated_models.py:224">
P2: The test `test_use_enum_values_keeps_runtime_field_values_as_plain_strings` claims to verify that `use_enum_values=True` keeps runtime values as plain strings, but all three assertions (equality with raw string, equality with enum member, and `isinstance(str)`) pass identically whether `use_enum_values` is `True` or `False`. This is because the generated enums are `str`-backed (`str, Enum`), so their members always compare equal to their string values and are always instances of `str`. The test cannot detect if `use_enum_values` were accidentally removed in a future regeneration. Consider adding an assertion that discriminates between the two modes, such as `assert not isinstance(relationship.cardinality, enums_module.RelationshipCardinality)` or `assert type(relationship.cardinality) is str`.</violation>
</file>
<file name="infrahub_sdk/schema/main.py">
<violation number="1" location="infrahub_sdk/schema/main.py:266">
P1: `AttributeSchema` claims to relax `extra="forbid"` inherited from `AttributeSchemaBaseWrite`, but its `model_config = ConfigDict(use_enum_values=True)` does not actually override the parent setting. In Pydantic v2, subclass `model_config` is merged with the parent's, so `extra="forbid"` remains in effect unless explicitly overridden. This means historical construction patterns that passed unrecognized keyword arguments — which the old `AttributeSchema` silently accepted — will now raise validation errors. The docstring explicitly says `extra="forbid"` is relaxed, but the implementation contradicts that intent.
To fix this, add `extra="ignore"` (or `"allow"`) to the subclass `ConfigDict` so it truly restores the permissive behavior the docstring promises.</violation>
<violation number="2" location="infrahub_sdk/schema/main.py:315">
P1: The public `SchemaRoot` model is missing the new `extensions` field that the generated write model (`InfrahubSchemaWrite`) now requires for `/api/schema/load`. It still serializes the legacy `node_extensions` key, so schema payloads constructed or validated through the public `SchemaRoot` entry point will not match the new backend contract. Consider adding `extensions: SchemaExtensionWrite | None = None` and aligning `to_schema_dict()` with the generated write shape, or documenting that `SchemaRoot` is deprecated in favor of the generated write model for load payloads.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| version: str | ||
| generics: list[GenericSchema] = Field(default_factory=list) | ||
| nodes: list[NodeSchema] = Field(default_factory=list) | ||
| node_extensions: list[NodeExtensionSchema] = Field(default_factory=list) |
There was a problem hiding this comment.
P1: The public SchemaRoot model is missing the new extensions field that the generated write model (InfrahubSchemaWrite) now requires for /api/schema/load. It still serializes the legacy node_extensions key, so schema payloads constructed or validated through the public SchemaRoot entry point will not match the new backend contract. Consider adding extensions: SchemaExtensionWrite | None = None and aligning to_schema_dict() with the generated write shape, or documenting that SchemaRoot is deprecated in favor of the generated write model for load payloads.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At infrahub_sdk/schema/main.py, line 315:
<comment>The public `SchemaRoot` model is missing the new `extensions` field that the generated write model (`InfrahubSchemaWrite`) now requires for `/api/schema/load`. It still serializes the legacy `node_extensions` key, so schema payloads constructed or validated through the public `SchemaRoot` entry point will not match the new backend contract. Consider adding `extensions: SchemaExtensionWrite | None = None` and aligning `to_schema_dict()` with the generated write shape, or documenting that `SchemaRoot` is deprecated in favor of the generated write model for load payloads.</comment>
<file context>
@@ -264,100 +249,105 @@ def unique_attributes(self) -> list[AttributeSchemaAPI]:
+ version: str
+ generics: list[GenericSchema] = Field(default_factory=list)
+ nodes: list[NodeSchema] = Field(default_factory=list)
+ node_extensions: list[NodeExtensionSchema] = Field(default_factory=list)
+
+ def to_schema_dict(self) -> dict[str, Any]:
</file context>
| @property | ||
| def supports_artifacts(self) -> bool: | ||
| """Return True if this schema supports artifact operations via CoreArtifactTarget inheritance. | ||
| model_config = ConfigDict(use_enum_values=True) |
There was a problem hiding this comment.
P1: AttributeSchema claims to relax extra="forbid" inherited from AttributeSchemaBaseWrite, but its model_config = ConfigDict(use_enum_values=True) does not actually override the parent setting. In Pydantic v2, subclass model_config is merged with the parent's, so extra="forbid" remains in effect unless explicitly overridden. This means historical construction patterns that passed unrecognized keyword arguments — which the old AttributeSchema silently accepted — will now raise validation errors. The docstring explicitly says extra="forbid" is relaxed, but the implementation contradicts that intent.
To fix this, add extra="ignore" (or "allow") to the subclass ConfigDict so it truly restores the permissive behavior the docstring promises.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At infrahub_sdk/schema/main.py, line 266:
<comment>`AttributeSchema` claims to relax `extra="forbid"` inherited from `AttributeSchemaBaseWrite`, but its `model_config = ConfigDict(use_enum_values=True)` does not actually override the parent setting. In Pydantic v2, subclass `model_config` is merged with the parent's, so `extra="forbid"` remains in effect unless explicitly overridden. This means historical construction patterns that passed unrecognized keyword arguments — which the old `AttributeSchema` silently accepted — will now raise validation errors. The docstring explicitly says `extra="forbid"` is relaxed, but the implementation contradicts that intent.
To fix this, add `extra="ignore"` (or `"allow"`) to the subclass `ConfigDict` so it truly restores the permissive behavior the docstring promises.</comment>
<file context>
@@ -264,100 +249,105 @@ def unique_attributes(self) -> list[AttributeSchemaAPI]:
- @property
- def supports_artifacts(self) -> bool:
- """Return True if this schema supports artifact operations via CoreArtifactTarget inheritance.
+ model_config = ConfigDict(use_enum_values=True)
- Only NodeSchemaAPI overrides this; all other schema types return False by design because
</file context>
| model_config = ConfigDict(use_enum_values=True) | |
| model_config = ConfigDict(extra="ignore", use_enum_values=True) |
| relationship = write_module.RelationshipSchemaWrite(name="interfaces", peer="InfraInterface", cardinality="one") | ||
| assert relationship.cardinality == "one" | ||
| assert relationship.cardinality == enums_module.RelationshipCardinality.ONE | ||
| assert isinstance(relationship.cardinality, str) |
There was a problem hiding this comment.
P2: The test test_use_enum_values_keeps_runtime_field_values_as_plain_strings claims to verify that use_enum_values=True keeps runtime values as plain strings, but all three assertions (equality with raw string, equality with enum member, and isinstance(str)) pass identically whether use_enum_values is True or False. This is because the generated enums are str-backed (str, Enum), so their members always compare equal to their string values and are always instances of str. The test cannot detect if use_enum_values were accidentally removed in a future regeneration. Consider adding an assertion that discriminates between the two modes, such as assert not isinstance(relationship.cardinality, enums_module.RelationshipCardinality) or assert type(relationship.cardinality) is str.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At tests/unit/test_schema_generated_models.py, line 224:
<comment>The test `test_use_enum_values_keeps_runtime_field_values_as_plain_strings` claims to verify that `use_enum_values=True` keeps runtime values as plain strings, but all three assertions (equality with raw string, equality with enum member, and `isinstance(str)`) pass identically whether `use_enum_values` is `True` or `False`. This is because the generated enums are `str`-backed (`str, Enum`), so their members always compare equal to their string values and are always instances of `str`. The test cannot detect if `use_enum_values` were accidentally removed in a future regeneration. Consider adding an assertion that discriminates between the two modes, such as `assert not isinstance(relationship.cardinality, enums_module.RelationshipCardinality)` or `assert type(relationship.cardinality) is str`.</comment>
<file context>
@@ -167,6 +169,61 @@ def test_extension_models_forbid_extra_fields(name: str) -> None:
+ relationship = write_module.RelationshipSchemaWrite(name="interfaces", peer="InfraInterface", cardinality="one")
+ assert relationship.cardinality == "one"
+ assert relationship.cardinality == enums_module.RelationshipCardinality.ONE
+ assert isinstance(relationship.cardinality, str)
+
+
</file context>
| assert isinstance(relationship.cardinality, str) | |
| assert isinstance(relationship.cardinality, str) | |
| assert not isinstance(relationship.cardinality, enums_module.RelationshipCardinality) |
There was a problem hiding this comment.
1 issue found across 4 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="infrahub_sdk/schema/main.py">
<violation number="1" location="infrahub_sdk/schema/main.py:281">
P1: The public constructible write class `NodeSchema` no longer inherits `_SchemaKindMixin`, so it loses the runtime `.kind` property and all capability helpers (`supports_artifact_definition`, `supports_hierarchy`, etc.) that were previously available. The generated `NodeSchemaWrite` base does not provide a replacement `kind`, so existing code that constructs `NodeSchema` and accesses `.kind` will now raise `AttributeError`. Consider either restoring the `_SchemaKindMixin` inheritance to `NodeSchema` and `GenericSchema`, adding a `kind` computed field to the generated write base, or documenting this as an intentional breaking change if write models are no longer meant to expose these helpers.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
There was a problem hiding this comment.
1 issue found across 2 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="tests/unit/test_schema_generated_models.py">
<violation number="1" location="tests/unit/test_schema_generated_models.py:224">
P2: The test `test_use_enum_values_keeps_runtime_field_values_as_plain_strings` claims to verify that `use_enum_values=True` keeps runtime values as plain strings, but all three assertions (equality with raw string, equality with enum member, and `isinstance(str)`) pass identically whether `use_enum_values` is `True` or `False`. This is because the generated enums are `str`-backed (`str, Enum`), so their members always compare equal to their string values and are always instances of `str`. The test cannot detect if `use_enum_values` were accidentally removed in a future regeneration. Consider adding an assertion that discriminates between the two modes, such as `assert not isinstance(relationship.cardinality, enums_module.RelationshipCardinality)` or `assert type(relationship.cardinality) is str`.</violation>
</file>
<file name="infrahub_sdk/schema/main.py">
<violation number="1" location="infrahub_sdk/schema/main.py:266">
P1: `AttributeSchema` claims to relax `extra="forbid"` inherited from `AttributeSchemaBaseWrite`, but its `model_config = ConfigDict(use_enum_values=True)` does not actually override the parent setting. In Pydantic v2, subclass `model_config` is merged with the parent's, so `extra="forbid"` remains in effect unless explicitly overridden. This means historical construction patterns that passed unrecognized keyword arguments — which the old `AttributeSchema` silently accepted — will now raise validation errors. The docstring explicitly says `extra="forbid"` is relaxed, but the implementation contradicts that intent.
To fix this, add `extra="ignore"` (or `"allow"`) to the subclass `ConfigDict` so it truly restores the permissive behavior the docstring promises.</violation>
<violation number="2" location="infrahub_sdk/schema/main.py:315">
P1: The public `SchemaRoot` model is missing the new `extensions` field that the generated write model (`InfrahubSchemaWrite`) now requires for `/api/schema/load`. It still serializes the legacy `node_extensions` key, so schema payloads constructed or validated through the public `SchemaRoot` entry point will not match the new backend contract. Consider adding `extensions: SchemaExtensionWrite | None = None` and aligning `to_schema_dict()` with the generated write shape, or documenting that `SchemaRoot` is deprecated in favor of the generated write model for load payloads.</violation>
</file>
<file name="infrahub_sdk/ctl/cli_commands.py">
<violation number="1" location="infrahub_sdk/ctl/cli_commands.py:384">
P2: The `api_items` list may fail static type checking. When initialized from `[node.convert_api() for node in schema_root.nodes]`, type checkers infer `list[NodeSchemaAPI]`. Adding `list[GenericSchemaAPI]` via `+=` then fails because `list` is invariant. Since the project uses mypy, annotate the list with `list[MainSchemaTypesAPI]` (or `list[NodeSchemaAPI | GenericSchemaAPI]`) so mixing node and generic API items is accepted by the type checker.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| schema.update({item.kind: item for item in schema_root.nodes + schema_root.generics}) | ||
| # `kind` is a read-model concept, so convert the write models loaded from disk to their | ||
| # API form to key them by kind consistently with the branch-fetched path below. | ||
| api_items = [node.convert_api() for node in schema_root.nodes] |
There was a problem hiding this comment.
P2: The api_items list may fail static type checking. When initialized from [node.convert_api() for node in schema_root.nodes], type checkers infer list[NodeSchemaAPI]. Adding list[GenericSchemaAPI] via += then fails because list is invariant. Since the project uses mypy, annotate the list with list[MainSchemaTypesAPI] (or list[NodeSchemaAPI | GenericSchemaAPI]) so mixing node and generic API items is accepted by the type checker.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At infrahub_sdk/ctl/cli_commands.py, line 384:
<comment>The `api_items` list may fail static type checking. When initialized from `[node.convert_api() for node in schema_root.nodes]`, type checkers infer `list[NodeSchemaAPI]`. Adding `list[GenericSchemaAPI]` via `+=` then fails because `list` is invariant. Since the project uses mypy, annotate the list with `list[MainSchemaTypesAPI]` (or `list[NodeSchemaAPI | GenericSchemaAPI]`) so mixing node and generic API items is accepted by the type checker.</comment>
<file context>
@@ -379,7 +379,11 @@ def protocols(
- schema.update({item.kind: item for item in schema_root.nodes + schema_root.generics})
+ # `kind` is a read-model concept, so convert the write models loaded from disk to their
+ # API form to key them by kind consistently with the branch-fetched path below.
+ api_items = [node.convert_api() for node in schema_root.nodes]
+ api_items += [generic.convert_api() for generic in schema_root.generics]
+ schema.update({item.kind: item for item in api_items})
</file context>
45502c5 to
622c56f
Compare
ajtmccarty
left a comment
There was a problem hiding this comment.
the main outstanding issue for me is finding some way to create the SchemaAPI classes without Mixins that have TYPE_CHECKING conditionals in them. there are also some comments from cubic that might need addressing
| the derived capability helpers and declares ``kind`` for the type checker. | ||
| """ | ||
|
|
||
| if TYPE_CHECKING: |
There was a problem hiding this comment.
I'm surprised this is necessary. if _SchemaKindMixin is only used in classes inheriting from BaseNodeReadSchema / BaseNodeWriteSchema, then I would think that name, namespace, etc. fields on the BaseNode... class would cover this case.
A Mixin with a TYPE_CHECKING conditional inside of it feels like a kludge. is there some other way to handle this?
There was a problem hiding this comment.
Hey — I was surprised by this too, so I had Claude dig into it. Sharing what it found, because I actually think the current form is the right call.
Why the block is there: at runtime this is fine — every class the mixin lands on (NodeSchemaAPI, GenericSchemaAPI, ProfileSchemaAPI, TemplateSchemaAPI) inherits from a generated read model that extends BaseNodeSchemaRead, where name / namespace / kind / attributes / … actually live, so MRO resolves everything. The issue is purely static: the type checker analyzes _SchemaKindMixin in isolation at its definition, where it has no base that provides those attributes, so self.kind etc. come back "unresolved." The TYPE_CHECKING block is just the contract — "my host supplies these" — and emits zero runtime code. It's the documented idiom for typing mixins whose attributes come from the eventual host class.
(Detail: kind is declared as a @property, not kind: str, because on the generated base it's a read-only @computed_field — a plain annotation would model it as a settable field and clash in the MRO.)
Why not just rely on the fields from BaseNodeSchemaRead: that only works at runtime. The whole point of a mixin here is to share these helpers across four distinct generated read classes; the mixin has no single base to inherit the fields from at its own definition site, which is exactly what the checker looks at.
Why the alternatives are worse:
- Make each mixin subclass its generated base (so fields are inherited): this removes the block, but turns the plain, field-less mixins into full pydantic models with diamond inheritance, and couples each mixin to one specific generated base — losing the "compose onto any of them" property that motivated the mixin. It also drags back the field-collection subtleties (and the
kindcomputed-field clash) we deliberately kept out. Net negative. - Protocol-typed
selfon every method: doesn't actually remove the contract, just relocates it — and now every helper (including the@propertygetters) carries aself: SomeProtocolannotation. More noise, not less. - Generate the helpers into the read models: this behavior is SDK-specific (e.g.
kind == "CoreArtifactDefinition"), not derivable from the schema, so it would leak hand-written logic into files that are supposed to be pure generated schema.
So the TYPE_CHECKING block is the least-intrusive option and a recognized pattern rather than a one-off hack — I'd like to keep it as-is.
There was a problem hiding this comment.
This sounds like a contract without any verification. the _SchemaKindMixin class seems like it indicates that any class using this Mixin needs to define name, namespace, inherit_from, and kind instance variables or properties, but I don't think there is any checking that actually verifies this. for example if NodeSchemaAPI does not define name, then I don't think it would be identified by type-checking. if I am wrong about that, then I will approve this pattern, but I can't approve something that has 3 strikes against it
- it's a Mixin, which I consider a code smell
- it uses TYPE_CHECKING, which I consider a crutch that hides import errors regardless of where it is used
- it isn't actually type checked
I think the right approach is to make _SchemaKindMixin and _SchemaRelAttrMixin one base class for the Node/Generic/Profile/TemplateSchemaAPI classes, even if it uses "diamond inheritance"
| assert any("inherited" in message for message in result.messages), result.messages | ||
| # The message must locate the offending field within the payload. | ||
| assert any("nodes[0].attributes[0]" in message for message in result.messages), result.messages |
There was a problem hiding this comment.
I think these error assertions could be a little more restrictive. I think there should only be 1 error and it should be possible to assert more, or all, of its text
same for other error message assertions in this test file
| # cardinality is typed with the RelationshipCardinality enum (use_enum_values keeps the runtime | ||
| # value a plain string); a valid enum value must still validate. |
There was a problem hiding this comment.
# test string for RelationshipCardinality is valid
| assert any("not_a_field" in message for message in result.messages), result.messages | ||
|
|
||
|
|
||
| def test_raise_on_error_raises_value_error_naming_field() -> None: |
There was a problem hiding this comment.
looks very similar to test_out_of_enum_value_is_rejected_naming_field_and_value
622c56f to
d40aa55
Compare
There was a problem hiding this comment.
All reported issues were addressed across 4 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
Add committed, generated write and read schema model variants under infrahub_sdk/schema/generated/. They are produced by the backend generator from the single source of truth in internal.py, filtered by a new field visibility axis (write ⊆ read ⊆ internal). The models are self-contained (pydantic + typing only) so they import with only the SDK installed, the write model retains extra="forbid", and constrained fields carry their allowed-value set as Literal[...] so the emitted JSON-schema is complete. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ft guard [INFP-234] Add validate_schema() to validate a schema payload against the generated write models with only the SDK installed (no server): it returns a field-level verdict rejecting non-settable/unknown fields and out-of-enum values. Add SDK unit tests for offline validation and a drift guard that asserts the generated write/read models are present, carry the do-not-edit header, and satisfy the write(extra=forbid)/read-superset invariants. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extend the offline write-contract validation so the attributes and relationships nested under extensions.nodes[*] are held to the same generated write models as node/generic-level ones. Previously only top-level nodes and generics were gated, letting read-level, unknown, and out-of-enum fields slip through on extension payloads. Add SDK offline tests covering extension attribute/relationship rejection with dotted error locations, plus breadth coverage for out-of-enum relationship cardinality/kind and read-level fields on relationships, generics, and nodes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…per-family Write/Read Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…f per-collection map Validate nodes and generics in one pass against the generated write document model; keep the separate extension gating (extension nodes are kind-only). Field-level dotted errors unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…t[str, Any] The computed_attribute block is now a dedicated model (ComputedAttributeWrite/Read) with a Literal kind and extra=forbid, so the write contract for a computed attribute is explicit and its kind/unknown-field errors are caught with a field-level location. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…_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>
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>
… models Extend the generated user-facing schema contract so it is complete: - Write variant gains NodeExtensionWrite / SchemaExtensionWrite (extra="forbid") and InfrahubSchemaWrite now carries an extensions field and forbids extra top-level keys, so the published write contract covers schema extensions. - Read variant gains read-only ProfileSchemaRead / TemplateSchemaRead so every read item is described by a generated model. - validate_schema now validates the whole root (nodes, generics, extensions) via InfrahubSchemaWrite in a single pass; the bespoke extension helper is retired while field-level dotted paths and the "(received: ...)" suffix are preserved. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Constrained fields in the generated user-facing write/read models were rendered as inline Literals. Emit dedicated (str, Enum) classes into a new self-contained generated/enums.py and type the fields with them, matching the SDK's historical enum names. Every generated model gains use_enum_values=True so runtime values stay plain strings; attribute/computed-attribute discriminated unions use Literal[Enum.MEMBER] discriminators. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… [INFP-234] Retire the hand-maintained model bodies in ``infrahub_sdk/schema/main.py`` and re-base every public name on the generated write/read models plus hand-written behavior mixins, keeping import paths, names, methods and envelopes stable. - Enums (``AttributeKind``, ``BranchSupportType``, ``RelationshipCardinality``, ...) are re-exported from ``generated.enums`` under the historical names. - Behavior mixins carry the 15 attribute/relationship helpers, the ``kind`` / ``supports_*`` / hierarchy flags, ``cardinality_is_*`` and write ``convert_api``. - Read ``*API`` models stay concrete subclasses of the generated ``*Read`` models so ``isinstance`` keeps working; write ``NodeSchema``/``GenericSchema``/ ``RelationshipSchema`` subclass the generated write models. - A thin constructible ``AttributeSchema``/``AttributeSchemaAPI`` (on the shared write/read base) keeps ``AttributeSchema(name=..., kind=...)`` working; nodes narrow ``attributes``/``relationships`` to those variants and still validate into the strict generated discriminated union on dump. - Envelopes (``SchemaRoot``, ``SchemaRootAPI``, ``BranchSchema``) stay hand-written on the generated inner models. Breaking: ``AttributeKind.STRING`` removed; write-model defaults now match the server contract (relationship min/max_count 0, node branch "aware", generate_profile True, generate_template False) and reject unknown fields. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Enum-typed generated fields defaulted to the raw string value, which failed mypy/ty against the enum annotation. Emit the enum member (use_enum_values coerces to the value at runtime). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… [INFP-234] The enum-typed RelationshipSchemaWrite.cardinality field makes ty flag the deliberate raw-string construction used to prove pydantic's use_enum_values runtime coercion. Add a scoped ty: ignore so the intent-preserving test passes python-lint. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
kind (namespace+name) and hash (server-computed) are read-only. Expose kind as a pydantic computed_field and hash as a plain field on the generated read base node model, so node/generic/profile/template read models inherit both. Drop the hand-written kind property from the schema kind mixin and the hand hash fields from the API read models; the write NodeSchema/GenericSchema no longer carry the read-only kind mixin. Retype CoreNodeBase._schema to the read union so static _schema.kind access stays valid. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reading a locally-authored (write) schema needs `.kind` attribute access (e.g. the protocols CLI command). Expose `kind` as a plain property on the write node models — derived from namespace+name, like on read — but do not serialize it: on write it stays a property (not a `@computed_field`) so it never enters the payload, where `extra="forbid"` would reject it on the round-trip through the write/load contract. `hash` remains read-only. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…se [INFP-234] Picks up the upstream `ordered` attribute field (now classified write-visible) in the generated schema models and syncs protocols.py with the current backend core models. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ct [INFP-234] The /api/schema/load write contract rejects read-only and internal fields. Callers commonly build a payload from a schema read back from Infrahub or a full model dump, which carries those fields. schema.load()/check() now project each payload onto the generated write models before sending — dropping read-only/internal keys and resolving the per-kind attribute and computed-attribute discriminated unions — so such payloads load cleanly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
b1fe1f8 to
07579bb
Compare
…INFP-234] Stripping read-only/internal fields client-side silently dropped fields the server is meant to reject (e.g. a node-only field on an extension), defeating the server's rejection contract and hiding user typos. The tolerate/reject decision now lives server-side in the schema-load endpoint instead. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Summary
Adds generated, user-facing write and read schema models to the SDK plus an offline
validate_schema()so a schema can be checked for correctness with no running Infrahub server. Part of the user-facing schema separation (opsmill/infrahub, INFP-234).Key Changes
infrahub_sdk/schema/generated/{write,read}.py— the write model is exactly what/api/schema/loadaccepts (constrained fields carry their allowed values asLiteral[...];extra="forbid"); read = write + visible-but-not-settable fields.validate_schema()(infrahub_sdk/schema/validate.py) — field-level offline validation with dotted error locations; also gates schemaextensions.protocols.py(pre-existing drift vs current backend schema).Related Context
Generated from the backend's schema definitions; consumed by the backend PR (opsmill/infrahub). Depends on nothing else in this repo.
Test Plan
14 offline-validation cases + generated-model guard pass, pydantic-only (no server).
Notes
Draft: the backend still ships a parallel hand-written model set; full consolidation of the SDK's hand-written schema models is tracked as a follow-up.
🤖 Generated with Claude Code
Summary by cubic
Generates user-facing write/read schema models and an offline
validate_schema()for full-document checks without a server, and backs public SDK schema types with the generated contract while separating read/write and tightening defaults and constraints (INFP-234).New Features
infrahub_sdk/schema/generated/{enums,write,read}.py; exportInfrahubSchema{Write,Read}and enums; read includes profile/template models; write keepsextra="forbid"and coversextensionsat the root.computed_attributeare typed models.validate_schema()validatesnodes,generics, andextensionsviaInfrahubSchemaWrite, returnsSchemaValidationResultwith dotted field errors; includes a drift guard.Refactors
CoreNodeBase._schemaretyped toMainSchemaTypesAPI.kindas a computed field,hashstored); write nodes expose a non-serializing.kindproperty for local access.schema.load()/schema.check()payloads; we no longer strip read-only/internal fields before sending (server enforces rejection).AttributeKind.STRINGremoved; write models reject unknown fields; defaults aligned with the server (relationshipmin_count/max_count=0; nodebranch="aware",generate_profile=True,generate_template=False).Written for commit d15ce11. Summary will update on new commits.