The corresponding Story for concrete Product object data persistence is as follows:
-
As a product manager, I need to be able to manage products in the system, so that their statuses and information can be kept current.
The code that fulfills this scenario is even simpler than the code for Artisan objects; it doesn't need any special handling of object properties, so from_data_dict can simply fall back to the default, defined in HMSMongoDataObject. It doesn't have any extraneous methods that are required, either, so a full, functional implementation really just boils down to the _data_dict_keys class attribute and the __init__, matches, and to_data_dict methods, with matches being implemented as a call to HMSMongoDataObject.matches:
class Product(BaseProduct, HMSMongoDataObject): """ Represents a Product in the context of the Central Office applications and services """ ################################### # Class attributes/constants # ################################### _data_dict_keys = [ 'name', 'summary', 'available', 'store_available', 'description', 'dimensions', 'metadata', 'shipping_weight', 'oid', 'created', 'modified', 'is_active', 'is_deleted' ]
The __init__ method has a long argument set, which should come as no surprise:
################################### # Object initialization # ################################### def __init__(self, # - Arguments from HMSMongoDataObject name:(str,), summary:(str,), available:(bool,), store_available:(bool,), # - Optional arguments: description:(str,None)=None, dimensions:(str,None)=None, metadata:(dict,)={}, shipping_weight:(int,)=0, # - Arguments from HMSMongoDataObject oid:(UUID,str,None)=None, created:(datetime,str,float,int,None)=None, modified:(datetime,str,float,int,None)=None, is_active:(bool,int,None)=None, is_deleted:(bool,int,None)=None, is_dirty:(bool,int,None)=None, is_new:(bool,int,None)=None, ): """ Object initialization. self .............. (Product instance, required) The instance to execute against name .............. (str, required) The name of the product summary ........... (str, required) A one-line summary of the product available ......... (bool, required) Flag indicating whether the product is considered available by the artisan who makes it store_available ... (bool, required) Flag indicating whether the product is considered available on the web- store by the Central Office description ....... (str, optional, defaults to None) A detailed description of the product dimensions ........ (str, optional, defaults to None) A measurement- description of the product metadata .......... (dict, optional, defaults to {}) A collection of metadata keys and values describing the product shipping_weight ... (int, optional, defaults to 0) The shipping- weight of the product oid ............... (UUID|str, optional, defaults to None) The unique identifier of the object's state-data record in the back-end data-store created ........... (datetime|str|float|int, optional, defaults to None) The date/time that the object was created modified .......... (datetime|str|float|int, optional, defaults to None) The date/time that the object was last modified is_active ......... (bool|int, optional, defaults to None) A flag indicating that the object is active is_deleted ........ (bool|int, optional, defaults to None) A flag indicating that the object should be considered deleted (and may be in the near future) is_dirty .......... (bool|int, optional, defaults to None) A flag indicating that the object's data needs to be updated in the back-end data-store is_new ............ (bool|int, optional, defaults to None) A flag indicating that the object's data needs to be created in the back-end data-store """ # - Call parent initializers if needed BaseProduct.__init__( self, name, summary, available, store_available, description, dimensions, metadata, shipping_weight ) HMSMongoDataObject.__init__(self, oid, created, modified, is_active, is_deleted, is_dirty, is_new ) # - Perform any other initialization needed
The implementations of matches and to_data_dict are very straightforward, as follows:
################################### # Instance methods # ################################### def matches(self, **criteria) -> (bool,): return HMSMongoDataObject.matches(self, **criteria) def to_data_dict(self): return { # - BaseProduct-derived items 'available':self.available, 'description':self.description, 'dimensions':self.dimensions, 'metadata':self.metadata, 'name':self.name, 'shipping_weight':self.shipping_weight, 'store_available':self.store_available, 'summary':self.summary, # - BaseDataObject-derived items 'created':datetime.strftime( self.created, self.__class__._data_time_string ), 'is_active':self.is_active, 'is_deleted':self.is_deleted, 'modified':datetime.strftime( self.modified, self.__class__._data_time_string ), 'oid':str(self.oid), }
The matches method may need to be reexamined later on, either during the creation of the Artisan Gateway service or when the various application UIs are being built, because while it works for most cases, it will not currently allow a get with any metadata criteria to return results unless criteria is the only value being searched for (no oids are passed). It's worth a more detailed look here and now, though, because it shows some aspects of how the data object code interacts with MongoDB.
First, let's create some example Product objects and save them, as follows:
# - An example product - A copper-and-emerald necklace: product = Product( 'Necklace #1', 'Showing some Product.get aspects', True, True, metadata={ 'metal':'Copper', 'gemstone':'Emerald', } ) product.save() # - Silver-and-emerald necklace: product = Product( 'Necklace #2', 'Showing some Product.get aspects', True, True, metadata={ 'metal':'Silver', 'gemstone':'Emerald', } ) product.save() # - Copper-and-sapphire necklace: product = Product( 'Necklace #3', 'Showing some Product.get aspects', True, True, metadata={ 'metal':'Copper', 'gemstone':'Sapphire', } ) product.save() # - Silver-and-sapphire necklace: product = Product( 'Necklace #4', 'Showing some Product.get aspects', True, True, metadata={ 'metal':'Silver', 'gemstone':'Sapphire', } ) product.save()
Finding products that have metadata indicating that they are made of silver and have sapphire gemstones is fairly straightforward, although it requires criteria specifications that look a little odd:
# - importing json so we can usefully print the results: import json criteria = { 'metadata':{ 'metal':'Silver', 'gemstone':'Sapphire', } }
Creating the criteria as a dict allows them to be passed to Product.get as a single keyword argument set, and allows the criteria specification to be as detailed as we need. We could, for example, add other metadata, specify a product name, or add any other object properties that appear in the data-dict representation of a Product (as returned by to_data_dict). The results will come back as a list of objects, and, by printing the data-dict representations of them, we can see the results:
products = Product.get(**criteria) print(json.dumps( [product.to_data_dict() for product in products], indent=4, sort_keys=True) )
Executing the preceding code yields the dataset for the one matching the Product, our silver and sapphire necklace, as follows:
It's worth mentioning that passing criteria doesn't have to be a multi-level dict, even for metadata values. Using criteria in this format is as follows:
criteria = { 'metadata.metal':'Silver', 'metadata.gemstone':'Sapphire', }
This criteria structure works just as well. The underlying find() method provided by a pymongo connection object treats dot-notation specifications of this sort as references to a nested object structure that looks much like the dict value shown previously, and will process the request accordingly.