From b5f8160f6c88db549e7a089d5e280b1ae0be47d6 Mon Sep 17 00:00:00 2001 From: Dan Wolfson Date: Tue, 29 Sep 2026 14:00:43 -0500 Subject: [PATCH 1/2] fix(sdk): leave TemplateRequestBody.deepCopy to Egeria's default (true) TemplateRequestBody.deep_copy defaulted to False while Egeria's TemplateOptions defaults deepCopy to true. Every create-from-template body is validated through the model and serialised with exclude_none=True, so any caller that did not mention deepCopy sent "deepCopy": false and Egeria copied only the top-level element -- none of the template's anchored attachments. A data set created from a PostgreSQL template therefore had no connection and could not be read, provisioned or surveyed, with no error at the time. This affected the generic create_elem_from_template, every typed *_element_from_template creator in AutomatedCuration, and Dr.Egeria's Create Element / Create Element. - deep_copy now defaults to None, so an unset value is left out of the JSON and Egeria's own default applies. - Dr.Egeria's Create Element gains an optional Deep Copy attribute (Bool, default true) mapped to deepCopy, for the rare case where a shallow copy is wanted. Signed-off-by: Dan Wolfson --- .../commands_asset_maker_compact.json | 22 +++++- md_processing/data/generated_format_sets.json | 32 ++++++++ md_processing/v2/asset_maker.py | 1 + pyegeria/models/models.py | 4 +- pyegeria/view/base_report_formats.py | 2 +- .../advanced/Asset Maker/Create_Element.md | 10 +++ .../test_template_deep_copy_default.py | 78 +++++++++++++++++++ 7 files changed, 146 insertions(+), 3 deletions(-) create mode 100644 tests/micro-tests/test_template_deep_copy_default.py diff --git a/md_processing/data/compact_commands/commands_asset_maker_compact.json b/md_processing/data/compact_commands/commands_asset_maker_compact.json index 96d450ab..1262bd41 100644 --- a/md_processing/data/compact_commands/commands_asset_maker_compact.json +++ b/md_processing/data/compact_commands/commands_asset_maker_compact.json @@ -879,6 +879,25 @@ "max_cardinality": 1, "level": "Advanced", "Journal Entry": "" + }, + "Deep Copy": { + "variable_name": "deep_copy", + "inUpdate": false, + "attr_labels": "", + "examples": "", + "default_value": "true", + "valid_values": [], + "existing_element": "", + "description": "If true (Egeria's default), the template's anchored attachments -- such as a connection and its endpoint -- are copied to the new element. Set to false to create only the top-level element.", + "generated": false, + "style": "Bool", + "user_specified": true, + "unique": false, + "input_required": false, + "min_cardinality": 0, + "max_cardinality": 1, + "level": "Advanced", + "Journal Entry": "2026-09-29 -- maps to TemplateRequestBody.deepCopy; pyegeria previously sent false by default." } }, "bundles": { @@ -920,7 +939,8 @@ "Parent ID", "Parent Relationship Type Name", "Parent Relationship Attributes", - "Parent at End1" + "Parent at End1", + "Deep Copy" ], "Journal Entry": "Added 2026-09-11 -- Asset Maker family, wraps automated_curation.py's create-element-from-template methods. OM_TYPE is best-effort (not yet confirmed against a live server's type system); create-only (no Update) since the underlying OMVS methods have no update counterpart." }, diff --git a/md_processing/data/generated_format_sets.json b/md_processing/data/generated_format_sets.json index b9bf39b1..31905ca9 100644 --- a/md_processing/data/generated_format_sets.json +++ b/md_processing/data/generated_format_sets.json @@ -23687,6 +23687,14 @@ "detail_spec": null, "valid_values": [] }, + { + "name": "Deep Copy", + "key": "deep_copy", + "value": null, + "format": false, + "detail_spec": null, + "valid_values": [] + }, { "name": "Initial Status", "key": "initial_status", @@ -23778,6 +23786,14 @@ "detail_spec": null, "valid_values": [] }, + { + "name": "Deep Copy", + "key": "deep_copy", + "value": null, + "format": false, + "detail_spec": null, + "valid_values": [] + }, { "name": "Initial Status", "key": "initial_status", @@ -23890,6 +23906,14 @@ "format": false, "detail_spec": null, "valid_values": [] + }, + { + "name": "Deep Copy", + "key": "deep_copy", + "value": null, + "format": false, + "detail_spec": null, + "valid_values": [] } ], "columns": [ @@ -23981,6 +24005,14 @@ "format": false, "detail_spec": null, "valid_values": [] + }, + { + "name": "Deep Copy", + "key": "deep_copy", + "value": null, + "format": false, + "detail_spec": null, + "valid_values": [] } ] } diff --git a/md_processing/v2/asset_maker.py b/md_processing/v2/asset_maker.py index b7a12aec..36da7419 100644 --- a/md_processing/v2/asset_maker.py +++ b/md_processing/v2/asset_maker.py @@ -65,6 +65,7 @@ async def apply_changes(self) -> str: "parentRelationshipTypeName": _v(attributes, "Parent Relationship Type Name"), "parentRelationshipProperties": _v(attributes, "Parent Relationship Attributes"), "parentAtEnd1": _v(attributes, "Parent at End1", True), + "deepCopy": _v(attributes, "Deep Copy"), } guid = await client._async_create_elem_from_template(body) display_name = f"{_v(attributes, 'Element Type Name')} element" diff --git a/pyegeria/models/models.py b/pyegeria/models/models.py index ee47adb3..f47ccd25 100644 --- a/pyegeria/models/models.py +++ b/pyegeria/models/models.py @@ -535,7 +535,9 @@ class TemplateRequestBody(RequestBody): template_guid: str replacement_properties: dict[str, Any] | None = None placeholder_property_values: dict[str, Any] = {} - deep_copy: bool | None = False + # None (not False): omitted from the JSON so Egeria's own default (TemplateOptions.deepCopy = true) + # applies. False made every create-from-template skip the template's anchored attachments. + deep_copy: bool | None = None effective_from: datetime | None = None effective_to: datetime | None = None template_substitute: bool | None = False diff --git a/pyegeria/view/base_report_formats.py b/pyegeria/view/base_report_formats.py index e1a0841e..3db7ae63 100644 --- a/pyegeria/view/base_report_formats.py +++ b/pyegeria/view/base_report_formats.py @@ -167,7 +167,7 @@ 'Digital-Product-DrE-Basic': FormatSet(target_type='Digital Product', heading='Digital-Product-DrE-Basic Attributes', description='Auto-generated format for Digital Product (Create, Basic).', family='Digital Product Manager', formats=[Format(types=['LIST'], attributes=[Column(name='Display Name', key='display_name'), Column(name='Qualified Name', key='qualified_name'), Column(name='GUID', key='guid'), Column(name='Description', key='description'), Column(name='Legal', key='legal'), Column(name='Authors', key='authors'), Column(name='Purpose', key='purpose'), Column(name='Current Version', key='current_version'), Column(name='Introduction Date', key='introduction_date'), Column(name='Maturity', key='maturity'), Column(name='Next Version Date', key='next_version_date'), Column(name='Product Name', key='product_name'), Column(name='Service Life', key='service_life'), Column(name='Withdrawal Date', key='withdrawal_date'), Column(name='Version Identifier', key='version_identifier'), Column(name='Content Status', key='content_status', valid_values=['DRAFT', 'PREPARED', 'PROPOSED', 'APPROVED', 'REJECTED', 'ACTIVE', 'DEPRECATED', 'OTHER']), Column(name='Category', key='category')]), Format(types=['ALL'], attributes=[Column(name='Category', key='category'), Column(name='Description', key='description'), Column(name='Display Name', key='display_name'), Column(name='GUID', key='guid'), Column(name='Legal', key='legal'), Column(name='Qualified Name', key='qualified_name'), Column(name='URL', key='url'), Column(name='Version Identifier', key='version_identifier'), Column(name='Authors', key='authors'), Column(name='Content Status', key='content_status', valid_values=['DRAFT', 'PREPARED', 'PROPOSED', 'APPROVED', 'REJECTED', 'ACTIVE', 'DEPRECATED', 'OTHER']), Column(name='Purpose', key='purpose'), Column(name='Current Version', key='current_version'), Column(name='Introduction Date', key='introduction_date'), Column(name='Maturity', key='maturity'), Column(name='Next Version Date', key='next_version_date'), Column(name='Product Name', key='product_name'), Column(name='Service Life', key='service_life'), Column(name='Withdrawal Date', key='withdrawal_date')])], action=ActionParameter(function='CollectionManager.find_collections', required_params=['search_string'], optional_params=['sequencing_order', 'sequencing_property', 'page_size', 'start_from', 'starts_with', 'ends_with', 'ignore_case', 'classification_names', 'metadata_element_subtypes', 'metadata_element_type'], spec_params={'metadata_element_type': 'DigitalProduct'})), 'Digital-Subscription-DrE-Advanced': FormatSet(target_type='Digital Subscription', heading='Digital-Subscription-DrE-Advanced Attributes', description='Auto-generated format for Digital Subscription (Create, Advanced).', family='Digital Product Manager', formats=[Format(types=['LIST'], attributes=[Column(name='Display Name', key='display_name'), Column(name='Qualified Name', key='qualified_name'), Column(name='GUID', key='guid'), Column(name='Effective From', key='effective_from'), Column(name='Effective Time', key='effective_time'), Column(name='Effective To', key='effective_to'), Column(name='Glossary Term', key='glossary_term'), Column(name='Is Own Anchor', key='is_own_anchor'), Column(name='Zone Membership', key='zone_membership'), Column(name='Additional Properties', key='additional_properties'), Column(name='Confidence Classification', key='confidence_classification', valid_values=['UNCLASSIFIED', 'AD_HOC', 'TRANSACTIONAL', 'AUTHORITATIVE', 'DERIVED', 'OBSOLETE', 'OTHER']), Column(name='Confidentiality Classification', key='confidentiality_classification', valid_values=['UNCLASSIFIED', 'INTERNAL', 'CONFIDENTIAL', 'SENSITIVE', 'RESTRICTED', 'OTHER']), Column(name='Criticality Classification', key='criticality_classification', valid_values=['UNCLASSIFIED', 'MARGINAL', 'IMPORTANT', 'CRITICAL', 'CATASTROPHIC', 'OTHER']), Column(name='Description', key='description'), Column(name='Identifier', key='identifier'), Column(name='Impact Classification', key='impact_classification', valid_values=['UNCLASSIFIED', 'LOW', 'MEDIUM', 'HIGH', 'OTHER']), Column(name='Legal', key='legal'), Column(name='Policy Management Point', key='policy_management_point'), Column(name='Retention Classification', key='retention_classification', valid_values=['UNCLASSIFIED', 'TEMPORARY', 'PROJECT_LIFETIME', 'TEAM_LIFETIME', 'CONTRACT_LIFETIME', 'REGULATED_LIFETIME', 'TIMEBOXED_LIFETIME', 'OTHER']), Column(name='Security Tags', key='security_tags'), Column(name='Authors', key='authors'), Column(name='Purpose', key='purpose'), Column(name='Agreement Type', key='agreement_type'), Column(name='Support Level', key='support_level'), Column(name='Classifications', key='classifications'), Column(name='Anchor Scope Name', key='anchor_scope_guid'), Column(name='Supplementary Properties', key='supplementary_properties'), Column(name='Version Identifier', key='version_identifier'), Column(name='Status', key='status'), Column(name='Content Status', key='content_status', valid_values=['DRAFT', 'PREPARED', 'PROPOSED', 'APPROVED', 'REJECTED', 'ACTIVE', 'DEPRECATED', 'OTHER']), Column(name='User Defined Content Status', key='user_defined_content_status'), Column(name='User Defined Status', key='user_defined_status'), Column(name='Category', key='category')]), Format(types=['ALL'], attributes=[Column(name='Effective From', key='effective_from'), Column(name='Effective Time', key='effective_time'), Column(name='Effective To', key='effective_to'), Column(name='Glossary Term', key='glossary_term'), Column(name='Is Own Anchor', key='is_own_anchor'), Column(name='Status', key='status'), Column(name='Zone Membership', key='zone_membership'), Column(name='Additional Properties', key='additional_properties'), Column(name='Category', key='category'), Column(name='Confidence Classification', key='confidence_classification', valid_values=['UNCLASSIFIED', 'AD_HOC', 'TRANSACTIONAL', 'AUTHORITATIVE', 'DERIVED', 'OBSOLETE', 'OTHER']), Column(name='Confidentiality Classification', key='confidentiality_classification', valid_values=['UNCLASSIFIED', 'INTERNAL', 'CONFIDENTIAL', 'SENSITIVE', 'RESTRICTED', 'OTHER']), Column(name='Criticality Classification', key='criticality_classification', valid_values=['UNCLASSIFIED', 'MARGINAL', 'IMPORTANT', 'CRITICAL', 'CATASTROPHIC', 'OTHER']), Column(name='Description', key='description'), Column(name='Display Name', key='display_name'), Column(name='GUID', key='guid'), Column(name='Identifier', key='identifier'), Column(name='Impact Classification', key='impact_classification', valid_values=['UNCLASSIFIED', 'LOW', 'MEDIUM', 'HIGH', 'OTHER']), Column(name='Legal', key='legal'), Column(name='Policy Management Point', key='policy_management_point'), Column(name='Qualified Name', key='qualified_name'), Column(name='Retention Classification', key='retention_classification', valid_values=['UNCLASSIFIED', 'TEMPORARY', 'PROJECT_LIFETIME', 'TEAM_LIFETIME', 'CONTRACT_LIFETIME', 'REGULATED_LIFETIME', 'TIMEBOXED_LIFETIME', 'OTHER']), Column(name='Security Tags', key='security_tags'), Column(name='URL', key='url'), Column(name='Version Identifier', key='version_identifier'), Column(name='Authors', key='authors'), Column(name='Content Status', key='content_status', valid_values=['DRAFT', 'PREPARED', 'PROPOSED', 'APPROVED', 'REJECTED', 'ACTIVE', 'DEPRECATED', 'OTHER']), Column(name='User Defined Content Status', key='user_defined_content_status'), Column(name='Purpose', key='purpose'), Column(name='Agreement Type', key='agreement_type'), Column(name='Support Level', key='support_level'), Column(name='User Defined Status', key='user_defined_status'), Column(name='Classifications', key='classifications'), Column(name='Anchor Scope Name', key='anchor_scope_guid'), Column(name='Supplementary Properties', key='supplementary_properties')])], action=ActionParameter(function='CollectionManager.find_collections', required_params=['search_string'], optional_params=['sequencing_order', 'sequencing_property', 'page_size', 'start_from', 'starts_with', 'ends_with', 'ignore_case', 'classification_names', 'metadata_element_subtypes', 'metadata_element_type'], spec_params={'metadata_element_type': 'DigitalSubscription'})), 'Digital-Subscription-DrE-Basic': FormatSet(target_type='Digital Subscription', heading='Digital-Subscription-DrE-Basic Attributes', description='Auto-generated format for Digital Subscription (Create, Basic).', family='Digital Product Manager', formats=[Format(types=['LIST'], attributes=[Column(name='Display Name', key='display_name'), Column(name='Qualified Name', key='qualified_name'), Column(name='GUID', key='guid'), Column(name='Description', key='description'), Column(name='Legal', key='legal'), Column(name='Authors', key='authors'), Column(name='Purpose', key='purpose'), Column(name='Agreement Type', key='agreement_type'), Column(name='Support Level', key='support_level'), Column(name='Version Identifier', key='version_identifier'), Column(name='Content Status', key='content_status', valid_values=['DRAFT', 'PREPARED', 'PROPOSED', 'APPROVED', 'REJECTED', 'ACTIVE', 'DEPRECATED', 'OTHER']), Column(name='Category', key='category')]), Format(types=['ALL'], attributes=[Column(name='Category', key='category'), Column(name='Description', key='description'), Column(name='Display Name', key='display_name'), Column(name='GUID', key='guid'), Column(name='Legal', key='legal'), Column(name='Qualified Name', key='qualified_name'), Column(name='URL', key='url'), Column(name='Version Identifier', key='version_identifier'), Column(name='Authors', key='authors'), Column(name='Content Status', key='content_status', valid_values=['DRAFT', 'PREPARED', 'PROPOSED', 'APPROVED', 'REJECTED', 'ACTIVE', 'DEPRECATED', 'OTHER']), Column(name='Purpose', key='purpose'), Column(name='Agreement Type', key='agreement_type'), Column(name='Support Level', key='support_level')])], action=ActionParameter(function='CollectionManager.find_collections', required_params=['search_string'], optional_params=['sequencing_order', 'sequencing_property', 'page_size', 'start_from', 'starts_with', 'ends_with', 'ignore_case', 'classification_names', 'metadata_element_subtypes', 'metadata_element_type'], spec_params={'metadata_element_type': 'DigitalSubscription'})), - 'Element-DrE-Advanced': FormatSet(target_type='Element', heading='Element-DrE-Advanced Attributes', description='Auto-generated format for Element (Create, Advanced).', family='Asset Maker', formats=[Format(types=['LIST'], attributes=[Column(name='Element Type Name', key='type_name'), Column(name='Template GUID', key='template_guid'), Column(name='Placeholder Property Values', key='placeholder_property_values'), Column(name='Template Properties', key='template_properties'), Column(name='Generic Initial Classifications', key='initial_classifications'), Column(name='Is Own Anchor', key='is_own_anchor'), Column(name='Effective From', key='effective_from'), Column(name='Effective To', key='effective_to'), Column(name='Effective Time', key='effective_time'), Column(name='Initial Status', key='initial_status', valid_values=['ACTIVE', 'DRAFT', 'PREPARED', 'PROPOSED', 'APPROVED', 'REJECTED', 'DEPRECATED', 'OTHER'])]), Format(types=['ALL'], attributes=[Column(name='Element Type Name', key='type_name'), Column(name='Template GUID', key='template_guid'), Column(name='Placeholder Property Values', key='placeholder_property_values'), Column(name='Template Properties', key='template_properties'), Column(name='Initial Status', key='initial_status', valid_values=['ACTIVE', 'DRAFT', 'PREPARED', 'PROPOSED', 'APPROVED', 'REJECTED', 'DEPRECATED', 'OTHER']), Column(name='Generic Initial Classifications', key='initial_classifications'), Column(name='Is Own Anchor', key='is_own_anchor'), Column(name='Effective From', key='effective_from'), Column(name='Effective To', key='effective_to'), Column(name='Effective Time', key='effective_time')])], action=ActionParameter(function='MetadataExpert.find_metadata_elements_with_string', required_params=['search_string'], optional_params=['sequencing_order', 'sequencing_property', 'page_size', 'start_from', 'starts_with', 'ends_with', 'ignore_case', 'classification_names', 'metadata_element_subtypes', 'metadata_element_type'])), + 'Element-DrE-Advanced': FormatSet(target_type='Element', heading='Element-DrE-Advanced Attributes', description='Auto-generated format for Element (Create, Advanced).', family='Asset Maker', formats=[Format(types=['LIST'], attributes=[Column(name='Element Type Name', key='type_name'), Column(name='Template GUID', key='template_guid'), Column(name='Placeholder Property Values', key='placeholder_property_values'), Column(name='Template Properties', key='template_properties'), Column(name='Generic Initial Classifications', key='initial_classifications'), Column(name='Is Own Anchor', key='is_own_anchor'), Column(name='Effective From', key='effective_from'), Column(name='Effective To', key='effective_to'), Column(name='Effective Time', key='effective_time'), Column(name='Deep Copy', key='deep_copy'), Column(name='Initial Status', key='initial_status', valid_values=['ACTIVE', 'DRAFT', 'PREPARED', 'PROPOSED', 'APPROVED', 'REJECTED', 'DEPRECATED', 'OTHER'])]), Format(types=['ALL'], attributes=[Column(name='Element Type Name', key='type_name'), Column(name='Template GUID', key='template_guid'), Column(name='Placeholder Property Values', key='placeholder_property_values'), Column(name='Template Properties', key='template_properties'), Column(name='Initial Status', key='initial_status', valid_values=['ACTIVE', 'DRAFT', 'PREPARED', 'PROPOSED', 'APPROVED', 'REJECTED', 'DEPRECATED', 'OTHER']), Column(name='Generic Initial Classifications', key='initial_classifications'), Column(name='Is Own Anchor', key='is_own_anchor'), Column(name='Effective From', key='effective_from'), Column(name='Effective To', key='effective_to'), Column(name='Effective Time', key='effective_time'), Column(name='Deep Copy', key='deep_copy')])], action=ActionParameter(function='MetadataExpert.find_metadata_elements_with_string', required_params=['search_string'], optional_params=['sequencing_order', 'sequencing_property', 'page_size', 'start_from', 'starts_with', 'ends_with', 'ignore_case', 'classification_names', 'metadata_element_subtypes', 'metadata_element_type'])), 'Element-DrE-Basic': FormatSet(target_type='Element', heading='Element-DrE-Basic Attributes', description='Auto-generated format for Element (Create, Basic).', family='Asset Maker', formats=[Format(types=['LIST'], attributes=[Column(name='Template GUID', key='template_guid'), Column(name='Placeholder Property Values', key='placeholder_property_values'), Column(name='Template Properties', key='template_properties')]), Format(types=['ALL'], attributes=[Column(name='Template GUID', key='template_guid'), Column(name='Placeholder Property Values', key='placeholder_property_values'), Column(name='Template Properties', key='template_properties')])], action=ActionParameter(function='MetadataExpert.find_metadata_elements_with_string', required_params=['search_string'], optional_params=['sequencing_order', 'sequencing_property', 'page_size', 'start_from', 'starts_with', 'ends_with', 'ignore_case', 'classification_names', 'metadata_element_subtypes', 'metadata_element_type'])), 'Embedded-Process-DrE-Advanced': FormatSet(target_type='Embedded Process', heading='Embedded-Process-DrE-Advanced Attributes', description='Auto-generated format for Embedded Process (Create, Advanced).', family='Action Author', formats=[Format(types=['LIST'], attributes=[Column(name='Display Name', key='display_name'), Column(name='Qualified Name', key='qualified_name'), Column(name='GUID', key='guid'), Column(name='Effective From', key='effective_from'), Column(name='Effective Time', key='effective_time'), Column(name='Effective To', key='effective_to'), Column(name='Glossary Term', key='glossary_term'), Column(name='Is Own Anchor', key='is_own_anchor'), Column(name='Zone Membership', key='zone_membership'), Column(name='Additional Properties', key='additional_properties'), Column(name='Confidence Classification', key='confidence_classification', valid_values=['UNCLASSIFIED', 'AD_HOC', 'TRANSACTIONAL', 'AUTHORITATIVE', 'DERIVED', 'OBSOLETE', 'OTHER']), Column(name='Confidentiality Classification', key='confidentiality_classification', valid_values=['UNCLASSIFIED', 'INTERNAL', 'CONFIDENTIAL', 'SENSITIVE', 'RESTRICTED', 'OTHER']), Column(name='Criticality Classification', key='criticality_classification', valid_values=['UNCLASSIFIED', 'MARGINAL', 'IMPORTANT', 'CRITICAL', 'CATASTROPHIC', 'OTHER']), Column(name='Description', key='description'), Column(name='Identifier', key='identifier'), Column(name='Impact Classification', key='impact_classification', valid_values=['UNCLASSIFIED', 'LOW', 'MEDIUM', 'HIGH', 'OTHER']), Column(name='Legal', key='legal'), Column(name='Policy Management Point', key='policy_management_point'), Column(name='Retention Classification', key='retention_classification', valid_values=['UNCLASSIFIED', 'TEMPORARY', 'PROJECT_LIFETIME', 'TEAM_LIFETIME', 'CONTRACT_LIFETIME', 'REGULATED_LIFETIME', 'TIMEBOXED_LIFETIME', 'OTHER']), Column(name='Security Tags', key='security_tags'), Column(name='Deployed Implementation Type', key='deployed_impl_type'), Column(name='Namespace Path', key='namespace_path'), Column(name='Resource Name', key='resource_name'), Column(name='Source', key='source'), Column(name='Formula', key='formula'), Column(name='Formula Type', key='formula_type'), Column(name='Expected Behaviour', key='expected_behaviour'), Column(name='Priority', key='priority'), Column(name='Classifications', key='classifications'), Column(name='Anchor Scope Name', key='anchor_scope_guid'), Column(name='Supplementary Properties', key='supplementary_properties'), Column(name='Version Identifier', key='version_identifier'), Column(name='Status', key='status'), Column(name='User Defined Status', key='user_defined_status'), Column(name='Category', key='category')]), Format(types=['ALL'], attributes=[Column(name='Effective From', key='effective_from'), Column(name='Effective Time', key='effective_time'), Column(name='Effective To', key='effective_to'), Column(name='Glossary Term', key='glossary_term'), Column(name='Is Own Anchor', key='is_own_anchor'), Column(name='Status', key='status'), Column(name='Zone Membership', key='zone_membership'), Column(name='Additional Properties', key='additional_properties'), Column(name='Category', key='category'), Column(name='Confidence Classification', key='confidence_classification', valid_values=['UNCLASSIFIED', 'AD_HOC', 'TRANSACTIONAL', 'AUTHORITATIVE', 'DERIVED', 'OBSOLETE', 'OTHER']), Column(name='Confidentiality Classification', key='confidentiality_classification', valid_values=['UNCLASSIFIED', 'INTERNAL', 'CONFIDENTIAL', 'SENSITIVE', 'RESTRICTED', 'OTHER']), Column(name='Criticality Classification', key='criticality_classification', valid_values=['UNCLASSIFIED', 'MARGINAL', 'IMPORTANT', 'CRITICAL', 'CATASTROPHIC', 'OTHER']), Column(name='Description', key='description'), Column(name='Display Name', key='display_name'), Column(name='GUID', key='guid'), Column(name='Identifier', key='identifier'), Column(name='Impact Classification', key='impact_classification', valid_values=['UNCLASSIFIED', 'LOW', 'MEDIUM', 'HIGH', 'OTHER']), Column(name='Legal', key='legal'), Column(name='Policy Management Point', key='policy_management_point'), Column(name='Qualified Name', key='qualified_name'), Column(name='Retention Classification', key='retention_classification', valid_values=['UNCLASSIFIED', 'TEMPORARY', 'PROJECT_LIFETIME', 'TEAM_LIFETIME', 'CONTRACT_LIFETIME', 'REGULATED_LIFETIME', 'TIMEBOXED_LIFETIME', 'OTHER']), Column(name='Security Tags', key='security_tags'), Column(name='URL', key='url'), Column(name='Version Identifier', key='version_identifier'), Column(name='Deployed Implementation Type', key='deployed_impl_type'), Column(name='Namespace Path', key='namespace_path'), Column(name='Resource Name', key='resource_name'), Column(name='Source', key='source'), Column(name='Formula', key='formula'), Column(name='Formula Type', key='formula_type'), Column(name='Expected Behaviour', key='expected_behaviour'), Column(name='Priority', key='priority'), Column(name='User Defined Status', key='user_defined_status'), Column(name='Classifications', key='classifications'), Column(name='Anchor Scope Name', key='anchor_scope_guid'), Column(name='Supplementary Properties', key='supplementary_properties')])], action=ActionParameter(function='MetadataExpert.find_metadata_elements_with_string', required_params=['search_string'], optional_params=['sequencing_order', 'sequencing_property', 'page_size', 'start_from', 'starts_with', 'ends_with', 'ignore_case', 'classification_names', 'metadata_element_subtypes', 'metadata_element_type'])), 'Embedded-Process-DrE-Basic': FormatSet(target_type='Embedded Process', heading='Embedded-Process-DrE-Basic Attributes', description='Auto-generated format for Embedded Process (Create, Basic).', family='Action Author', formats=[Format(types=['LIST'], attributes=[Column(name='Display Name', key='display_name'), Column(name='Qualified Name', key='qualified_name'), Column(name='GUID', key='guid'), Column(name='Description', key='description'), Column(name='Legal', key='legal'), Column(name='Deployed Implementation Type', key='deployed_impl_type'), Column(name='Namespace Path', key='namespace_path'), Column(name='Resource Name', key='resource_name'), Column(name='Priority', key='priority'), Column(name='Version Identifier', key='version_identifier'), Column(name='Category', key='category')]), Format(types=['ALL'], attributes=[Column(name='Category', key='category'), Column(name='Description', key='description'), Column(name='Display Name', key='display_name'), Column(name='GUID', key='guid'), Column(name='Legal', key='legal'), Column(name='Qualified Name', key='qualified_name'), Column(name='URL', key='url'), Column(name='Version Identifier', key='version_identifier'), Column(name='Deployed Implementation Type', key='deployed_impl_type'), Column(name='Namespace Path', key='namespace_path'), Column(name='Resource Name', key='resource_name'), Column(name='Priority', key='priority')])], action=ActionParameter(function='MetadataExpert.find_metadata_elements_with_string', required_params=['search_string'], optional_params=['sequencing_order', 'sequencing_property', 'page_size', 'start_from', 'starts_with', 'ends_with', 'ignore_case', 'classification_names', 'metadata_element_subtypes', 'metadata_element_type'])), diff --git a/sample-data/templates/advanced/Asset Maker/Create_Element.md b/sample-data/templates/advanced/Asset Maker/Create_Element.md index 919dee1b..13fd3373 100644 --- a/sample-data/templates/advanced/Asset Maker/Create_Element.md +++ b/sample-data/templates/advanced/Asset Maker/Create_Element.md @@ -233,6 +233,16 @@ ___ > **Default Value**: true +### Deep Copy +> **Input Required**: False + +> **Attribute Type**: Bool + +> **Description**: If true (Egeria's default), the template's anchored attachments -- such as a connection and its endpoint -- are copied to the new element. Set to false to create only the top-level element. + +> **Default Value**: true + + ### Status > **Input Required**: False diff --git a/tests/micro-tests/test_template_deep_copy_default.py b/tests/micro-tests/test_template_deep_copy_default.py new file mode 100644 index 00000000..86c0dda8 --- /dev/null +++ b/tests/micro-tests/test_template_deep_copy_default.py @@ -0,0 +1,78 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Contributors to the ODPi Egeria project. +""" +TemplateRequestBody.deep_copy used to default to False, while Egeria's +TemplateOptions defaults deepCopy to true. Every create-from-template body is +validated through the model and serialised with exclude_none=True, so a caller +that never mentioned deepCopy sent "deepCopy": false -- and Egeria created the +element without any of the template's anchored attachments (e.g. a PostgreSQL +data set with no connection). An unset value must now be left out, so +Egeria's own default applies. +""" +import json +from typing import Any, cast + +import pytest +from pydantic import TypeAdapter + +from md_processing.v2.asset_maker import AssetMakerProcessor +from md_processing.v2.extraction import DrECommand +from pyegeria.models import TemplateRequestBody + +ADAPTER = TypeAdapter(TemplateRequestBody) +TEMPLATE_GUID = "3f9a0ab3-072c-4cb1-a14f-6e0492e00dd7" + + +def _serialised(body: dict) -> dict: + # Mirrors _async_create_element_from_template's validate + dump. + return json.loads(ADAPTER.validate_python(body).model_dump_json(exclude_none=True, by_alias=True)) + + +def test_unset_deep_copy_is_left_to_egeria_default(): + sent = _serialised({"class": "TemplateRequestBody", "templateGUID": TEMPLATE_GUID}) + assert "deepCopy" not in sent + + +@pytest.mark.parametrize("value", [True, False]) +def test_explicit_deep_copy_is_sent(value): + sent = _serialised({"class": "TemplateRequestBody", "templateGUID": TEMPLATE_GUID, "deepCopy": value}) + assert sent["deepCopy"] is value + + +class _FakeCuration: + def __init__(self): + self.bodies = [] + + async def _async_create_elem_from_template(self, body): + self.bodies.append(body) + return "cccccccc-0000-0000-0000-000000000003" + + +class _FakeClient: + def __init__(self): + self.automated_curation = _FakeCuration() + + +@pytest.mark.asyncio +@pytest.mark.parametrize("attributes, expected", [ + ({}, None), + ({"Deep Copy": {"value": False}}, False), + ({"Deep Copy": {"value": True}}, True), +]) +async def test_create_element_maps_deep_copy(attributes, expected): + client = _FakeClient() + p = AssetMakerProcessor(client=cast(Any, client), + command=DrECommand(verb="Create", object_type="Element", attributes={}, + raw_block="## Create Element"), context={}) + p.get_command_spec = lambda: {} + p.parsed_output = {"qualified_name": None, + "attributes": {"Template GUID": {"value": TEMPLATE_GUID}, **attributes}} + p.render_result_markdown = lambda guid: _async_value(guid) + + await p.apply_changes() + + assert client.automated_curation.bodies[0]["deepCopy"] is expected + + +async def _async_value(value): + return value From 89fb7146ca2df6dd7652cce0be97263f81959937 Mon Sep 17 00:00:00 2001 From: Dan Wolfson Date: Tue, 29 Sep 2026 14:20:08 -0500 Subject: [PATCH 2/2] fix(sdk): leave isOwnAnchor, startsWith and ignoreCase to Egeria's defaults A sweep of every request model's bool defaults against the Egeria bean it mirrors (following each Java class's extends chain) found three more that differed, besides deepCopy: - NewOpenMetadataElementRequestBody.is_own_anchor: pyegeria False, AnchorOptions.isOwnAnchor true - SearchStringRequestBody.starts_with: pyegeria True, SearchOptions false - SearchStringRequestBody.ignore_case: pyegeria False, SearchOptions true All three now default to None, so an unset value is omitted and Egeria's default applies; callers that pass an explicit value are unaffected. The find_* method signatures still pass starts_with explicitly (pyegeria's prefix-match convention), so search behaviour through those methods is unchanged. The sweep now reports no mismatches. Validate results for all 21 Dr.Egeria test documents are identical with and without this change. Signed-off-by: Dan Wolfson --- pyegeria/models/models.py | 10 +++++++--- .../test_template_deep_copy_default.py | 17 +++++++++++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/pyegeria/models/models.py b/pyegeria/models/models.py index f47ccd25..6a9951d2 100644 --- a/pyegeria/models/models.py +++ b/pyegeria/models/models.py @@ -600,7 +600,8 @@ class NewOpenMetadataElementRequestBody(RequestBody): type_name: str | None = None initial_classifications: Dict[str, InitialClassifications] | None = None anchor_guid: str | None = None - is_own_anchor: bool | None = False + # None: omitted so Egeria's AnchorOptions default (isOwnAnchor=true) applies. + is_own_anchor: bool | None = None effective_from: datetime | None = None effective_to: datetime | None = None properties: dict | None = None @@ -693,9 +694,12 @@ class FilterRequestBody(ResultsRequestBody): class SearchStringRequestBody(ResultsRequestBody): class_: Annotated[Literal["SearchStringRequestBody"], Field(alias="class")] search_string: str | None = None - starts_with: bool = True + # None: omitted from the JSON so Egeria's SearchOptions defaults apply + # (startsWith=false, ignoreCase=true). Callers that want prefix or + # case-sensitive matching pass the value explicitly. + starts_with: bool | None = None ends_with: bool = False - ignore_case: bool = False + ignore_case: bool | None = None class FindRequestBody(ResultsRequestBody): diff --git a/tests/micro-tests/test_template_deep_copy_default.py b/tests/micro-tests/test_template_deep_copy_default.py index 86c0dda8..bcd273f4 100644 --- a/tests/micro-tests/test_template_deep_copy_default.py +++ b/tests/micro-tests/test_template_deep_copy_default.py @@ -76,3 +76,20 @@ async def test_create_element_maps_deep_copy(attributes, expected): async def _async_value(value): return value + + +# The same rule for the other request models whose pyegeria defaults differed +# from the Egeria beans they mirror: an unset value is left out, so Egeria's +# default applies (AnchorOptions.isOwnAnchor=true, SearchOptions.startsWith=false, +# SearchOptions.ignoreCase=true). +@pytest.mark.parametrize("model_name, body, omitted", [ + ("NewOpenMetadataElementRequestBody", {"class": "NewOpenMetadataElementRequestBody"}, ["isOwnAnchor"]), + ("SearchStringRequestBody", {"class": "SearchStringRequestBody", "searchString": "x"}, + ["startsWith", "ignoreCase"]), +]) +def test_unset_flags_are_left_to_egeria_defaults(model_name, body, omitted): + import pyegeria.models as models + sent = json.loads(TypeAdapter(getattr(models, model_name)).validate_python(body) + .model_dump_json(exclude_none=True, by_alias=True)) + for key in omitted: + assert key not in sent