From d6418246ad02a4a028bcd4dc13d583622465d2d5 Mon Sep 17 00:00:00 2001 From: David Shea Date: Fri, 10 Aug 2018 16:02:51 -0400 Subject: [PATCH] In composer-cli, request all results Add a limit argument to all potentially paginated results, equal to whatever the composer backend is the total number of results. This still has the potential to provide truncated data if the number of results increases between the two HTTP requests. Resolves: #404 (cherry picked from commit ee98d87cea1974c6da9958adf58f6ea1e558e525) --- src/composer/cli/blueprints.py | 4 +-- src/composer/cli/modules.py | 2 +- src/composer/cli/projects.py | 2 +- src/composer/http_client.py | 49 ++++++++++++++++++++++++++++++++++ 4 files changed, 53 insertions(+), 4 deletions(-) diff --git a/src/composer/cli/blueprints.py b/src/composer/cli/blueprints.py index d4fc33b7..1872e2fe 100644 --- a/src/composer/cli/blueprints.py +++ b/src/composer/cli/blueprints.py @@ -72,7 +72,7 @@ def blueprints_list(socket_path, api_version, args, show_json=False): blueprints list """ api_route = client.api_url(api_version, "/blueprints/list") - result = client.get_url_json(socket_path, api_route) + result = client.get_url_json_unlimited(socket_path, api_route) (rc, exit_now) = handle_api_result(result, show_json) if exit_now: return rc @@ -119,7 +119,7 @@ def blueprints_changes(socket_path, api_version, args, show_json=False): blueprints changes Display the changes for each blueprint. """ api_route = client.api_url(api_version, "/blueprints/changes/%s" % (",".join(argify(args)))) - result = client.get_url_json(socket_path, api_route) + result = client.get_url_json_unlimited(socket_path, api_route) (rc, exit_now) = handle_api_result(result, show_json) if exit_now: return rc diff --git a/src/composer/cli/modules.py b/src/composer/cli/modules.py index cd377838..0a775738 100644 --- a/src/composer/cli/modules.py +++ b/src/composer/cli/modules.py @@ -37,7 +37,7 @@ def modules_cmd(opts): return 1 api_route = client.api_url(opts.api_version, "/modules/list") - result = client.get_url_json(opts.socket, api_route) + result = client.get_url_json_unlimited(opts.socket, api_route) (rc, exit_now) = handle_api_result(result, opts.json) if exit_now: return rc diff --git a/src/composer/cli/projects.py b/src/composer/cli/projects.py index 39c56085..f2a5b683 100644 --- a/src/composer/cli/projects.py +++ b/src/composer/cli/projects.py @@ -59,7 +59,7 @@ def projects_list(socket_path, api_version, args, show_json=False): projects list """ api_route = client.api_url(api_version, "/projects/list") - result = client.get_url_json(socket_path, api_route) + result = client.get_url_json_unlimited(socket_path, api_route) (rc, exit_now) = handle_api_result(result, show_json) if exit_now: return rc diff --git a/src/composer/http_client.py b/src/composer/http_client.py index ebbac2af..ff356bbf 100644 --- a/src/composer/http_client.py +++ b/src/composer/http_client.py @@ -20,6 +20,7 @@ log = logging.getLogger("composer-cli") import os import sys import json +from urlparse import urlparse, urlunparse from composer.unix_socket import UnixHTTPConnectionPool @@ -35,6 +36,29 @@ def api_url(api_version, url): """ return os.path.normpath("/api/v%s/%s" % (api_version, url)) +def append_query(url, query): + """Add a query argument to a URL + + The query should be of the form "param1=what¶m2=ever", i.e., no + leading '?'. The new query data will be appended to any existing + query string. + + :param url: The original URL + :type url: str + :param query: The query to append + :type query: str + :returns: The new URL with the query argument included + :rtype: str + """ + + url_parts = urlparse(url) + if url_parts.query: + new_query = url_parts.query + "&" + query + else: + new_query = query + return urlunparse([url_parts[0], url_parts[1], url_parts[2], + url_parts[3], new_query, url_parts[5]]) + def get_url_raw(socket_path, url): """Return the raw results of a GET request @@ -69,6 +93,31 @@ def get_url_json(socket_path, url): r = http.request("GET", url) return json.loads(r.data.decode('utf-8')) +def get_url_json_unlimited(socket_path, url): + """Return the JSON results of a GET request + + For URLs that use offset/limit arguments, this command will + fetch all results for the given request. + + :param socket_path: Path to the Unix socket to use for API communication + :type socket_path: str + :param url: URL to request + :type url: str + :returns: The json response from the server + :rtype: dict + """ + http = UnixHTTPConnectionPool(socket_path) + + # Start with limit=0 to just get the number of objects + total_url = append_query(url, "limit=0") + r_total = http.request("GET", total_url) + json_total = json.loads(r_total.data.decode('utf-8')) + + # Add the "total" returned by limit=0 as the new limit + unlimited_url = append_query(url, "limit=%d" % json_total["total"]) + r_unlimited = http.request("GET", unlimited_url) + return json.loads(r_unlimited.data.decode('utf-8')) + def delete_url_json(socket_path, url): """Send a DELETE request to the url and return JSON response