class Importer(Declarations.Mixin.IOMixin): file_to_import = LargeBinary(nullable=False) offset = Integer(default=0) nb_grouped_lines = Integer(nullable=False, default=50) commit_at_each_grouped = Boolean(default=True) check_import = Boolean(default=False) def run(self, blokname=None): return self.get_model(self.mode)(self, blokname=blokname).run() def get_key_mapping(self, key): Mapping = self.registry.IO.Mapping return Mapping.get(self.model, key) def commit(self): if self.check_import: return False elif not self.commit_at_each_grouped: return False self.registry.commit() return True def str2value(self, value, ctype, external_id=False, model=None): formater = self.get_formater(ctype) if external_id: return formater.externalIdStr2value(value, model) return formater.str2value(value, model)
class DecrementCommitment(Model.REA.Entity): """ Commitment is a promise or obligation of economic agents to perform an economic event in the future. For example, line items on a sales order represent commitments to sell goods. # Model-Driven Design Using Business Patterns # Authors: Hruby, Pavel # ISBN-10 3-540-30154-2 Springer Berlin Heidelberg New York # ISBN-13 978-3-540-30154-7 Springer Berlin Heidelberg New York """ id = Integer(primary_key=True, foreign_key=Model.REA.Entity.use('id')) recipient = Many2One(label="Agent recipient", model=Model.REA.Agent, nullable=False) resource = Many2One(label="Reservation Resource", model=Model.REA.Resource, nullable=False) value = Decimal(label="Value decrement", default=decimalType(0)) fulfilled = Boolean(label="Is Fulfilled", default=False) def fulfill(self): """ :return: True if commitment is fulfilled """ if not self.fulfilled: self.registry.REA.DecrementEvent.create_event_from_commitment(self) self.fulfilled = True return True return False
class Test(Mixin.ConditionalForbidDelete): id = Integer(primary_key=True) forbid_delete = Boolean(default=False) def check_if_forbid_delete_condition_is_true(self): return self.forbid_delete
class Test(Mixin.ConditionalForbidUpdate): id = Integer(primary_key=True) forbid_update = Boolean(default=False) name = String() def check_if_forbid_update_condition_is_true(self, **changed): return self.forbid_update
class Test: id = Integer(primary_key=True) val = Boolean(default=False) @listen('Model.Test', 'before_insert') def my_event(cls, mapper, connection, target): target.val = True
class BooleanReadOnly(Mixin.ConditionalForbidUpdate, Mixin.ConditionalForbidDelete): readonly = Boolean(default=False) def check_if_forbid_update_condition_is_true(self, **previous_values): return previous_values.get('readonly', self.readonly) def check_if_forbid_delete_condition_is_true(self): return self.readonly
class Test(Mixin.ConditionalReadOnly): id = Integer(primary_key=True) readonly = Boolean(default=False) name = String() def check_if_forbid_update_condition_is_true(self, **previous_values): return previous_values.get('readonly', self.readonly) def check_if_forbid_delete_condition_is_true(self): return self.readonly
class Todo: id = Integer(label="Identifier", primary_key=True) task = String(label="Task description") done = Boolean(default=False) def __str__(self): return "%s is %s" % ( self.task, "over. Good job!" if self.done else "waiting for you!" )
class Team(): class FuretUIAdapter(Adapter): @Adapter.tag('active') def tag_is_active(self, querystring, query): query = query.filter(self.Model.active.is_(True)) return query id = Integer(primary_key=True) first_name = String(nullable=False) last_name = String(nullable=False) active = Boolean(default=True)
class Resource(Declarations.Model.FuretUI.Menu, Declarations.Mixin.FuretUIMenu, Declarations.Mixin.FuretUIMenuParent): resource = Many2One(model=Declarations.Model.FuretUI.Resource, nullable=False) default = Boolean(default=False) tags = String() order_by = String() filters = Json(default={}) def check_acl(self): return self.resource.check_acl()
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 PredictionModelInput(Mixin.IdColumn): """Sets of inputs for a model""" input_order = Integer(label='Number in the inputs vector', nullable=False) input_name = String(label='Name of the input', nullable=False) is_internal_feature = Boolean(label='Is internal feature', default=True, nullable=False) prediction_model = Many2One(label='Model to feed with', model=PredictionModel, nullable=False, one2many='model_inputs') input_vector = Many2One( label='Input group used to predict', model=PredictionInputVector, ) def __repr__(self): msg = '<PredictionModelInput: ' \ 'input_order={self.input_order}>, input_name={self.input_name}, ' \ 'prediction_model={self.prediction_model}>' return msg.format(self=self)
class WmsSplitterOperation: """Mixin for operations on a single input that can split. This is to be applied after :class:`Mixin.WmsSingleInputOperation <anyblok_wms_base.core.operation.single_input.WmsSingleInputOperation>`. Use :class:`WmsSplitterSingleInputOperation` to get both at once. It defines the :attr:`quantity` field to express that the Operation only works on some of the quantity held by the PhysObj of the single input. In case the Operation's :attr:`quantity` is less than in the PhysObj record, a :class:`Split <.split.Split>` will be inserted properly in history, and the Operation implementation can ignore quantities completely, as it will always, in truth, work on the whole of the input it will see. Subclasses can use the :attr:`partial` field if they need to know if that happened, but this should be useful only in special cases. """ quantity = Decimal(default=1) """The quantity this Operation will work on. Can be less than the quantity of our single input. """ partial = Boolean(label="Operation induced a split") """Record if a Split will be or has been inserted in the history. Such insertions should happen if the operation's original PhysObj have greater quantity than the operation needs. This is useful because once the Split is executed, this information can't be deduced from the quantities involved any more. """ @classmethod def define_table_args(cls): return super(WmsSplitterOperation, cls).define_table_args() + ( CheckConstraint('quantity > 0', name='positive_qty'), ) def specific_repr(self): return ("input={self.input!r}, " "quantity={self.quantity}").format(self=self) @classmethod def check_create_conditions(cls, state, dt_execution, inputs=None, quantity=None, **kwargs): super(WmsSplitterOperation, cls).check_create_conditions(state, dt_execution, inputs=inputs, quantity=quantity, **kwargs) phobj = inputs[0].obj if quantity is None: raise OperationMissingQuantityError( cls, "The 'quantity keyword argument must be passed to " "the create() method") if quantity > phobj.quantity: raise OperationQuantityError( cls, "Can't split a greater quantity ({op_quantity}) than held in " "{input} (which have quantity={input.obj.quantity})", op_quantity=quantity, input=inputs[0]) def check_execute_conditions(self): """Check that the quantity (after possible Split) is as on the input. If a Split has been inserted, then this calls the base class for the input of the Split, instead of ``self``, because the input of ``self`` is in that case the outcome of the Split, and it's normal that it's in state ``future``: the Split will be executed during ``self.execute()``, which comes once the present method has agreed. """ if self.quantity != self.input.obj.quantity: raise OperationQuantityError( self, "Can't execute planned for a different quantity {op_quantity} " "than held in its input {input} " "(which have quantity={input.obj.quantity}). " "If it's less, a Split should have occured first ", input=input) if self.partial: self.input.outcome_of.check_execute_conditions() else: super(WmsSplitterOperation, self).check_execute_conditions() @classmethod def before_insert(cls, state='planned', follows=None, inputs=None, quantity=None, dt_execution=None, dt_start=None, **kwargs): """Override to introduce a Split if needed In case the value of :attr:`quantity` does not match the one from the ``goods`` field, a :class:`Split <.split.Split>` is inserted transparently in the history, as if it'd been there in the first place: subclasses can implement :meth:`after_insert` as if the quantities were matching from the beginning. """ avatar = inputs[0] partial = quantity < avatar.obj.quantity if not partial: return inputs, None Split = cls.registry.Wms.Operation.Split split = Split.create(input=avatar, quantity=quantity, state=state, dt_execution=dt_execution) return [split.wished_outcome], dict(partial=partial) def execute_planned(self): """Execute the :class:`Split <.split.Split>` if any, then self.""" if self.partial: split_op = next(iter(self.follows)) split_op.execute(dt_execution=self.dt_execution) super(WmsSplitterOperation, self).execute_planned() self.registry.flush()
class Sequence: """Database sequences. This Model allows applications to define and use Database sequences easily. It is a rewrapping of `SQLAlchemy sequences <http://docs.sqlalchemy.org/en/latest/core/defaults.html #sqlalchemy.schema.Sequence>`_, with additional formatting capabilities to use them, e.g, in fields of applicative Models. Sample usage:: sequence = registry.System.Sequence.insert( code="string code", formater="One prefix {seq} One suffix") .. seealso:: The :attr:`formater` field. To get the next formatted value of the sequence:: sequence.nextval() Full example in a Python shell:: >>> seq = Sequence.insert(code='SO', formater="{code}-{seq:06d}") >>> seq.nextval() 'SO-000001' >>> seq.nextval() 'SO-000002' You can create a Sequence without gap warranty using `no_gap` while creating the sequence:: >>> seq = Sequence.insert( code='SO', formater="{code}-{seq:06d}", no_gap=True) >>> commit() >>> # Transaction 1: >>> Sequence.nextvalBy(code='SO') 'SO-000001' >>> # Concurrent transaction 2: >>> Sequence.nextvalBy(code='SO') ... sqlalchemy.exc.OperationalError: (psycopg2.errors.LockNotAvailable) ... """ _cls_seq_name = 'system_sequence_seq_name' id = Integer(primary_key=True) code = String(nullable=False) start = Integer(default=1) current = Integer(default=None) seq_name = String(nullable=False) """Name of the sequence in the database. Most databases identify sequences by names which must be globally unique. If not passed at insertion, the value of this field is automatically generated. """ formater = String(nullable=False, default="{seq}") """Python format string to render the sequence values. This format string is used in :meth:`nextval`. Within it, you can use the following variables: * seq: current value of the underlying database sequence * code: :attr:`code` field * id: :attr:`id` field """ no_gap = Boolean(default=False, nullable=False) """If no_gap is False, it will use Database sequence. Otherwise, if `True` it will ensure there is no gap while getting next value locking the sequence row until transaction is released (rollback/commit). If a concurrent transaction try to get a lock an `sqlalchemy.exc.OperationalError: (psycopg2.errors.LockNotAvailable)` exception is raised. """ @classmethod def initialize_model(cls): """ Create the sequence to determine name """ super(Sequence, cls).initialize_model() seq = SQLASequence(cls._cls_seq_name) seq.create(cls.registry.bind) to_create = getattr(cls.registry, '_need_sequence_to_create_if_not_exist', ()) if to_create is None: return for vals in to_create: if cls.query().filter(cls.code == vals['code']).count(): continue formatter = vals.get('formater') if formatter is None: del vals['formater'] cls.insert(**vals) @classmethod def create_sequence(cls, values): """Create the database sequence for an instance of Sequence Model. :return: suitable field values for insertion of the Model instance :rtype: dict """ seq_name = values.get('seq_name') start = values.setdefault('start', 1) if values.get("no_gap"): values.setdefault('seq_name', values.get("code", "no_gap")) else: if seq_name is None: seq_id = cls.registry.execute(SQLASequence(cls._cls_seq_name)) seq_name = '%s_%d' % (cls.__tablename__, seq_id) values['seq_name'] = seq_name seq = SQLASequence(seq_name, start=start) seq.create(cls.registry.bind) return values @classmethod def insert(cls, **kwargs): """Overwrite to call :meth:`create_sequence` on the fly.""" return super(Sequence, cls).insert(**cls.create_sequence(kwargs)) @classmethod def multi_insert(cls, *args): """Overwrite to call :meth:`create_sequence` on the fly.""" res = [cls.create_sequence(x) for x in args] return super(Sequence, cls).multi_insert(*res) def nextval(self): """Format and return the next value of the sequence. :rtype: str """ if self.no_gap: self.refresh(with_for_update={"nowait": True}) nextval = self.start if self.current is None else self.current + 1 else: nextval = self.registry.execute(SQLASequence(self.seq_name)) self.update(current=nextval) return self.formater.format(code=self.code, seq=nextval, id=self.id) @classmethod def nextvalBy(cls, **crit): """Return next value of the first Sequence matching given criteria. :param crit: criteria to match, e.g., ``code=SO`` :return: :meth:`next_val` result for the first matching Sequence, or ``None`` if there's no match. """ filters = [getattr(cls, k) == v for k, v in crit.items()] seq = cls.query().filter(*filters).first() if seq is None: return None return seq.nextval()
class Column(System.Field): name = String(primary_key=True) model = String(primary_key=True) autoincrement = Boolean(label="Auto increment") foreign_key = String() primary_key = Boolean() unique = Boolean() nullable = Boolean() remote_model = String() def _description(self): res = super(Column, self)._description() res.update(nullable=self.nullable, primary_key=self.primary_key, model=self.remote_model) return res @classmethod def get_cname(self, field, cname): """ Return the real name of the column :param field: the instance of the column :param cname: Not use here :rtype: string of the real column name """ return cname @classmethod def add_field(cls, cname, column, model, table, ftype): """ Insert a column definition :param cname: name of the column :param column: instance of the column :param model: namespace of the model :param table: name of the table of the model :param ftype: type of the AnyBlok Field """ Model = cls.anyblok.get(model) if hasattr(Model, anyblok_column_prefix + cname): c = getattr(Model, anyblok_column_prefix + cname) else: c = column.property.columns[0] # pragma: no cover autoincrement = c.autoincrement if autoincrement == 'auto': autoincrement = (True if c.primary_key and ftype == 'Integer' else False) vals = dict(autoincrement=autoincrement, code=table + '.' + cname, model=model, name=cname, foreign_key=c.info.get('foreign_key'), label=c.info.get('label'), nullable=c.nullable, primary_key=c.primary_key, ftype=ftype, remote_model=c.info.get('remote_model'), unique=c.unique) cls.insert(**vals) @classmethod def alter_field(cls, column, meta_column, ftype): """ Update an existing column :param column: instance of the Column model to update :param meta_column: instance of the SqlAlchemy column :param ftype: type of the AnyBlok Field """ Model = cls.anyblok.get(column.model) if hasattr(Model, anyblok_column_prefix + column.name): c = getattr(Model, anyblok_column_prefix + column.name) else: c = meta_column.property.columns[0] # pragma: no cover autoincrement = c.autoincrement if autoincrement == 'auto': autoincrement = (True if c.primary_key and ftype == 'Integer' else False) if column.autoincrement != autoincrement: column.autoincrement = autoincrement # pragma: no cover for col in ('nullable', 'primary_key', 'unique'): if getattr(column, col) != getattr(c, col): setattr(column, col, getattr(c, col)) # pragma: no cover for col in ('foreign_key', 'label', 'remote_model'): if getattr(column, col) != c.info.get(col): setattr(column, col, c.info.get(col)) # pragma: no cover if column.ftype != ftype: column.ftype = ftype # pragma: no cover
class Parameter: """System Parameter""" key = String(primary_key=True) value = Json(nullable=False) multi = Boolean(default=False) @classmethod def set(cls, key, value): """ Insert or Update parameter for the key :param key: key to save :param value: value to save """ multi = False if not isinstance(value, dict): value = {'value': value} else: multi = True if cls.is_exist(key): param = cls.from_primary_keys(key=key) param.update(value=value, multi=multi) else: cls.insert(key=key, value=value, multi=multi) @classmethod def is_exist(cls, key): """ Check if one parameter exist for the key :param key: key to check :rtype: Boolean, True if exist """ query = cls.query().filter(cls.key == key) return True if query.count() else False @classmethod def get(cls, key): """ Return the value of the key :param key: key to check :rtype: return value :exception: ExceptionParameter """ if not cls.is_exist(key): raise ParameterException("unexisting key %r" % key) param = cls.from_primary_keys(key=key) if param.multi: return param.value return param.value['value'] @classmethod def pop(cls, key): """Remove return the value of the key :param key: key to check :rtype: return value :exception: ExceptionParameter """ if not cls.is_exist(key): raise ParameterException("unexisting key %r" % key) param = cls.from_primary_keys(key=key) if param.multi: res = param.value else: res = param.value['value'] param.delete() return res
class RelationShip(System.Field): name = String(primary_key=True) model = String(primary_key=True) local_columns = String() remote_columns = String() remote_name = String() remote_model = String(nullable=False) remote = Boolean(default=False) nullable = Boolean() def _description(self): res = super(RelationShip, self)._description() remote_name = self.remote_name or '' local_columns = [] if self.local_columns: local_columns = [x.strip() for x in self.local_columns.split(',')] remote_columns = [] if self.remote_columns: remote_columns = [ x.strip() for x in self.remote_columns.split(',') ] res.update( nullable=self.nullable, model=self.remote_model, remote_name=remote_name, local_columns=local_columns, remote_columns=remote_columns, ) return res @classmethod def add_field(cls, rname, relation, model, table, ftype): """ Insert a relationship definition :param rname: name of the relationship :param relation: instance of the relationship :param model: namespace of the model :param table: name of the table of the model :param ftype: type of the AnyBlok Field """ local_columns = relation.info.get('local_columns', "") if isinstance(local_columns, list): local_columns = ','.join(local_columns) remote_columns = relation.info.get('remote_columns', "") if isinstance(remote_columns, list): remote_columns = ','.join(remote_columns) remote_model = relation.info.get('remote_model') remote_name = relation.info.get('remote_name') label = relation.info.get('label') nullable = relation.info.get('nullable', True) vals = dict(code=table + '.' + rname, model=model, name=rname, local_columns=local_columns, remote_model=remote_model, remote_name=remote_name, remote_columns=remote_columns, label=label, nullable=nullable, ftype=ftype) cls.insert(**vals) if remote_name: remote_type = "Many2One" if ftype == "Many2One": remote_type = "One2Many" elif ftype == 'Many2Many': remote_type = "Many2Many" elif ftype == "One2One": remote_type = "One2One" m = cls.registry.get(remote_model) vals = dict(code=m.__tablename__ + '.' + remote_name, model=remote_model, name=remote_name, local_columns=remote_columns, remote_model=model, remote_name=rname, remote_columns=local_columns, label=remote_name.capitalize().replace('_', ' '), nullable=True, ftype=remote_type, remote=True) cls.insert(**vals) @classmethod def alter_field(cls, field, field_, ftype): field.label = field_.info['label']
class View: mode_name = None id = Integer(primary_key=True) order = Integer(sequence='web__view_order_seq', nullable=False) action = Many2One(model=Model.Web.Action, one2many='views', nullable=False, foreign_key_options={'ondelete': 'cascade'}) mode = Selection(selections='get_mode_choices', nullable=False) template = String(nullable=False) add_delete = Boolean(default=True) add_new = Boolean(default=True) add_edit = Boolean(default=True) @classmethod def get_mode_choices(cls): """ Return the View type available :rtype: dict(registry name: label) """ return { 'Model.Web.View.List': 'List view', 'Model.Web.View.Thumbnail': 'Thumbnails view', 'Model.Web.View.Form': 'Form view', } @classmethod def define_mapper_args(cls): mapper_args = super(View, cls).define_mapper_args() mapper_args.update({ 'polymorphic_identity': '', 'polymorphic_on': cls.mode, }) return mapper_args def render(self): buttons = [{'label': b.label, 'buttonId': b.method} for b in self.action.buttons + self.buttons if b.mode == 'action'] return { 'type': 'UPDATE_VIEW', 'viewId': str(self.id), 'viewType': self.mode_name, 'creatable': self.action.add_new and self.add_new or False, 'deletable': self.action.add_delete and self.add_delete or False, 'editable': self.action.add_edit and self.add_edit or False, 'model': self.action.model, 'buttons': buttons, } @classmethod def bulk_render(cls, actionId=None, viewId=None, **kwargs): action = cls.registry.Web.Action.query().get(int(actionId)) buttons = [{'label': b.label, 'buttonId': b.method} for b in action.buttons if b.mode == 'action'] return { 'type': 'UPDATE_VIEW', 'viewId': viewId, 'viewType': cls.mode_name, 'creatable': action.add_new or False, 'deletable': action.add_delete or False, 'editable': action.add_edit or False, 'model': action.model, 'buttons': buttons, } @classmethod def get_view_render(cls, viewId, params): try: view = cls.query().get(int(viewId)) except ValueError: _View = getattr(cls, viewId.split('-')[0]) if 'actionId' not in params: params['actionId'] = viewId.split('-')[-1] return _View.bulk_render(**params) else: return view.render()
class JourneyWish(Mixin.UuidColumn, Mixin.TrackModel, Mixin.WorkFlow): """This model has Model.JourneyWish as a namespace. It is intented for storing journey wishes that represents travels intentions. For instance, an intention may be caracterized by start date, with an a time frame delimited by earlier and latest departure times, departure and arrival stations, passengers, maximum price, etc... Implemented fields are the following : * Departure station : Many2One relationship with Model.Station * Arrival station : Many2One relationship with Model.Station * Start date : DateTime representing earlier time after which train may leave the departure station * End date : DateTime representing latest time after which train may leave departure station. * Passengers : Many2Many relationship to Model.Passenger * Transportation mean : String, containing type of transport required by user (train, coach, etc...) * Active : boolean that stores if wish has to be processed * Activation date : Date, represent the moment when the wish could start being processed""" user = Many2One( label="User", model=Model.User, nullable=False, one2many="wishes" ) departure = Many2One( label="Departure Station", model=Model.Station, nullable=True, one2many="departures", ) arrival = Many2One( label="Arrival Station", model=Model.Station, nullable=True, one2many="arrivals", ) from_date = DateTime(label="Earlier Departure Date", nullable=True) end_date = DateTime(label="Latest Departure Date", nullable=True) activation_date = Date(label="Activation Date", nullable=True) passengers = Many2Many( model=Model.Passenger, join_table="join_passengers_by_wishes", remote_columns="uuid", local_columns="uuid", m2m_remote_columns="p_uuid", m2m_local_columns="w_uuid", many2many="wishes", ) transportation_mean = String( label="Transportation Mean", default="any", nullable=True ) active = Boolean(label="Active Wish", nullable=True) @classmethod def get_workflow_definition(cls): """This method is aimed at defining workflow used for model Model.JourneyWish""" return { "draft": { "default": True, "allowed_to": ["running", "pending"], "apply_change": "deactivate", }, "running": { "allowed_to": ["cancelled", "expired"], "apply_change": "activate", }, "pending": { "allowed_to": ["cancelled", "expired", "running"], "apply_change": "deactivate", }, "expired": {"apply_change": "deactivate"}, "cancelled": {"apply_change": "deactivate"}, } def activate(self, from_state): if from_state == "draft" and not self.activation_date: self.activation_date = date.today() self.active = True def deactivate(self, from_state): self.active = False def check_state(self): """This method is aimed at being used in order to automatically set workflow state, depending on record attributes.""" if self.state == "pending": # If wish is pending, check is activation_date is set if self.activation_date and self.activation_date <= date.today(): self.state_to("running") elif self.state == "running": # If wish is already running, check that from_date is not in the # past try: now = datetime.now().astimezone() except ValueError: # Python3.5 and below do not support astimezone on 'naive' # dates provided by datetime.now() now = datetime.now(timezone.utc).astimezone() if self.from_date and self.from_date < now: self.state_to("expired")
class Model: """Models assembled""" def __str__(self): if self.description: return self.description return self.name name = String(size=256, primary_key=True) table = String(size=256) schema = String() is_sql_model = Boolean(label="Is a SQL model") description = Function(fget='get_model_doc_string') def get_model_doc_string(self): """ Return the docstring of the model """ m = self.registry.get(self.name) if hasattr(m, '__doc__'): return m.__doc__ or '' return '' @listen('Model.System.Model', 'Update Model') def listener_update_model(cls, model): cls.registry.System.Cache.invalidate(model, '_fields_description') cls.registry.System.Cache.invalidate(model, 'getFieldType') cls.registry.System.Cache.invalidate(model, 'get_primary_keys') cls.registry.System.Cache.invalidate( model, 'find_remote_attribute_to_expire') cls.registry.System.Cache.invalidate(model, 'find_relationship') cls.registry.System.Cache.invalidate(model, 'get_hybrid_property_columns') @classmethod def get_field_model(cls, field): ftype = field.property.__class__.__name__ if ftype == 'ColumnProperty': return cls.registry.System.Column elif ftype == 'RelationshipProperty': return cls.registry.System.RelationShip else: raise Exception('Not implemented yet') @classmethod def get_field(cls, model, cname): if cname in model.loaded_fields.keys(): field = model.loaded_fields[cname] Field = cls.registry.System.Field else: field = getattr(model, cname) Field = cls.get_field_model(field) return field, Field @classmethod def update_fields(cls, model, table): fsp = cls.registry.loaded_namespaces_first_step m = cls.registry.get(model) # remove useless column Field = cls.registry.System.Field query = Field.query() query = query.filter(Field.model == model) query = query.filter(Field.name.notin_(m.loaded_columns)) for model_ in query.all(): if model_.entity_type == 'Model.System.RelationShip': if model_.remote: continue RelationShip = cls.registry.System.RelationShip Q = RelationShip.query() Q = Q.filter(RelationShip.name == model_.remote_name) Q = Q.filter(RelationShip.model == model_.remote_model) Q.delete() model_.delete() # add or update new column for cname in m.loaded_columns: ftype = fsp[model][cname].__class__.__name__ field, Field = cls.get_field(m, cname) cname = Field.get_cname(field, cname) query = Field.query() query = query.filter(Field.model == model) query = query.filter(Field.name == cname) if query.count(): Field.alter_field(query.first(), field, ftype) else: Field.add_field(cname, field, model, table, ftype) @classmethod def add_fields(cls, model, table): fsp = cls.registry.loaded_namespaces_first_step m = cls.registry.get(model) is_sql_model = len(m.loaded_columns) > 0 cls.insert(name=model, table=table, schema=m.__db_schema__, is_sql_model=is_sql_model) for cname in m.loaded_columns: field, Field = cls.get_field(m, cname) cname = Field.get_cname(field, cname) ftype = fsp[model][cname].__class__.__name__ Field.add_field(cname, field, model, table, ftype) @classmethod def update_list(cls): """ Insert and update the table of models :exception: Exception """ for model in cls.registry.loaded_namespaces.keys(): try: # TODO need refactor, then try except pass whenever refactor # not apply m = cls.registry.get(model) table = '' if hasattr(m, '__tablename__'): table = m.__tablename__ _m = cls.query('name').filter(cls.name == model) if _m.count(): cls.update_fields(model, table) else: cls.add_fields(model, table) if m.loaded_columns: cls.fire('Update Model', model) except Exception as e: logger.exception(str(e)) # remove model and field which are not in loaded_namespaces query = cls.query() query = query.filter( cls.name.notin_(cls.registry.loaded_namespaces.keys())) Field = cls.registry.System.Field for model_ in query.all(): Q = Field.query().filter(Field.model == model_.name) for field in Q.all(): field.delete() model_.delete()
class Request: id = Integer(label="Identifier", primary_key=True) """Primary key. In this model, the ordering of ``id`` ordering is actually important (whereas on many others, it's a matter of habit to have a serial id): the smaller it is, the older the Request. Requests have to be reserved in order. Note that ``serial`` columns in PostgreSQL don't induce conflicts, as the sequence is evaluated out of transaction. """ purpose = Jsonb() """Flexible field to describe what the reservations will be for. This is typically used by a planner, to produce an appropriate chain of Operations to fulfill that purpose. Example: in a simple sales system, we would record a sale order reference here, and the planner would then take the related PhysObj and issue (planned) Moves and Departures for their 'present' or 'future' Avatars. """ reserved = Boolean(nullable=False, default=False) """Indicates that all reservations are taken. TODO: find a way to represent if the Request is partially done ? Some use-cases would require planning partial deliveries and the like in that case. """ planned = Boolean(nullable=False, default=False) """Indicates that the planner has finished with that Request. It's better than deleting, because it allows to cancel all Operations, set this back to ``True``, and plan again. """ txn_owned_reservations = set() """The set of Request ids whose current transaction owns reservations.""" @classmethod @contextmanager def claim_reservations(cls, query=None, **filter_by): """Context manager to claim ownership over this Request's reservations. This is meant for planners and works on fully reserved Requests. Example:: Request = registry.Wms.Reservation.Request with Request.claim_reservations() as req_id: request = Request.query().get(req_id) (...) read request.purpose, plan Operations (...) By calling this, the current transaction becomes responsible for all Request's reservations, meaning that it has the liberty to issue any Operation affecting its PhysObj or their Avatars. :return: id of claimed Request :param dict filter_by: direct filtering criteria to add to the query, e.g, a planner looking for planning to be done would pass ``planned=False``. :param query: if specified, is used to form the final SQL query, instead of creating a new one. The passed query must have the present model class in its ``FROM`` clause and return only the ``id`` column of the present model. The criteria of ``filter_by`` are still applied if also provided. This is safe with respect to concurrency: no other transaction can claim the same Request (guaranteed by a PostgreSQL lock). The session will forget about this Request as soon as one exits the ``with`` statement, and the underlying PG lock is released at the end of the transaction. TODO for now it's a context manager. I'd found it more elegant to tie it to the transaction, to get automatic release, without a ``with`` syntax, but that requires more digging into SQLAlchemy and Anyblok internals. TODO I think FOR UPDATE actually creates a new internal PG row (table bloat). Shall we switch to advisory locks (see PG doc) with an harcoded mapping to an integer ? If that's true, then performance-wise it's equivalent for us to set the txn id in some service column (but that would require inconditional cleanup, a complication) """ if query is None: query = cls.query('id') if filter_by is not None: query = query.filter_by(reserved=True, **filter_by) # issues a SELECT FOR UPDATE SKIP LOCKED (search # 'with_for_update' within # http://docs.sqlalchemy.org/en/latest/core/selectable.html # also, noteworthy, SKIP LOCKED appeared within PostgreSQL 9.5 # (https://www.postgresql.org/docs/current/static/release-9-5.html) cols = query.with_for_update(skip_locked=True, of=cls).order_by(cls.id).first() request_id = None if cols is None else cols[0] if request_id is not None: cls.txn_owned_reservations.add(request_id) yield request_id if request_id is not None: cls.txn_owned_reservations.discard(request_id) def is_txn_reservations_owner(self): """Tell if transaction is the owner of this Request's reservations. :return: ``True`` if the current transaction has claimed ownership, using the :meth:``claim_reservations`` method. """ return self.id in self.txn_owned_reservations def reserve(self): """Try and perform reservation for all RequestItems. :return: ``True`` if all reservations are now taken :rtype: bool Should not fail if reservations are already done. """ Item = self.registry.Wms.Reservation.RequestItem # could use map() and all(), but it's not recommended style # if there are strong side effects. all_reserved = True for item in Item.query().filter(Item.request == self).all(): all_reserved = all_reserved and item.reserve() self.reserved = all_reserved return all_reserved @classmethod def lock_unreserved(cls, batch_size, query_filter=None, offset=0): """Take exclusivity over not yet reserved Requests This is used in :ref:`Reservers <arch_reserver>` implementations. :param int batch: maximum of reservations to lock at once. Since reservations have to be taken in order, this produces a hard error in case there's a conflicting database lock, instead of skipping them like :meth:`claim_reservations` does. This conflicts in particular locks taken with :meth`claim_reservations`, but in principle, only :ref:`reservers <arch_reserver>` should take locks over reservation Requests that are not reserved yet, and these should not run in concurrency (or in a very controlled way, using ``query_filter``). """ query = cls.query().filter(cls.reserved.is_(False)) if query_filter is not None: query = query_filter(query) query = query.with_for_update(nowait=True).order_by(cls.id) try: return query.limit(batch_size).offset(offset).all() except sqlalchemy.exc.OperationalError as op_err: cls.registry.rollback() raise cls.ReservationsLocked(op_err) class ReservationsLocked(RuntimeError): """Used to rewrap concurrency errors while taking locks.""" def __init__(self, db_exc): self.db_exc = db_exc @classmethod def reserve_all(cls, batch_size=10, nb_attempts=5, retry_delay=1, query_filter=None): """Try and perform all reservations for pending Requests. This walks all pending (:attr:`reserved` equal to ``False``) Requests that haven't been reserved from the oldest and locks them by batches of ``batch_size``. Reservation is attempted for each request, in order, meaning that each request will grab as much PhysObj as it can before the next one gets processed. :param int batch_size: number of pending Requests to grab at each iteration :param nb_attempts: number of attempts (in the face of conflicts) for each batch :param retry_delay: time to wait before retrying to grab a batch (hoping other transactions holding locks would have released them) :param query_filter: optional function to add filtering to the query used to grab the reservations. The caller can use this to implement controlled concurrency in the reservation process: several processes can focus on different Requests, as long as they don't compete for PhysObj to reserve. The transaction is committed for each batch, and that's essential for proper operation under concurrency. """ skip = 0 while True: # TODO log.info count = 1 while True: try: requests = cls.lock_unreserved(batch_size, offset=skip, query_filter=query_filter) except cls.ReservationsLocked: # TODO log.warning if count == nb_attempts: raise time.sleep(retry_delay) count += 1 else: break if not requests: break for request in requests: if not request.reserve(): skip += 1 cls.registry.commit()
class BooleanForbidUpdate(Mixin.ConditionalForbidUpdate): forbid_update = Boolean(default=False) def check_if_forbid_update_condition_is_true(self, **previous_values): return previous_values.get('forbid_update', self.forbid_update)
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 BooleanForbidDelete(Mixin.ConditionalForbidDelete): forbid_delete = Boolean(default=False) def check_if_forbid_delete_condition_is_true(self): return self.forbid_delete
class List(Model.Web.View, Mixin.Multi): "List View" mode_name = 'List' unclickable = False id = Integer( primary_key=True, foreign_key=Model.Web.View.use('id').options(ondelete="CASCADE") ) selectable = Boolean(default=False) default_sort = String() @classmethod def define_mapper_args(cls): mapper_args = super(List, cls).define_mapper_args() mapper_args.update({ 'polymorphic_identity': 'Model.Web.View.List', }) return mapper_args @classmethod def field_for_(cls, field, fields2read, **kwargs): res = { 'name': field['id'], 'label': field['label'], 'component': 'furet-ui-list-field-' + field['type'].lower(), } if 'sortable' in kwargs: field['sortable'] = bool(eval(kwargs['sortable'], {}, {})) if 'help' in kwargs: field['tooltip'] = kwargs['help'] fields2read.append(field['id']) for k in field: if k in ('id', 'label', 'nullable', 'primary_key'): continue elif k == 'type': res['numeric'] = ( True if field['type'] in ('Integer', 'Float', 'Decimal') else False ) elif k == 'model': if field[k]: res[k] = field[k] else: res[k] = field[k] return res @classmethod def field_for_relationship(cls, field, fields2read, **kwargs): f = field.copy() Model = cls.registry.get(f['model']) Mapping = cls.registry.IO.Mapping if 'display' in kwargs: display = kwargs['display'] for op in ('!=', '==', '<', '<=', '>', '>='): display = display.replace(op, ' ') display = display.replace('!', '') fields = [] for d in display: if 'fields.' in d: fields.append(d.split('.')[1]) f['display'] = kwargs['display'] del kwargs['display'] else: fields = Model.get_display_fields(mode=cls.__registry_name__) f['display'] = " + ', ' + ".join(['fields.' + x for x in fields]) if 'action' in kwargs: action = Mapping.get('Model.Web.Action', kwargs['action']) del kwargs['action'] else: action = Model.get_default_action(mode=cls.__registry_name__) if 'menu' in kwargs: menu = Mapping.get('Model.Web.Menu', kwargs['menu']) del kwargs['menu'] else: menu = Model.get_default_menu_linked_with_action( action=action, mode=cls.__registry_name__) f['actionId'] = str(action.id) if action else None f['menuId'] = str(menu.id) if menu else None fields2read.append([field['id'], fields]) return cls.field_for_(f, [], **kwargs) @classmethod def field_for_Many2One(cls, field, fields2read, **kwargs): return cls.field_for_relationship(field, fields2read, **kwargs) @classmethod def field_for_One2One(cls, field, fields2read, **kwargs): f = field.copy() f['type'] = 'Many2One' return cls.field_for_relationship(field, fields2read, **kwargs) @classmethod def field_for_One2Many(cls, field, fields2read, **kwargs): return cls.field_for_relationship(field, fields2read, **kwargs) @classmethod def field_for_Many2Many(cls, field, fields2read, **kwargs): return cls.field_for_relationship(field, fields2read, **kwargs) @classmethod def field_for_BigInteger(cls, field, fields2read, **kwargs): f = field.copy() f['type'] = 'Integer' return cls.field_for_(f, fields2read, **kwargs) @classmethod def field_for_SmallInteger(cls, field, fields2read, **kwargs): f = field.copy() f['type'] = 'Integer' return cls.field_for_(f, fields2read, **kwargs) @classmethod def field_for_LargeBinary(cls, field, fields2read, **kwargs): f = field.copy() f['type'] = 'file' return cls.field_for_(f, fields2read, **kwargs) @classmethod def field_for_Sequence(cls, field, fields2read, **kwargs): f = field.copy() f['type'] = 'string' res = cls.field_for_(f, fields2read, **kwargs) res['readonly'] = True return res @classmethod def field_for_Selection(cls, field, fields2read, **kwargs): f = field.copy() if 'selections' in kwargs: f['selections'] = loads(kwargs['selections']) del kwargs['selections'] if 'selections' not in f: f['selections'] = {} if isinstance(f['selections'], list): f['selections'] = dict(f['selections']) return cls.field_for_(f, fields2read, **kwargs) @classmethod def field_for_UUID(cls, field, fields2read, **kwargs): f = field.copy() f['type'] = 'string' res = cls.field_for_(f, fields2read, **kwargs) res['readonly'] = True return res @classmethod def bulk_render(cls, actionId=None, viewId=None, **kwargs): action = cls.registry.Web.Action.query().get(int(actionId)) res = super(List, cls).bulk_render( actionId=actionId, viewId=viewId, **kwargs) Model = cls.registry.get(action.model) headers = [] search = [] fd = Model.fields_description() fields = list(fd.keys()) fields.sort() fields2read = [] for field_name in fields: field = fd[field_name] if field['type'] in ('FakeColumn', 'Many2Many', 'One2Many', 'Function'): continue meth = 'field_for_' + field['type'] if hasattr(cls, meth): headers.append(getattr(cls, meth)(field, fields2read)) else: headers.append(cls.field_for_(field, fields2read)) search.append({ 'key': field_name, 'label': field['label'], 'type': 'search', }) buttons2 = [{'label': b.label, 'buttonId': b.method} for b in action.buttons if b.mode == 'more'] res.update({ 'selectable': False, 'onSelect': 'Form-%d' % action.id, 'headers': headers, 'search': search, 'buttons': [], 'onSelect_buttons': buttons2, 'fields': fields2read, }) return res def render(self): res = super(List, self).render() Model = self.registry.get(self.action.model) fd = Model.fields_description() template = self.registry.furetui_views.get_template( self.template, tostring=False) fields2read = [] headers = [] for field in template.findall('.//field'): attributes = deepcopy(field.attrib) field = fd[attributes.pop('name')] _type = attributes.pop('type', field['type']) meth = 'field_for_' + _type if hasattr(self.__class__, meth): headers.append(getattr(self.__class__, meth)( field, fields2read, **attributes)) else: headers.append(self.__class__.field_for_( field, fields2read, **attributes)) buttons = [{'label': b.label, 'buttonId': b.method} for b in self.action.buttons if b.mode == 'action'] buttons2 = [{'label': b.label, 'buttonId': b.method} for b in self.action.buttons if b.mode == 'more'] colors = {c.name: c.condition for c in self.colors} res.update({ 'onSelect': self.get_form_view(), 'selectable': self.selectable, 'default_sort': self.default_sort.split(' '), 'empty': '', # TODO 'headers': headers, 'search': self.registry.Web.Action.Search.get_from_view(self), 'buttons': buttons, 'onSelect_buttons': buttons2, 'colors': colors, 'fields': fields2read, }) return res
class Regular: """A regular worker, processing time slices. A time slice is the batch operation equivalent of a day's work, or if one prefers, a team's shift. """ id = Integer(label="Identifier", primary_key=True) pid = Integer() done_timeslice = Integer(label="Latest done timeslice", nullable=False, default=0) max_timeslice = Integer(label="Greatest timeslice to run") active = Boolean() sales_per_timeslice = Integer(default=10) other = set() simulate_sleep = 10 conflicts = 0 """Used to report number of database conflicts.""" def process_one(self): """To be implemented by concrete subclasses. The default implementation is a stub that simply sleeps for a while. """ time.sleep(random.randrange(self.simulate_sleep) / 100.0) return False def begin_timeslice(self): """Do all business logic that has to be done at the timeslice start. """ @property def current_timeslice(self): done = self.done_timeslice if done is None: return 1 return done + 1 def __str__(self): # in tests, id and pid can be None, hence let's avoid %d return "Regular Worker (id=%s, pid=%s)" % (self.id, self.pid) def stop(self): self.registry.rollback() self.active = False self.registry.commit() return def run_timeslice(self): tsl = self.current_timeslice self_str = str(self) logger.info("%s, starting timeslice %d", self_str, tsl) self.begin_timeslice() logger.info( "%s, begin sequence for timeslice %d finished, now " "proceeding to normal work", self, tsl) # it's important to start with a fresh MVCC snapshot # no matter what (especially requests due to logging) self.registry.commit() proceed = True while proceed: try: op = self.process_one() self.registry.commit() if op is None: proceed = False elif op is not True: logger.info("%s, %s(id=%d) done and committed", self_str, op[0], op[1]) except KeyboardInterrupt: raise except OperationalError as exc: if isinstance(exc.orig, TransactionRollbackError): self.conflicts += 1 logger.warning("%s, got conflict: %s", self_str, exc) else: logger.exception("%s, catched exception in main loop", self_str) self.registry.rollback() except: self.registry.rollback() logger.exception("%s, exception in process_one()", self_str) self.done_timeslice = tsl if tsl == self.max_timeslice: self.active = False self.registry.commit() logger.info( "%s, finished timeslice %d. " "Cumulated number of conflicts: %d", self_str, tsl, self.conflicts) sys.stderr.flush() self.registry.session.execute("NOTIFY timeslice_finished, '%d'" % tsl) self.registry.commit() def wait_others(self, timeslice): self.registry.session.execute("LISTEN timeline_finished") while 1: self.registry.commit() conn = self.registry.session.connection().connection if select.select([conn], [], [], self.simulate_sleep) == ([], [], []): logger.warning("Timeout in LISTEN") if self.all_finished(timeslice): return else: conn.poll() while conn.notifies: notify = conn.notifies.pop(0) logger.debug( "Process %d got end signal for process_id %d, " "timeslice %s", os.getpid(), notify.pid, notify.payload) if self.all_finished(timeslice): return def all_finished(self, timeslice): cls = self.__class__ query = cls.query().filter(cls.done_timeslice < timeslice, cls.active.is_(True)) if query.count(): logger.info("%s: timeslice %d not done yet for %s", self, timeslice, [p.pid for p in query.all()]) return False return True
class Parameter: """Applications parameters. This Model is provided by ``anyblok-core`` to give applications a uniform way of specifying in-database configuration. It is a simple key/value representation, where values can be of any type that can be encoded as JSON. A simple access API is provided with the :meth:`get`, :meth:`set`, :meth:`is_exist` and further methods. """ key = String(primary_key=True) value = Json(nullable=False) multi = Boolean(default=False) @classmethod def set(cls, key, value): """ Insert or update parameter value for a key. .. note:: if the key already exists, the value will be updated :param str key: key to save :param value: value to save """ multi = False if not isinstance(value, dict): value = {'value': value} else: multi = True if cls.is_exist(key): param = cls.from_primary_keys(key=key) param.update(value=value, multi=multi) else: cls.insert(key=key, value=value, multi=multi) @classmethod def is_exist(cls, key): """ Check if one parameter exist for the key :param key: key to check :rtype: bool """ query = cls.query().filter(cls.key == key) return True if query.count() else False @classmethod def get(cls, key, default=NOT_PROVIDED): """ Return the value of the key :param key: key whose value to retrieve :param default: default value if key does not exists :return: associated value :rtype: anything JSON encodable :raises ParameterException: if the key doesn't exist and default is not set. """ if not cls.is_exist(key): if default is NOT_PROVIDED: raise ParameterException( "unexisting key %r" % key) return default param = cls.from_primary_keys(key=key) if param.multi: return param.value return param.value['value'] @classmethod def pop(cls, key): """Remove the given key and return the associated value. :param str key: the key to remove :return: the value before removal :rtype: any JSON encodable type :raises ParameterException: if the key wasn't present """ if not cls.is_exist(key): raise ParameterException( "unexisting key %r" % key) param = cls.from_primary_keys(key=key) if param.multi: res = param.value else: res = param.value['value'] param.delete() return res
class WkHtml2Pdf: id = Integer(primary_key=True, nullable=False) label = String(nullable=False) created_at = DateTime(nullable=False, default=datetime.now) updated_at = DateTime(nullable=False, default=datetime.now, auto_update=True) copies = Integer(nullable=False, default=1) grayscale = Boolean(default=False) lowquality = Boolean(default=False) dpi = Integer() page_offset = Integer(nullable=False, default=0) minimum_font_size = Integer() margin_bottom = Integer(nullable=False, default=10) margin_left = Integer(nullable=False, default=10) margin_right = Integer(nullable=False, default=10) margin_top = Integer(nullable=False, default=10) orientation = Selection(selections={ 'Landscape': 'Landscape', 'Portrait': 'Portrait' }, nullable=False, default='Portrait') page = Many2One(model='Model.Attachment.WkHtml2Pdf.Page', nullable=False) background = Boolean(default=True) collate = Boolean(default=True) encoding = String(default='utf-8', nullable=False) images = Boolean(default=True) javascript = Boolean(default=True) local_file_access = Boolean(default=True) javascript_delay = Integer(nullable=False, default=200) load_error_handling = Selection(selections='get_error_handling', default='abort', nullable=False) load_media_error_handling = Selection(selections='get_error_handling', default='abort', nullable=False) @classmethod def get_error_handling(cls): return {x: x.capitalize() for x in ('abort', 'ignore', 'skip')} @classmethod def define_table_args(cls): table_args = super(WkHtml2Pdf, cls).define_table_args() return table_args + ( CheckConstraint('copies > 0', name="copies_upper_than_0"), CheckConstraint('page_offset >= 0', name="offset_upper_than_0"), CheckConstraint('margin_bottom >= 0', name="marge_bottom_upper_than_0"), CheckConstraint('margin_left >= 0', name="marge_left_upper_than_0"), CheckConstraint('margin_right >= 0', name="marge_right_upper_than_0"), CheckConstraint('margin_top >= 0', name="marge_top_upper_than_0"), CheckConstraint('javascript_delay >= 0', name="js_delay_upper_than_0"), ) def options_from_self(self): options = [] for option in ('margin_bottom', 'margin_right', 'margin_left', 'margin_top', 'orientation', 'encoding', 'javascript_delay', 'load_error_handling', 'load_media_error_handling', 'copies', 'dpi', 'minimum_font_size'): val = getattr(self, option) if val is not None: options.append('--' + option.replace('_', '-')) options.append(str(val)) for option in ('grayscale', 'lowquality'): val = getattr(self, option) if val is not None: options.append('--' + option.replace('_', '-')) options.extend(self.page.get_options()) for option in ('background', 'images', 'collate'): val = getattr(self, option) options.append(('--' if val else '--no-') + option) for option in ('javascript', 'local_file_access'): val = getattr(self, option) options.append(('--enable-' if val else '--disable-') + option.replace('_', '-')) return options def options_from_configuration(self): options = [] if not Configuration.get('wkhtmltopdf_unquiet'): options.append('--quiet') if Configuration.get('wkhtmltopdf_debug_javascript'): options.append('--debug-javascript') else: options.append('--no-debug-javascript') return options def cast_html2pdf(self, prefix, html_content): """Cast html document to a pdf document :param prefix: prefix use for the tempory document :param html_content: html file (bytes) :rtype: bytes :exception: WkHtml2PdfException """ tmp_dir = tempfile.mkdtemp(prefix + '-html2pdf') html_path = os.path.join(tmp_dir, 'in.html') pdf_path = os.path.join(tmp_dir, 'out.pdf') with open(html_path, 'wb') as fd: fd.write(html_content) cmd = ['wkhtmltopdf'] cmd.extend(self.options_from_self()) cmd.extend(self.options_from_configuration()) cmd.extend([html_path, pdf_path]) logger.debug('Rendering PDF, cmd=%r', cmd) wkhtmltopdf = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) out, err = wkhtmltopdf.communicate() if wkhtmltopdf.returncode != 0: logger.error("wkhtmltopdf failure with stdout=%r, stderr=%r", out, err) raise WkHtml2PdfException( ("wkhtmltopdf {cmd} in dir {tmp_dir} failed with code " "{code}, check error log for details").format( cmd=' '.join(cmd), tmp_dir=tmp_dir, code=wkhtmltopdf.returncode)) logger.debug("wkhtmltopdf finished stdout=%r, stderr=%r", out, err) with open(pdf_path, 'rb') as fd: file_ = fd.read() try: shutil.rmtree(tmp_dir) except Exception: logger.warning("Could not clean up temporary directory %r", tmp_dir, exc_info=True) return file_