def _create_simulator(self, params): board = params.get('board', 'telemetry') if board == 'telemetry': message = from_json(""" { "name":"telemetry_board", "ver":"2017-09-23", "power": { "computer":1, "fan":1, "mount":1, "cameras":1, "weather":1, "main":1 }, "current": {"main":387,"fan":28,"mount":34,"cameras":27}, "amps": {"main":1083.60,"fan":50.40,"mount":61.20,"cameras":27.00}, "humidity":42.60, "temperature":[13.01,12.81,19.75], "temp_00":15.50 } """) elif board == 'camera': message = from_json(""" { "name":"camera_board", "inputs":6, "camera_00":1, "camera_01":1, "accelerometer": {"x":-7.02, "y":6.95, "z":1.70, "o": 6}, "humidity":59.60, "temperature":[13.01,12.81,19.75], "temp_00":12.50 } """) elif board == 'json_object': # Produce an output that is json, but not what we expect message = {} else: raise Exception( 'Unknown board: {}'.format(board)) # pragma: no cover # The elements of these queues are of type bytes. This means we aren't fully controlling # the baudrate unless the chunk_size is 1, but that should be OK. chunk_size = params.get('chunk_size', 20) self.json_queue = queue.Queue( maxsize=params.get('read_buffer_size', 10000)) self.relay_queue = queue.Queue( maxsize=params.get('write_buffer_size', 100)) self.device_simulator = ArduinoSimulator(message, self.relay_queue, self.json_queue, chunk_size, self.stop)
def test_quantity(): json_str = serializers.to_json(dict(foo='42 deg', bar='foo deg')) assert json_str == '{"foo": "42 deg", "bar": "foo deg"}' json_obj = serializers.from_json(json_str) # Make sure we made a quantity when we could. assert json_obj['foo'] == 42 * u.deg # And not when we can't. assert json_obj['bar'] == 'foo deg'
def find(self, collection, obj_id): collection_fn = self._get_file(collection) obj = None with suppress(FileNotFoundError): with open(collection_fn, 'r') as f: for line in f: if obj_id in line: obj = from_json(line) break return obj
def get_current(self, collection): current_fn = self._get_file(collection, permanent=False) try: with open(current_fn) as f: msg = from_json(f.read()) return msg except FileNotFoundError: self._warn("No record found for {}".format(collection)) return None
def set_config(key, new_value, host='localhost', port='6563', parse=True): """Set config item in config server. Given a `key` entry, update the config to match. The `key` is a dot accessible string, as given by [scalpl](https://pypi.org/project/scalpl/). See Examples in `get_config` for details. Examples: >>> from astropy import units as u >>> set_config('location.horizon', 35 * u.degree) {'location.horizon': <Quantity 35. deg>} >>> get_config(key='location.horizon') <Quantity 35. deg> >>> set_config('location.horizon', 30 * u.degree) {'location.horizon': <Quantity 30. deg>} Args: key (str): The key to update, see Examples in `get_config` for details. new_value (scalar|object): The new value for the key, can be any serializable object. host (str, optional): The config server host, defaults to '127.0.0.1'. port (str, optional): The config server port, defaults to 6563. parse (bool, optional): If response should be parsed by `~panoptes.utils.serializers.from_json`, default True. Returns: dict: The updated config entry. Raises: Exception: Raised if the config server is not available. """ url = f'http://{host}:{port}/set-config' json_str = serializers.to_json({key: new_value}) config_entry = None try: # We use our own serializer so pass as `data` instead of `json`. response = requests.post(url, data=json_str, headers={'Content-Type': 'application/json'}) if not response.ok: raise Exception(f'Cannot access config server: {response.text}') except Exception as e: get_root_logger().info(f'Problem with set_config: {e!r}') else: if parse: config_entry = serializers.from_json( response.content.decode('utf8')) else: config_entry = response.json() return config_entry
def receive_message(self, blocking=True, flags=0, timeout_ms=0): """Receive a message Receives a message for the current subscriber. Blocks by default, pass `flags=zmq.NOBLOCK` for non-blocking. Args: blocking (bool, optional): If True, blocks until message received or timeout__ms elapsed (if timeout_ms > 0). flag (int, optional): Any valid recv flag, e.g. zmq.NOBLOCK timeout_ms (int, optional): Time in milliseconds to wait for a message to arrive. Only applies if blocking is True. Returns: tuple(str, dict): Tuple containing the topic and a dict """ topic = None msg_obj = None if not blocking: flags = flags | zmq.NOBLOCK elif timeout_ms > 0: # Wait until a message is available or the timeout expires. # TODO(jamessynge): Remove flags=..., confirm that works with # the default flags value of zmq.POLLIN. self.socket.poll(timeout=timeout_ms, flags=(zmq.POLLIN | zmq.POLLOUT)) # Don't block at this point, because we will have waited as long # as necessary. flags = flags | zmq.NOBLOCK try: # Ugh. So ugly with the strings. message = self.socket.recv_string(flags=flags) except Exception as e: self.logger.warning(f'error in receive_message: {e!r}') else: topic, msg = message.split(' ', maxsplit=1) try: msg_obj = from_json(msg) except Exception as e: self.logger.warning(f'Error parsing message: {msg} {e!r}') return topic, msg_obj
def get_and_parse_reading(self, retry_limit=5): """Reads a line of JSON text and returns the decoded value, along with the current time. Args: retry_limit: Number of lines to read in an attempt to get one that parses as JSON. Returns: A pair (tuple) of (timestamp, decoded JSON line). The timestamp is the time of completion of the readline operation. """ for _ in range(max(1, retry_limit)): (ts, line) = self.get_reading() if not line: continue with suppress(error.InvalidDeserialization): data = from_json(line) if data: return (ts, data) return None
def read(self, timeout=5): while True: response = self.theskyx.read() if response is not None or timeout == 0: break else: time.sleep(1) timeout -= 1 # Default object. response_obj = { "response": response, "success": False, } try: response_obj = from_json(response) except (TypeError, error.InvalidDeserialization) as e: self.logger.warning(f"Error: {e!r}: {response}") return response_obj
def find(self, collection, obj_id): with self.lock: obj = self.collections.get(collection, {}).get(obj_id) if obj: obj = from_json(obj) return obj
def get_current(self, collection): with self.lock: obj = self.current.get(collection, None) if obj: obj = from_json(obj) return obj
def test_bad_deserialization(): with pytest.raises(error.InvalidDeserialization): serializers.from_json('foobar')
def test_roundtrip_json(obj): config_str = serializers.to_json(obj) config = serializers.from_json(config_str) assert config['name'] == obj['name'] assert config['location']['latitude'] == obj['location']['latitude']
def get_config(key=None, host='localhost', port='6563', parse=True, default=None): """Get a config item from the config server. Return the config entry for the given `key`. If `key=None` (default), return the entire config. Nested keys can be specified as a string, as per [scalpl](https://pypi.org/project/scalpl/). Examples: >>> get_config(key='name') 'Generic PANOPTES Unit' >>> get_config(key='location.horizon') <Quantity 30. deg> >>> get_config(key='location.horizon', parse=False) '30.0 deg' >>> get_config(key='cameras.devices[1].model') 'canon_gphoto2' >>> # Returns `None` if key is not found. >>> foobar = get_config(key='foobar') >>> foobar is None True >>> # But you can supply a default. >>> get_config(key='foobar', default='baz') 'baz' >>> # Can use Quantities as well >>> from astropy import units as u >>> get_config(key='foobar', default=42 * u.meter) <Quantity 42. m> Args: key (str): The key to update, see Examples in `get_config` for details. host (str, optional): The config server host, defaults to '127.0.0.1'. port (str, optional): The config server port, defaults to 6563. parse (bool, optional): If response should be parsed by `~panoptes.utils.serializers.from_json`, default True. default (str, optional): The config server port, defaults to 6563. Returns: dict: The corresponding config entry. Raises: Exception: Raised if the config server is not available. """ url = f'http://{host}:{port}/get-config' config_entry = default try: response = requests.post(url, json={'key': key}) except Exception as e: get_root_logger().info(f'Problem with get_config: {e!r}') else: if not response.ok: get_root_logger().info( f'Problem with get_config: {response.content!r}') else: if response.text != 'null\n': if parse: config_entry = serializers.from_json( response.content.decode('utf8')) else: config_entry = response.json() if config_entry is None: config_entry = default return config_entry
def get_config(key=None, host=None, port=None, endpoint='get-config', parse=True, default=None, verbose=True): """Get a config item from the config server. Return the config entry for the given ``key``. If ``key=None`` (default), return the entire config. Nested keys can be specified as a string, as per `scalpl <https://pypi.org/project/scalpl/>`_. Examples: .. doctest:: >>> get_config(key='name') 'Testing PANOPTES Unit' >>> get_config(key='location.horizon') <Quantity 30. deg> >>> # With no parsing, the raw string (including quotes) is returned. >>> get_config(key='location.horizon', parse=False) '"30.0 deg"' >>> get_config(key='cameras.devices[1].model') 'canon_gphoto2' >>> # Returns `None` if key is not found. >>> foobar = get_config(key='foobar') >>> foobar is None True >>> # But you can supply a default. >>> get_config(key='foobar', default='baz') 'baz' >>> # Can use Quantities as well >>> from astropy import units as u >>> get_config(key='foobar', default=42 * u.meter) <Quantity 42. m> Notes: By default all calls to this function will log at the `trace` level because there are some calls (e.g. during POCS operation) that will be quite noisy. Setting `verbose=True` changes those to `debug` log levels for an individual call. Args: key (str): The key to update, see Examples in :func:`get_config` for details. host (str, optional): The config server host. First checks for PANOPTES_CONFIG_HOST env var, defaults to 'localhost'. port (str or int, optional): The config server port. First checks for PANOPTES_CONFIG_HOST env var, defaults to 6563. endpoint (str, optional): The relative url endpoint to use for getting the config items, default 'get-config'. See `server_is_running()` for example of usage. parse (bool, optional): If response should be parsed by :func:`panoptes.utils.serializers.from_json`, default True. default (str, optional): The config server port, defaults to 6563. verbose (bool, optional): Determines the output log level, defaults to True (i.e. `debug` log level). See notes for details. Returns: dict: The corresponding config entry. Raises: Exception: Raised if the config server is not available. """ log_level = 'DEBUG' if verbose else 'TRACE' host = host or os.getenv('PANOPTES_CONFIG_HOST', 'localhost') port = port or os.getenv('PANOPTES_CONFIG_PORT', 6563) url = f'http://{host}:{port}/{endpoint}' config_entry = default try: logger.log(log_level, f'Calling get_config on url={url!r} with key={key!r}') response = requests.post(url, json={'key': key, 'verbose': verbose}) if not response.ok: # pragma: no cover raise InvalidConfig( f'Config server returned invalid JSON: response.content={response.content!r}' ) except Exception as e: logger.warning(f'Problem with get_config: {e!r}') else: response_text = response.text.strip() logger.log(log_level, f'Decoded response_text={response_text!r}') if response_text != 'null': logger.log( log_level, f'Received config key={key!r} response_text={response_text!r}' ) if parse: logger.log( log_level, f'Parsing config results: response_text={response_text!r}' ) config_entry = from_json(response_text) else: config_entry = response_text if config_entry is None: logger.log(log_level, f'No config entry found, returning default={default!r}') config_entry = default logger.log(log_level, f'Config key={key!r}: config_entry={config_entry!r}') return config_entry
def set_config(key, new_value, host=None, port=None, parse=True): """Set config item in config server. Given a `key` entry, update the config to match. The `key` is a dot accessible string, as given by `scalpl <https://pypi.org/project/scalpl/>`_. See Examples in :func:`get_config` for details. Examples: .. doctest:: >>> from astropy import units as u >>> # Can use astropy units. >>> set_config('location.horizon', 35 * u.degree) {'location.horizon': <Quantity 35. deg>} >>> get_config(key='location.horizon') <Quantity 35. deg> >>> # String equivalent works for 'deg', 'm', 's'. >>> set_config('location.horizon', '30 deg') {'location.horizon': <Quantity 30. deg>} Args: key (str): The key to update, see Examples in :func:`get_config` for details. new_value (scalar|object): The new value for the key, can be any serializable object. host (str, optional): The config server host. First checks for PANOPTES_CONFIG_HOST env var, defaults to 'localhost'. port (str or int, optional): The config server port. First checks for PANOPTES_CONFIG_HOST env var, defaults to 6563. parse (bool, optional): If response should be parsed by :func:`panoptes.utils.serializers.from_json`, default True. Returns: dict: The updated config entry. Raises: Exception: Raised if the config server is not available. """ host = host or os.getenv('PANOPTES_CONFIG_HOST', 'localhost') port = port or os.getenv('PANOPTES_CONFIG_PORT', 6563) url = f'http://{host}:{port}/set-config' json_str = to_json({key: new_value}) config_entry = None try: # We use our own serializer so pass as `data` instead of `json`. logger.info(f'Calling set_config on url={url!r}') response = requests.post(url, data=json_str, headers={'Content-Type': 'application/json'}) if not response.ok: # pragma: no cover raise Exception(f'Cannot access config server: {response.text}') except Exception as e: logger.warning(f'Problem with set_config: {e!r}') else: if parse: config_entry = from_json(response.content.decode('utf8')) else: config_entry = response.json() return config_entry