Skip to content

Commit eb45631

Browse files
committed
Add set_status/get_status APIs, refactor _sync_from_diff_element, move to Pydantic 1.7.x
1 parent cbc503c commit eb45631

File tree

5 files changed

+152
-62
lines changed

5 files changed

+152
-62
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
## Unreleased
44

5+
- Now requires Pydantic 1.7.2 or later
6+
7+
- Added `set_status()` API so that DiffSyncModel implementations can provide details for create/update/delete logging
58
- `DiffSync.get_by_uids()` now raises `ObjectNotFound` if any of the provided uids cannot be located.
69
- `DiffSync.get()` raises `ObjectNotFound` or `ValueError` on failure, instead of returning `None`.
710
- #34 - in diff dicts, change keys `src`/`dst`/`_src`/`_dst` to `-` and `+`

diffsync/__init__.py

Lines changed: 117 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from inspect import isclass
2121
from 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
2424
import structlog # type: ignore
2525

2626
from .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+
8797
class 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
# ------------------------------------------------------------------------------

poetry.lock

Lines changed: 24 additions & 19 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ include = [
1616

1717
[tool.poetry.dependencies]
1818
python = "^3.7"
19-
pydantic = "^1.6.1"
19+
pydantic = "^1.7.2"
2020
structlog = "^20.1.0"
2121
colorama = {version = "^0.4.3", optional = true}
2222

0 commit comments

Comments
 (0)