def __init__(self, _redis_host='localhost', _redis_port=6379): self.event_store = EventStoreClient() self.consumers = Consumers('read-model', [self.get_entity, self.get_entities, self.get_mails, self.get_unbilled_orders, self.get_unshipped_orders, self.get_delivered_orders]) self.domain_model = DomainModel( redis.StrictRedis(host=_redis_host, port=_redis_port, decode_responses=True) ) self.subscriptions = {} self.locks = {}
class MailService(object): """ Mail Service class. """ def __init__(self): self.consumers = Consumers('mail-service', [self.send]) self.event_store = EventStoreClient() def start(self): logging.info('starting ...') self.consumers.start() self.consumers.wait() def stop(self): self.consumers.stop() logging.info('stopped.') def send(self, _req): if not _req['to'] or not _req['msg']: return {"error": "missing mandatory parameter 'to' and/or 'msg'"} # trigger event self.event_store.publish( 'mail', create_event('mail_sent', { "recipient": _req['to'], "message": _req['msg'] }))
class CustomerService(object): """ Customer Service class. """ def __init__(self): self.event_store = EventStoreClient() self.consumers = Consumers('customer-service', [ self.create_customers, self.update_customer, self.delete_customer ]) @staticmethod def _create_entity(_name, _email): """ Create a customer entity. :param _name: The name of the customer. :param _email: The em2ail address of the customer. :return: A dict with the entity properties. """ return {'entity_id': str(uuid.uuid4()), 'name': _name, 'email': _email} def start(self): logging.info('starting ...') self.consumers.start() self.consumers.wait() def stop(self): self.consumers.stop() logging.info('stopped.') def create_customers(self, _req): customers = _req if isinstance(_req, list) else [_req] customer_ids = [] for customer in customers: try: new_customer = CustomerService._create_entity( customer['name'], customer['email']) except KeyError: return { "error": "missing mandatory parameter 'name' and/or 'email'" } # trigger event self.event_store.publish( 'customer', create_event('entity_created', new_customer)) customer_ids.append(new_customer['entity_id']) return {"result": customer_ids} def update_customer(self, _req): try: customer = CustomerService._create_entity(_req['name'], _req['email']) except KeyError: return { "error": "missing mandatory parameter 'name' and/or 'email'" } try: customer['entity_id'] = _req['entity_id'] except KeyError: return {"error": "missing mandatory parameter 'entity_id'"} # trigger event self.event_store.publish('customer', create_event('entity_updated', customer)) return {"result": True} def delete_customer(self, _req): try: customer_id = _req['entity_id'] except KeyError: return {"error": "missing mandatory parameter 'entity_id'"} rsp = send_message('read-model', 'get_entity', { 'name': 'customer', 'id': customer_id }) if 'error' in rsp: rsp['error'] += ' (from read-model)' return rsp customer = rsp['result'] if not customer: return {"error": "could not find customer"} # trigger event self.event_store.publish('customer', create_event('entity_deleted', customer)) return {"result": True}
def __init__(self): self.event_store = EventStoreClient() self.consumers = Consumers('customer-service', [ self.create_customers, self.update_customer, self.delete_customer ])
def __init__(self): self.consumers = Consumers('mail-service', [self.send]) self.event_store = EventStoreClient()
class ReadModel(object): """ Read Model class. """ def __init__(self, _redis_host='localhost', _redis_port=6379): self.event_store = EventStoreClient() self.consumers = Consumers('read-model', [self.get_entity, self.get_entities, self.get_mails, self.get_unbilled_orders, self.get_unshipped_orders, self.get_delivered_orders]) self.domain_model = DomainModel( redis.StrictRedis(host=_redis_host, port=_redis_port, decode_responses=True) ) self.subscriptions = {} self.locks = {} @staticmethod def _deduce_entities(_events): """ Deduce entities from events. :param _events: A list with events. :return: A dict mapping entity ID -> entity data. """ if not _events: return {} # find 'created' events result = {json.loads(e[1]['event_data'])['entity_id']: json.loads(e[1]['event_data']) for e in filter(lambda x: x[1]['event_action'] == 'entity_created', _events)} # remove 'deleted' events deleted = {json.loads(e[1]['event_data'])['entity_id']: json.loads(e[1]['event_data']) for e in filter(lambda x: x[1]['event_action'] == 'entity_deleted', _events)} for d_id, d_data in deleted.items(): del result[d_id] # change 'updated' events updated = {json.loads(e[1]['event_data'])['entity_id']: json.loads(e[1]['event_data']) for e in filter(lambda x: x[1]['event_action'] == 'entity_updated', _events)} for u_id, u_data in updated.items(): result[u_id] = u_data return result def _track_entities(self, _name, _event): """ Keep track of entity events. :param _name: The entity name. :param _event: The event data. """ if not self.domain_model.exists(_name): return entity = json.loads(_event.event_data) if _event.event_action == 'entity_created': self.domain_model.create(_name, entity) if _event.event_action == 'entity_deleted': self.domain_model.delete(_name, entity) if _event.event_action == 'entity_updated': self.domain_model.update(_name, entity) def _query_entities(self, _name): """ Query all entities of a given name. :param _name: The entity name. :return: A dict mapping entity ID -> entity. """ entities = self.domain_model.retrieve(_name) if entities: return entities if _name not in self.locks: self.locks[_name] = threading.Lock() with self.locks[_name]: entities = self.domain_model.retrieve(_name) if entities: return entities # deduce entities events = self.event_store.get(_name) entities = self._deduce_entities(events) # cache entities for entity in entities.values(): self.domain_model.create(_name, entity) # track entities tracking_handler = functools.partial(self._track_entities, _name) self.event_store.subscribe(_name, tracking_handler) self.subscriptions[_name] = tracking_handler return entities def _query_defined_entities(self, _name, _props): """ Query entities with defined properities. :param _name: The entity name. :param _props: A dict mapping property name -> property value(s). :return: A dict mapping entity ID -> entity. """ result = {} for entity_id, entity in self._query_entities(_name).items(): for prop_name, prop_value in _props.items(): if not isinstance(prop_value, list): prop_value = [prop_value] if prop_name in entity and entity[prop_name] in prop_value: result[entity_id] = entity return result def _unbilled_orders(self): """ Query all unbilled orders, i.e. orders w/o corresponding billing. :return: a dict mapping entity ID -> entity. """ orders = self._query_entities('order') billings = self._query_entities('billing') unbilled = orders.copy() for billing_id, billing in billings.items(): order_ids_to_remove = list(filter(lambda x: x == billing['order_id'], orders)) if not order_ids_to_remove: raise Exception(f'could not find order {billing["order_id"]} for billing {billing_id}') if order_ids_to_remove[0] not in unbilled: raise Exception(f'could not find order {order_ids_to_remove[0]}') del unbilled[order_ids_to_remove[0]] return unbilled def _unshipped_orders(self): """ Query all unshipped orders, i.e. orders w/o corresponding shipping done. :return: a dict mapping entity ID -> entity. """ orders = self._query_entities('order') shippings = self._query_entities('shipping') unshipped = orders.copy() for shipping_id, shippings in shippings.items(): order_ids_to_remove = list(filter(lambda x: x == shippings['order_id'], orders)) if not order_ids_to_remove: raise Exception(f'could not find order {shippings["order_id"]} for shipping {shipping_id}') if order_ids_to_remove[0] not in unshipped: raise Exception(f'could not find order {order_ids_to_remove[0]}') del unshipped[order_ids_to_remove[0]] return unshipped def _delivered_orders(self): """ Query all delivered orders. :return: a dict mapping entity ID -> entity. """ shippings = self._query_entities('shipping') return list(filter(lambda x: x['delivered'], shippings.values())) def start(self): logging.info('starting ...') self.consumers.start() self.consumers.wait() def stop(self): for name, handler in self.subscriptions.items(): self.event_store.unsubscribe(name, handler) self.consumers.stop() logging.info('stopped.') def get_entity(self, _req): if 'name' not in _req: return { "error": "missing mandatory parameter 'name'" } if 'id' in _req: return { 'result': self._query_entities(_req['name']).get(_req['id']) } elif 'props' in _req and isinstance(_req['props'], dict): result = list(self._query_defined_entities(_req['name'], _req['props']).values()) if len(result) <= 1: return { 'result': result[0] if result else None } else: return { 'error': 'more than 1 result found' } else: return { 'result': 'invalid parameters' } def get_entities(self, _req): if 'name' not in _req: return { "error": "missing mandatory parameter 'name'" } elif 'ids' in _req and isinstance(_req['ids'], list): return { 'result': [self._query_entities(_req['name']).get(_id) for _id in _req['ids']] } elif 'props' in _req and isinstance(_req['props'], dict): return { 'result': list(self._query_defined_entities(_req['name'], _req['props']).values()) } else: return { 'result': list(self._query_entities(_req['name']).values()) } def get_mails(self, _req): return { 'result': self.event_store.get('mail') or [] } def get_unbilled_orders(self, _req): return { 'result': self._unbilled_orders() } def get_unshipped_orders(self, _req): return { 'result': self._unshipped_orders() } def get_delivered_orders(self, _req): return { 'result': self._delivered_orders() }
def __init__(self): self.event_store = EventStoreClient() self.consumers = Consumers('billing-service', [self.create_billings, self.update_billing, self.delete_billing])
class BillingService(object): """ Billing Service class. """ def __init__(self): self.event_store = EventStoreClient() self.consumers = Consumers('billing-service', [self.create_billings, self.update_billing, self.delete_billing]) @staticmethod def _create_entity(_order_id, _amount): """ Create a billing entity. :param _order_id: The order ID the billing belongs to. :param _amount: Total amount to pay. :return: A dict with the entity properties. """ return { 'entity_id': str(uuid.uuid4()), 'order_id': _order_id, 'amount': _amount } @staticmethod def _check_amount(_billing): rsp = send_message('read-model', 'get_entity', {'name': 'order', 'id': _billing['order_id']}) order = rsp['result'] rsp = send_message('read-model', 'get_entity', {'name': 'cart', 'id': order['cart_id']}) cart = rsp['result'] rsp = send_message('read-model', 'get_entities', {'name': 'product', 'ids': cart['product_ids']}) products = rsp['result'] amount = sum([int(product['price']) for product in products]) return amount == int(_billing['amount']) def start(self): logging.info('starting ...') self.consumers.start() self.consumers.wait() def stop(self): self.consumers.stop() logging.info('stopped.') def create_billings(self, _req): billings = _req if isinstance(_req, list) else [_req] billing_ids = [] for billing in billings: res = self._check_amount(billing) if not res: return { 'error': 'amount is not accurate' } try: new_billing = BillingService._create_entity(billing['order_id'], billing['amount']) except KeyError: return { "error": "missing mandatory parameter 'order_id' and/or 'amount'" } # trigger event self.event_store.publish('billing', create_event('entity_created', new_billing)) billing_ids.append(new_billing['entity_id']) return { "result": billing_ids } def update_billing(self, _req): try: billing_id = _req['entity_id'] except KeyError: return { "error": "missing mandatory parameter 'entity_id'" } rsp = send_message('read-model', 'get_entitiy', {'name': 'billing', 'id': billing_id}) if 'error' in rsp: rsp['error'] += ' (from read-model)' return rsp billing = rsp['result'] if not billing: return { "error": "could not find billing" } # set new props billing['entity_id'] = billing_id try: billing['order_id'] = _req['order_id'] billing['amount'] = _req['amount'] except KeyError: return { "result": "missing mandatory parameter 'order_id' and/or 'amount" } res = self._check_amount(billing) if not res: return { 'error': 'amount is not accurate' } # trigger event self.event_store.publish('billing', create_event('entity_updated', billing)) return { "result": True } def delete_billing(self, _req): try: billing_id = _req['entity_id'] except KeyError: return { "error": "missing mandatory parameter 'entity_id'" } rsp = send_message('read-model', 'get_entitiy', {'name': 'billing', 'id': billing_id}) if 'error' in rsp: rsp['error'] += ' (from read-model)' return rsp billing = rsp['result'] if not billing: return { "error": "could not find billing" } # trigger event self.event_store.publish('billing', create_event('entity_deleted', billing)) return { "result": True }
def __init__(self): self.event_store = EventStoreClient() self.consumers = Consumers( 'product-service', [self.create_products, self.update_product, self.delete_product])
class ProductService(object): """ Product Service class. """ def __init__(self): self.event_store = EventStoreClient() self.consumers = Consumers( 'product-service', [self.create_products, self.update_product, self.delete_product]) @staticmethod def _create_entity(_name, _price): """ Create a product entity. :param _name: The name of the product. :param _price: The price of the product. :return: A dict with the entity properties. """ return {'entity_id': str(uuid.uuid4()), 'name': _name, 'price': _price} def start(self): logging.info('starting ...') self.consumers.start() self.consumers.wait() def stop(self): self.consumers.stop() logging.info('stopped.') def create_products(self, _req): products = _req if isinstance(_req, list) else [_req] product_ids = [] for product in products: try: new_product = ProductService._create_entity( product['name'], product['price']) except KeyError: return { "error": "missing mandatory parameter 'name' and/or 'price'" } # trigger event self.event_store.publish( 'product', create_event('entity_created', new_product)) product_ids.append(new_product['entity_id']) return {"result": product_ids} def update_product(self, _req): try: product_id = _req['entity_id'] except KeyError: return {"error": "missing mandatory parameter 'entity_id'"} rsp = send_message('read-model', 'get_entity', { 'name': 'product', 'id': product_id }) if 'error' in rsp: rsp['error'] += ' (from read-model)' return rsp product = rsp['result'] if not product: return {"error": "could not find product"} # set new props product['entity_id'] = product_id try: product['name'] = _req['name'] product['price'] = _req['price'] except KeyError: return { "result": "missing mandatory parameter 'name' and/or 'price" } # trigger event self.event_store.publish('product', create_event('entity_updated', product)) return {"result": True} def delete_product(self, _req): try: product_id = _req['entity_id'] except KeyError: return {"error": "missing mandatory parameter 'entity_id'"} rsp = send_message('read-model', 'get_entity', { 'name': 'product', 'id': product_id }) if 'error' in rsp: rsp['error'] += ' (from read-model)' return rsp product = rsp['result'] if not product: return {"error": "could not find product"} # trigger event self.event_store.publish('product', create_event('entity_deleted', product)) return {"result": True}
class OrderService(object): """ Order Service class. """ def __init__(self): self.event_store = EventStoreClient() self.consumers = Consumers('order-service', [self.create_orders, self.update_order, self.delete_order]) @staticmethod def _create_entity(_cart_id, _status='CREATED'): """ Create an order entity. :param _cart_id: The cart ID the order is for. :param _status: The current status of the order, defaults to CREATED. Other options are OUT_OF_STOCK, IN_STOCK, CLEARED, UNCLEARED, SHIPPED and DELIVERED. :return: A dict with the entity properties. """ return { 'entity_id': str(uuid.uuid4()), 'cart_id': _cart_id, 'status': _status, } def start(self): logging.info('starting ...') self.event_store.subscribe('billing', self.billing_created) self.event_store.subscribe('billing', self.billing_deleted) self.event_store.subscribe('shipping', self.shipping_created) self.event_store.subscribe('shipping', self.shipping_updated) self.consumers.start() self.consumers.wait() def stop(self): self.event_store.unsubscribe('billing', self.billing_created) self.event_store.unsubscribe('billing', self.billing_deleted) self.event_store.unsubscribe('shipping', self.shipping_created) self.event_store.unsubscribe('shipping', self.shipping_updated) self.consumers.stop() logging.info('stopped.') def create_orders(self, _req): orders = _req if isinstance(_req, list) else [_req] order_ids = [] for order in orders: try: new_order = OrderService._create_entity(order['cart_id']) except KeyError: return { "error": "missing mandatory parameter 'cart_id'" } # trigger event self.event_store.publish('order', create_event('entity_created', new_order)) order_ids.append(new_order['entity_id']) return { "result": order_ids } def update_order(self, _req): try: order_id = _req['entity_id'] except KeyError: return { "error": "missing mandatory parameter 'entity_id'" } rsp = send_message('read-model', 'get_entity', {'name': 'order', 'id': order_id}) if 'error' in rsp: rsp['error'] += ' (from read-model)' return rsp order = rsp['result'] if not order: return { "error": "could not find order" } # set new props order['entity_id'] = order_id try: order['cart_id'] = _req['cart_id'] order['status'] = _req['status'] except KeyError: return { "result": "missing mandatory parameter 'cart_id' and/or 'status" } # trigger event self.event_store.publish('order', create_event('entity_updated', order)) return { "result": True } def delete_order(self, _req): try: order_id = _req['entity_id'] except KeyError: return { "error": "missing mandatory parameter 'entity_id'" } rsp = send_message('read-model', 'get_entity', {'name': 'order', 'id': order_id}) if 'error' in rsp: rsp['error'] += ' (from read-model)' return rsp order = rsp['result'] if not order: return { "error": "could not find order" } # trigger event self.event_store.publish('order', create_event('entity_deleted', order)) return { "result": True } def billing_created(self, _item): if _item.event_action != 'entity_created': return billing = json.loads(_item.event_data) rsp = send_message('read-model', 'get_entity', {'name': 'order', 'id': billing['order_id']}) order = rsp['result'] if not order['status'] == 'IN_STOCK': return order['status'] = 'CLEARED' self.event_store.publish('order', create_event('entity_updated', order)) def billing_deleted(self, _item): if _item.event_action != 'entity_delted': return billing = json.loads(_item.event_data) rsp = send_message('read-model', 'get_entity', {'name': 'order', 'id': billing['order_id']}) order = rsp['result'] if not order['status'] == 'CLEARED': return order['status'] = 'UNCLEARED' self.event_store.publish('order', create_event('entity_updated', order)) def shipping_created(self, _item): if _item.event_action != 'entity_created': return shipping = json.loads(_item.event_data) rsp = send_message('read-model', 'get_entity', {'name': 'order', 'id': shipping['order_id']}) order = rsp['result'] if not order['status'] == 'CLEARED': return order['status'] = 'SHIPPED' self.event_store.publish('order', create_event('entity_updated', order)) def shipping_updated(self, _item): if _item.event_action != 'entity_updated': return shipping = json.loads(_item.event_data) if not shipping['delivered']: return rsp = send_message('read-model', 'get_entity', {'name': 'order', 'id': shipping['order_id']}) order = rsp['result'] order['status'] = 'DELIVERED' self.event_store.publish('order', create_event('entity_updated', order))
class ShippingService(object): """ Shipping Service class. """ def __init__(self): self.event_store = EventStoreClient() self.consumers = Consumers('shipping-service', [ self.create_shippings, self.update_shipping, self.delete_shipping ]) @staticmethod def _create_entity(_order_id, _delivered=0): """ Create a shipping entity. :param _order_id: The order which is shipped. :param _delivered: Optional TS indicating delivery, defaults to 0 (not delivered). :return: A dict with the entity properties. """ return { 'entity_id': str(uuid.uuid4()), 'order_id': _order_id, 'delivered': _delivered } def start(self): logging.info('starting ...') self.event_store.subscribe('billing', self.billing_created) self.consumers.start() self.consumers.wait() def stop(self): self.event_store.unsubscribe('billing', self.billing_created) self.consumers.stop() logging.info('stopped.') def create_shippings(self, _req): shippings = _req if isinstance(_req, list) else [_req] shipping_ids = [] for shipping in shippings: try: new_shipping = ShippingService._create_entity( shipping['order_id']) except KeyError: return { "error": "missing mandatory parameter 'order_id' and/or 'delivered'" } # trigger event self.event_store.publish( 'shipping', create_event('entity_created', new_shipping)) shipping_ids.append(new_shipping['entity_id']) return {"result": shipping_ids} def update_shipping(self, _req): try: shipping_id = _req['entity_id'] except KeyError: return {"error": "missing mandatory parameter 'entity_id'"} rsp = send_message('read-model', 'get_entity', { 'name': 'shipping', 'id': shipping_id }) if 'error' in rsp: rsp['error'] += ' (from read-model)' return rsp shipping = rsp['result'] if not shipping: return {"error": "could not find shipping"} # set new props shipping['entity_id'] = shipping_id try: shipping['order_id'] = _req['order_id'] shipping['delivered'] = _req['delivered'] except KeyError: return { "result": "missing mandatory parameter 'order_id' and/or 'delivered" } # trigger event self.event_store.publish('shipping', create_event('entity_updated', shipping)) return {"result": True} def delete_shipping(self, _req): try: shipping_id = _req['entity_id'] except KeyError: return {"error": "missing mandatory parameter 'entity_id'"} rsp = send_message('read-model', 'get_entity', { 'name': 'shipping', 'id': shipping_id }) if 'error' in rsp: rsp['error'] += ' (from read-model)' return rsp shipping = rsp['result'] if not shipping: return {"error": "could not find shipping"} # trigger event self.event_store.publish('shipping', create_event('entity_deleted', shipping)) return {"result": True} def billing_created(self, _item): if _item.event_action != 'entity_created': return billing = json.loads(_item.event_data) shipping = ShippingService._create_entity(billing['order_id']) self.event_store.publish('shipping', create_event('entity_created', shipping))
class InventoryService(object): """ Inventory Service class. """ def __init__(self): self.event_store = EventStoreClient() self.consumers = Consumers('inventory-service', [ self.create_inventories, self.update_inventory, self.delete_inventory ]) @staticmethod def _create_entity(_product_id, _amount): """ Create an inventory entity. :param _product_id: The product ID the inventory is for. :param _amount: The amount of products in the inventory. :return: A dict with the entity properties. """ return { 'entity_id': str(uuid.uuid4()), 'product_id': _product_id, 'amount': _amount } def _incr_inventory(self, _product_id, _value=1): rsp = send_message('read-model', 'get_entity', { 'name': 'inventory', 'props': { 'product_id': _product_id } }) if 'error' in rsp: raise Exception(rsp['error'] + ' (from read-model)') inventory = rsp['result'] if not inventory: logging.error( "could not find inventory for product {}".format(_product_id)) return False inventory['amount'] = int( inventory['amount']) - (_value if _value else 1) # trigger event self.event_store.publish('inventory', create_event('entity_updated', inventory)) return True def _decr_inventory(self, _product_id, _value=1): rsp = send_message('read-model', 'get_entity', { 'name': 'inventory', 'props': { 'product_id': _product_id } }) if 'error' in rsp: raise Exception(rsp['error'] + ' (from read-model)') inventory = rsp['result'] if not inventory: logging.warning( "could not find inventory for product {}".format(_product_id)) return False if int(inventory['amount']) - (_value if _value else 1) < 0: logging.info("product {} is out of stock".format(_product_id)) return False inventory['amount'] = int( inventory['amount']) - (_value if _value else 1) # trigger event self.event_store.publish('inventory', create_event('entity_updated', inventory)) return True def _decr_from_cart(self, _cart): try: product_ids = _cart['product_ids'] except KeyError: raise Exception("missing mandatory parameter 'product_ids'") rsp = send_message('read-model', 'get_entities', { 'name': 'inventory', 'props': { 'product_id': product_ids } }) if 'error' in rsp: raise Exception(rsp['error'] + ' (from read-model)') inventories = rsp['result'] # count products product_counts = [] for inventory in inventories: found = product_ids.count(inventory['product_id']) # check amount if found > int(inventory['amount']): logging.info("product {} is out of stock".format( inventory['product_id'])) return False product_counts.append((inventory, found)) # decrement inventory for inventory, count in product_counts: inventory['amount'] = int(inventory['amount']) - count # trigger event self.event_store.publish('inventory', create_event('entity_updated', inventory)) return True def start(self): logging.info('starting ...') self.event_store.subscribe('order', self.order_created) self.event_store.subscribe('order', self.order_deleted) self.consumers.start() self.consumers.wait() def stop(self): self.event_store.unsubscribe('order', self.order_created) self.event_store.unsubscribe('order', self.order_deleted) self.consumers.stop() logging.info('stopped.') def create_inventories(self, _req): inventory = _req if isinstance(_req, list) else [_req] inventory_ids = [] for inventory in inventory: try: new_inventory = InventoryService._create_entity( inventory['product_id'], inventory['amount']) except KeyError: return { "error": "missing mandatory parameter 'product_id' and/or 'amount'" } # trigger event self.event_store.publish( 'inventory', create_event('entity_created', new_inventory)) inventory_ids.append(new_inventory['entity_id']) return {"result": inventory_ids} def update_inventory(self, _req): try: inventory_id = _req['entity_id'] except KeyError: return {"error": "missing mandatory parameter 'entity_id'"} rsp = send_message('read-model', 'get_entity', { 'name': 'inventory', 'id': inventory_id }) if 'error' in rsp: rsp['error'] += ' (from read-model)' return rsp inventory = rsp['result'] if not inventory: return {"error": "could not find inventory"} # set new props inventory['entity_id'] = inventory_id try: inventory['product_id'] = _req['product_id'] inventory['amount'] = _req['amount'] except KeyError: return { "result": "missing mandatory parameter 'product_id' and/or 'amount" } # trigger event self.event_store.publish('inventory', create_event('entity_updated', inventory)) return {"result": True} def delete_inventory(self, _req): try: inventory_id = _req['entity_id'] except KeyError: return {"error": "missing mandatory parameter 'entity_id'"} rsp = send_message('read-model', 'get_entity', { 'name': 'inventory', 'id': inventory_id }) if 'error' in rsp: rsp['error'] += ' (from read-model)' return rsp inventory = rsp['result'] if not inventory: return {"error": "could not find inventory"} # trigger event self.event_store.publish('inventory', create_event('entity_deleted', inventory)) return {"result": True} def order_created(self, _item): if _item.event_action != 'entity_created': return order = json.loads(_item.event_data) rsp = send_message('read-model', 'get_entity', { 'name': 'cart', 'id': order['cart_id'] }) cart = rsp['result'] result = self._decr_from_cart(cart) order['status'] = 'IN_STOCK' if result else 'OUT_OF_STOCK' self.event_store.publish('order', create_event('entity_updated', order)) def order_deleted(self, _item): if _item.event_action != 'entity_deleted': return order = json.loads(_item.event_data) if order['status'] != 'IN_STOCK': return rsp = send_message('read-model', 'get_entity', { 'name': 'cart', 'id': order['cart_id'] }) cart = rsp['result'] [ self._incr_inventory(product_id) for product_id in cart['product_ids'] ]
def __init__(self): self.event_store = EventStoreClient() self.consumers = Consumers('inventory-service', [ self.create_inventories, self.update_inventory, self.delete_inventory ])
class CartService(object): """ Cart Service class. """ def __init__(self): self.event_store = EventStoreClient() self.consumers = Consumers( 'cart-service', [self.create_carts, self.update_cart, self.delete_cart]) @staticmethod def _create_entity(_customer_id, _product_ids): """ Create a cart entity. :param _customer_id: The customer ID. :param _product_ids: The product IDs. :return: A dict with the entity properties. """ return { 'entity_id': str(uuid.uuid4()), 'customer_id': _customer_id, 'product_ids': _product_ids } @staticmethod def _check_inventory(_product_ids): product_counts = collections.Counter(_product_ids) for product_id, amount in product_counts.items(): rsp = send_message('read-model', 'get_entity', { 'name': 'inventory', 'props': { 'product_id': product_id } }) if 'error' in rsp: rsp['error'] += ' (from read-model)' raise Exception(rsp['error']) inventory = rsp['result'] if not inventory or int(inventory['amount']) - amount < 0: return False, product_id return True, None def start(self): logging.info('starting ...') self.consumers.start() self.consumers.wait() def stop(self): self.consumers.stop() logging.info('stopped.') def create_carts(self, _req): carts = _req if isinstance(_req, list) else [_req] cart_ids = [] for cart in carts: res, product_id = self._check_inventory(cart['product_ids']) if not res: return { 'error': 'product {} is out of stock'.format(product_id) } try: new_cart = CartService._create_entity(cart['customer_id'], cart['product_ids']) except KeyError: return { "error": "missing mandatory parameter 'customer_id' and/or 'product_ids'" } # trigger event self.event_store.publish('cart', create_event('entity_created', new_cart)) cart_ids.append(new_cart['entity_id']) return {"result": cart_ids} def update_cart(self, _req): try: cart_id = _req['entity_id'] except KeyError: return {"error": "missing mandatory parameter 'entity_id'"} rsp = send_message('read-model', 'get_entity', { 'name': 'order', 'props': { 'cart_id': cart_id } }) if 'error' in rsp: rsp['error'] += ' (from read-model)' return rsp order = rsp['result'] if order and not order['status'] == 'CREATED': return {"error": "order {} in progress".format(order['entity_id'])} rsp = send_message('read-model', 'get_entity', { 'name': 'cart', 'id': cart_id }) if 'error' in rsp: rsp['error'] += ' (from read-model)' return rsp cart = rsp['result'] if not cart: return {"error": "could not find cart {}".format(cart_id)} # set new props cart['entity_id'] = cart_id try: cart['customer_id'] = _req['customer_id'] cart['product_ids'] = _req['product_ids'] except KeyError: return { "result": "missing mandatory parameter 'customer_id' and/or 'product_ids" } res, product_id = self._check_inventory(cart['product_ids']) if not res: return {'error': 'product {} is out of stock'.format(product_id)} # trigger event self.event_store.publish('cart', create_event('entity_updated', cart)) return {"result": True} def delete_cart(self, _req): try: cart_id = _req['entity_id'] except KeyError: return {"error": "missing mandatory parameter 'entity_id'"} rsp = send_message('read-model', 'get_entity', { 'name': 'cart', 'id': cart_id }) if 'error' in rsp: rsp['error'] += ' (from read-model)' return rsp cart = rsp['result'] if not cart: return {"error": "could not find cart"} # trigger event self.event_store.publish('cart', create_event('entity_deleted', cart)) return {"result": True}