diff --git a/photoapp/cli.py b/photoapp/cli.py index 28f363d..0ec9417 100644 --- a/photoapp/cli.py +++ b/photoapp/cli.py @@ -14,15 +14,16 @@ import appdirs APPNAME = "photocli" -DEFAULT_CONFIG = {"uri": None, - "lastbatch": None} +DEFAULT_CONFIG = {"context": "default", + "contexts": {"default": {"uri": None, + "upload_workers": 2}}} class CliConfig(dict): - def __init__(self, appname, default): + def __init__(self, appname): self.confdir = appdirs.user_config_dir(appname) - self.confpath = os.path.join(self.confdir, "cli.json") # ~/Library/Application Support/name/cli.json - super().__init__(**default) + self.confpath = os.path.join(self.confdir, "cli.json") # ~/Library/Application Support/photocli/cli.json + super().__init__(**DEFAULT_CONFIG) self.init_config() def init_config(self): @@ -41,11 +42,41 @@ class CliConfig(dict): json.dump(self, f, indent=4) def __setitem__(self, key, value): + """ + prevents setting of invalid config keys as they are not defined + """ if key not in self: raise KeyError(key) super().__setitem__(key, value) self.write() + @property + def context(self): + return self["context"] + + def set_server(self, context_name, server_uri): + self["contexts"][context_name]["uri"] = server_uri + self.write() + + def select_context(self, context_name): + if context_name not in self["contexts"]: + raise KeyError("Context '{}' is not defined".format(context_name)) + return self["contexts"][context_name] + + def set_context(self, context_name): + if context_name not in self["contexts"]: + raise KeyError("Context '{}' is not defined".format(context_name)) + self["context"] = context_name + self.write() + + def edit_context(self, context_name, context): + self["contexts"][context_name] = context + self.write() + + def delete_context(self, context_name): + del self["context_name"] + self.write() + class PhotoApiClient(object): def __init__(self, base_url, passwd=None): @@ -135,6 +166,8 @@ def get_args(): parser = argparse.ArgumentParser(description="photo library cli") # TODO nicer uri parser parser.add_argument("--host", default=os.environ.get("PHOTOLIB_URI", None), help="photo api server address") + parser.add_argument("--context", help="api server context defined in config file", + default=os.environ.get("PHOTOLIB_CONTEXT", None)) parser.add_argument("-y", "--yes", action="store_true", help="assume yes for all prompts") sp_action = parser.add_subparsers(dest="action", help="action to take") @@ -189,20 +222,91 @@ def get_args(): p_delete = p_useraction.add_parser('delete', help='delete users') p_delete.add_argument("-u", "--username", help="username", required=True) + p_ctx = sp_action.add_parser("ctx", help="change server contexts") + p_ctx.add_argument("new_context", help="context name") + + p_editctx = sp_action.add_parser("update-context", help="create/edit server contexts") + p_editctx.add_argument("context_name", help="context name to modify") + p_editctx.add_argument("--set-host", help="photo api server address") + p_editctx.add_argument("--delete", action="store_true", help="delete the context") + + sp_action.add_parser("list-contexts", help="list available contexts") + return parser.parse_args(), parser def main(): - config = CliConfig(APPNAME, DEFAULT_CONFIG) args, parser = get_args() + config = CliConfig(APPNAME) - uri = urlparse(args.host or config["uri"]) + # figure out the selected context - first the --context flag, then conf file, then "default" + context = args.context or config.context + + # default context is "sticky" and remembers the last --host used + if args.host and context == "default": # TODO move this after parsing + new_default = urlparse(args.host) + if not new_default.netloc: + parser.error("--host uri is invalid") + config.set_server("default", new_default.geturl()) + + if args.host and context != "default": + parser.error("--host not allowed for non-default contexts. Use `ctx` to switch to the default context or use " + "`update-context --host HOST` to create a new context.") + + # these commands manipulate the config file so we don't check it yet + # change or edit contexts + if args.action == "ctx": + try: + config.set_context(args.new_context) + except KeyError as ke: + parser.error("{}. Use `update-context` to define it.".format(ke.args[0])) + return + + elif args.action == "update-context": + try: + ctx = config.select_context(args.context_name) + except KeyError: + ctx = DEFAULT_CONFIG["contexts"]["default"] + + if args.set_host: + new_uri = urlparse(args.set_host) + if not new_uri.netloc: + parser.error("--set-host uri is invalid") + ctx["uri"] = new_uri.geturl() + + if args.delete: + try: + config.delete_context(args.context_name) + print("Context deleted") + except KeyError: + pass + return + + if not ctx["uri"]: + parser.error("New context must include a host, use `--set-host`.") + + config.edit_context(args.context_name, ctx) + print("Context updated") + return + + elif args.action == "list-contexts": + for entry in sorted(config["contexts"].keys()): + print("{}{}".format(entry, " (active)" if entry == context else "")) + return + + # commands below here need a valid config file and api client + # validate the config + try: + server_conf = config.select_context(context) + except KeyError as ke: + parser.error("{}. Use `update-context` to define it.".format(ke.args[0])) + + uri = urlparse(server_conf["uri"]) if not uri.netloc: parser.error("need --host, $PHOTOLIB_URI, or config file") - config["uri"] = uri.geturl() - + # create the client port = f":{uri.port}" if uri.port else "" client = PhotoApiClient(f"{uri.scheme}://{uri.hostname}{port}", (uri.username, uri.password, ))