Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13,029 changes: 6,696 additions & 6,333 deletions PYEGERIA_ISSUES.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions commands/cat/glossary_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ def create_term(server, url, userid, password, glossary_name, term_name, summary
"anchorScopeGUID" : glossary_guid,
"isOwnAnchor": True,
"parentAtEnd1": True,
"elementProperties": {
"properties": {
"class": "GlossaryTermProperties",
"qualifiedName": qualified_name,
"displayName": term_name,
Expand All @@ -137,8 +137,8 @@ def create_term(server, url, userid, password, glossary_name, term_name, summary
"examples": examples,
"usage": usage,
"publishVersionIdentifier": version,
"contentStatus": status,
},
"initialStatus": status,
}

term_guid = m_client.create_glossary_term(body_slimmer(body)
Expand Down
15 changes: 6 additions & 9 deletions commands/tech/gen_report_specs.py
Original file line number Diff line number Diff line change
Expand Up @@ -379,20 +379,17 @@ def build_format_sets_from_commands(
extra_constraints = _safe_parse_constraints(cmd_obj.get("extra_constraints"))
action = None
if find_method or extra_find:
# `function` is a required (non-Optional) field on ActionParameter,
# but a command can legitimately have only an analytic_function and
# no find_method at all (e.g. a dedicated "View Growth"-style command
# with no element-listing path) -- an empty string is a valid, if
# unused, value; exec_report_spec's SERIES path never reads it.
# A command can have only an analytic_function and no find_method
# (e.g. a "View Growth"-style command with no element-listing path);
# `function` is then left unset (ISSUE-141).
action = ActionParameter(
function=find_method or "",
function=find_method or None,
required_params=["search_string"] if find_method else [],
optional_params=OPTIONAL_SEARCH_PARAMS if find_method else [],
spec_params=constraints or {},
analytic_function=extra_find or None,
analytic_spec_params=(extra_constraints or {}) if extra_find else {},
)
if extra_find:
action.analytic_function = extra_find
action.analytic_spec_params = extra_constraints or {}

formats = _build_generated_formats(columns, default_types=default_types)
if not formats:
Expand Down
8 changes: 5 additions & 3 deletions md_processing/md_processing_utils/common_md_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from pyegeria.core.utils import (camel_to_title_case)
from pyegeria.core._globals import DEBUG_LEVEL, GovernanceDomains, resolve_enum
from md_processing.md_processing_utils.message_constants import message_types
from md_processing.md_processing_utils.element_status import element_status_properties

# Constants
EGERIA_METADATA_STORE = os.environ.get("EGERIA_METADATA_STORE", "active-metadata-store")
Expand Down Expand Up @@ -303,7 +304,6 @@ def set_create_body(object_type: str, attributes: dict)->dict:
"parentAtEnd1": attributes.get('Parent at End1', {}).get('value', True),
"anchorScopeGUID": attributes.get('Anchor Scope ID', {}).get('guid', None),
"properties": "",
"initialStatus": attributes.get('Status', {}).get('value', "ACTIVE"),
"initialClassifications": {}}

return body
Expand Down Expand Up @@ -394,7 +394,7 @@ def set_element_prop_body(object_type: str, qualified_name: str, attributes: dic
prop_name = _to_egeria_type_name(object_type)
display_name = attributes.get('Display Name', {}).get('value', None)

return {
body = {
"class": prop_name + "Properties",
"typeName": prop_name,
"displayName": attributes.get('Display Name', {}).get('value', None),
Expand All @@ -404,7 +404,6 @@ def set_element_prop_body(object_type: str, qualified_name: str, attributes: dic
"identifier": attributes.get('Identifier', {}).get('value', None),
"url": attributes.get('URL', {}).get('value', None),
"authors": attributes.get('Authors', {}).get('value', []),
"contentStatus": attributes.get('Content Status', {}).get('value', None) or attributes.get('Status', {}).get('value', None),
"userDefinedContentStatus": attributes.get('User Defined Content Status', {}).get('value', None),
"versionIdentifier": attributes.get('Version Identifier', {}).get('value', None),
"effectiveFrom": attributes.get('Effective From', {}).get('value', None),
Expand All @@ -415,6 +414,9 @@ def set_element_prop_body(object_type: str, qualified_name: str, attributes: dic
"serviceLevels": attributes.get('Service Levels', {}).get('value', None),
"legal": attributes.get('Legal', {}).get('value', None),
}
# contentStatus / activityStatus / deploymentStatus, whichever this type has (ISSUE-113).
body.update(element_status_properties(body["class"], attributes))
return body

def set_collection_manager_body(object_type: str, qualified_name: str, attributes: dict) -> dict:
"""
Expand Down
109 changes: 109 additions & 0 deletions md_processing/md_processing_utils/element_status.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# SPDX-License-Identifier: Apache-2.0
# Copyright Contributors to the ODPi Egeria project.
"""
Map Dr.Egeria's status attributes onto the status property an element type actually has.

Egeria no longer accepts `initialStatus` on new-element requests (removed 2025-12-15).
Each type carries its own kind of status in its properties instead: `contentStatus`
(authored content -- glossary terms, collections, data structures, ...),
`activityStatus` (actions and processes -- notes, to-dos, meetings, ...),
`deploymentStatus` (infrastructure, solution components, capabilities, ...), or
none at all (actors, plain assets, comments). See ISSUE-113.
"""
from __future__ import annotations

from loguru import logger

from md_processing.md_processing_utils.element_status_fields import STATUS_FIELDS_BY_PROPERTIES_CLASS

# Dr.Egeria attribute -> Egeria property, for the type-specific status attributes.
EXPLICIT_STATUS_ATTRIBUTES = {
"Content Status": "contentStatus",
"Activity Status": "activityStatus",
"Deployment Status": "deploymentStatus",
}

# Valid values of each status enum (ContentStatus, ActivityStatus, DeploymentStatus).
VALID_STATUS_VALUES = {
"contentStatus": {"DRAFT", "PREPARED", "PROPOSED", "APPROVED", "REJECTED", "ACTIVE", "DEPRECATED",
"OBSOLETE", "OTHER"},
"activityStatus": {"REQUESTED", "APPROVED", "WAITING", "ACTIVATING", "IN_PROGRESS", "PAUSED", "FOR_INFO",
"COMPLETED", "INVALID", "IGNORED", "FAILED", "CANCELLED", "ABANDONED", "OTHER"},
"deploymentStatus": {"PROPOSED", "UNDER_DEVELOPMENT", "DEVELOPMENT_COMPLETE", "APPROVED_FOR_DEPLOYMENT",
"REJECTED", "STANDBY", "ACTIVE", "DISABLED", "FAILED", "OTHER"},
}

# Where a written `Status` value fits more than one of a type's fields, the first wins.
_PREFERENCE = ("contentStatus", "activityStatus", "deploymentStatus")


def _value(attributes: dict, name: str):
value = (attributes.get(name) or {}).get("value")
return value if value not in (None, "") else None


def _is_default(attributes: dict, name: str) -> bool:
return bool((attributes.get(name) or {}).get("is_default"))


def element_status_properties(properties_class: str, attributes: dict) -> dict:
"""Return the status properties to put in a `properties_class` body.

Values the user wrote come first; values the parser filled in from spec
defaults only fill a property nothing written has set.

1. A written `Content Status` / `Activity Status` / `Deployment Status` is sent
when the type has that property, and dropped with a warning when it does not.
2. A written `Status` goes to the first of the type's status properties not yet
set whose enum accepts the value.
3. Defaulted type-specific attributes fill their property if it is still unset.
4. A defaulted `Status` only ever fills `contentStatus`, as before, so a default
never starts overwriting an activity or deployment status (an upsert re-run
would otherwise reset it).

A properties class this table does not know keeps the old behaviour: the
type-specific attributes pass through and `Status` goes to `contentStatus`.
"""
known = properties_class in STATUS_FIELDS_BY_PROPERTIES_CLASS
supported = STATUS_FIELDS_BY_PROPERTIES_CLASS.get(properties_class, ())
has = (lambda field: field in supported) if known else (lambda field: True)
result: dict = {}

def explicit(defaults: bool):
for attr, field in EXPLICIT_STATUS_ATTRIBUTES.items():
value = _value(attributes, attr)
if value is None or _is_default(attributes, attr) != defaults or field in result:
continue
if not has(field):
if not defaults:
logger.warning(f"'{attr}' ignored: {properties_class} has no {field} property.")
continue
result[field] = value

explicit(defaults=False)

status = _value(attributes, "Status")
status_is_default = _is_default(attributes, "Status")
if status is not None and not status_is_default:
upper = str(status).strip().upper()
candidates = [f for f in _PREFERENCE if f in supported] if known else ["contentStatus"]
placed = False
for field in candidates:
if field not in result and upper in VALID_STATUS_VALUES[field]:
result[field] = upper
placed = True
break
if not placed and known:
if not supported:
logger.warning(f"'Status' ignored: {properties_class} has no status property.")
elif not any(f in result for f in candidates):
logger.warning(f"'Status' value '{status}' ignored: not valid for {properties_class}'s "
f"{' / '.join(supported)}.")

explicit(defaults=True)

if status is not None and status_is_default and "contentStatus" not in result and has("contentStatus"):
upper = str(status).strip().upper()
if upper in VALID_STATUS_VALUES["contentStatus"]:
result["contentStatus"] = upper
return result
Loading
Loading