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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ B.load()

# it will show the difference between both systems
diff_a_b = A.diff_from(B)
diff.print_detailed()
print(diff.str())

# it will update System A to align with the current status of system B
A.sync_from(B)
Expand Down
111 changes: 73 additions & 38 deletions dsync/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from collections.abc import Iterable as ABCIterable, Mapping as ABCMapping
import enum
from inspect import isclass
from typing import ClassVar, Iterable, List, Mapping, MutableMapping, Optional, Tuple, Type, Union
from typing import ClassVar, Dict, Iterable, List, Mapping, MutableMapping, Optional, Text, Tuple, Type, Union

from pydantic import BaseModel
import structlog # type: ignore
Expand Down Expand Up @@ -187,28 +187,42 @@ def __repr__(self):
def __str__(self):
return self.get_unique_id()

def print_detailed(self, dsync: "Optional[DSync]" = None, indent: int = 0):
"""Print this model and its children."""
def dict(self, **kwargs) -> dict:
"""Convert this DSyncModel to a dict, excluding the dsync field by default as it is not serializable."""
if "exclude" not in kwargs:
kwargs["exclude"] = {"dsync"}
return super().dict(**kwargs)

def json(self, **kwargs) -> str:
"""Convert this DSyncModel to a JSON string, excluding the dsync field by default as it is not serializable."""
if "exclude" not in kwargs:
kwargs["exclude"] = {"dsync"}
if "exclude_defaults" not in kwargs:
kwargs["exclude_defaults"] = True
return super().json(**kwargs)

def str(self, include_children: bool = True, indent: int = 0) -> str:
"""Build a detailed string representation of this DSyncModel and optionally its children."""
margin = " " * indent
if not dsync:
dsync = self.dsync
print(f"{margin}{self.get_type()}: {self.get_unique_id()}")
output = f"{margin}{self.get_type()}: {self.get_unique_id()}: {self.get_attrs()}"
for modelname, fieldname in self._children.items():
print(f"{margin} {modelname}")
output += f"\n{margin} {fieldname}"
child_ids = getattr(self, fieldname)
if not child_ids:
print(f"{margin} (none)")
for child_id in child_ids:
child = None
if dsync:
child = dsync.get(modelname, child_id)
if not child:
print(f"{margin} {child_id} (no details available)")
else:
child.print_detailed(dsync, indent + 4)
output += ": []"
elif not self.dsync or not include_children:
output += f": {child_ids}"
else:
for child_id in child_ids:
child = self.dsync.get(modelname, child_id)
if not child:
output += f"\n{margin} {child_id} (details unavailable)"
else:
output += "\n" + child.str(include_children=include_children, indent=indent + 4)
return output

@classmethod
def create(cls, dsync: "DSync", ids: dict, attrs: dict) -> Optional["DSyncModel"]:
def create(cls, dsync: "DSync", ids: Mapping, attrs: Mapping) -> Optional["DSyncModel"]:
"""Instantiate this class, along with any platform-specific data creation.

Args:
Expand All @@ -225,7 +239,7 @@ def create(cls, dsync: "DSync", ids: dict, attrs: dict) -> Optional["DSyncModel"
"""
return cls(**ids, dsync=dsync, **attrs)

def update(self, attrs: dict) -> Optional["DSyncModel"]:
def update(self, attrs: Mapping) -> Optional["DSyncModel"]:
"""Update the attributes of this instance, along with any platform-specific data updates.

Args:
Expand Down Expand Up @@ -256,7 +270,7 @@ def delete(self) -> Optional["DSyncModel"]:
return self

@classmethod
def get_type(cls) -> str:
def get_type(cls) -> Text:
"""Return the type AKA modelname of the object or the class

Returns:
Expand All @@ -265,7 +279,7 @@ def get_type(cls) -> str:
return cls._modelname

@classmethod
def create_unique_id(cls, **identifiers) -> str:
def create_unique_id(cls, **identifiers) -> Text:
"""Construct a unique identifier for this model class.

Args:
Expand All @@ -274,19 +288,19 @@ def create_unique_id(cls, **identifiers) -> str:
return "__".join(str(identifiers[key]) for key in cls._identifiers)

@classmethod
def get_children_mapping(cls) -> Mapping[str, str]:
def get_children_mapping(cls) -> Mapping[Text, Text]:
"""Get the mapping of types to fieldnames for child models of this model."""
return cls._children

def get_identifiers(self) -> dict:
def get_identifiers(self) -> Mapping:
"""Get a dict of all identifiers (primary keys) and their values for this object.

Returns:
dict: dictionary containing all primary keys for this device, as defined in _identifiers
"""
return self.dict(include=set(self._identifiers))

def get_attrs(self) -> dict:
def get_attrs(self) -> Mapping:
"""Get all the non-primary-key attributes or parameters for this object.

Similar to Pydantic's `BaseModel.dict()` method, with the following key differences:
Expand All @@ -299,7 +313,7 @@ def get_attrs(self) -> dict:
"""
return self.dict(include=set(self._attributes))

def get_unique_id(self) -> str:
def get_unique_id(self) -> Text:
"""Get the unique ID of an object.

By default the unique ID is built based on all the primary keys defined in `_identifiers`.
Expand All @@ -309,7 +323,7 @@ def get_unique_id(self) -> str:
"""
return self.create_unique_id(**self.get_identifiers())

def get_shortname(self) -> str:
def get_shortname(self) -> Text:
"""Get the (not guaranteed-unique) shortname of an object, if any.

By default the shortname is built based on all the keys defined in `_shortname`.
Expand Down Expand Up @@ -427,16 +441,28 @@ def load(self):
"""Load all desired data from whatever backend data source into this instance."""
# No-op in this generic class

def print_detailed(self, indent: int = 0):
"""Recursively print this DSync and its contained models."""
def dict(self, exclude_defaults: bool = True, **kwargs) -> Mapping:
"""Represent the DSync contents as a dict, as if it were a Pydantic model."""
data: Dict[str, Dict[str, Dict]] = {}
for modelname in self._data:
data[modelname] = {}
for unique_id, model in self._data[modelname].items():
data[modelname][unique_id] = model.dict(exclude_defaults=exclude_defaults, **kwargs)
return data

def str(self, indent: int = 0) -> str:
"""Build a detailed string representation of this DSync."""
margin = " " * indent
output = ""
for modelname in self.top_level:
print(f"{margin}{modelname}")
output += f"{margin}{modelname}"
models = self.get_all(modelname)
if not models:
print(f"{margin} (none)")
for model in models:
model.print_detailed(self, indent + 2)
output += ": []"
else:
for model in models:
output += "\n" + model.str(indent=indent + 2)
return output

# ------------------------------------------------------------------------------
# Synchronization between DSync instances
Expand Down Expand Up @@ -505,13 +531,13 @@ def _sync_from_diff_element(
log.debug("Attempting object creation")
if obj:
raise ObjectNotCreated(f"Failed to create {object_class.get_type()} {element.keys} - it exists!")
obj = object_class.create(dsync=self, ids=element.keys, attrs={key: diffs[key]["src"] for key in diffs})
obj = object_class.create(dsync=self, ids=element.keys, attrs=diffs["src"])
log.info("Created successfully", status="success")
elif element.action == "update":
log.debug("Attempting object update")
if not obj:
raise ObjectNotUpdated(f"Failed to update {object_class.get_type()} {element.keys} - not found!")
obj = obj.update(attrs={key: diffs[key]["src"] for key in diffs})
obj = obj.update(attrs=diffs["src"])
log.info("Updated successfully", status="success")
elif element.action == "delete":
log.debug("Attempting object deletion")
Expand Down Expand Up @@ -579,7 +605,9 @@ def diff_to(self, target: "DSync", diff_class: Type[Diff] = Diff, flags: DSyncFl
# Object Storage Management
# ------------------------------------------------------------------------------

def get(self, obj: Union[str, DSyncModel, Type[DSyncModel]], identifier: Union[str, dict]) -> Optional[DSyncModel]:
def get(
self, obj: Union[Text, DSyncModel, Type[DSyncModel]], identifier: Union[Text, Mapping]
) -> Optional[DSyncModel]:
"""Get one object from the data store based on its unique id.

Args:
Expand All @@ -589,16 +617,23 @@ def get(self, obj: Union[str, DSyncModel, Type[DSyncModel]], identifier: Union[s
if isinstance(obj, str):
modelname = obj
if not hasattr(self, obj):
return None
object_class = getattr(self, obj)
object_class = None
else:
object_class = getattr(self, obj)
else:
object_class = obj
modelname = obj.get_type()

if isinstance(identifier, str):
uid = identifier
else:
elif object_class:
uid = object_class.create_unique_id(**identifier)
else:
self._log.warning(
f"Tried to look up a {modelname} by identifier {identifier}, "
"but don't know how to convert that to a uid string",
)
return None

return self._data[modelname].get(uid)

Expand All @@ -618,7 +653,7 @@ def get_all(self, obj):

return self._data[modelname].values()

def get_by_uids(self, uids: List[str], obj) -> List[DSyncModel]:
def get_by_uids(self, uids: List[Text], obj) -> List[DSyncModel]:
"""Get multiple objects from the store by their unique IDs/Keys and type.

Args:
Expand Down
Loading