Example #1
0
async def device(request: Request, device_id: str) -> HTTPResponse:
    """Read or write to the specified device.

    This endpoint provides read/write access to all devices via their deterministic
    GUID. The underlying implementations for read and write are the same as the
    ``/read/{device}`` and ``/write/wait/{device}`` endpoints, respectively.

    Args:
        request: The Sanic request object.
        device_id: The ID of the device that is being read from/written to.

    Returns:
        A JSON-formatted HTTP response with the possible statuses:
          * 200: OK
          * 400: Invalid JSON provided / Invalid parameter(s)
          * 404: Device not found
          * 405: Device does not support reading/writing
          * 500: Catchall processing error
    """
    if request.method == 'GET':
        try:
            return utils.http_json_response(
                await cmd.read_device(device_id),
            )
        except Exception:
            logger.exception('failed to read device', id=device_id)
            raise

    else:
        try:
            data = request.json
        except Exception as e:
            raise errors.InvalidUsage(
                'invalid json: unable to parse POSTed body as JSON'
            ) from e

        # Validate that the incoming payload has an 'action' field defined. This
        # field is required. All other fields are optional.
        if isinstance(data, dict):
            data = [data]
        for item in data:
            if 'action' not in item:
                raise errors.InvalidUsage(
                    'invalid json: key "action" is required in payload, but not found'
                )

        try:
            return utils.http_json_response(
                await cmd.write_sync(
                    device_id=device_id,
                    payload=data,
                ),
            )
        except Exception:
            logger.exception('failed to write synchronously', id=device_id, payload=data)
            raise
Example #2
0
async def read(request: Request) -> HTTPResponse:
    """Read data from devices which match the set of provided tags.

    Reading data is returned for only those devices which match all of the
    specified tags.

    Args:
        request: The Sanic request object.

    Query Parameters:
        ns: The default namespace to use for specified tags without explicit namespaces.
            Only one default namespace may be specified. (default: ``default``)
        tags: The tags to filter devices by. Multiple tag groups may be specified by
            providing multiple ``tags`` query parameters, e.g. ``?tags=foo&tags=bar``.
            Each tag group may consist of one or more comma-separated tags. Each tag
            group only selects devices which match all of the tags in the group. If
            multiple tag groups are specified, the result is the union of the matches
            from each individual tag group.
        plugin: The ID of the plugin to get device readings from. If not specified,
            all plugins are considered valid for reading.

    Returns:
        A JSON-formatted HTTP response with the possible statuses:
          * 200: OK
          * 400: Invalid parameter(s)
          * 500: Catchall processing error
    """
    namespace = 'default'
    param_ns = request.args.getlist('ns')
    if param_ns:
        if len(param_ns) > 1:
            raise errors.InvalidUsage(
                'invalid parameter: only one default namespace may be specified',
            )
        namespace = param_ns[0]

    tag_groups = []
    param_tags = request.args.getlist('tags')
    if param_tags:
        for group in param_tags:
            tag_groups.append(group.split(','))

    plugin_id = request.args.get('plugin', None)
    if plugin_id and plugin_id not in plugin.manager.plugins:
        raise errors.InvalidUsage(
            'invalid parameter: specified plugin ID does not correspond with known plugin',
        )

    try:
        return utils.http_json_response(
            await cmd.read(
                ns=namespace,
                tag_groups=tag_groups,
                plugin_id=plugin_id,
            ), )
    except Exception:
        logger.exception('failed to read device(s)',
                         namespace=namespace,
                         tag_groups=tag_groups)
        raise
Example #3
0
def test_http_json_response_from_list():
    actual = utils.http_json_response([1, 2, 3])

    assert isinstance(actual, HTTPResponse)
    assert actual.body == b'[1,2,3]'
    assert actual.status == 200
    assert actual.content_type == 'application/json'
Example #4
0
def test_http_json_response_from_dict():
    actual = utils.http_json_response({'status': 'ok'})

    assert isinstance(actual, HTTPResponse)
    assert actual.body == b'{"status":"ok"}'
    assert actual.status == 200
    assert actual.content_type == 'application/json'
Example #5
0
def test_http_json_response_from_list_pretty(mock_get):
    actual = utils.http_json_response(['a', 'b', 'c'])

    assert isinstance(actual, HTTPResponse)
    assert actual.body == b'[\n  "a",\n  "b",\n  "c"\n]\n'
    assert actual.status == 200
    assert actual.content_type == 'application/json'

    mock_get.assert_called_with('pretty_json')
Example #6
0
def test_http_json_response_from_dict_pretty(mock_get):
    actual = utils.http_json_response({'status': 'ok'})

    assert isinstance(actual, HTTPResponse)
    assert actual.body == b'{\n  "status": "ok"\n}\n'
    assert actual.status == 200
    assert actual.content_type == 'application/json'

    mock_get.assert_called_with('pretty_json')
Example #7
0
async def sync_write(request: Request, device_id: str) -> HTTPResponse:
    """Write data to a device synchronously, waiting for the write to complete.

    The length of time it takes for a write to complete depends on both the device
    and its plugin. It is up to the caller to define a sane request timeout so the
    request does not prematurely terminate.

    Args:
        request: The Sanic request object.
        device_id: The ID of the device that is being written to.

    Returns:
        A JSON-formatted HTTP response with the possible statuses:
          * 200: OK
          * 400: Invalid JSON provided
          * 404: Device not found
          * 405: Device does not support writing
          * 500: Catchall processing error
    """
    logger.debug('parsing request body', payload=request.body)

    try:
        data = request.json
    except Exception as e:
        raise errors.InvalidUsage(
            'invalid json: unable to parse POSTed body as JSON') from e

    # Validate that the incoming payload has an 'action' field defined. This
    # field is required. All other fields are optional.
    if isinstance(data, dict):
        data = [data]
    for item in data:
        if 'action' not in item:
            raise errors.InvalidUsage(
                'invalid json: key "action" is required in payload, but not found'
            )

    try:
        return utils.http_json_response(
            await cmd.write_sync(
                device_id=device_id,
                payload=data,
            ), )
    except Exception:
        logger.exception('failed to write synchronously',
                         id=device_id,
                         payload=data)
        raise
Example #8
0
async def async_write(request: Request, device_id: str) -> HTTPResponse:
    """Write data to a device in an asynchronous manner.

    The write will generate a transaction ID for each write payload to the
    specified device. The transaction can be checked later via the ``transaction``
    endpoint.

    Args:
        request: The Sanic request object.
        device_id: The ID of the device that is being written to.

    Returns:
        A JSON-formatted HTTP response with the possible statuses:
          * 200: OK
          * 400: Invalid JSON provided
          * 404: Device not found
          * 405: Device does not support writing
          * 500: Catchall processing error
    """
    logger.debug('parsing request body', payload=request.body)

    try:
        data = request.json
    except Exception as e:
        raise errors.InvalidUsage(
            'invalid json: unable to parse POSTed body as JSON') from e

    # Validate that the incoming payload has an 'action' field defined. This
    # field is required. All other fields are optional.
    if isinstance(data, dict):
        data = [data]
    for item in data:
        if 'action' not in item:
            raise errors.InvalidUsage(
                'invalid json: key "action" is required in payload, but not found'
            )

    try:
        return utils.http_json_response(
            await cmd.write_async(
                device_id=device_id,
                payload=data,
            ), )
    except Exception:
        logger.exception('failed to write asynchronously',
                         id=device_id,
                         payload=data)
        raise
Example #9
0
async def transactions(request: Request) -> HTTPResponse:
    """Get a list of all transactions currently being tracked by Synse Server.

    Args:
        request: The Sanic request object.

    Returns:
        A JSON-formatted HTTP response with the possible statuses:
          * 200: OK
          * 500: Catchall processing error
    """
    try:
        return utils.http_json_response(await cmd.transactions(), )
    except Exception:
        logger.exception('failed to list transactions')
        raise
Example #10
0
async def plugin_health(request: Request) -> HTTPResponse:
    """Get a summary of the health of registered plugins.

    Args:
        request: The Sanic request object.

    Returns:
        A JSON-formatted HTTP response with the possible statuses:
          * 200: OK
          * 500: Catchall processing error
    """
    try:
        return utils.http_json_response(await cmd.plugin_health(), )
    except Exception:
        logger.exception('failed to get plugin health')
        raise
Example #11
0
async def test(request: Request) -> HTTPResponse:
    """A dependency and side-effect free check to see whether Synse Server
    is reachable and responsive.

    This endpoint does not have any internal data dependencies. A failure
    may indicate that Synse Server is not serving (e.g. still starting up
    or experiencing a failure), or that it is not reachable on the network.

    Args:
        request: The Sanic request object.

    Returns:
        A JSON-formatted HTTP response with the possible statuses:
          * 200: OK
          * 500: Catchall processing error
    """
    return utils.http_json_response(await cmd.test(), )
Example #12
0
async def info(request: Request, device_id: str) -> HTTPResponse:
    """Get detailed information about the specified device.

    Args:
        request: The Sanic request object.
        device_id: The ID of the device to get information for.

    Returns:
        A JSON-formatted HTTP response with the possible statuses:
          * 200: OK
          * 404: Device not found
          * 500: Catchall processing error
    """
    try:
        return utils.http_json_response(await cmd.info(device_id), )
    except Exception:
        logger.exception('failed to get device info', id=device_id)
        raise
Example #13
0
async def plugin_info(request: Request, plugin_id: str) -> HTTPResponse:
    """Get detailed information on the specified plugin.

    Args:
        request: The Sanic request object.
        plugin_id: The ID of the plugin to get information for.

    Returns:
        A JSON-formatted HTTP response with the possible statuses:
          * 200: OK
          * 404: Plugin not found
          * 500: Catchall processing error
    """
    try:
        return utils.http_json_response(await cmd.plugin(plugin_id), )
    except Exception:
        logger.exception('failed to get plugin info', id=plugin_id)
        raise
Example #14
0
async def plugins(request: Request) -> HTTPResponse:
    """Get a summary of all the plugins currently registered with Synse Server.

    Args:
        request: The Sanic request object.

    Returns:
        A JSON-formatted HTTP response with the possible statuses:
          * 200: OK
          * 500: Catchall processing error
    """
    refresh = request.args.get('refresh', 'false').lower() == 'true'

    try:
        return utils.http_json_response(await cmd.plugins(refresh=refresh, ), )
    except Exception:
        logger.exception('failed to get plugins')
        raise
Example #15
0
async def transaction(request: Request, transaction_id: str) -> HTTPResponse:
    """Get the status of a write transaction.

    Args:
        request: The Sanic request object.
        transaction_id: The ID of the write transaction to get the status of.

    Returns:
        A JSON-formatted HTTP response with the possible statuses:
          * 200: OK
          * 404: Transaction not found
          * 500: Catchall processing error
    """
    try:
        return utils.http_json_response(await
                                        cmd.transaction(transaction_id), )
    except Exception:
        logger.exception('failed to get transaction info', id=transaction_id)
        raise
Example #16
0
async def version(request: Request) -> HTTPResponse:
    """Get the version information for the Synse Server instance.

    The API version provided by this endpoint should be used in subsequent
    versioned requests to the instance.

    Args:
        request: The Sanic request object.

    Returns:
        A JSON-formatted HTTP response with the possible statuses:
          * 200: OK
          * 500: Catchall processing error
    """
    try:
        return utils.http_json_response(await cmd.version(), )
    except Exception:
        logger.exception('failed to get version info')
        raise
Example #17
0
async def config(request: Request) -> HTTPResponse:
    """Get the unified configuration for the Synse Server instance.

    This endpoint is provided as a convenient way to determine the settings
    that the Synse Server instance is running with. Synse Server can be configured
    with default values, values from file, and values from the environment. This
    endpoint provides the final joined configuration of all config sources.

    Args:
        request: The Sanic request object.

    Returns:
        A JSON-formatted HTTP response with the possible statuses:
          * 200: OK
          * 500: Catchall processing error
    """
    try:
        return utils.http_json_response(await cmd.config(), )
    except Exception:
        logger.exception('failed to get server config')
        raise
Example #18
0
async def tags(request: Request) -> HTTPResponse:
    """List all of the tags which are currently associated with devices
    in the system.

    By default, all non-ID tags are listed.

    Args:
        request: The Sanic request object.

    Query Parameters:
        ns: The tag namespace(s) to use when searching for tags. If specifying multiple
            namespaces, they can be passed in as a comma-separated list, e.g.
            ``?ns=ns1,ns2,ns3``, or via multiple ``ns`` parameters, e.g.
            ``?ns=ns1&ns=ns2&ns=ns3``. (default: ``default``)
        ids: A flag which determines whether ``id`` tags are included in the
            response. (default: ``false``)

    Returns:
        A JSON-formatted HTTP response with the possible statuses:
          * 200: OK
          * 500: Catchall processing error
    """
    namespaces = []
    param_ns = request.args.getlist('ns')
    if param_ns:
        for namespace in param_ns:
            namespaces.extend(namespace.split(','))

    include_ids = request.args.get('ids', 'false').lower() == 'true'

    try:
        return utils.http_json_response(
            await cmd.tags(
                namespaces,
                with_id_tags=include_ids,
            ),
        )
    except Exception:
        logger.exception('failed to get device tags')
        raise
Example #19
0
async def read_device(request: Request, device_id: str) -> HTTPResponse:
    """Read from the specified device.

    This endpoint is equivalent to the ``read`` endpoint, specifying the ID tag
    for the device.

    Args:
        request: The Sanic request object.
        device_id: The ID of the device to read.

    Returns:
        A JSON-formatted HTTP response with the possible statuses:
          * 200: OK
          * 404: Device not found
          * 405: Device does not support reading
          * 500: Catchall processing error
    """
    try:
        return utils.http_json_response(await cmd.read_device(device_id), )
    except Exception:
        logger.exception('failed to read device', id=device_id)
        raise
Example #20
0
    def default(self, request: Request, exception: Exception) -> HTTPResponse:
        """The default error handler for exceptions.

        This handles errors which have no error handler assigned. Synse Server
        does not register any other custom error handlers, so all exceptions
        raised by the application will be caught and handled here.
        """
        logger.info('creating error response for request', error=exception)

        if isinstance(exception, SanicException):
            return super(SynseErrorHandler, self).default(request, exception)

        if not isinstance(exception, SynseError):
            # Setting __cause__ on the exception is effectively the same
            # as what happens when you `raise NewException() from old_exception
            new = SynseError(str(exception))
            new.__cause__ = exception
            exception = new

        return utils.http_json_response(
            body=exception.make_response(),
            status=exception.http_code,
        )
Example #21
0
async def scan(request: Request) -> HTTPResponse:
    """List the devices that Synse knows about.

    This endpoint provides an aggregated view of all devices exposed to
    Synse Server by each of the registered plugins. By default, the scan
    results are sorted by a combination key of 'plugin,sortIndex,id'.

    Args:
        request: The Sanic request object.

    Query Parameters:
        ns: The default namespace to use for specified tags without explicit namespaces.
            Only one default namespace may be specified. (default: ``default``)
        tags: The tags to filter devices by. Multiple tag groups may be specified by
            providing multiple ``tags`` query parameters, e.g. ``?tags=foo&tags=bar``.
            Each tag group may consist of one or more comma-separated tags. Each tag
            group only selects devices which match all of the tags in the group. If
            multiple tag groups are specified, the result is the union of the matches
            from each individual tag group.
        force: Force a re-scan (rebuild the internal cache). This will take longer than
            a scan which uses the cache. (default: false)
        sort: Specify the fields to sort by. Multiple fields may be specified as a
            comma-separated string, e.g. "plugin,id". The "tags" field can not be used
            for sorting. (default: "plugin,sortIndex,id", where ``sortIndex`` is an
            internal sort preference which a plugin can optionally specify.)

    Returns:
        A JSON-formatted HTTP response with the possible statuses:
          * 200: OK
          * 400: Invalid parameter(s)
          * 500: Catchall processing error
    """
    namespace = 'default'
    param_ns = request.args.getlist('ns')
    if param_ns:
        if len(param_ns) > 1:
            raise errors.InvalidUsage(
                'invalid parameter: only one namespace may be specified', )
        namespace = param_ns[0]

    tag_groups = []
    param_tags = request.args.getlist('tags')
    if param_tags:
        for group in param_tags:
            tag_groups.append(group.split(','))

    force = request.args.get('force', 'false').lower() == 'true'

    sort_keys = 'plugin,sortIndex,id'
    param_sort = request.args.getlist('sort')
    if param_sort:
        if len(param_sort) > 1:
            raise errors.InvalidUsage(
                'invalid parameter: only one sort key may be specified', )
        sort_keys = param_sort[0]

    try:
        return utils.http_json_response(
            await cmd.scan(
                ns=namespace,
                tag_groups=tag_groups,
                force=force,
                sort=sort_keys,
            ), )
    except Exception:
        logger.exception('failed to get devices (scan)')
        raise