Skip to content

Commit 5b7ba1d

Browse files
Merge pull request #23 from networktocode/gfm-serialization
String builder, serialization to dict
2 parents ad6e6c1 + c1341fc commit 5b7ba1d

File tree

10 files changed

+499
-130
lines changed

10 files changed

+499
-130
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ B.load()
1313

1414
# it will show the difference between both systems
1515
diff_a_b = A.diff_from(B)
16-
diff.print_detailed()
16+
print(diff.str())
1717

1818
# it will update System A to align with the current status of system B
1919
A.sync_from(B)

dsync/__init__.py

Lines changed: 73 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from collections.abc import Iterable as ABCIterable, Mapping as ABCMapping
1919
import enum
2020
from inspect import isclass
21-
from typing import ClassVar, Iterable, List, Mapping, MutableMapping, Optional, Tuple, Type, Union
21+
from typing import ClassVar, Dict, Iterable, List, Mapping, MutableMapping, Optional, Text, Tuple, Type, Union
2222

2323
from pydantic import BaseModel
2424
import structlog # type: ignore
@@ -187,28 +187,42 @@ def __repr__(self):
187187
def __str__(self):
188188
return self.get_unique_id()
189189

190-
def print_detailed(self, dsync: "Optional[DSync]" = None, indent: int = 0):
191-
"""Print this model and its children."""
190+
def dict(self, **kwargs) -> dict:
191+
"""Convert this DSyncModel to a dict, excluding the dsync field by default as it is not serializable."""
192+
if "exclude" not in kwargs:
193+
kwargs["exclude"] = {"dsync"}
194+
return super().dict(**kwargs)
195+
196+
def json(self, **kwargs) -> str:
197+
"""Convert this DSyncModel to a JSON string, excluding the dsync field by default as it is not serializable."""
198+
if "exclude" not in kwargs:
199+
kwargs["exclude"] = {"dsync"}
200+
if "exclude_defaults" not in kwargs:
201+
kwargs["exclude_defaults"] = True
202+
return super().json(**kwargs)
203+
204+
def str(self, include_children: bool = True, indent: int = 0) -> str:
205+
"""Build a detailed string representation of this DSyncModel and optionally its children."""
192206
margin = " " * indent
193-
if not dsync:
194-
dsync = self.dsync
195-
print(f"{margin}{self.get_type()}: {self.get_unique_id()}")
207+
output = f"{margin}{self.get_type()}: {self.get_unique_id()}: {self.get_attrs()}"
196208
for modelname, fieldname in self._children.items():
197-
print(f"{margin} {modelname}")
209+
output += f"\n{margin} {fieldname}"
198210
child_ids = getattr(self, fieldname)
199211
if not child_ids:
200-
print(f"{margin} (none)")
201-
for child_id in child_ids:
202-
child = None
203-
if dsync:
204-
child = dsync.get(modelname, child_id)
205-
if not child:
206-
print(f"{margin} {child_id} (no details available)")
207-
else:
208-
child.print_detailed(dsync, indent + 4)
212+
output += ": []"
213+
elif not self.dsync or not include_children:
214+
output += f": {child_ids}"
215+
else:
216+
for child_id in child_ids:
217+
child = self.dsync.get(modelname, child_id)
218+
if not child:
219+
output += f"\n{margin} {child_id} (details unavailable)"
220+
else:
221+
output += "\n" + child.str(include_children=include_children, indent=indent + 4)
222+
return output
209223

210224
@classmethod
211-
def create(cls, dsync: "DSync", ids: dict, attrs: dict) -> Optional["DSyncModel"]:
225+
def create(cls, dsync: "DSync", ids: Mapping, attrs: Mapping) -> Optional["DSyncModel"]:
212226
"""Instantiate this class, along with any platform-specific data creation.
213227
214228
Args:
@@ -225,7 +239,7 @@ def create(cls, dsync: "DSync", ids: dict, attrs: dict) -> Optional["DSyncModel"
225239
"""
226240
return cls(**ids, dsync=dsync, **attrs)
227241

228-
def update(self, attrs: dict) -> Optional["DSyncModel"]:
242+
def update(self, attrs: Mapping) -> Optional["DSyncModel"]:
229243
"""Update the attributes of this instance, along with any platform-specific data updates.
230244
231245
Args:
@@ -256,7 +270,7 @@ def delete(self) -> Optional["DSyncModel"]:
256270
return self
257271

258272
@classmethod
259-
def get_type(cls) -> str:
273+
def get_type(cls) -> Text:
260274
"""Return the type AKA modelname of the object or the class
261275
262276
Returns:
@@ -265,7 +279,7 @@ def get_type(cls) -> str:
265279
return cls._modelname
266280

267281
@classmethod
268-
def create_unique_id(cls, **identifiers) -> str:
282+
def create_unique_id(cls, **identifiers) -> Text:
269283
"""Construct a unique identifier for this model class.
270284
271285
Args:
@@ -274,19 +288,19 @@ def create_unique_id(cls, **identifiers) -> str:
274288
return "__".join(str(identifiers[key]) for key in cls._identifiers)
275289

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

281-
def get_identifiers(self) -> dict:
295+
def get_identifiers(self) -> Mapping:
282296
"""Get a dict of all identifiers (primary keys) and their values for this object.
283297
284298
Returns:
285299
dict: dictionary containing all primary keys for this device, as defined in _identifiers
286300
"""
287301
return self.dict(include=set(self._identifiers))
288302

289-
def get_attrs(self) -> dict:
303+
def get_attrs(self) -> Mapping:
290304
"""Get all the non-primary-key attributes or parameters for this object.
291305
292306
Similar to Pydantic's `BaseModel.dict()` method, with the following key differences:
@@ -299,7 +313,7 @@ def get_attrs(self) -> dict:
299313
"""
300314
return self.dict(include=set(self._attributes))
301315

302-
def get_unique_id(self) -> str:
316+
def get_unique_id(self) -> Text:
303317
"""Get the unique ID of an object.
304318
305319
By default the unique ID is built based on all the primary keys defined in `_identifiers`.
@@ -309,7 +323,7 @@ def get_unique_id(self) -> str:
309323
"""
310324
return self.create_unique_id(**self.get_identifiers())
311325

312-
def get_shortname(self) -> str:
326+
def get_shortname(self) -> Text:
313327
"""Get the (not guaranteed-unique) shortname of an object, if any.
314328
315329
By default the shortname is built based on all the keys defined in `_shortname`.
@@ -427,16 +441,28 @@ def load(self):
427441
"""Load all desired data from whatever backend data source into this instance."""
428442
# No-op in this generic class
429443

430-
def print_detailed(self, indent: int = 0):
431-
"""Recursively print this DSync and its contained models."""
444+
def dict(self, exclude_defaults: bool = True, **kwargs) -> Mapping:
445+
"""Represent the DSync contents as a dict, as if it were a Pydantic model."""
446+
data: Dict[str, Dict[str, Dict]] = {}
447+
for modelname in self._data:
448+
data[modelname] = {}
449+
for unique_id, model in self._data[modelname].items():
450+
data[modelname][unique_id] = model.dict(exclude_defaults=exclude_defaults, **kwargs)
451+
return data
452+
453+
def str(self, indent: int = 0) -> str:
454+
"""Build a detailed string representation of this DSync."""
432455
margin = " " * indent
456+
output = ""
433457
for modelname in self.top_level:
434-
print(f"{margin}{modelname}")
458+
output += f"{margin}{modelname}"
435459
models = self.get_all(modelname)
436460
if not models:
437-
print(f"{margin} (none)")
438-
for model in models:
439-
model.print_detailed(self, indent + 2)
461+
output += ": []"
462+
else:
463+
for model in models:
464+
output += "\n" + model.str(indent=indent + 2)
465+
return output
440466

441467
# ------------------------------------------------------------------------------
442468
# Synchronization between DSync instances
@@ -505,13 +531,13 @@ def _sync_from_diff_element(
505531
log.debug("Attempting object creation")
506532
if obj:
507533
raise ObjectNotCreated(f"Failed to create {object_class.get_type()} {element.keys} - it exists!")
508-
obj = object_class.create(dsync=self, ids=element.keys, attrs={key: diffs[key]["src"] for key in diffs})
534+
obj = object_class.create(dsync=self, ids=element.keys, attrs=diffs["src"])
509535
log.info("Created successfully", status="success")
510536
elif element.action == "update":
511537
log.debug("Attempting object update")
512538
if not obj:
513539
raise ObjectNotUpdated(f"Failed to update {object_class.get_type()} {element.keys} - not found!")
514-
obj = obj.update(attrs={key: diffs[key]["src"] for key in diffs})
540+
obj = obj.update(attrs=diffs["src"])
515541
log.info("Updated successfully", status="success")
516542
elif element.action == "delete":
517543
log.debug("Attempting object deletion")
@@ -579,7 +605,9 @@ def diff_to(self, target: "DSync", diff_class: Type[Diff] = Diff, flags: DSyncFl
579605
# Object Storage Management
580606
# ------------------------------------------------------------------------------
581607

582-
def get(self, obj: Union[str, DSyncModel, Type[DSyncModel]], identifier: Union[str, dict]) -> Optional[DSyncModel]:
608+
def get(
609+
self, obj: Union[Text, DSyncModel, Type[DSyncModel]], identifier: Union[Text, Mapping]
610+
) -> Optional[DSyncModel]:
583611
"""Get one object from the data store based on its unique id.
584612
585613
Args:
@@ -589,16 +617,23 @@ def get(self, obj: Union[str, DSyncModel, Type[DSyncModel]], identifier: Union[s
589617
if isinstance(obj, str):
590618
modelname = obj
591619
if not hasattr(self, obj):
592-
return None
593-
object_class = getattr(self, obj)
620+
object_class = None
621+
else:
622+
object_class = getattr(self, obj)
594623
else:
595624
object_class = obj
596625
modelname = obj.get_type()
597626

598627
if isinstance(identifier, str):
599628
uid = identifier
600-
else:
629+
elif object_class:
601630
uid = object_class.create_unique_id(**identifier)
631+
else:
632+
self._log.warning(
633+
f"Tried to look up a {modelname} by identifier {identifier}, "
634+
"but don't know how to convert that to a uid string",
635+
)
636+
return None
602637

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

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

619654
return self._data[modelname].values()
620655

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

0 commit comments

Comments
 (0)