2020from inspect import isclass
2121from typing import ClassVar , Dict , Iterable , List , Mapping , MutableMapping , Optional , Text , Tuple , Type , Union
2222
23- from pydantic import BaseModel
23+ from pydantic import BaseModel , PrivateAttr
2424import structlog # type: ignore
2525
2626from .diff import Diff , DiffElement
@@ -84,6 +84,16 @@ class DiffSyncFlags(enum.Flag):
8484 """
8585
8686
87+ class DiffSyncModelStatusValues (enum .Enum ):
88+ """Flag values that can be set as a DiffSyncModel's `_status` when performing a sync."""
89+
90+ NONE = ""
91+ SUCCESS = "success"
92+ FAILURE = "failure"
93+ ERROR = "error"
94+ UNKNOWN = "unknown"
95+
96+
8797class DiffSyncModel (BaseModel ):
8898 """Base class for all DiffSync object models.
8999
@@ -144,6 +154,12 @@ class DiffSyncModel(BaseModel):
144154 diffsync : Optional ["DiffSync" ] = None
145155 """Optional: the DiffSync instance that owns this model instance."""
146156
157+ _status : DiffSyncModelStatusValues = PrivateAttr (DiffSyncModelStatusValues .NONE )
158+ """Status of the last attempt at creating/updating/deleting this model."""
159+
160+ _status_message : str = PrivateAttr ("" )
161+ """Message, if any, associated with the create/update/delete status value."""
162+
147163 class Config : # pylint: disable=too-few-public-methods
148164 """Pydantic class configuration."""
149165
@@ -221,10 +237,18 @@ def str(self, include_children: bool = True, indent: int = 0) -> str:
221237 output += f"\n { margin } { child_id } (ERROR: details unavailable)"
222238 return output
223239
240+ def set_status (self , status : DiffSyncModelStatusValues , message : Text = "" ):
241+ """Update the status (and optionally status message) of this model in response to a create/update/delete call."""
242+ self ._status = status
243+ self ._status_message = message
244+
224245 @classmethod
225246 def create (cls , diffsync : "DiffSync" , ids : Mapping , attrs : Mapping ) -> Optional ["DiffSyncModel" ]:
226247 """Instantiate this class, along with any platform-specific data creation.
227248
249+ Subclasses must call `super().create()`; they may wish to then override the default status information
250+ by calling `set_status()` to provide more context (such as details of any interactions with underlying systems).
251+
228252 Args:
229253 diffsync: The master data store for other DiffSyncModel instances that we might need to reference
230254 ids: Dictionary of unique-identifiers needed to create the new object
@@ -237,11 +261,16 @@ def create(cls, diffsync: "DiffSync", ids: Mapping, attrs: Mapping) -> Optional[
237261 Raises:
238262 ObjectNotCreated: if an error occurred.
239263 """
240- return cls (** ids , diffsync = diffsync , ** attrs )
264+ model = cls (** ids , diffsync = diffsync , ** attrs )
265+ model .set_status (DiffSyncModelStatusValues .SUCCESS , "Created successfully" )
266+ return model
241267
242268 def update (self , attrs : Mapping ) -> Optional ["DiffSyncModel" ]:
243269 """Update the attributes of this instance, along with any platform-specific data updates.
244270
271+ Subclasses must call `super().update()`; they may wish to then override the default status information
272+ by calling `set_status()` to provide more context (such as details of any interactions with underlying systems).
273+
245274 Args:
246275 attrs: Dictionary of attributes to update on the object
247276
@@ -255,18 +284,24 @@ def update(self, attrs: Mapping) -> Optional["DiffSyncModel"]:
255284 for attr , value in attrs .items ():
256285 # TODO: enforce that only attrs in self._attributes can be updated in this way?
257286 setattr (self , attr , value )
287+
288+ self .set_status (DiffSyncModelStatusValues .SUCCESS , "Updated successfully" )
258289 return self
259290
260291 def delete (self ) -> Optional ["DiffSyncModel" ]:
261292 """Delete any platform-specific data corresponding to this instance.
262293
294+ Subclasses must call `super().delete()`; they may wish to then override the default status information
295+ by calling `set_status()` to provide more context (such as details of any interactions with underlying systems).
296+
263297 Returns:
264298 DiffSyncModel: this instance, if all data was successfully deleted.
265299 None: if data deletion failed in such a way that child objects of this model should not be deleted.
266300
267301 Raises:
268302 ObjectNotDeleted: if an error occurred.
269303 """
304+ self .set_status (DiffSyncModelStatusValues .SUCCESS , "Deleted successfully" )
270305 return self
271306
272307 @classmethod
@@ -336,6 +371,10 @@ def get_shortname(self) -> Text:
336371 return "__" .join ([str (getattr (self , key )) for key in self ._shortname ])
337372 return self .get_unique_id ()
338373
374+ def get_status (self ) -> Tuple [DiffSyncModelStatusValues , Text ]:
375+ """Get the status of the last create/update/delete operation on this object, and any associated message."""
376+ return (self ._status , self ._status_message )
377+
339378 def add_child (self , child : "DiffSyncModel" ):
340379 """Add a child reference to an object.
341380
@@ -543,13 +582,12 @@ def _sync_from_diff_element(
543582 logger: Parent logging context
544583 parent_model: Parent object to update (`add_child`/`remove_child`) if the sync creates/deletes an object.
545584 """
546- # pylint: disable=too-many-branches
547- # GFM: I made a few attempts at refactoring this to reduce the branching, but found that it was less readable.
548- # So let's live with the slightly too high number of branches (14/12) for now.
549585 log = logger or self ._log
550586 object_class = getattr (self , element .type )
587+ obj : Optional [DiffSyncModel ]
551588 try :
552- obj : Optional [DiffSyncModel ] = self .get (object_class , element .keys )
589+ obj = self .get (object_class , element .keys )
590+ obj .set_status (DiffSyncModelStatusValues .UNKNOWN )
553591 except ObjectNotFound :
554592 obj = None
555593 # Get the attributes that actually differ between source and dest
@@ -561,35 +599,12 @@ def _sync_from_diff_element(
561599 diffs = diffs ,
562600 )
563601
564- try :
565- if element .action == "create" :
566- log .debug ("Attempting object creation" )
567- if obj :
568- raise ObjectNotCreated (f"Failed to create { object_class .get_type ()} { element .keys } - it exists!" )
569- obj = object_class .create (diffsync = self , ids = element .keys , attrs = diffs ["+" ])
570- log .info ("Created successfully" , status = "success" )
571- elif element .action == "update" :
572- log .debug ("Attempting object update" )
573- if not obj :
574- raise ObjectNotUpdated (f"Failed to update { object_class .get_type ()} { element .keys } - not found!" )
575- obj = obj .update (attrs = diffs ["+" ])
576- log .info ("Updated successfully" , status = "success" )
577- elif element .action == "delete" :
578- log .debug ("Attempting object deletion" )
579- if not obj :
580- raise ObjectNotDeleted (f"Failed to delete { object_class .get_type ()} { element .keys } - not found!" )
581- obj = obj .delete ()
582- log .info ("Deleted successfully" , status = "success" )
583- else :
584- if flags & DiffSyncFlags .LOG_UNCHANGED_RECORDS :
585- log .debug ("No action needed" , status = "success" )
586- except ObjectCrudException as exception :
587- log .error (str (exception ), status = "error" )
588- if not flags & DiffSyncFlags .CONTINUE_ON_FAILURE :
589- raise
590- else :
591- if obj is None :
592- log .warning ("Non-fatal failure encountered" , status = "failure" )
602+ result = self ._perform_sync_action (element , log , obj , diffs .get ("+" , {}), flags )
603+
604+ if element .action is not None :
605+ self ._log_sync_action (element , log , obj , result )
606+
607+ obj = result
593608
594609 if obj is None :
595610 log .warning ("Not syncing children" )
@@ -611,6 +626,73 @@ def _sync_from_diff_element(
611626 for child in element .get_children ():
612627 self ._sync_from_diff_element (child , flags = flags , parent_model = obj , logger = logger )
613628
629+ def _perform_sync_action ( # pylint: disable=too-many-arguments
630+ self ,
631+ element : DiffElement ,
632+ logger : structlog .BoundLogger ,
633+ obj : Optional [DiffSyncModel ],
634+ attrs : Mapping ,
635+ flags : DiffSyncFlags ,
636+ ) -> Optional [DiffSyncModel ]:
637+ """Helper to _sync_from_diff_element to handle performing the actual model object create/update/delete."""
638+ object_class = getattr (self , element .type )
639+ try :
640+ if element .action == "create" :
641+ logger .debug ("Attempting object creation" )
642+ if obj :
643+ raise ObjectNotCreated (f"Failed to create { object_class .get_type ()} { element .keys } - it exists!" )
644+ return object_class .create (diffsync = self , ids = element .keys , attrs = attrs )
645+ if element .action == "update" :
646+ logger .debug ("Attempting object update" )
647+ if not obj :
648+ raise ObjectNotUpdated (f"Failed to update { object_class .get_type ()} { element .keys } - not found!" )
649+ return obj .update (attrs = attrs )
650+ if element .action == "delete" :
651+ logger .debug ("Attempting object deletion" )
652+ if not obj :
653+ raise ObjectNotDeleted (f"Failed to delete { object_class .get_type ()} { element .keys } - not found!" )
654+ return obj .delete ()
655+ if obj :
656+ obj .set_status (DiffSyncModelStatusValues .SUCCESS , "No action needed" )
657+ if flags & DiffSyncFlags .LOG_UNCHANGED_RECORDS :
658+ self ._log_sync_action (element , logger , obj , obj )
659+ return obj
660+ except ObjectCrudException as exception :
661+ # Something went wrong catastrophically
662+ if flags & DiffSyncFlags .CONTINUE_ON_FAILURE :
663+ if obj :
664+ obj .set_status (DiffSyncModelStatusValues .ERROR , str (exception ))
665+ return obj
666+ raise
667+
668+ @staticmethod
669+ def _log_sync_action (
670+ element : DiffElement ,
671+ logger : structlog .BoundLogger ,
672+ obj : Optional [DiffSyncModel ],
673+ result : Optional [DiffSyncModel ],
674+ ):
675+ """Helper to _sync_from_diff_element to handle logging the result of a specific model create/update/delete."""
676+ if result is not None :
677+ status , message = result .get_status ()
678+ else :
679+ # Something went wrong and was handled gracefully, but a failure nonetheless
680+ status , message = (DiffSyncModelStatusValues .FAILURE , "Non-fatal failure encountered" )
681+ if obj :
682+ # Did the model itself provide more detailed status?
683+ candidate_status , candidate_message = obj .get_status ()
684+ if candidate_status in (DiffSyncModelStatusValues .FAILURE , DiffSyncModelStatusValues .ERROR ):
685+ status , message = candidate_status , candidate_message
686+
687+ if element .action is None :
688+ logger .debug (message , status = status .value )
689+ elif status == DiffSyncModelStatusValues .SUCCESS :
690+ logger .info (message , status = status .value )
691+ elif status == DiffSyncModelStatusValues .FAILURE :
692+ logger .warning (message , status = status .value )
693+ else :
694+ logger .error (message , status = status .value )
695+
614696 # ------------------------------------------------------------------------------
615697 # Diff calculation and construction
616698 # ------------------------------------------------------------------------------
0 commit comments