1818from  collections .abc  import  Iterable  as  ABCIterable , Mapping  as  ABCMapping 
1919import  enum 
2020from  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
2323from  pydantic  import  BaseModel 
2424import  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 : Dict , attrs : Dict ) ->  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 : Dict ) ->  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 ) ->  Dict :
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 ) ->  Dict :
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 ) ->  dict :
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 
@@ -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 , Dict ]
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