class VersionedDocument: versioned_document_uuid = UUID() versioned_document_version = String() versioned_document = Function(fget='get_versioned_document', fset='set_versioned_document', fdel='del_versioned_document') def get_versioned_document(self): Document = self.registry.Attachment.Document query = Document.query() query = query.filter( Document.uuid == self.versioned_document_uuid, Document.version == self.versioned_document_version) return query.one_or_none() def set_versioned_document(self, document): if document.uuid is None: raise NoneValueException("Uuid value is None") self.versioned_document_uuid = document.uuid self.versioned_document_version = document.version def del_versioned_document(self): self.versioned_document_uuid = None self.versioned_document_version = None @hybrid_method def is_versioned_document(self, document): return (self.versioned_document_uuid == document.uuid and self.versioned_document_version == document.version)
class LatestDocument: latest_document_uuid = UUID() latest_document = Function(fget='get_latest_document', fset='set_latest_document', fdel='del_latest_document') def get_latest_document(self): Document = self.registry.Attachment.Document.Latest query = Document.query() query = query.filter(Document.uuid == self.latest_document_uuid) return query.one_or_none() def set_latest_document(self, document): if document.uuid is None: raise NoneValueException("Uuid value is None") if document.type != 'latest': raise NotLatestException( "You try to set a versioned document, this action is " "forbidden") self.latest_document_uuid = document.uuid def del_latest_document(self): self.latest_document_uuid = None @hybrid_method def is_latest_document(self, document): if document.type != 'latest': raise NotLatestException( "You try to compare the latest document with a versioned " "document, this action is forbidden") return self.latest_document_uuid == document.uuid
class WeatherStation(Model.Iot.State): """Weather state from APRS-IS packet""" STATE_TYPE = "WEATHER_STATION" uuid: uuid4 = UUID( primary_key=True, default=uuid4, binary=False, foreign_key=Model.Iot.State.use("uuid").options(ondelete="CASCADE"), ) sensor_date: DateTime = DateTime( label="Sensor timestamp", index=True, ) station_code: str = String(label="Station code", unique=False, index=True, nullable=False) wind_direction: D = Decimal(label="Wind direction") wind_speed: D = Decimal(label="Wind Speed (km/h ?)") wind_gust: D = Decimal(label="Wind gust (km/h ?)") temperature: D = Decimal(label="Thermometer (°C)") rain_1h: D = Decimal(label="rain (mm/1h)") rain_24h: D = Decimal(label="rain (mm/24h)") rain_since_midnight: D = Decimal(label="rain (mm/since midnight)") humidity: D = Decimal(label="Humidity (%)") pressure: D = Decimal(label="Pressure (hPa)") luminosity: D = Decimal(label="Luminosity/irradiation (W/m2)") @classmethod def define_table_args(cls): res = super().define_table_args() return res + (UniqueConstraint(cls.sensor_date, cls.station_code), ) @classmethod def get_device_state( cls, code: str, ) -> Union["registry.Model.Iot.State.Relay", "registry.Model.Iot.State.DesiredRelay", "registry.Model.Iot.State.Thermometer", "registry.Model.Iot.State.FuelGauge", "registry.Model.Iot.State.WeatherStation", ]: """Overwrite parent method to sort on sensor date and set fake date while creating empty state """ Device = cls.registry.Iot.Device state = (cls.query().join(Device).filter(Device.code == code).order_by( cls.registry.Iot.State.WeatherStation.sensor_date.desc()).first()) if not state: device = Device.query().filter_by(code=code).one() # We don't want to instert a new state here, just creating # a default instance state = cls(device=device, sensor_date=datetime.now()) cls.registry.flush() return state
class FuelGauge(Model.Iot.State): STATE_TYPE = "FUEL_GAUGE" uuid: uuid4 = UUID( primary_key=True, default=uuid4, binary=False, foreign_key=Model.Iot.State.use("uuid").options(ondelete="CASCADE"), ) level: int = Integer(label="Fuel level (mm)")
class MyTemplate(Declarations.Model.Attachment.Template): TYPE = 'MyTemplate' uuid = UUID(primary_key=True, nullable=False, binary=False, foreign_key=Declarations.Model.Attachment.Template.use('uuid')) other_option = String() def render(self, data): return self.__class__.file_
class Relay(Model.Iot.State): STATE_TYPE = "RELAY" uuid: uuid4 = UUID( primary_key=True, default=uuid4, binary=False, foreign_key=Model.Iot.State.use("uuid").options(ondelete="CASCADE"), ) is_open: bool = Boolean(label="Is open ?", default=True) """Current circuit state. is_open == True means circuit open, it's turned
class Format(Attachment.Template): """Simple python format templating""" TYPE = TYPE uuid = UUID( primary_key=True, nullable=False, binary=False, foreign_key=Attachment.Template.use('uuid').options(ondelete='cascade')) contenttype = String(nullable=False) def render(self, data): template = self.get_template() return bytes(template.format(**data), encoding='utf-8') def update_document(self, document, file_, data): super(Format, self).update_document(document, file_, data) document.contenttype = self.contenttype
class Thing(): uuid = UUID(primary_key=True, default=uuid1, binary=False) name = String(label="Name", unique=True, nullable=False) create_date = DateTime(default=datetime.now, nullable=False) edit_date = DateTime(default=datetime.now, nullable=False) secret = Password(crypt_context={'schemes': ['pbkdf2_sha512']}, nullable=False) example = Many2One(label="Example", model=Model.Example, one2many="things", nullable=False) def __str__(self): return ('{self.name}').format(self=self) def __repr__(self): msg = ('<Thing: {self.name}, {self.uuid}>') return msg.format(self=self)
class Message(Declarations.Mixin.DramatiqMessageStatus): """Message model for dramatiq""" id = UUID(primary_key=True, nullable=False) updated_at = DateTime() message = Json(nullable=False) def __str__(self): return '<Message (id={self.id}, status={self.status.label})>'.format( self=self) @classmethod def insert(cls, *args, **kwargs): """Over write the insert to add the first history line""" self = super(Message, cls).insert(*args, **kwargs) self.updated_at = datetime.now() self.registry.Dramatiq.Message.History.insert( status=self.status, created_at=self.updated_at, message=self) return self @classmethod def get_instance_of(cls, message): """Called by the middleware to get the model instance of the message""" return cls.query().filter(cls.id == message.message_id).one_or_none() def update_status(self, status, error=None): """Called by the middleware to change the status and history""" logger.info("Update message %s status => %s", self, dict(self.STATUSES).get(status)) self.status = status self.updated_at = datetime.now() self.registry.Dramatiq.Message.History.insert( status=status, created_at=self.updated_at, message=self, error=error)
class Thermometer(Model.Iot.State): STATE_TYPE = "TEMPERATURE" uuid: uuid4 = UUID( primary_key=True, default=uuid4, binary=False, foreign_key=Model.Iot.State.use("uuid").options(ondelete="CASCADE"), ) celsius: D = Decimal(label="Thermometer (°C)") @classmethod def get_last_value(cls, device_code: str): Device = cls.registry.Iot.Device last = (cls.query().join(Device).filter( Device.code == device_code).order_by( cls.registry.Iot.State.create_date.desc()).first()) if last: return last.celsius return 0 @classmethod def wait_min_return_desired_temperature(cls, device_sensor: DEVICE, device_min: DEVICE, device_max: DEVICE): # wait return temperature to get the DEVICE.MIN_RET_DESIRED.value # temperature before start burner again Device = cls.registry.Iot.Device State = cls.registry.Iot.State min_temp = cls.get_last_value(device_min.value) max_temp = cls.get_last_value(device_max.value) last_pick = (cls.query().join(Device).filter( Device.code == device_sensor.value).filter( cls.celsius >= max_temp).order_by( State.create_date.desc()).first()) if not last_pick: return False last_curve = (cls.query().join(Device).filter( Device.code == device_sensor.value).filter( cls.celsius <= min_temp).filter( State.create_date > last_pick.create_date).order_by( State.create_date.desc()).first()) if not last_curve: return True return False @classmethod def get_living_room_avg(cls, date: datetime = None, minutes=5): avg = cls.get_sensor_avg(DEVICE.LIVING_ROOM_SENSOR.value, date=date, minutes=minutes) if not avg: return 0 return avg @classmethod def get_sensor_avg(cls, device_code: str, date: datetime = None, minutes: int = 15): if not date: date = datetime.now() Device = cls.registry.Iot.Device return (cls.query(func.avg( cls.celsius).label("average")).join(Device).filter( Device.code == device_code).filter( cls.create_date > date - timedelta(minutes=minutes), cls.create_date <= date, ).group_by(Device.code).scalar())
class DesiredRelay(Model.Iot.State): STATE_TYPE = "DESIRED_RELAY" uuid: uuid4 = UUID( primary_key=True, default=uuid4, binary=False, foreign_key=Model.Iot.State.use("uuid").options(ondelete="CASCADE"), ) is_open: bool = Boolean(label="Is open ?", default=True) """Current circuit state. is_open == True means circuit open, it's turned off""" @classmethod def get_device_state(cls, code: str, date: datetime = None) -> "DesiredRelay": """return desired state for given device code""" mode = ThermostatMode( cls.registry.System.Parameter.get("mode", default="manual")) if mode is ThermostatMode.manual: return super().get_device_state(code) else: if not date: date = datetime.now() return { "BURNER": cls.get_burner_thermostat_desired_state, "ENGINE": cls.get_engine_thermostat_desired_state, }[code](date) @classmethod def get_burner_thermostat_desired_state(cls, date: datetime) -> "DesiredRelay": Thermometer = cls.registry.Iot.State.Thermometer Range = cls.registry.Iot.Thermostat.Range celsius_avg = Thermometer.get_living_room_avg(date=date, minutes=5) if not celsius_avg: # there are no thermometer working # user should move to manual mode return cls(is_open=True) if Thermometer.wait_min_return_desired_temperature( DEVICE.DEPARTURE_SENSOR, DEVICE.MIN_RET_DESIRED, DEVICE.MAX_DEP_DESIRED, ): # departure is to hot or waiting under ret min desired return cls(is_open=True) celsius_desired = Range.get_desired_living_room_temperature(date) if celsius_avg >= celsius_desired: # living room is already warm return cls(is_open=True) if Thermometer.get_last_value( DEVICE.RETURN_SENOR.value) >= Thermometer.get_last_value( DEVICE.MAX_RET_DESIRED.value): # return temperature is to hot return cls(is_open=True) if Thermometer.wait_min_return_desired_temperature( DEVICE.RETURN_SENOR, DEVICE.MIN_RET_DESIRED, DEVICE.MAX_RET_DESIRED, ): # wait return temperature to get the DEVICE.MIN_RET_DESIRED.value # temperature before start burner again return cls(is_open=True) return cls(is_open=False) @classmethod def get_engine_thermostat_desired_state(cls, date: datetime, delta_minutes=120 ) -> "DesiredRelay": """ If burner relay was on in last delta_minutes, or if return water - living room is more than diff config value then engine must be turned on """ Thermometer = cls.registry.Iot.State.Thermometer Relay = cls.registry.Iot.State.Relay Device = cls.registry.Iot.Device count_states = (Relay.query().join(Device).filter( Relay.create_date > date - timedelta(minutes=delta_minutes), Relay.create_date <= date, Device.code == "BURNER", Relay.is_open == False, )) living_room_avg_temp = Thermometer.get_living_room_avg(date=date, minutes=5) return_temp = Thermometer.get_last_value(DEVICE.RETURN_SENOR.value) min_diff = Thermometer.get_last_value(DEVICE.MIN_DIFF_DESIRED.value) return cls( is_open=not ((count_states.count() > 0) or (return_temp - living_room_avg_temp > min_diff)))
class Document: DOCUMENT_TYPE = None uuid = UUID(primary_key=True, binary=False, nullable=False) version_number = Integer(primary_key=True, nullable=False) version = Function(fget="get_version") created_at = DateTime(default=datetime.now, nullable=False) historied_at = DateTime() data = Json(default=dict) file_added_at = DateTime() filename = String(size=256) contenttype = String() filesize = Integer() file = LargeBinary() hash = String(size=256) type = Selection(selections={ 'latest': 'Latest', 'history': 'History' }, nullable=False) previous_doc_uuid = UUID() previous_doc_version_number = Integer() previous_version = Function(fget="get_previous_version") next_version = Function(fget="get_next_version") previous_versions = Function(fget="get_previous_versions") @classmethod def get_file_fields(cls): return [ 'file', 'file_added_at', 'contenttype', 'filename', 'hash', 'filesize' ] def get_file(self): return self.to_dict(*self.get_file_fields()) def set_file(self, file_): self.file = file_ def get_version(self): return "V-%06d" % self.version_number def get_previous_version(self): Doc = self.anyblok.Attachment.Document query = Doc.query() query = query.filter(Doc.uuid == self.previous_doc_uuid) query = query.filter( Doc.version_number == self.previous_doc_version_number) return query.one_or_none() def get_next_version(self): Doc = self.anyblok.Attachment.Document query = Doc.query() query = query.filter(Doc.previous_doc_uuid == self.uuid) query = query.filter( Doc.previous_doc_version_number == self.version_number) return query.one_or_none() def get_previous_versions(self): res = [] current = self while current.previous_version: current = current.previous_version res.append(current) return res @classmethod def define_mapper_args(cls): mapper_args = super(Document, cls).define_mapper_args() if cls.__registry_name__ == 'Model.Attachment.Document': mapper_args.update({'polymorphic_on': cls.type}) mapper_args.update({'polymorphic_identity': cls.DOCUMENT_TYPE}) return mapper_args @classmethod def insert(cls, *args, **kwargs): if cls.__registry_name__ == 'Model.Attachment.Document': return cls.anyblok.Attachment.Document.Latest.insert( *args, **kwargs) return super(Document, cls).insert(*args, **kwargs) @classmethod def query(cls, *args, **kwargs): query = super(Document, cls).query(*args, **kwargs) if cls.__registry_name__ != 'Model.Attachment.Document': query = query.filter(cls.type == cls.DOCUMENT_TYPE) return query def has_file(self): if self.file: return True return False @classmethod def filter_has_not_file(cls): return cls.file == None # noqa
class Job: """The job is an execution of an instance of task""" STATUS_NEW = "new" STATUS_WAITING = "waiting" STATUS_RUNNING = "running" STATUS_FAILED = "failed" STATUS_DONE = "done" STATUSES = [ (STATUS_NEW, "New"), (STATUS_WAITING, "Waiting"), (STATUS_RUNNING, "Running"), (STATUS_FAILED, "Failed"), (STATUS_DONE, "Done"), ] uuid = UUID(primary_key=True, nullable=False, default=uuid1, binary=False) create_at = DateTime(default=datetime.now, nullable=False) update_at = DateTime(default=datetime.now, nullable=False, auto_update=True) run_at = DateTime() data = Json(nullable=False) status = Selection(selections=STATUSES, default=STATUS_NEW, nullable=False) task = Many2One(model=Declarations.Model.Dramatiq.Task, nullable=False) main_job = Many2One(model='Model.Dramatiq.Job', one2many="sub_jobs") error = Text() @actor_send() def run(cls, job_uuid=None): """dramatiq actor to execute a specific task""" autocommit = EnvironmentManager.get('job_autocommit', True) try: job = cls.query().filter(cls.uuid == job_uuid).one() job.status = cls.STATUS_RUNNING if autocommit: cls.registry.commit() # use to save the state job.task.run(job) if autocommit: cls.registry.commit() # use to save the state except Exception as e: logger.error(str(e)) cls.registry.rollback() job.status = cls.STATUS_FAILED job.error = str(e) if autocommit: cls.registry.commit() # use to save the state raise e def lock(self): """lock the job to be sure that only one thread execute the run_next""" Job = self.__class__ while True: try: Job.query().with_for_update(nowait=True).filter( Job.uuid == self.uuid).one() break except OperationalError: sleep(1) def call_main_job(self): """Call the main job if exist to do the next action of the main job""" if self.main_job: self.main_job.lock() self.main_job.task.run_next(self.main_job)
class Shipment(Mixin.UuidColumn, Mixin.TrackModel): """ Shipment """ statuses = dict(new="New", label="Label", transit="Transit", delivered="Delivered", exception="Exception", error="Error") service = Many2One(label="Shipping service", model=Declarations.Model.Delivery.Carrier.Service, one2many='shipments', nullable=False) sender_address = Many2One(label="Sender address", model=Declarations.Model.Address, column_names=["sender_address_uuid"], nullable=False) recipient_address = Many2One(label="Recipient address", model=Declarations.Model.Address, column_names=["recipient_address_uuid"], nullable=False) reason = String(label="Reason reference") pack = String(label="Pack reference") status = Selection(label="Shipping status", selections=statuses, default='new', nullable=False) properties = Jsonb(label="Properties") document_uuid = UUID(label="Carrier slip document reference") document = Function(fget='get_latest_document') cn23_document_uuid = UUID(label="Carrier slip document reference") cn23_document = Function(fget='get_latest_cn23_document') tracking_number = String(label="Carrier tracking number") def _get_latest_cocument(self, document_uuid): Document = self.registry.Attachment.Document.Latest query = Document.query().filter_by(uuid=document_uuid) return query.one_or_none() def get_latest_document(self): return self._get_latest_cocument(self.document_uuid) def get_latest_cn23_document(self): return self._get_latest_cocument(self.cn23_document_uuid) def create_label(self): """Retrieve a shipping label from shipping service """ if not self.status == 'new': return return self.service.create_label(shipment=self) def get_label_status(self): """Retrieve a shipping label from shipping service """ if self.status in ('new', 'delivered', 'error'): return return self.service.get_label_status(shipment=self) @classmethod def get_labels_status(cls): status = ['label', 'transit', 'exception'] shipments = cls.query().filter(cls.status.in_(status)).all() for shipment in shipments: shipment.get_label_status() def _save_document(self, document, binary_file, content_type): document.set_file(binary_file) document.filesize = len(binary_file) document.contenttype = content_type hash = hashlib.sha256() hash.update(binary_file) document.hash = hash.digest() self.registry.flush() # flush to update version in document def save_document(self, binary_file, content_type): document = self.document if document is None: document = self.registry.Attachment.Document.insert( data={'shipment': str(self.uuid)}) self.document_uuid = document.uuid self._save_document(document, binary_file, content_type) def save_cn23_document(self, binary_file, content_type): document = self.cn23_document if document is None: document = self.registry.Attachment.Document.insert( data={'shipment': str(self.uuid)}) self.cn23_document_uuid = document.uuid self._save_document(document, binary_file, content_type)
class UuidColumn: """ `UUID` id primary key mixin """ uuid = UUID(primary_key=True, default=uuid4, binary=False)
class Jinja(Mixin.WkHtml2Pdf, Attachment.Template): """Jinja templating""" TYPE = TYPE uuid = UUID(primary_key=True, nullable=False, binary=False, foreign_key=Attachment.Template.use('uuid').options( ondelete='cascade')) jinja_paths = Text(nullable=False) contenttype = Selection(selections={ 'text/html': 'HTML', 'application/pdf': 'PDF', }, default='application/pdf', nullable=False) options = Json(default={}, nullable=False) wkhtml2pdf_configuration = Many2One( model=Declarations.Model.Attachment.WkHtml2Pdf, nullable=True) def check_flush_validity(self): super(Jinja, self).check_flush_validity() if self.contenttype == 'application/pdf': if not self.wkhtml2pdf_configuration: raise TemplateJinjaException( "No WkHtml2Pdf configuration for %r" % self) def update_document(self, document, file_, data): super(Jinja, self).update_document(document, file_, data) document.contenttype = self.contenttype def render(self, data): if self.contenttype == 'text/html': return self.render_html(data) return self.render_pdf(data) def render_html(self, data): jinja_paths = [] if self.jinja_paths: for jinja_path in self.jinja_paths.split(','): jinja_path = format_path(jinja_path.strip()) if not os.path.isdir(jinja_path): raise TemplateJinjaException("%r must be a folder" % jinja_path) jinja_paths.append(jinja_path) jinja_env = SandboxedEnvironment( loader=jinja2.FileSystemLoader(jinja_paths), undefined=jinja2.StrictUndefined, ) parser = self.get_parser() if not hasattr(parser, 'serialize_jinja_options'): raise TemplateJinjaException( ("The parser %r must declare a method " "'serialize_jinja_options' for %r") % (parser, self)) options = self.get_parser().serialize_jinja_options(self.options) return jinja_env.from_string(self.get_template()).render( data=data, str=str, **options).encode('utf-8') def render_pdf(self, data): html_content = self.render_html(data) return self.wkhtml2pdf(html_content)