From d72c7c144e2d66cdb3fb09f54ea4149146bb212d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Fri, 31 Mar 2017 18:48:13 +0200 Subject: [PATCH] Make octoprint_client support multiple instances Adjusted cli implementation accordingly --- src/octoprint/cli/client.py | 75 ++++++--- src/octoprint_client/__init__.py | 270 ++++++++++++++----------------- 2 files changed, 171 insertions(+), 174 deletions(-) diff --git a/src/octoprint/cli/client.py b/src/octoprint/cli/client.py index cafcd181..070806ad 100644 --- a/src/octoprint/cli/client.py +++ b/src/octoprint/cli/client.py @@ -29,6 +29,7 @@ def client_commands(): @client_commands.group("client", context_settings=dict(ignore_unknown_options=True)) +@click.option("--apikey", "-a", type=click.STRING) @click.option("--host", "-h", type=click.STRING) @click.option("--port", "-p", type=click.INT) @click.option("--httpuser", type=click.STRING) @@ -36,11 +37,29 @@ def client_commands(): @click.option("--https", is_flag=True) @click.option("--prefix", type=click.STRING) @click.pass_context -def client(ctx, host, port, httpuser, httppass, https, prefix): +def client(ctx, apikey, host, port, httpuser, httppass, https, prefix): """Basic API client.""" try: - ctx.obj.settings = init_settings(get_ctx_obj_option(ctx, "basedir", None), get_ctx_obj_option(ctx, "configfile", None)) - octoprint_client.init_client(ctx.obj.settings, https=https, httpuser=httpuser, httppass=httppass, host=host, port=port, prefix=prefix) + if not host or not port or not apikey: + settings = init_settings(get_ctx_obj_option(ctx, "basedir", None), get_ctx_obj_option(ctx, "configfile", None)) + + if not host: + host = settings.get(["server", "host"]) + host = host if host != "0.0.0.0" else "127.0.0.1" + if not port: + port = settings.getInt(["server", "port"]) + + if not apikey: + apikey = settings.get(["api", "key"]) + + baseurl = octoprint_client.build_base_url(https=https, + httpuser=httpuser, + httppass=httppass, + host=host, + port=port, + prefix=prefix) + + ctx.obj.client = octoprint_client.Client(baseurl, apikey) except FatalStartupError as e: click.echo(e.message, err=True) click.echo("There was a fatal error initializing the client.", err=True) @@ -60,27 +79,30 @@ def log_response(response, status_code=True, body=True, headers=False): @client.command("get") @click.argument("path") -def get(path): +@click.pass_context +def get(ctx, path): """Performs a GET request against the specified server path.""" - r = octoprint_client.get(path) + r = ctx.obj.client.get(path) log_response(r) @client.command("post_json") @click.argument("path") @click.argument("data", type=JsonStringParamType()) -def post_json(path, data): +@click.pass_context +def post_json(ctx, path, data): """POSTs JSON data to the specified server path.""" - r = octoprint_client.post_json(path, data) + r = ctx.obj.client.post_json(path, data) log_response(r) @client.command("patch_json") @click.argument("path") @click.argument("data", type=JsonStringParamType()) -def patch_json(path, data): +@click.pass_context +def patch_json(ctx, path, data): """PATCHes JSON data to the specified server path.""" - r = octoprint_client.patch(path, data, encoding="json") + r = ctx.obj.client.patch(path, data, encoding="json") log_response(r) @@ -89,7 +111,8 @@ def patch_json(path, data): @click.argument("file_path", type=click.Path(exists=True, dir_okay=False, resolve_path=True)) @click.option("--json", is_flag=True) @click.option("--yaml", is_flag=True) -def post_from_file(path, file_path, json_flag, yaml_flag): +@click.pass_context +def post_from_file(ctx, path, file_path, json_flag, yaml_flag): """POSTs JSON data to the specified server path.""" if json_flag or yaml_flag: if json_flag: @@ -100,12 +123,12 @@ def post_from_file(path, file_path, json_flag, yaml_flag): with open(file_path, "rb") as fp: data = yaml.safe_load(fp) - r = octoprint_client.post_json(path, data) + r = ctx.obj.client.post_json(path, data) else: with open(file_path, "rb") as fp: data = fp.read() - r = octoprint_client.post(path, data) + r = ctx.obj.client.post(path, data) log_response(r) @@ -117,13 +140,14 @@ def post_from_file(path, file_path, json_flag, yaml_flag): @click.option("--int", "-i", "int_params", multiple=True, nargs=2, type=click.Tuple([unicode, int])) @click.option("--float", "-f", "float_params", multiple=True, nargs=2, type=click.Tuple([unicode, float])) @click.option("--bool", "-b", "bool_params", multiple=True, nargs=2, type=click.Tuple([unicode, bool])) -def command(path, command, str_params, int_params, float_params, bool_params): +@click.pass_context +def command(ctx, path, command, str_params, int_params, float_params, bool_params): """Sends a JSON command to the specified server path.""" data = dict() params = str_params + int_params + float_params + bool_params for param in params: data[param[0]] = param[1] - r = octoprint_client.post_command(path, command, additional=data) + r = ctx.obj.client.post_command(path, command, additional=data) log_response(r, body=False) @@ -133,26 +157,29 @@ def command(path, command, str_params, int_params, float_params, bool_params): @click.option("--parameter", "-P", "params", multiple=True, nargs=2, type=click.Tuple([unicode, unicode])) @click.option("--file-name", type=click.STRING) @click.option("--content-type", type=click.STRING) -def upload(path, file_path, params, file_name, content_type): +@click.pass_context +def upload(ctx, path, file_path, params, file_name, content_type): """Uploads the specified file to the specified server path.""" data = dict() for param in params: data[param[0]] = param[1] - r = octoprint_client.upload(path, file_path, additional=data, file_name=file_name, content_type=content_type) + r = ctx.obj.client.upload(path, file_path, additional=data, file_name=file_name, content_type=content_type) log_response(r) @client.command("delete") @click.argument("path") -def delete(path): +@click.pass_context +def delete(ctx, path): """Sends a DELETE request to the specified server path.""" - r = octoprint_client.delete(path) + r = ctx.obj.client.delete(path) log_response(r) @client.command("listen") -def listen(): +@click.pass_context +def listen(ctx): def on_connect(ws): click.echo(">>> Connected!") @@ -168,11 +195,11 @@ def listen(): def on_message(ws, message_type, message_payload): click.echo("Message: {}, Payload: {}".format(message_type, json.dumps(message_payload))) - socket = octoprint_client.connect_socket(on_connect=on_connect, - on_close=on_close, - on_error=on_error, - on_heartbeat=on_heartbeat, - on_message=on_message) + socket = ctx.obj.client.create_socket(on_connect=on_connect, + on_close=on_close, + on_error=on_error, + on_heartbeat=on_heartbeat, + on_message=on_message) click.echo(">>> Waiting for client to exit") try: diff --git a/src/octoprint_client/__init__.py b/src/octoprint_client/__init__.py index b62a144c..e5fb3cdd 100644 --- a/src/octoprint_client/__init__.py +++ b/src/octoprint_client/__init__.py @@ -8,8 +8,15 @@ __copyright__ = "Copyright (C) 2015 The OctoPrint Project - Released under terms import requests import time -apikey = None -baseurl = None + +def build_base_url(https=False, httpuser=None, httppass=None, host=None, port=None, prefix=None): + protocol = "https" if https else "http" + httpauth = "{}:{}@".format(httpuser, httppass) if httpuser and httppass else "" + host = host if host else "127.0.0.1" + port = ":{}".format(port) if port else ":5000" + prefix = prefix if prefix else "" + + return "{}://{}{}{}{}".format(protocol, httpauth, host, port, prefix) class SocketTimeout(BaseException): @@ -202,183 +209,146 @@ class SocketClient(object): # a reconnect, that's a failure return False +class Client(object): -def build_base_url(https=False, httpuser=None, httppass=None, host=None, port=None, prefix=None): - protocol = "https" if https else "http" - httpauth = "{}:{}@".format(httpuser, httppass) if httpuser and httppass else "" - host = host if host else "127.0.0.1" - port = ":{}".format(port) if port else ":5000" - prefix = prefix if prefix else "" + def __init__(self, baseurl, apikey): + self.baseurl = baseurl + self.apikey = apikey - return "{}://{}{}{}{}".format(protocol, httpauth, host, port, prefix) + def prepare_request(self, method=None, path=None, params=None): + url = None + if self.baseurl: + while path.startswith("/"): + path = path[1:] + url = self.baseurl + "/" + path + return requests.Request(method=method, url=url, params=params, headers={"X-Api-Key": self.apikey}).prepare() + def request(self, method, path, data=None, files=None, encoding=None, params=None): + s = requests.Session() + request = self.prepare_request(method, path, params=params) + if data or files: + if encoding == "json": + request.prepare_body(None, None, json=data) + else: + request.prepare_body(data, files=files) + response = s.send(request) + return response -def init_client(settings, https=False, httpuser=None, httppass=None, host=None, port=None, prefix=None): - """ - Initializes the API client with the provided settings. + def get(self, path, params=None): + return self.request("GET", path, params=params) - Basically a convenience method to set ``apikey`` and ``baseurl`` from settings - and/or command line arguments. + def post(self, path, data, encoding=None, params=None): + return self.request("POST", path, data=data, encoding=encoding, params=params) - Arguments: - settings (octoprint.settings.Settings): A :class:`~octoprint.settings.Settings` instance to use - for client configuration - https (bool): Whether to connect via HTTPS (True) or not (False, default) - httpuser (str or None): HTTP Basic Auth username to use. No Basic Auth will be - used if unset. - httppass (str or None): HTTP Basic Auth password to use. No Basic Auth will be - used if unset. - host (str or None): Host to connect to, overrides data from settings if set. - port (int or None): Port to connect to, overrides data from settings if set. - prefix (str or None): Path prefix, overrides data from settings if set. - """ - settings_host = settings.get(["server", "host"]) - settings_port = settings.getInt(["server", "port"]) - settings_apikey = settings.get(["api", "key"]) + def post_json(self, path, data, params=None): + return self.post(path, data, encoding="json", params=params) - global apikey, baseurl - apikey = settings_apikey - baseurl = build_base_url(https=https, - httpuser=httpuser, - httppass=httppass, - host=host or settings_host if settings_host != "0.0.0.0" else "127.0.0.1", - port=port or settings_port, - prefix=prefix) + def post_command(self, path, command, additional=None): + data = dict(command=command) + if additional: + data.update(additional) + return self.post_json(path, data, params=data) -def prepare_request(method=None, path=None, params=None): - url = None - if baseurl: - while path.startswith("/"): - path = path[1:] - url = baseurl + "/" + path - return requests.Request(method=method, url=url, params=params, headers={"X-Api-Key": apikey}).prepare() + def upload(self, path, file_path, additional=None, file_name=None, content_type=None, params=None): + import os -def request(method, path, data=None, files=None, encoding=None, params=None): - s = requests.Session() - request = prepare_request(method, path, params=params) - if data or files: - if encoding == "json": - request.prepare_body(None, None, json=data) - else: - request.prepare_body(data, files=files) - response = s.send(request) - return response + if not os.path.isfile(file_path): + raise ValueError("{} cannot be uploaded since it is not a file".format(file_path)) -def get(path, params=None): - return request("GET", path, params=params) + if file_name is None: + file_name = os.path.basename(file_path) -def post(path, data, encoding=None, params=None): - return request("POST", path, data=data, encoding=encoding, params=params) + with open(file_path, "rb") as fp: + if content_type: + files = dict(file=(file_name, fp, content_type)) + else: + files = dict(file=(file_name, fp)) -def post_json(path, data, params=None): - return post(path, data, encoding="json", params=params) + response = self.request("POST", path, data=additional, files=files, params=params) -def post_command(path, command, additional=None): - data = dict(command=command) - if additional: - data.update(additional) - return post_json(path, data, params=data) + return response -def upload(path, file_path, additional=None, file_name=None, content_type=None, params=None): - import os + def delete(self, path, params=None): + return self.request("DELETE", path, params=params) - if not os.path.isfile(file_path): - raise ValueError("{} cannot be uploaded since it is not a file".format(file_path)) + def patch(self, path, data, encoding=None, params=None): + return self.request("PATCH", path, data=data, encoding=encoding, params=params) - if file_name is None: - file_name = os.path.basename(file_path) + def put(self, path, data, encoding=None, params=None): + return self.request("PUT", path, data=data, encoding=encoding, params=params) - with open(file_path, "rb") as fp: - if content_type: - files = dict(file=(file_name, fp, content_type)) - else: - files = dict(file=(file_name, fp)) + def create_socket(self, **kwargs): + import uuid + import random + import json - response = request("POST", path, data=additional, files=files, params=params) + # creates websocket URL for SockJS according to + # - http://sockjs.github.io/sockjs-protocol/sockjs-protocol-0.3.3.html#section-37 + # - http://sockjs.github.io/sockjs-protocol/sockjs-protocol-0.3.3.html#section-50 + url = "ws://{}/sockjs/{:0>3d}/{}/websocket".format( + self.baseurl[self.baseurl.find("//") + 2:], # host + port + prefix, but no protocol + random.randrange(0, stop=999), # server_id + uuid.uuid4() # session_id + ) + use_ssl = self.baseurl.startswith("https:") - return response + on_open_cb = kwargs.get("on_open", None) + on_heartbeat_cb = kwargs.get("on_heartbeat", None) + on_message_cb = kwargs.get("on_message", None) + on_close_cb = kwargs.get("on_close", None) + on_error_cb = kwargs.get("on_error", None) + daemon = kwargs.get("daemon", True) -def delete(path, params=None): - return request("DELETE", path, params=params) + def on_message(ws, message): + message_type = message[0] -def patch(path, data, encoding=None, params=None): - return request("PATCH", path, data=data, encoding=encoding, params=params) - -def put(path, data, encoding=None, params=None): - return request("PUT", path, data=data, encoding=encoding, params=params) - -def connect_socket(**kwargs): - import uuid - import random - import json - - # creates websocket URL for SockJS according to - # - http://sockjs.github.io/sockjs-protocol/sockjs-protocol-0.3.3.html#section-37 - # - http://sockjs.github.io/sockjs-protocol/sockjs-protocol-0.3.3.html#section-50 - url = "ws://{}/sockjs/{:0>3d}/{}/websocket".format( - baseurl[baseurl.find("//") + 2:], # host + port + prefix, but no protocol - random.randrange(0, stop=999), # server_id - uuid.uuid4() # session_id - ) - use_ssl = baseurl.startswith("https:") - - on_open_cb = kwargs.get("on_open", None) - on_heartbeat_cb = kwargs.get("on_heartbeat", None) - on_message_cb = kwargs.get("on_message", None) - on_close_cb = kwargs.get("on_close", None) - on_error_cb = kwargs.get("on_error", None) - daemon = kwargs.get("daemon", True) - - def on_message(ws, message): - message_type = message[0] - - if message_type == "h": - # "heartbeat" message - if callable(on_heartbeat_cb): - on_heartbeat_cb(ws) + if message_type == "h": + # "heartbeat" message + if callable(on_heartbeat_cb): + on_heartbeat_cb(ws) + return + elif message_type == "o": + # "open" message + return + elif message_type == "c": + # "close" message return - elif message_type == "o": - # "open" message - return - elif message_type == "c": - # "close" message - return - if not callable(on_message_cb): - return + if not callable(on_message_cb): + return - message_body = message[1:] - if not message_body: - return + message_body = message[1:] + if not message_body: + return - data = json.loads(message_body) + data = json.loads(message_body) - if message_type == "m": - data = [data,] + if message_type == "m": + data = [data,] - for d in data: - for internal_type, internal_message in d.items(): - on_message_cb(ws, internal_type, internal_message) + for d in data: + for internal_type, internal_message in d.items(): + on_message_cb(ws, internal_type, internal_message) - def on_open(ws): - if callable(on_open_cb): - on_open_cb(ws) + def on_open(ws): + if callable(on_open_cb): + on_open_cb(ws) - def on_close(ws): - if callable(on_close_cb): - on_close_cb(ws) + def on_close(ws): + if callable(on_close_cb): + on_close_cb(ws) - def on_error(ws, error): - if callable(on_error_cb): - on_error_cb(ws, error) + def on_error(ws, error): + if callable(on_error_cb): + on_error_cb(ws, error) - socket = SocketClient(url, - use_ssl=use_ssl, - daemon=daemon, - on_open=on_open, - on_message=on_message, - on_close=on_close, - on_error=on_error) - socket.connect() + socket = SocketClient(url, + use_ssl=use_ssl, + daemon=daemon, + on_open=on_open, + on_message=on_message, + on_close=on_close, + on_error=on_error) + socket.connect() - return socket + return socket