class Path(db.Model): id = db.Column(db.UUID(as_uuid=True), primary_key=True, server_default=db.text('gen_random_uuid()')) lot_id = db.Column(db.UUID(as_uuid=True), db.ForeignKey(Lot.id), nullable=False) lot = db.relationship(Lot, backref=db.backref('paths', lazy=True, collection_class=set, cascade=CASCADE_OWN), primaryjoin=Lot.id == lot_id) path = db.Column(LtreeType, nullable=False) created = db.Column(db.TIMESTAMP(timezone=True), server_default=db.text('CURRENT_TIMESTAMP')) created.comment = """ When Devicehub created this. """ __table_args__ = ( # dag.delete_edge needs to disable internally/temporarily the unique constraint db.UniqueConstraint(path, name='path_unique', deferrable=True, initially='immediate'), db.Index('path_gist', path, postgresql_using='gist'), db.Index('path_btree', path, postgresql_using='btree'), db.Index('lot_id_index', lot_id, postgresql_using='hash')) def __init__(self, lot: Lot) -> None: super().__init__(lot=lot) self.path = UUIDLtree(lot.id) @classmethod def add(cls, parent_id: uuid.UUID, child_id: uuid.UUID): """Creates an edge between parent and child.""" db.session.execute(db.func.add_edge(str(parent_id), str(child_id))) @classmethod def delete(cls, parent_id: uuid.UUID, child_id: uuid.UUID): """Deletes the edge between parent and child.""" db.session.execute(db.func.delete_edge(str(parent_id), str(child_id))) @classmethod def has_lot(cls, parent_id: uuid.UUID, child_id: uuid.UUID) -> bool: parent_id = UUIDLtree.convert(parent_id) child_id = UUIDLtree.convert(child_id) return bool( db.session.execute( "SELECT 1 from path where path ~ '*.{}.*.{}.*'".format( parent_id, child_id)).first())
class User(Thing): __table_args__ = {'schema': 'common'} id = Column(UUID(as_uuid=True), default=uuid4, primary_key=True) email = Column(EmailType, nullable=False, unique=True) password = Column( PasswordType(max_length=STR_SIZE, onload=lambda **kwargs: dict( schemes=app.config['PASSWORD_SCHEMES'], **kwargs))) token = Column(UUID(as_uuid=True), default=uuid4, unique=True, nullable=False) inventories = db.relationship(Inventory, backref=db.backref('users', lazy=True, collection_class=set), secondary=lambda: UserInventory.__table__, collection_class=set) # todo set restriction that user has, at least, one active db def __init__(self, email, password=None, inventories=None) -> None: """ Creates an user. :param email: :param password: :param inventories: A set of Inventory where the user has access to. If none, the user is granted access to the current inventory. """ inventories = inventories or {Inventory.current} super().__init__(email=email, password=password, inventories=inventories) def __repr__(self) -> str: return '<User {0.email}>'.format(self) @property def type(self) -> str: return self.__class__.__name__ @property def individual(self): """The individual associated for this database, or None.""" return next(iter(self.individuals), None)
class Session(Thing): __table_args__ = {'schema': 'common'} id = Column(BigInteger, Sequence('device_seq'), primary_key=True) expired = Column(BigInteger, default=0) token = Column(UUID(as_uuid=True), default=uuid4, unique=True, nullable=False) type = Column(IntEnum(SessionType), default=SessionType.Internal, nullable=False) user_id = db.Column(db.UUID(as_uuid=True), db.ForeignKey(User.id)) user = db.relationship(User, backref=db.backref('sessions', lazy=True, collection_class=set), collection_class=set) def __str__(self) -> str: return '{0.token}'.format(self)
class Lot(Thing): id = db.Column(UUID(as_uuid=True), primary_key=True) # uuid is generated on init by default name = db.Column(CIText(), nullable=False) description = db.Column(CIText()) description.comment = """A comment about the lot.""" closed = db.Column(db.Boolean, default=False, nullable=False) closed.comment = """ A closed lot cannot be modified anymore. """ devices = db.relationship(Device, backref=db.backref('lots', lazy=True, collection_class=set), secondary=lambda: LotDevice.__table__, lazy=True, collection_class=set) """ The **children** devices that the lot has. Note that the lot can have more devices, if they are inside descendant lots. """ parents = db.relationship( lambda: Lot, viewonly=True, lazy=True, collection_class=set, secondary=lambda: LotParent.__table__, primaryjoin=lambda: Lot.id == LotParent.child_id, secondaryjoin=lambda: LotParent.parent_id == Lot.id, cascade='refresh-expire', # propagate changes outside ORM backref=db.backref('children', viewonly=True, lazy=True, cascade='refresh-expire', collection_class=set)) """The parent lots.""" all_devices = db.relationship( Device, viewonly=True, lazy=True, collection_class=set, secondary=lambda: LotDeviceDescendants.__table__, primaryjoin=lambda: Lot.id == LotDeviceDescendants.ancestor_lot_id, secondaryjoin=lambda: LotDeviceDescendants.device_id == Device.id) """All devices, including components, inside this lot and its descendants. """ def __init__(self, name: str, closed: bool = closed.default.arg, description: str = None) -> None: """ Initializes a lot :param name: :param closed: """ super().__init__(id=uuid.uuid4(), name=name, closed=closed, description=description) Path(self) # Lots have always one edge per default. @property def type(self) -> str: return self.__class__.__name__ @property def url(self) -> urlutils.URL: """The URL where to GET this event.""" return urlutils.URL(url_for_resource(Lot, item_id=self.id)) @property def descendants(self): return self.descendantsq(self.id) @classmethod def descendantsq(cls, id): _id = UUIDLtree.convert(id) return (cls.id == Path.lot_id) & Path.path.lquery( exp.cast('*.{}.*'.format(_id), LQUERY)) @classmethod def roots(cls): """Gets the lots that are not under any other lot.""" return cls.query.join(cls.paths).filter(db.func.nlevel(Path.path) == 1) def add_children(self, *children): """Add children lots to this lot. This operation is highly costly as it forces refreshing many models in session. """ for child in children: if isinstance(child, Lot): Path.add(self.id, child.id) db.session.refresh(child) else: assert isinstance(child, uuid.UUID) Path.add(self.id, child) # We need to refresh the models involved in this operation # outside the session / ORM control so the models # that have relationships to this model # with the cascade 'refresh-expire' can welcome the changes db.session.refresh(self) def remove_children(self, *children): """Remove children lots from this lot. This operation is highly costly as it forces refreshing many models in session. """ for child in children: if isinstance(child, Lot): Path.delete(self.id, child.id) db.session.refresh(child) else: assert isinstance(child, uuid.UUID) Path.delete(self.id, child) db.session.refresh(self) def delete(self): """Deletes the lot. This method removes the children lots and children devices orphan from this lot and then marks this lot for deletion. """ self.remove_children(*self.children) db.session.delete(self) def _refresh_models_with_relationships_to_lots(self): session = db.Session.object_session(self) for model in session: if isinstance(model, (Device, Lot, Path)): session.expire(model) def __contains__(self, child: Union['Lot', Device]): if isinstance(child, Lot): return Path.has_lot(self.id, child.id) elif isinstance(child, Device): device = db.session.query(LotDeviceDescendants) \ .filter(LotDeviceDescendants.device_id == child.id) \ .filter(LotDeviceDescendants.ancestor_lot_id == self.id) \ .one_or_none() return device else: raise TypeError( 'Lot only contains devices and lots, not {}'.format( child.__class__)) def __repr__(self) -> str: return '<Lot {0.name} devices={0.devices!r}>'.format(self)
class Deliverynote(Thing): id = db.Column(UUID(as_uuid=True), primary_key=True) # uuid is generated on init by default document_id = db.Column(CIText(), nullable=False) creator_id = db.Column(UUID(as_uuid=True), db.ForeignKey(User.id), nullable=False, default=lambda: g.user.id) creator = db.relationship(User, primaryjoin=creator_id == User.id) supplier_email = db.Column(CIText(), db.ForeignKey(User.email), nullable=False, default=lambda: g.user.email) supplier = db.relationship( User, primaryjoin=lambda: Deliverynote.supplier_email == User.email) receiver_address = db.Column(CIText(), db.ForeignKey(User.email), nullable=False, default=lambda: g.user.email) receiver = db.relationship( User, primaryjoin=lambda: Deliverynote.receiver_address == User.email) date = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) date.comment = 'The date the DeliveryNote initiated' amount = db.Column(db.Integer, check_range('amount', min=0, max=100), default=0) # The following fields are supposed to be 0:N relationships # to SnapshotDelivery entity. # At this stage of implementation they will treated as a # comma-separated string of the devices expexted/transfered expected_devices = db.Column(JSONB, nullable=False) # expected_devices = db.Column(db.ARRAY(JSONB, dimensions=1), nullable=False) transferred_devices = db.Column(db.ARRAY(db.Integer, dimensions=1), nullable=True) transfer_state = db.Column(IntEnum(TransferState), default=TransferState.Initial, nullable=False) transfer_state.comment = TransferState.__doc__ lot_id = db.Column(UUID(as_uuid=True), db.ForeignKey(Lot.id), nullable=False) lot = db.relationship(Lot, backref=db.backref('deliverynote', uselist=False, lazy=True), lazy=True, primaryjoin=Lot.id == lot_id) def __init__(self, document_id: str, amount: str, date, supplier_email: str, expected_devices: Iterable, transfer_state: TransferState) -> None: """Initializes a delivery note """ super().__init__(id=uuid.uuid4(), document_id=document_id, amount=amount, date=date, supplier_email=supplier_email, expected_devices=expected_devices, transfer_state=transfer_state) @property def type(self) -> str: return self.__class__.__name__ @property def url(self) -> urlutils.URL: """The URL where to GET this action.""" return urlutils.URL(url_for_resource(Deliverynote, item_id=self.id)) def delete(self): """Deletes the deliverynote. This method removes the delivery note. """ db.session.delete(self) def __repr__(self) -> str: return '<Deliverynote {0.document_id}>'.format(self)