async def patch_port(request: core_api.APIRequest, port_id: str, params: Attributes) -> None: port = core_ports.get(port_id) if port is None: raise core_api.APIError(404, 'no-such-port') await set_port_attrs(port, params, ignore_extra_attrs=False)
async def delete_port_history(request: core_api.APIRequest, port_id: str) -> None: port = core_ports.get(port_id) if port is None: raise core_api.APIError(404, 'no-such-port') query = request.query from_str = query.get('from') if from_str is None: raise core_api.APIError(400, 'missing-field', field='from') try: from_timestamp = int(from_str) except ValueError: raise core_api.APIError(400, 'invalid-field', field='from') from None if from_timestamp < 0: raise core_api.APIError(400, 'invalid-field', field='from') to_str = query.get('to') if to_str is None: raise core_api.APIError(400, 'missing-field', field='to') try: to_timestamp = int(to_str) except ValueError: raise core_api.APIError(400, 'invalid-field', field='to') from None if to_timestamp < 0: raise core_api.APIError(400, 'invalid-field', field='to') await core_history.remove_samples([port], from_timestamp, to_timestamp)
async def periodic_send_values(self) -> None: while True: ports = [ core_ports.get(port_id) for port_id in self._fields.keys() ] port_values = {p.get_id(): p.get_last_read_value() for p in ports} field_values = { self._fields[id_]: value for id_, value in port_values.items() } try: if field_values: await self.send_values(field_values, datetime.datetime.utcnow()) else: self.debug('not sending empty values') except asyncio.CancelledError: self.debug('periodic send values loop cancelled') break except Exception as e: self.error('sending values failed: %s', e, exc_info=True) await asyncio.sleep(self._period)
async def patch_port(request: core_api.APIRequest, port_id: str, params: Attributes) -> None: port = core_ports.get(port_id) if port is None: raise core_api.APIError(404, 'no such port') def unexpected_field_msg(field: str) -> str: if field in port.get_non_modifiable_attrs(): return 'attribute not modifiable: {field}' else: return 'no such attribute: {field}' core_api_schema.validate(params, await port.get_schema(), unexpected_field_msg=unexpected_field_msg) # Step validation for name, value in params.items(): attrdef = port.ATTRDEFS[name] 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, f'invalid 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 e: errors_by_name[attr_name] = e if params: await asyncio.wait([set_attr(name, value) for name, value in params.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, f'invalid field: {name}') elif isinstance(error, core_ports.PortTimeout): raise core_api.APIError(504, 'port timeout') elif isinstance(error, core_ports.PortError): raise core_api.APIError(502, f'port error: {error}') else: # Transform any unhandled exception into APIError(500) raise core_api.APIError(500, str(error)) await port.save()
async def delete_port(request: core_api.APIRequest, port_id: str) -> None: port = core_ports.get(port_id) if not port: raise core_api.APIError(404, 'no-such-port') if not isinstance(port, core_vports.VirtualPort): raise core_api.APIError(400, 'port-not-removable') await port.remove() await core_vports.remove(port_id)
async def patch_port_value(request: core_api.APIRequest, port_id: str, params: PortValue) -> None: port = core_ports.get(port_id) if port is None: raise core_api.APIError(404, 'no-such-port') try: core_api_schema.validate(params, await port.get_value_schema()) except core_api.APIError: # Transform any validation error into an invalid-field APIError for value raise core_api.APIError(400, 'invalid-value') from None value = params # Step validation step = await port.get_attr('step') min_ = await port.get_attr('min') if None not in (step, min_) and step != 0 and (value - min_) % step: raise core_api.APIError(400, 'invalid-value') if not port.is_enabled(): raise core_api.APIError(400, 'port-disabled') if not await port.is_writable(): raise core_api.APIError(400, 'read-only-port') old_value = port.get_last_read_value() try: await port.write_transformed_value(value, reason=core_ports.CHANGE_REASON_API) except core_ports.PortTimeout as e: raise core_api.APIError(504, 'port-timeout') from e except core_ports.PortError as e: raise core_api.APIError(502, 'port-error', code=str(e)) from e except core_api.APIError: raise except Exception as e: # Transform any unhandled exception into APIError(500) raise core_api.APIError(500, 'unexpected-error', message=str(e)) from e await core_main.update() # If port value hasn't really changed, use 202 Accepted to inform consumer that new value hasn't been applied yet current_value = port.get_last_read_value() if (old_value == current_value) and (old_value != value): port.debug("API supplied value hasn't been applied right away") raise core_api.APIAccepted()
async def get_port_value(request: core_api.APIRequest, port_id: str) -> NullablePortValue: port = core_ports.get(port_id) if port is None: raise core_api.APIError(404, 'no-such-port') if not port.is_enabled(): return # TODO # Given the fact that get_last_read_value() simply returns last cached read value, and the fact that API specs # indicate that 502/504 be returned by GET /port/[id]/value in case of errors, we should remember the last error # generated by read_value() and return it here, if any. return port.get_last_read_value()
async def patch_port_value(request: core_api.APIRequest, port_id: str, params: PortValue) -> None: port = core_ports.get(port_id) if port is None: raise core_api.APIError(404, 'no such port') core_api_schema.validate(params, await port.get_value_schema(), invalid_request_msg='invalid value') value = params # Step validation step = await port.get_attr('step') _min = await port.get_attr('min') if None not in (step, _min) and step != 0 and (value - _min) % step: raise core_api.APIError(400, 'invalid field: value') if not port.is_enabled(): raise core_api.APIError(400, 'port disabled') if not await port.is_writable(): raise core_api.APIError(400, 'read-only port') old_value = port.get_value() try: await port.write_transformed_value(value, reason=core_ports.CHANGE_REASON_API) except core_ports.PortTimeout as e: raise core_api.APIError(504, 'port timeout') from e except core_ports.PortError as e: raise core_api.APIError(502, f'port error: {e}') from e except core_api.APIError: raise except Exception as e: # Transform any unhandled exception into APIError(500) raise core_api.APIError(500, str(e)) from e await main.update() # If port value hasn't really changed, trigger a value-change to inform consumer that new value has been ignored current_value = port.get_value() if old_value == current_value: port.debug('API supplied value was ignored') port.trigger_value_change()
async def patch_port_sequence(request: core_api.APIRequest, port_id: str, params: GenericJSONDict) -> None: port = core_ports.get(port_id) if port is None: raise core_api.APIError(404, 'no-such-port') core_api_schema.validate(params, core_api_schema.PATCH_PORT_SEQUENCE) values = params['values'] delays = params['delays'] repeat = params['repeat'] if len(values) != len(delays): raise core_api.APIError(400, 'invalid-field', field='delays') value_schema = await port.get_value_schema() step = await port.get_attr('step') min_ = await port.get_attr('min') for value in values: # Translate any APIError generated when validating value schema into an invalid-field APIError on value try: core_api_schema.validate(value, value_schema) except core_api.APIError: raise core_api.APIError(400, 'invalid-field', field='values') from None # Step validation if None not in (step, min_) and step != 0 and (value - min_) % step: raise core_api.APIError(400, 'invalid-field', field='values') if not port.is_enabled(): raise core_api.APIError(400, 'port-disabled') if not await port.is_writable(): raise core_api.APIError(400, 'read-only-port') if await port.get_attr('expression'): raise core_api.APIError(400, 'port-with-expression') try: await port.set_sequence(values, delays, repeat) except Exception as e: # Transform any unhandled exception into APIError(500) raise core_api.APIError(500, 'unexpected-error', message=str(e)) from e
async def post_port_sequence(request: core_api.APIRequest, port_id: str, params: GenericJSONDict) -> None: port = core_ports.get(port_id) if port is None: raise core_api.APIError(404, 'no such port') core_api_schema.validate(params, core_api_schema.POST_PORT_SEQUENCE) values = params['values'] delays = params['delays'] repeat = params['repeat'] if len(values) != len(delays): raise core_api.APIError(400, 'invalid field: delays') value_schema = await port.get_value_schema() step = await port.get_attr('step') _min = await port.get_attr('min') for value in values: core_api_schema.validate(value, value_schema, invalid_request_msg='invalid field: values') # Step validation if None not in (step, _min) and step != 0 and (value - _min) % step: raise core_api.APIError(400, 'invalid field: values') if not port.is_enabled(): raise core_api.APIError(400, 'port disabled') if not await port.is_writable(): raise core_api.APIError(400, 'read-only port') if await port.get_attr('expression'): raise core_api.APIError(400, 'port with expression') try: await port.set_sequence(values, delays, repeat) except Exception as e: # Transform any unhandled exception into APIError(500) raise core_api.APIError(500, str(e)) from e
async def add_virtual_port(attrs: GenericJSONDict) -> core_ports.BasePort: id_ = attrs['id'] type_ = attrs['type'] min_ = attrs.get('min') max_ = attrs.get('max') integer = attrs.get('integer') step = attrs.get('step') choices = attrs.get('choices') core_api.logger.debug('adding port "%s"', id_) if core_ports.get(id_): raise core_api.APIError(400, 'duplicate-port') if len(core_vports.all_port_args()) >= settings.core.virtual_ports: raise core_api.APIError(400, 'too-many-ports') await core_vports.add(id_, type_, min_, max_, integer, step, choices) port = await core_ports.load_one( 'qtoggleserver.core.vports.VirtualPort', { 'id_': id_, 'type_': type_, 'min_': min_, 'max_': max_, 'integer': integer, 'step': step, 'choices': choices }, trigger_add= False # Will trigger add event manually, later, after we've enabled the port ) # A virtual port is enabled by default await port.enable() await port.save() await port.trigger_add() return port
async def post_ports(request: core_api.APIRequest, params: GenericJSONDict) -> Attributes: core_api_schema.validate(params, core_api_schema.POST_PORTS) port_id = params['id'] port_type = params['type'] _min = params.get('min') _max = params.get('max') integer = params.get('integer') step = params.get('step') choices = params.get('choices') if core_ports.get(port_id): raise core_api.APIError(400, 'duplicate port') if len(core_vports.all_settings()) >= settings.core.virtual_ports: raise core_api.APIError(400, 'too many ports') core_vports.add(port_id, port_type, _min, _max, integer, step, choices) port = await core_ports.load_one( 'qtoggleserver.core.vports.VirtualPort', { 'port_id': port_id, '_type': port_type, '_min': _min, '_max': _max, 'integer': integer, 'step': step, 'choices': choices } ) # A virtual port is enabled by default await port.enable() await port.save() return await port.to_json()
async def get_port_history(request: core_api.APIRequest, port_id: str) -> GenericJSONList: port = core_ports.get(port_id) if port is None: raise core_api.APIError(404, 'no-such-port') query = request.query from_str = query.get('from') timestamps_str = query.get('timestamps') if from_str is None and timestamps_str is None: raise core_api.APIError(400, 'missing-field', field='from') if from_str: try: from_timestamp = int(from_str) except ValueError: raise core_api.APIError(400, 'invalid-field', field='from') if from_timestamp < 0: raise core_api.APIError(400, 'invalid-field', field='from') else: from_timestamp = None to_str = query.get('to') to_timestamp = int(time.time() * 1000) if to_str is not None: try: to_timestamp = int(to_str) except ValueError: raise core_api.APIError(400, 'invalid-field', field='to') from None if to_timestamp < 0: raise core_api.APIError(400, 'invalid-field', field='to') limit_str = query.get('limit') limit = 1000 # default if limit_str is not None: try: limit = int(limit_str) except ValueError: raise core_api.APIError(400, 'invalid-field', field='limit') from None if limit < 1 or limit > 10000: raise core_api.APIError(400, 'invalid-field', field='limit') timestamps = None if timestamps_str is not None: timestamps = timestamps_str.split(',') try: timestamps = [int(t) for t in timestamps] except ValueError: raise core_api.APIError(400, 'invalid-field', field='timestamps') if any((t < 0) for t in timestamps): raise core_api.APIError(400, 'invalid-field', field='timestamps') if timestamps is not None: samples = await core_history.get_samples_by_timestamp(port, timestamps) else: samples = await core_history.get_samples_slice(port, from_timestamp, to_timestamp, limit) return list(samples)
async def put_ports(request: core_api.APIRequest, params: GenericJSONList) -> None: if not settings.core.backup_support: raise core_api.APIError(404, 'no-such-function') core_api_schema.validate( params, core_api_schema.PUT_PORTS ) core_api.logger.debug('restoring ports') # Disable event handling during the processing of this request, as we're going to trigger a full-update at the end core_events.disable() # Temporarily disable core updating (port polling, expression evaluating and value-change handling) core_main.disable_updating() try: # Remove all (local) virtual ports for port in core_ports.get_all(): if not isinstance(port, core_vports.VirtualPort): continue await port.remove() await core_vports.remove(port.get_id()) # Reset ports await core_ports.reset() if settings.slaves.enabled: await slaves.reset_ports() for port in core_ports.get_all(): await port.reset() add_port_schema = dict(core_api_schema.POST_PORTS) add_port_schema['additionalProperties'] = True # Restore supplied attributes for attrs in params: id_ = attrs.get('id') if id_ is None: core_api.logger.warning('ignoring entry without id') continue port = core_ports.get(id_) # Virtual ports must be added first (unless they belong to a slave) virtual = attrs.get('virtual') if port is not None: # Port already exists so it probably belongs to a slave virtual = False for slave in slaves_devices.get_all(): if id_.startswith(f'{slave.get_name()}.'): # id indicates that port belongs to a slave virtual = False break if 'provisioning' in attrs: # A clear indication that port belongs to a slave virtual = False if virtual: await wrap_error_with_port_id( id_, core_api_schema.validate, attrs, add_port_schema ) port = await wrap_error_with_port_id( id_, add_virtual_port, attrs ) if port is None: core_api.logger.warning('ignoring unknown port id "%s"', id_) continue if isinstance(port, slaves_ports.SlavePort): core_api.logger.debug('restoring slave port "%s"', id_) # For slave ports, ignore any attributes that are not kept on master attrs = {n: v for n, v in attrs.items() if n in ('tag', 'expression', 'expires')} else: core_api.logger.debug('restoring local port "%s"', id_) await wrap_error_with_port_id( id_, set_port_attrs, port, attrs, ignore_extra_attrs=True ) finally: core_main.enable_updating() core_events.enable() await core_events.trigger_full_update() core_api.logger.debug('ports restore done')
def get_port(self) -> core_ports.BasePort: return core_ports.get(self.port_id)