Source code for ska.mccs.utils

"""
Module for MCCS utils
"""
from functools import wraps
import json
import jsonschema
from tango import Except, ErrSeverity


[docs]def call_with_json(func, **kwargs): """ Allows the calling of a command that accepts a JSON string as input, with the actual unserialised parameters. :param func: the function to call :ptype func: callable :param kwargs: parameters to be jsonified and passed to func :ptype kwargs: any :return: the return value of func :example: Suppose you need to use MccsMaster.Allocate() to command a master device to allocate certain stations and tiles to a subarray. Allocate() accepts a single JSON string argument. Instead of parameters={"id": id, "stations": stations, "tiles": tiles} json_string=json.dumps(parameters) master.Allocate(json_string) save yourself the trouble and call_with_json(master.Allocate, id=id, stations=stations, tiles=tiles) """ return func(json.dumps(kwargs))
[docs]class json_input: """ Method decorator that parses and validates JSON input into a python object. The wrapped method is thus called with a JSON string, but can be implemented as if it had been passed an object. If the string cannot be parsed as JSON, an exception is raised. :param schema_path: an optional path to a schema against which the JSON should be validated. Not working at the moment, so leave it None. :ptype: string :example: Conceptually, MccsMaster.Allocate() takes as arguments a subarray id, an array of stations, and an array of tiles. In practice, however, these arguments are encoded into a JSON string. Implement the function with its conceptual parameters, then wrap it in this decorator: @json_input def MccsMaster.Allocate(id, stations, tiles): The decorator will provide the JSON interface and handle the decoding for you. """ def __init__(self, schema_path=None): """ Initialises a callable json_input object, to function as a device method generator. """ self.schema = None if schema_path is not None: try: with open(schema_path, 'r') as schema_file: schema_string = schema_file.read() except FileNotFoundError: self._throw( "@json_input", "JSON schema file not found at {}".format(schema_path) ) try: self.schema = json.loads(schema_string) except json.JSONDecodeError as error: self._throw( "@json_input", "Invalid JSON. Input is:\n{}\nParser error is\n{}".format( schema_string, error ) ) def __call__(self, func): """ The decorator method. Makes this class callable, and ensures that when called on a device method, a wrapped method is returned. :param func: The target of the decorator :type func: function """ @wraps(func) def wrapped(cls, json_string): json_object = self._parse( json_string, func.__name__ ) return func(cls, **json_object) return wrapped def _parse(self, json_string, origin): """ Parses and validates the JSON string input. :param json_string: a string, purportedly a JSON-encoded object :type json_string: str :param origin: the origin of this check; used to construct a helpful DevFailed exception. :type origin: str :raises json.JSONDecodeError if the string cannot be decoded as a JSON string :raises jsonschema.ValidationError if the decoded JSON object does not validate against a schema """ try: json_object = json.loads(json_string) except json.JSONDecodeError as error: self._throw( origin, "Not valid JSON. Input is:\n{}\nParser error is\n{}".format( json_string, error ) ) if self.schema is None: return json_object try: jsonschema.validate(json_object, self.schema) except jsonschema.ValidationError as error: self._throw( origin, "JSON object does not validate: {}".format(error.message) ) return json_object def _throw(self, origin, reason): """ Helper method that constructs and throws a ``Tango.DevFailed`` exception. :param origin: the origin of the error; typically the name of the command that the check was being conducted for. :type origin: string :param reason: the reason for the error :type reason: string """ Except.throw_exception( "API_CommandFailed", "{}: {}".format(origin, reason), origin, ErrSeverity.ERR )