From e14b93daea6bfa7161aba31a48e26215592012eb Mon Sep 17 00:00:00 2001 From: Dan Wolfson Date: Thu, 3 Sep 2026 14:46:55 -0500 Subject: [PATCH] docs(solution-architect): ISSUE-84 fix -- correct fictional NewSolutionElementRequestBody/initialStatus docstrings create_solution_blueprint/create_solution_component (async+sync, 4 docstrings total) documented a NewSolutionElementRequestBody body class with an initialStatus field as the way to set Draft status at creation. That class and field appear in zero .http ground-truth files anywhere in this repo -- never a real Egeria API surface, not merely unmodeled -- so following the documented shape either hard-failed client-side validation (create_solution_blueprint, class literal mismatch) or silently dropped the field and created the element ACTIVE regardless of intent (create_solution_component, NewElementRequestBody's extra='ignore'). Also found and fixed while walking these: create_solution_blueprint's first example body carried leftover userDefinedStatus/lifecycleStatus fields with a missing comma, invalid JSON even as a copy-paste example. Fix: all four docstrings now document contentStatus inside properties instead -- confirmed against Egeria-api-solution-architect.http's own updateSolutionBlueprintStatus example, and content_status is already a real field on ReferenceableProperties, the base class both SolutionBlueprintProperties and SolutionComponentProperties derive from. No model change needed; NewElementRequestBody.properties is a bare dict so contentStatus already passes through untouched. Verified locally: the old shape still fails validation as expected, the corrected shape validates cleanly with contentStatus preserved through to serialized JSON. Not live-verified against a real Egeria server (no access from this environment) -- reported by dwolfson-1b/trellis via ISSUE-84, who has live access to confirm contentStatus=DRAFT actually takes effect server-side at creation time. Full detail in PYEGERIA_ISSUES.md ISSUE-84. Signed-off-by: Dan Wolfson --- PYEGERIA_ISSUES.md | 147 ++++++++++++++++++++++++++++ pyegeria/omvs/solution_architect.py | 125 ++++++++++++++++------- 2 files changed, 238 insertions(+), 34 deletions(-) diff --git a/PYEGERIA_ISSUES.md b/PYEGERIA_ISSUES.md index 155166ef..5a8c2094 100644 --- a/PYEGERIA_ISSUES.md +++ b/PYEGERIA_ISSUES.md @@ -629,6 +629,153 @@ on an Egeria Server capability that doesn't exist yet — but the pyegeria/ Dr.Egeria-side work each will need once that capability ships is written into the entry now, so it isn't rediscovered from scratch later. +### ISSUE-84: `SolutionArchitect.create_solution_blueprint`'s own docstring documents a `NewSolutionElementRequestBody` body (with `initialStatus`) for Draft-status creation — that class does not exist as a pydantic model, so the documented shape fails client-side validation before any HTTP call + +**Status:** fixed 2026-09-03 — docstring-only fix (see "Fix landed" below). +Reported by a consumer (trellis/Resource Explorer's `BlueprintMaterializer`, +`docs/blueprint-materialization-plan.md` Phase A). + +**Confirmed in both**: this checkout's installed pyegeria (5.3.4.23, via +the trellis venv) and this repo's own working tree (6.1.9-dev) — +`grep -rn "class NewSolutionElementRequestBody" pyegeria/models/*.py` +returns nothing in either; the class is documented in +`SolutionArchitect.create_solution_blueprint`'s (and its `_async_*` +sibling's) docstring — twice, once per method, `omvs/solution_architect.py` +lines ~2325 and ~2440 in this tree — as the way to set `initialStatus` +(DRAFT/PREPARED/PROPOSED/APPROVED/REJECTED/ACTIVE/DISABLED/DEPRECATED/OTHER) +on a newly-created blueprint, distinct from `NewElementRequestBody` which +"sets the status to ACTIVE." No such model backs that documented shape. + +**What actually happens**: `create_solution_blueprint`'s real validation +path (`ServerClient._async_create_element_body_request` → +`validate_new_element_request` → `self._validate_body(self. +_new_element_request_adapter.validate_python, body)`) uses a bare +`TypeAdapter(NewElementRequestBody)` (set in `ServerClient.__init__`) — +`NewElementRequestBody.class_` is `Annotated[Literal["NewElementRequestBody"], +Field(alias="class")]`, a strict literal. Any other `class` value — +including the documented `"NewSolutionElementRequestBody"` — fails pydantic +validation locally, before any network call, raising +`PyegeriaInvalidParameterException` with `additional_info={"reason": +"Request body failed validation", "validation_errors": [...]}`. The +resulting exception message reads exactly like a server-side rejection +("Egeria rejected the new SolutionBlueprint: ...VALIDATION_ERROR_1... +Invalid parameters were provided...") and cost real debugging time on the +consumer side tracing it back to "not actually an Egeria call at all." + +Reproduced directly: +```python +from pyegeria.models import NewElementRequestBody +from pydantic import TypeAdapter +TypeAdapter(NewElementRequestBody).validate_python({ + "class": "NewSolutionElementRequestBody", "isOwnAnchor": True, + "initialStatus": "DRAFT", + "properties": {"class": "SolutionBlueprintProperties", + "qualifiedName": "test", "displayName": "test"}, +}) +# ValidationError: Input should be 'NewElementRequestBody' +# [type=literal_error, input_value='NewSolutionElementRequestBody', ...] +``` + +**Scope beyond `create_solution_blueprint`**: `create_solution_component` (and +its `_async_*` sibling) carries the identical docstring text and the +identical gap — confirmed by walking each `NewSolutionElementRequestBody` +docstring reference in `omvs/solution_architect.py` (lines 2254/2372/3542/3629 +in this tree) back to its enclosing method: `_async_create_solution_blueprint`/ +`create_solution_blueprint` (the two checked above) and +`_async_create_solution_component`/`create_solution_component`. Both public +create-element methods in this file document the same unbacked Draft-status +path. Not checked beyond `solution_architect.py`: worth a full grep across +`omvs/*.py` for any other method documenting `NewSolutionElementRequestBody`, +`NewGovernanceElementRequestBody`, or any other `New*ElementRequestBody` +variant that isn't `NewElementRequestBody` itself, since the same +"documented in the docstring, no model behind it" shape could recur +per-family. + +**Candidate fix** (not applied — for whoever picks this up): either (a) +define a real `NewSolutionElementRequestBody` pydantic model (subclassing +or extending `NewElementRequestBody` with an `initial_status` field and its +own `class_: Literal["NewSolutionElementRequestBody"]`) and register it +alongside `NewElementRequestBody` wherever `_new_element_request_adapter` +is built (likely needs a `Union`/discriminated-union `TypeAdapter`, not a +single-model one, so both class names validate), or (b) if +`initialStatus` is actually accepted by `NewElementRequestBody` itself on +the Egeria server side despite not being a declared pydantic field, add +`initial_status: str | None = None` directly to `NewElementRequestBody` +and drop the docstring's separate-class framing — cheaper, but needs +confirming server-side that `NewElementRequestBody` + `initialStatus` is +accepted (not verified here; the consumer's own fallback, tracked in their +own Backlog, is to use `NewElementRequestBody` with no `initialStatus` at +all, which creates the element ACTIVE, not Draft, until this is resolved). + +**Consumer-side workaround, now supersedable**: `BlueprintMaterializer. +materialize_blueprint_element` (trellis/resource_explorer, `surveyors/ +arch_recovery/blueprint_materializer.py`) sends `class: "NewElementRequestBody"` +with no status field at all, same as their pre-existing `ComponentMaterializer` +— blueprints materialize ACTIVE, not Draft. With this fix landed, that +workaround can be upgraded to set `contentStatus: "DRAFT"` inside +`properties` on the same `NewElementRequestBody` to get the originally +intended Draft-status behavior — not applied here since this is a +consumer-side change, tracked for `dwolfson-1b`/trellis to pick up. + +**Fix landed 2026-09-03 — docstring correction, no model change needed.** +Checked ground truth first: `NewSolutionElementRequestBody` and +`initialStatus` appear in **zero** `.http` files anywhere in this repo +(`grep -rln "NewSolutionElementRequestBody\|initialStatus" "pyegeria/http +clients/"` returns nothing) — the documented shape was never a real Egeria +API surface, not merely an unmodeled one, which rules out candidate fix (a) +(defining a real model) and (b) (adding `initial_status` to +`NewElementRequestBody`) from the original write-up. The actual, real +mechanism for setting status on these elements already exists: +`Egeria-api-solution-architect.http`'s own `updateSolutionBlueprintStatus` +example sets a blueprint's status via `"contentStatus": "ACTIVE"` inside an +`AuthoredReferenceableProperties`-shaped `properties` object — and +`content_status: str | None = None` is already a real field on +`ReferenceableProperties` (`pyegeria/models/models.py`), the base class +both `SolutionBlueprintProperties` and (transitively) +`SolutionComponentProperties` derive from. Since `NewElementRequestBody. +properties` is a bare `dict` (no nested pydantic validation), passing +`contentStatus` inside it at *creation* time was always possible — the +docstrings just never said so, and instead pointed at a body shape that +doesn't exist anywhere. + +Rewrote all four docstrings (`_async_create_solution_blueprint`/ +`create_solution_blueprint`, `_async_create_solution_component`/ +`create_solution_component`, `omvs/solution_architect.py`) to drop every +reference to `NewSolutionElementRequestBody`/`initialStatus` and document +`contentStatus` inside `properties` instead, with a `NewElementRequestBody` +example that now sets `"contentStatus": "DRAFT"`. Two extra defects found +and fixed while doing this walk, both worse than the one originally +reported: +- `_async_create_solution_blueprint`'s *first* ("no lifecycle") example + body already carried stray `"userDefinedStatus"`/`"lifecycleStatus"` + fields left over from the fictional shape — with a **missing comma** + after `"lifecycleStatus": "DRAFT"`, making it invalid JSON even as a + copy-paste example, independent of the pydantic-validation bug. +- `_async_create_solution_component`/`create_solution_component`'s only + example already used the real `"class": "NewElementRequestBody"` (so it + passed validation) but paired it with the fictional top-level + `"initialStatus"` field anyway. This is a *different, silent* failure + mode from the reported one: `NewElementRequestBody` inherits + `PyegeriaModel`'s `extra='ignore'`, so `initialStatus` validated + successfully and was silently dropped before serialization — no + exception at all, just an element that always came out ACTIVE regardless + of the caller's intent to set DRAFT. Exactly the class of bug flagged + generally in this file's own `pyegeria/core/` gotcha note about + request-body models silently dropping undeclared fields. + +Verified locally (no live server access from this environment): the +original repro's `TypeAdapter(NewElementRequestBody).validate_python(...)` +call with the old `NewSolutionElementRequestBody`/`initialStatus` shape +still fails as expected (proving the understanding of the original bug is +correct), and the corrected shape +(`{"class": "NewElementRequestBody", "properties": {..., +"contentStatus": "DRAFT"}}`) validates cleanly with `contentStatus` +preserved intact through to the serialized JSON. **Not live-verified +against a real Egeria server** — `dwolfson-1b`/trellis has live access; +recommend they confirm `contentStatus: "DRAFT"` at creation actually lands +as the blueprint/component's status server-side before switching +`BlueprintMaterializer` off its ACTIVE-only workaround. + ### ISSUE-82: `pyegeria/omvs/valid_metadata.py` sends the literal query string `typeName=None` whenever `type_name` is Python `None` — breaks every Type-Name-omitted (global) Valid Metadata Value, in 12 of 14 methods across `ValidMetadataManager` **Status:** fixed and live-verified 2026-08-28 — all 12 affected methods diff --git a/pyegeria/omvs/solution_architect.py b/pyegeria/omvs/solution_architect.py index 78cc2071..d0bac405 100644 --- a/pyegeria/omvs/solution_architect.py +++ b/pyegeria/omvs/solution_architect.py @@ -2250,9 +2250,10 @@ def get_info_supply_chain_by_guid(self, guid: str = None, body: dict = None, add @dynamic_catch async def _async_create_solution_blueprint(self, body: dict | NewElementRequestBody) -> str: - """ Create a solution blueprint. To set a lifecycle status - use a NewSolutionElementRequestBody which has a default status of DRAFT. Using a - NewElementRequestBody sets the status to ACTIVE. + """ Create a solution blueprint. To set a lifecycle status other than the + server's default (ACTIVE), set `contentStatus` inside `properties` + (see ISSUE-84: there is no separate request-body class for this -- + `NewElementRequestBody` is the only body this endpoint accepts). Async version. Parameters @@ -2305,8 +2306,6 @@ async def _async_create_solution_blueprint(self, body: dict | NewElementRequestB "displayName": "add short name here", "description": "add description here", "versionIdentifier": "add version here", - "userDefinedStatus" : "add status here", - "lifecycleStatus": "DRAFT" "additionalProperties": { "property1": "propertyValue1", "property2": "propertyValue2" @@ -2316,13 +2315,27 @@ async def _async_create_solution_blueprint(self, body: dict | NewElementRequestB } } - To set a lifecycle use: - - Set initialStatus which can be DRAFT, PREPARED, PROPPOSED, APPROVED, REJECTED, ACTIVE, DISABLED, DEPRECATED, - OTHER. If other is used, set userDefinedStatus. + ISSUE-84 (fixed 2026-09-03): this docstring previously described a + separate "NewSolutionElementRequestBody" body (with a top-level + "initialStatus" field) as the way to set a lifecycle status other + than ACTIVE -- and the plain example above it carried stray + "userDefinedStatus"/"lifecycleStatus" fields left over from that + same fictional shape (with a missing comma, making it invalid + JSON even as an example). None of that was ever real -- confirmed + absent from both pyegeria/models/ and every .http ground-truth + file in this repo -- so following it raised a client-side + PyegeriaInvalidParameterException before any HTTP call, which read + exactly like an Egeria-side rejection. There is only one body this + endpoint accepts: "NewElementRequestBody". To set the status, + include "contentStatus" inside "properties" instead -- confirmed + against Egeria-api-solution-architect.http's own + updateSolutionBlueprintStatus example, which sets a blueprint's + status via "contentStatus" the same way, and "content_status" is + already a real field on ReferenceableProperties (the base class of + SolutionBlueprintProperties): { - "class" : "NewSolutionElementRequestBody", + "class" : "NewElementRequestBody", "anchorGUID" : "add guid here", "isOwnAnchor": false, "parentGUID": "add guid here", @@ -2344,7 +2357,7 @@ async def _async_create_solution_blueprint(self, body: dict | NewElementRequestB "displayName": "add short name here", "description": "add description here", "versionIdentifier": "add version for this blueprint", - "userDefinedStatus" : "add status here if initialStatus=OTHER", + "contentStatus" : "DRAFT", "additionalProperties": { "property1" : "propertyValue1", "property2" : "propertyValue2" @@ -2352,7 +2365,6 @@ async def _async_create_solution_blueprint(self, body: dict | NewElementRequestB "effectiveFrom": "{{$isoTimestamp}}", "effectiveTo": "{{$isoTimestamp}}" }, - "initialStatus" : "DRAFT", "externalSourceGUID": "add guid here", "externalSourceName": "add qualified name here", "effectiveTime" : "{{$isoTimestamp}}", @@ -2368,9 +2380,10 @@ async def _async_create_solution_blueprint(self, body: dict | NewElementRequestB @dynamic_catch def create_solution_blueprint(self, body: dict | NewElementRequestBody) -> str: - """ Create a solution blueprint. To set a lifecycle status - use a NewSolutionElementRequestBody which has a default status of DRAFT. Using a - NewElementRequestBody sets the status to ACTIVE. + """ Create a solution blueprint. To set a lifecycle status other than the + server's default (ACTIVE), set `contentStatus` inside `properties` + (see ISSUE-84: there is no separate request-body class for this -- + `NewElementRequestBody` is the only body this endpoint accepts). Parameters ---------- @@ -2431,13 +2444,24 @@ def create_solution_blueprint(self, body: dict | NewElementRequestBody) -> str: } } - To set a lifecycle use: - - Set initialStatus which can be DRAFT, PREPARED, PROPPOSED, APPROVED, REJECTED, ACTIVE, DISABLED, DEPRECATED, - OTHER. If other is used, set userDefinedStatus. + ISSUE-84 (fixed 2026-09-03): this docstring previously described a + separate "NewSolutionElementRequestBody" body (with a top-level + "initialStatus" field) as the way to set a lifecycle status other + than ACTIVE. That class was never real -- confirmed absent from + both pyegeria/models/ and every .http ground-truth file in this + repo -- so following it raised a client-side + PyegeriaInvalidParameterException before any HTTP call, which read + exactly like an Egeria-side rejection. There is only one body this + endpoint accepts: "NewElementRequestBody". To set the status, + include "contentStatus" inside "properties" instead -- confirmed + against Egeria-api-solution-architect.http's own + updateSolutionBlueprintStatus example, which sets a blueprint's + status via "contentStatus" the same way, and "content_status" is + already a real field on ReferenceableProperties (the base class of + SolutionBlueprintProperties): { - "class" : "NewSolutionElementRequestBody", + "class" : "NewElementRequestBody", "anchorGUID" : "add guid here", "isOwnAnchor": false, "parentGUID": "add guid here", @@ -2459,7 +2483,7 @@ def create_solution_blueprint(self, body: dict | NewElementRequestBody) -> str: "displayName": "add short name here", "description": "add description here", "versionIdentifier": "add version for this blueprint", - "userDefinedStatus" : "add status here if initialStatus=OTHER", + "contentStatus" : "DRAFT", "additionalProperties": { "property1" : "propertyValue1", "property2" : "propertyValue2" @@ -2467,7 +2491,6 @@ def create_solution_blueprint(self, body: dict | NewElementRequestBody) -> str: "effectiveFrom": "{{$isoTimestamp}}", "effectiveTo": "{{$isoTimestamp}}" }, - "initialStatus" : "DRAFT", "externalSourceGUID": "add guid here", "externalSourceName": "add qualified name here", "effectiveTime" : "{{$isoTimestamp}}", @@ -3538,9 +3561,10 @@ def get_solution_blueprints_by_name(self, name: Optional[str] = None, body: dict @dynamic_catch async def _async_create_solution_component(self, body: dict | NewElementRequestBody) -> str: - """Create a solution component. To set a lifecycle status - use a NewSolutionElementRequestBody which has a default status of DRAFT. Using a - NewElementRequestBody sets the status to ACTIVE. + """Create a solution component. To set a lifecycle status other than the + server's default (ACTIVE), set `contentStatus` inside `properties` + (see ISSUE-84: there is no separate request-body class for this -- + `NewElementRequestBody` is the only body this endpoint accepts). Async version. Parameters @@ -3567,7 +3591,24 @@ async def _async_create_solution_component(self, body: dict | NewElementRequestB Notes ---- - With lifecycle: + ISSUE-84 (fixed 2026-09-03): this example previously included a + top-level "initialStatus" field and a "userDefinedStatus" field + inside "properties", framed (in this method's twin docstring + elsewhere in this file) as requiring a separate + "NewSolutionElementRequestBody" body class. Neither of those was + ever real -- confirmed absent from both pyegeria/models/ and every + .http ground-truth file in this repo. Because the top-level "class" + here was already the real "NewElementRequestBody", this particular + shape did NOT fail validation -- `NewElementRequestBody` silently + ignores unknown fields (PyegeriaModel's `extra='ignore'`), so + "initialStatus" was dropped with no warning and the component was + always created ACTIVE regardless of the intent to set DRAFT. To set + the status, include "contentStatus" inside "properties" instead -- + confirmed against Egeria-api-solution-architect.http's own + updateSolutionBlueprintStatus example, which sets status via + "contentStatus" the same way, and "content_status" is already a + real field on ReferenceableProperties (the base class + SolutionComponentProperties ultimately derives from). Body structure: { @@ -3600,7 +3641,7 @@ async def _async_create_solution_component(self, body: dict | NewElementRequestB "solutionComponentType": "add optional type for this component", "versionIdentifier": "add version for this component", "plannedDeployedImplementationType": "add details of the type of implementation for this component", - "userDefinedStatus" : "Add own status here if initialStatus=OTHER", + "contentStatus" : "DRAFT", "additionalProperties": { "property1" : "propertyValue1", "property2" : "propertyValue2" @@ -3608,7 +3649,6 @@ async def _async_create_solution_component(self, body: dict | NewElementRequestB "effectiveFrom": "{{$isoTimestamp}}", "effectiveTo": "{{$isoTimestamp}}" }, - "initialStatus" : "DRAFT", "externalSourceGUID": "add guid here", "externalSourceName": "add qualified name here", "effectiveTime" : "{{$isoTimestamp}}", @@ -3625,9 +3665,10 @@ async def _async_create_solution_component(self, body: dict | NewElementRequestB @dynamic_catch def create_solution_component(self, body: dict | NewElementRequestBody) -> str: - """Create a solution component. To set a lifecycle status - use a NewSolutionElementRequestBody which has a default status of DRAFT. Using a - NewElementRequestBody sets the status to ACTIVE. + """Create a solution component. To set a lifecycle status other than the + server's default (ACTIVE), set `contentStatus` inside `properties` + (see ISSUE-84: there is no separate request-body class for this -- + `NewElementRequestBody` is the only body this endpoint accepts). Parameters ---------- @@ -3652,7 +3693,24 @@ def create_solution_component(self, body: dict | NewElementRequestBody) -> str: Notes ---- - With lifecycle: + ISSUE-84 (fixed 2026-09-03): this example previously included a + top-level "initialStatus" field and a "userDefinedStatus" field + inside "properties", framed (in this method's twin docstring + elsewhere in this file) as requiring a separate + "NewSolutionElementRequestBody" body class. Neither of those was + ever real -- confirmed absent from both pyegeria/models/ and every + .http ground-truth file in this repo. Because the top-level "class" + here was already the real "NewElementRequestBody", this particular + shape did NOT fail validation -- `NewElementRequestBody` silently + ignores unknown fields (PyegeriaModel's `extra='ignore'`), so + "initialStatus" was dropped with no warning and the component was + always created ACTIVE regardless of the intent to set DRAFT. To set + the status, include "contentStatus" inside "properties" instead -- + confirmed against Egeria-api-solution-architect.http's own + updateSolutionBlueprintStatus example, which sets status via + "contentStatus" the same way, and "content_status" is already a + real field on ReferenceableProperties (the base class + SolutionComponentProperties ultimately derives from). Body structure: { @@ -3685,7 +3743,7 @@ def create_solution_component(self, body: dict | NewElementRequestBody) -> str: "solutionComponentType": "add optional type for this component", "versionIdentifier": "add version for this component", "plannedDeployedImplementationType": "add details of the type of implementation for this component", - "userDefinedStatus" : "Add own status here if initialStatus=OTHER", + "contentStatus" : "DRAFT", "additionalProperties": { "property1" : "propertyValue1", "property2" : "propertyValue2" @@ -3693,7 +3751,6 @@ def create_solution_component(self, body: dict | NewElementRequestBody) -> str: "effectiveFrom": "{{$isoTimestamp}}", "effectiveTo": "{{$isoTimestamp}}" }, - "initialStatus" : "DRAFT", "externalSourceGUID": "add guid here", "externalSourceName": "add qualified name here", "effectiveTime" : "{{$isoTimestamp}}",