Source code for composer.http_client

#
# Copyright (C) 2018  Red Hat, Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
import logging
log = logging.getLogger("composer-cli")

import os
import sys
import json
from urllib.parse import urlparse, urlunparse

from composer.unix_socket import UnixHTTPConnectionPool

[docs]def api_url(api_version, url): """Return the versioned path to the API route :param api_version: The version of the API to talk to. eg. "0" :type api_version: str :param url: The API route to talk to :type url: str :returns: The full url to use for the route and API version :rtype: str """ return os.path.normpath("/api/v%s/%s" % (api_version, url))
[docs]def append_query(url, query): """Add a query argument to a URL The query should be of the form "param1=what&param2=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]])
[docs]def get_url_raw(socket_path, url): """Return the raw results of a GET 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 raw response from the server :rtype: str """ http = UnixHTTPConnectionPool(socket_path) r = http.request("GET", url) if r.status == 400: err = json.loads(r.data.decode("utf-8")) if "status" in err and err["status"] == False: msgs = [e["msg"] for e in err["errors"]] raise RuntimeError(", ".join(msgs)) return r.data.decode('utf-8')
[docs]def get_url_json(socket_path, url): """Return the JSON results of a GET 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) r = http.request("GET", url) return json.loads(r.data.decode('utf-8'))
[docs]def get_url_json_unlimited(socket_path, url, total_fn=None): """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 """ def default_total_fn(data): """Return the total number of available results""" return data["total"] 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')) # Where to get the total from if not total_fn: total_fn = default_total_fn # Add the "total" returned by limit=0 as the new limit unlimited_url = append_query(url, "limit=%d" % total_fn(json_total)) r_unlimited = http.request("GET", unlimited_url) return json.loads(r_unlimited.data.decode('utf-8'))
[docs]def delete_url_json(socket_path, url): """Send a DELETE request to the url and return JSON response :param socket_path: Path to the Unix socket to use for API communication :type socket_path: str :param url: URL to send DELETE to :type url: str :returns: The json response from the server :rtype: dict """ http = UnixHTTPConnectionPool(socket_path) r = http.request("DELETE", url) return json.loads(r.data.decode("utf-8"))
[docs]def post_url(socket_path, url, body): """POST raw data to the URL :param socket_path: Path to the Unix socket to use for API communication :type socket_path: str :param url: URL to send POST to :type url: str :param body: The data for the body of the POST :type body: str :returns: The json response from the server :rtype: dict """ http = UnixHTTPConnectionPool(socket_path) r = http.request("POST", url, body=body.encode("utf-8")) return json.loads(r.data.decode("utf-8"))
[docs]def post_url_toml(socket_path, url, body): """POST a TOML string to the URL :param socket_path: Path to the Unix socket to use for API communication :type socket_path: str :param url: URL to send POST to :type url: str :param body: The data for the body of the POST :type body: str :returns: The json response from the server :rtype: dict """ http = UnixHTTPConnectionPool(socket_path) r = http.request("POST", url, body=body.encode("utf-8"), headers={"Content-Type": "text/x-toml"}) return json.loads(r.data.decode("utf-8"))
[docs]def post_url_json(socket_path, url, body): """POST some JSON data to the URL :param socket_path: Path to the Unix socket to use for API communication :type socket_path: str :param url: URL to send POST to :type url: str :param body: The data for the body of the POST :type body: str :returns: The json response from the server :rtype: dict """ http = UnixHTTPConnectionPool(socket_path) r = http.request("POST", url, body=body.encode("utf-8"), headers={"Content-Type": "application/json"}) return json.loads(r.data.decode("utf-8"))
[docs]def get_filename(headers): """Get the filename from the response header :param response: The urllib3 response object :type response: Response :raises: RuntimeError if it cannot find a filename in the header :returns: Filename from content-disposition header :rtype: str """ log.debug("Headers = %s", headers) if "content-disposition" not in headers: raise RuntimeError("No Content-Disposition header; cannot get filename") try: k, _, v = headers["content-disposition"].split(";")[1].strip().partition("=") if k != "filename": raise RuntimeError("No filename= found in content-disposition header") except RuntimeError: raise except Exception as e: raise RuntimeError("Error parsing filename from content-disposition header: %s" % str(e)) return os.path.basename(v)
[docs]def download_file(socket_path, url, progress=True): """Download a file, saving it to the CWD with the included filename :param socket_path: Path to the Unix socket to use for API communication :type socket_path: str :param url: URL to send POST to :type url: str """ http = UnixHTTPConnectionPool(socket_path) r = http.request("GET", url, preload_content=False) if r.status == 400: err = json.loads(r.data.decode("utf-8")) if not err["status"]: msgs = [e["msg"] for e in err["errors"]] raise RuntimeError(", ".join(msgs)) filename = get_filename(r.headers) if os.path.exists(filename): msg = "%s exists, skipping download" % filename log.error(msg) raise RuntimeError(msg) with open(filename, "wb") as f: while True: data = r.read(10 * 1024**2) if not data: break f.write(data) if progress: data_written = f.tell() if data_written > 5 * 1024**2: sys.stdout.write("%s: %0.2f MB \r" % (filename, data_written / 1024**2)) else: sys.stdout.write("%s: %0.2f kB\r" % (filename, data_written / 1024)) sys.stdout.flush() print("") r.release_conn() return 0