Skip to content

Commit

Permalink
dyobject: support class level config loader and ignoring class variab…
Browse files Browse the repository at this point in the history
…les.

Signed-off-by: Chris Lapa <chris@lapa.com.au>
  • Loading branch information
GusBricker committed Aug 13, 2018
1 parent cd53692 commit 4b0ebca
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 21 deletions.
24 changes: 20 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,20 @@ DyStore represents a DynamoDB table. The structure of your tables should be repr

#### DyObject
DyObject represents an object in a DyStore. They allow seemless serialization of Python objects to a DynamoDB table with minimal extra code. The following class variables must be set when inheriting DyObject:
- *TABLE_NAME* = 'Shard1' # Table to save this shard to in AWS
- *REGION_NAME* = 'us-east-2' # Region to sav eto in AWS
- *PRIMARY_KEY_NAME* = 'IDX' # Primary key name to use
- *PATH* = '$.birth_details' # Path in parent object or None if is root object
- *TABLE_NAME* = 'Shard1' # Table to save this shard to in AWS
- *REGION_NAME* = 'us-east-2' # Region to sav eto in AWS
- *PRIMARY_KEY_NAME* = 'IDX' # Primary key name to use
- *PATH* = '$.birth_details' # Path in parent object or None if is root object
- *IGNORE_LIST* = `[]` # Variables to ignore during serialisation
- *CONFIG_LOADER* = config_loader() # Class level config loader to prevent having to pass into `save()` and `load()`

#### Config Loading
The `config_loader(config, data)` is a callback that is used in DyStore and DyObject calls to control certain operations of dynamo-store. The first parameter `config` specifies which configuration item is being queried, and the second parameter `data` is context sensitive data for the call.

The `config` parameter will be one of the `CONFIG_LOADER_*` class variables defined in DyStore or DyObject, they are documented in the API documentation below.

DyObject supports both config_loader passed into `save()` and `load()` as well as a class level config loader using the CONFIG_LOADER class variable. The passed in config loader takes precendence over class variable instance.


#### Example DyStore usage

Expand Down Expand Up @@ -258,6 +262,16 @@ class DyObject(object):
"""
PATH = None
"""
Config loader callable to use when config queries are made
"""
CONFIG_LOADER = None
"""
Variable names to ignore during serialization
"""
IGNORE_LIST = []
"""
Invoked on object load when class cant be determined.
config_loader(DyObject.CONFIG_LOADER_DICT_TO_KEY, {'key': key, 'value': value})
Expand All @@ -272,6 +286,7 @@ class DyObject(object):
Saves this object to the store.
:param primary_key: Primary key to use.
:param config_loader: Config loader to be used: config_loader(config, data) returns setting
A class wide config loader can also be set, however the passed in config loader takes preference.
:returns: key of object written
"""
Expand All @@ -282,6 +297,7 @@ class DyObject(object):
:param cls: Class to instantiate
:param primary_key: Primary key of object to load.
:param config_loader: Config loader to be used: config_loader(config, data) returns setting
A class wide config loader can also be set, however the passed in config loader takes preference.
:returns: cls object
"""
```
59 changes: 42 additions & 17 deletions dynamo_store/object.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,16 @@ class DyObject(object):
"""
PATH = None

"""
Config loader callable to use when config queries are made
"""
CONFIG_LOADER = None

"""
Variable names to ignore during serialization
"""
IGNORE_LIST = []

"""
Invoked on object load when class cant be determined.
config_loader(DyObject.CONFIG_LOADER_DICT_TO_KEY, {'key': key, 'value': value})
Expand All @@ -36,19 +46,22 @@ class DyObject(object):
def __init__(self):
self.__primary_key = None

@property
def store(self):
return DyStore(table_name=self.TABLE_NAME,
primary_key_name=self.PRIMARY_KEY_NAME,
path=self.PATH,
region=self.REGION_NAME)
@classmethod
def store(cls):
return DyStore(table_name=cls.TABLE_NAME,
primary_key_name=cls.PRIMARY_KEY_NAME,
path=cls.PATH,
region=cls.REGION_NAME)

def to_dict(self):
d = {'__class__': self.__class__.__qualname__,
'__module__': self.__module__}
for name in dir(self):
if name in self.IGNORE_LIST:
continue

value = getattr(self, name)
if name.startswith('__') or callable(value) or name == 'store':
if name.startswith('__') or callable(value):
continue

if isinstance(value, DyObject):
Expand All @@ -57,32 +70,45 @@ def to_dict(self):
d[name] = value
return d

@staticmethod
def from_dict(obj, data, config_loader=None):
@classmethod
def from_dict(cls, data, config_loader=None):
obj = cls()

for key, value in data.items():
if key in cls.IGNORE_LIST:
continue

if isinstance(value, dict):
if value.get('__class__') and value.get('__module__'):
klass = value['__class__']
module = value['__module__']
module = importlib.import_module(module)
class_ = getattr(module, klass)
logger.debug('Instantiating: %s' % class_)
child_obj = class_()
class_.from_dict(child_obj, value, config_loader=config_loader)
child_obj = class_.from_dict(value, config_loader=config_loader)
setattr(obj, key, child_obj)
elif config_loader and callable(config_loader):
class_ = config_loader(DyObject.CONFIG_LOADER_DICT_TO_CLASS, {'key': key, 'value': value})

if class_ and issubclass(class_, DyObject):
logger.debug('Instantiating: %s' % class_)
child_obj = class_.from_dict(value, config_loader=config_loader)
setattr(obj, key, child_obj)
else:
setattr(obj, key, value)
elif cls.CONFIG_LOADER and callable(cls.CONFIG_LOADER):
class_ = cls.CONFIG_LOADER(DyObject.CONFIG_LOADER_DICT_TO_CLASS, {'key': key, 'value': value})
if class_ and issubclass(class_, DyObject):
logger.debug('Instantiating: %s' % class_)
child_obj = class_()
class_.from_dict(child_obj, value, config_loader=config_loader)
child_obj = class_.from_dict(value, config_loader=config_loader)
setattr(obj, key, child_obj)
else:
setattr(obj, key, value)
else:
setattr(obj, key, value)
elif key not in ['__class__', '__module__']:
setattr(obj, key, value)
return obj

def save(self, primary_key=None, config_loader=None):
"""
Expand All @@ -96,7 +122,7 @@ def save(self, primary_key=None, config_loader=None):
if hasattr(self, "__primary_key") and self.__primary_key:
primary_key = self.__primary_key

key = self.store.write(d, primary_key=primary_key, config_loader=config_loader)
key = self.store().write(d, primary_key=primary_key, config_loader=config_loader)
if key:
self.__primary_key = key

Expand All @@ -111,9 +137,8 @@ def load(cls, primary_key, config_loader=None):
:param config_loader: Config loader to be used: config_loader(config, data) returns setting
:returns: cls object
"""
obj = cls()
success, data = obj.store.read(primary_key, config_loader=config_loader)
success, data = cls.store().read(primary_key, config_loader=config_loader)
if not success:
raise Exception('Couldnt read from store using pk: %s' % primary_key)
cls.from_dict(obj, data, config_loader=config_loader)
obj = cls.from_dict(data, config_loader=config_loader)
return obj
37 changes: 37 additions & 0 deletions tests/test_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,14 @@ class Base(DyObject):
TABLE_NAME = 'DynamoStoreRootDB'
REGION_NAME = 'us-east-2'
PRIMARY_KEY_NAME = 'ID'
IGNORE_LIST = ['address']

def __init__(self):
self.firstname = None
self.lastname = None
self.location = Location()
self.birth_details = BirthDetails()
self.address = None

def loader1(config, data):
if config == DyStore.CONFIG_LOADER_LOAD_KEY:
Expand All @@ -85,6 +87,7 @@ def loader1(config, data):

def test_can_write_read_objects(root_store, base_item):
orig = Base()
orig.address = '123 fake st'
orig.firstname = 'john'
orig.lastname = 'smith'
orig.location.city = 'Osaka'
Expand All @@ -96,6 +99,7 @@ def test_can_write_read_objects(root_store, base_item):
key = orig.save(config_loader=loader1)

o = Base.load(key, config_loader=loader1)
assert o.address == None
assert o.firstname == 'john'
assert o.lastname == 'smith'
assert isinstance(o.location, Location)
Expand Down Expand Up @@ -135,6 +139,39 @@ def test_can_guess_objects(root_store, base_item):
assert key

o = Base.load(key, config_loader=loader2)
assert o.address == None
assert o.firstname == 'john'
assert o.lastname == 'smith'
assert isinstance(o.location, Location)
assert o.location.city == 'Osaka'
assert o.location.country == 'Japan'
assert isinstance(o.location.geolocation, GeoLocation)
assert o.location.geolocation.lattitude == '90.00'
assert o.location.geolocation.longitude == '90.00'
assert isinstance(o.birth_details, BirthDetails)
assert o.birth_details.dob == '12/2/1995'
assert o.birth_details.hospital == 'Kosei Nenkin'

class BaseInternalLoader(DyObject):
TABLE_NAME = 'DynamoStoreRootDB'
REGION_NAME = 'us-east-2'
PRIMARY_KEY_NAME = 'ID'
IGNORE_LIST = ['address']
CONFIG_LOADER = loader2

def __init__(self):
self.firstname = None
self.lastname = None
self.location = Location()
self.birth_details = BirthDetails()
self.address = None

def test_can_guess_objects_with_internal_loader(root_store, base_item):
key = root_store.write(deepcopy(base_item), primary_key=test_pk)
assert key

o = BaseInternalLoader.load(key, config_loader=loader2)
assert o.address == None
assert o.firstname == 'john'
assert o.lastname == 'smith'
assert isinstance(o.location, Location)
Expand Down

0 comments on commit 4b0ebca

Please sign in to comment.