async def save_sample(port: core_ports.BasePort, timestamp: int) -> None: value = port.get_last_read_value() if value is None: logger.debug('skipping null sample of %s (timestamp = %s)', port, timestamp) return logger.debug('saving sample of %s (value = %s, timestamp = %s)', port, json_utils.dumps(value), timestamp) record = {'pid': port.get_id(), 'val': value, 'ts': timestamp} await persist.insert(PERSIST_COLLECTION, record)
async def get_samples_by_timestamp( port: core_ports.BasePort, timestamps: List[int]) -> Iterable[GenericJSONDict]: port_filter = { 'pid': port.get_id(), } now_ms = int(time.time() * 1000) samples_cache = _samples_cache.setdefault(port.get_id(), {}) INEXISTENT = {} query_tasks = [] for timestamp in timestamps: # Look it up in cache sample = samples_cache.get(timestamp, INEXISTENT) if sample is INEXISTENT: filt = dict(port_filter, ts={'le': timestamp}) task = persist.query(PERSIST_COLLECTION, filt=filt, sort='-ts', limit=1) else: task = asyncio.Future() task.set_result([sample]) query_tasks.append(task) task_results = await asyncio.gather(*query_tasks) samples = [] for i, task_result in enumerate(task_results): timestamp = timestamps[i] query_results = list(task_result) if query_results: sample = query_results[0] samples.append(sample) else: samples.append(None) # Add sample to cache if it's old enough if now_ms - timestamp > _CACHE_TIMESTAMP_MIN_AGE: samples_cache[timestamp] = samples[-1] return ({ 'value': r['val'], 'timestamp': r['ts'] } if r is not None else None for r in samples)
async def get_samples_slice( port: core_ports.BasePort, from_timestamp: Optional[int] = None, to_timestamp: Optional[int] = None, limit: Optional[int] = None, sort_desc: bool = False) -> Iterable[GenericJSONDict]: filt = { 'pid': port.get_id(), } if from_timestamp is not None: filt.setdefault('ts', {})['ge'] = from_timestamp if to_timestamp is not None: filt.setdefault('ts', {})['lt'] = to_timestamp sort = 'ts' if sort_desc: sort = f'-{sort}' results = await persist.query(PERSIST_COLLECTION, filt=filt, sort=sort, limit=limit) return ({'value': r['val'], 'timestamp': r['ts']} for r in results)
async def on_value_change(self, event: core_events.Event, port: core_ports.BasePort, old_value: NullablePortValue, new_value: NullablePortValue, attrs: Attributes) -> None: # When period is specified, periodic_send_values() will take care of sending values if self._period is not None: return # Look up port id -> field number mapping; if not present, values for this port are not configured for sending field_no = self._fields.get(port.get_id()) if field_no is None: return self._values_cache[field_no] = new_value # Don't send samples more often than min_period now = time.time() if now - self._last_send_time < self._min_period: return self._last_send_time = now created_at = datetime.datetime.fromtimestamp(event.get_timestamp(), tz=pytz.UTC) try: await self.send_values(self._values_cache, created_at) except Exception as e: self.error('sending values failed: %s', e, exc_info=True) self._values_cache = {}
async def on_port_remove(self, event: core_events.Event, port: core_ports.BasePort, attrs: Attributes) -> None: context = self.get_common_context(event) context.update({ 'port': port, 'attrs': attrs, 'value': port.get_last_read_value() }) await self.push_template_message(event, context)
async def on_port_update(self, event: core_events.Event, port: core_ports.BasePort, old_attrs: Attributes, new_attrs: Attributes, changed_attrs: Dict[str, Tuple[Attribute, Attribute]], added_attrs: Attributes, removed_attrs: Attributes) -> None: context = self.get_common_context(event) context.update({ 'port': port, 'old_attrs': old_attrs, 'new_attrs': new_attrs, 'changed_attrs': changed_attrs, 'added_attrs': added_attrs, 'removed_attrs': removed_attrs, 'value': port.get_last_read_value() }) await self.push_template_message(event, context)
async def check_loops(port: core_ports.BasePort, expression: Expression) -> None: seen_ports = {port} async def check_loops_rec(level: int, e: Expression) -> int: if isinstance(e, PortValue): p = e.get_port() if not p: return 0 # A loop is detected when we stumble upon the initial port at a level deeper than 1 if port is p and level > 1: return level # Avoid visiting the same port twice if p in seen_ports: return 0 seen_ports.add(p) expr = await p.get_expression() if expr: lv = await check_loops_rec(level + 1, expr) if lv: return lv return 0 elif isinstance(e, Function): for arg in e.args: lv = await check_loops_rec(level, arg) if lv: return lv return 0 if await check_loops_rec(1, expression) > 1: raise CircularDependency(port.get_id())
def force_eval_expressions(port: core_ports.BasePort = None) -> None: logger.debug('forcing expression evaluation for %s', port or 'all ports') _force_eval_expression_ports.add(port) if port: port.reset_change_reason()
async def set_port_attrs(port: core_ports.BasePort, attrs: GenericJSONDict, ignore_extra_attrs: bool) -> None: non_modifiable_attrs = await port.get_non_modifiable_attrs() def unexpected_field_code(field: str) -> str: if field in non_modifiable_attrs: return 'attribute-not-modifiable' else: return 'no-such-attribute' schema = await port.get_schema() if ignore_extra_attrs: schema = dict(schema) schema['additionalProperties'] = True # Ignore non-existent and non-modifiable attributes core_api_schema.validate( attrs, schema, unexpected_field_code=unexpected_field_code, unexpected_field_name='attribute' ) # Step validation attrdefs = await port.get_attrdefs() for name, value in attrs.items(): attrdef = attrdefs.get(name) if attrdef is None: continue step = attrdef.get('step') min_ = attrdef.get('min') if None not in (step, min_) and step != 0 and (value - min_) % step: raise core_api.APIError(400, 'invalid-field', field=name) errors_by_name = {} async def set_attr(attr_name: str, attr_value: Attribute) -> None: core_api.logger.debug('setting attribute %s = %s on %s', attr_name, json_utils.dumps(attr_value), port) try: await port.set_attr(attr_name, attr_value) except Exception as e1: errors_by_name[attr_name] = e1 value = attrs.pop('value', None) if attrs: await asyncio.wait([set_attr(n, v) for n, v in attrs.items()]) if errors_by_name: name, error = next(iter(errors_by_name.items())) if isinstance(error, core_api.APIError): raise error elif isinstance(error, core_ports.InvalidAttributeValue): raise core_api.APIError(400, 'invalid-field', field=name, details=error.details) elif isinstance(error, core_ports.PortTimeout): raise core_api.APIError(504, 'port-timeout') elif isinstance(error, core_ports.PortError): raise core_api.APIError(502, 'port-error', code=str(error)) else: # Transform any unhandled exception into APIError(500) raise core_api.APIError(500, 'unexpected-error', message=str(error)) from error # If value is supplied among attrs, use it to update port value, but in background and ignoring any errors if value is not None and port.is_enabled(): asyncio.create_task(port.write_transformed_value(value, reason=core_ports.CHANGE_REASON_API)) await port.save()