# Copyright (c) 2016 Weitian LI # MIT license """ Handle the configurations operations with the client. """ import os import logging import tornado.web from tornado.escape import json_decode, json_encode from .base import BaseRequestHandler from ...errors import ConfigError logger = logging.getLogger(__name__) class ConfigsAJAXHandler(BaseRequestHandler): """ Handle the AJAX requests from the client to manipulate the configurations. """ def initialize(self): """Hook for subclass initialization. Called for each request.""" self.configs = self.application.configmanager @tornado.web.authenticated def get(self): """ Handle the READ-ONLY configuration manipulations. Supported actions: - get: Get the specified/all configuration values - validate: Validate the configurations and response the errors NOTE ---- READ-WRITE configuration manipulations should be handled by the ``self.post()`` method. """ action = self.get_argument("action", "get") data = {} errors = {} if action == "get": keys = json_decode(self.get_argument("keys", "null")) data, errors = self._get_configs(keys=keys) success = True elif action == "validate": __, errors = self.configs.check_all(raise_exception=False) success = True else: # ERROR: bad action success = False reason = "Bad request action: {0}".format(action) # if success: response = {"action": action, "data": data, "errors": errors} logger.debug("Response: {0}".format(response)) self.set_header("Content-Type", "application/json; charset=UTF-8") self.write(json_encode(response)) else: logger.warning("Request failed: {0}".format(reason)) self.send_error(400, reason=reason) @tornado.web.authenticated def post(self): """ Handle the READ-WRITE configuration manipulations. Supported actions: - set: Set the specified configuration(s) to the posted value(s) - reset: Reset the configurations to its backup defaults - load: Load the supplied user configuration file - save: Save current configurations to file NOTE ---- READ-ONLY configuration manipulations should be handled by the ``self.get()`` method. """ request = json_decode(self.request.body) logger.debug("Received request: {0}".format(request)) action = request.get("action") data = {} errors = {} if action == "set": # Set the values of the specified options try: errors = self._set_configs(data=request["data"]) success = True except KeyError: success = False reason = "'data' is missing" elif action == "reset": # Reset the configurations to the defaults success = self._reset_configs() elif action == "load": # Load the supplied user configuration file try: success, reason = self._load_configs(request["userconfig"]) except KeyError: success = False reason = "'userconfig' is missing" elif action == "save": # Save current configurations to file try: success, reason = self._save_configs(request["outfile"], request["clobber"]) except KeyError: success = False reason = "'outfile' or 'clobber' is missing" else: # ERROR: bad action success = False reason = "Bad request action: {0}".format(action) # if success: response = {"action": action, "data": data, "errors": errors} logger.debug("Response: {0}".format(response)) self.set_header("Content-Type", "application/json; charset=UTF-8") self.write(json_encode(response)) else: logger.warning("Request failed: {0}".format(reason)) self.send_error(400, reason=reason) def _get_configs(self, keys=None): """Get the values of the config options specified by the given keys. Parameters ---------- keys : list[str], optional A list of keys specifying the config options whose values will be obtained. If ``keys=None``, then all the configurations values are dumped. Returns ------- data : dict A dictionary with keys the same as the input keys, and values the corresponding config option values. errors : dict When error occurs (e.g., invalid key), then the specific errors with details are stored in this dictionary. """ if keys is None: # Dump all the configurations data = self.configs.dump(flatten=True) data["userconfig"] = self.configs.userconfig errors = {} else: data = {} errors = {} for key in keys: if key == "userconfig": data["userconfig"] = self.configs.userconfig else: try: data[key] = self.configs.getn(key) except KeyError as e: errors[key] = str(e) # return (data, errors) def _set_configs(self, data): """ Set the values of the config options specified by the given keys to the corresponding supplied data. NOTE ---- The ``userconfig`` needs special handle. The ``workdir`` and ``configfile`` options should be ignored. Parameters ---------- data : dict A dictionary of key-value pairs, with keys specifying the config options whose value will be changed, and values the new values to which config options will be set. NOTE: If want to set the ``userconfig`` option, an *absolute path* must be provided. Returns ------- errors : dict When error occurs (e.g., invalid key, invalid values), then the specific errors with details are stored in this dictionary. """ errors = {} for key, value in data.items(): if key in ["workdir", "configfile"]: # Ignore "workdir" and "configfile" continue elif key == "userconfig": if os.path.isabs(os.path.expanduser(value)): self.configs.userconfig = value else: errors[key] = "Not an absolute path" else: try: self.configs.setn(key, value) except (KeyError, ConfigError) as e: errors[key] = str(e) # return errors def _reset_configs(self): """Reset the configurations to the defaults.""" self.configs.reset() return True def _load_configs(self, userconfig): """Load configurations from the provided user configuration file. Parameters ---------- userconfig: str The filepath to the user configuration file, which must be an *absolute path*. Returns ------- success : bool ``True`` if the operation succeeded, otherwise, ``False``. error : str If failed, this ``error`` saves the details, otherwise, ``None``. """ success = False error = None if os.path.isabs(os.path.expanduser(userconfig)): try: self.configs.read_userconfig(userconfig) success = True except ConfigError as e: error = str(e) else: error = "Not an absolute path" return (success, error) def _save_configs(self, outfile, clobber=False): """Save current configurations to file. Parameters ---------- outfile: str The filepath to the output configuration file, which must be an *absolute path*. clobber : bool, optional Whether overwrite the output file if already exists? Returns ------- success : bool ``True`` if the operation succeeded, otherwise, ``False``. error : str If failed, this ``error`` saves the details, otherwise, ``None``. """ success = False error = None try: self.configs.save(outfile, clobber=clobber) success = True except (ValueError, OSError) as e: error = str(e) return (success, error)