You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficcontrol.apache.org by ra...@apache.org on 2019/04/04 18:59:44 UTC
[trafficcontrol] branch master updated: Added an implementation of
the to-access functions to Python client package (#3266)
This is an automated email from the ASF dual-hosted git repository.
rawlin pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficcontrol.git
The following commit(s) were added to refs/heads/master by this push:
new dd28e74 Added an implementation of the to-access functions to Python client package (#3266)
dd28e74 is described below
commit dd28e749f8c2ec53152433f38dbf0dc6728dd43c
Author: ocket8888 <oc...@gmail.com>
AuthorDate: Thu Apr 4 12:59:39 2019 -0600
Added an implementation of the to-access functions to Python client package (#3266)
* Added an implementation of the to-access functions to Python client package
* proper exit codes
* function argument formatting is now like <arg>: <type> = <default>
* Documented exit codes and switched to snake_case for exposed objects
* removed unused and/or commented-out code
* Reduced all scripts to stubs that call common functionality
* Command line flags `_` -> `-`, fixed missing imports, and `TO_URL` now parsed via urllib
* Fixed urlparse netloc hack for determining hostname
---
docs/source/tools/index.rst | 1 +
docs/source/tools/{index.rst => toaccess.rst} | 19 +-
traffic_control/clients/python/setup.py | 11 +
.../clients/python/to_access/__init__.py | 454 +++++++++++++++++++++
.../clients/python/trafficops/restapi.py | 88 +++-
5 files changed, 557 insertions(+), 16 deletions(-)
diff --git a/docs/source/tools/index.rst b/docs/source/tools/index.rst
index c834ab7..f747d1b 100644
--- a/docs/source/tools/index.rst
+++ b/docs/source/tools/index.rst
@@ -26,3 +26,4 @@ This is a living list of tools used to interact with, test, and develop for the
compare
python_client
traffic_vault_util
+ toaccess
diff --git a/docs/source/tools/index.rst b/docs/source/tools/toaccess.rst
similarity index 73%
copy from docs/source/tools/index.rst
copy to docs/source/tools/toaccess.rst
index c834ab7..ca42807 100644
--- a/docs/source/tools/index.rst
+++ b/docs/source/tools/toaccess.rst
@@ -13,16 +13,13 @@
.. limitations under the License.
..
-.. _tools:
+.. _toaccess-module:
-*****
-Tools
-*****
-This is a living list of tools used to interact with, test, and develop for the Traffic Control CDN.
+********
+toaccess
+********
-.. toctree::
- :maxdepth: 2
-
- compare
- python_client
- traffic_vault_util
+.. automodule:: to_access
+ :members:
+ :undoc-members:
+ :show-inheritance:
diff --git a/traffic_control/clients/python/setup.py b/traffic_control/clients/python/setup.py
index f32219c..e897aa2 100755
--- a/traffic_control/clients/python/setup.py
+++ b/traffic_control/clients/python/setup.py
@@ -64,6 +64,17 @@ with open(os.path.join(HERE, "README.rst")) as fd:
"requests>=2.13.0",
"munch>=2.1.1",
],
+ entry_points={
+ 'console_scripts': [
+ 'toget=to_access:get',
+ 'toput=to_access:put',
+ 'topost=to_access:post',
+ 'todelete=to_access:delete',
+ 'tooptions=to_access:options',
+ 'tohead=to_access:head',
+ 'topatch=to_access:patch'
+ ],
+ },
extras_require={
"dev": [
"pylint>=2.0,<3.0"
diff --git a/traffic_control/clients/python/to_access/__init__.py b/traffic_control/clients/python/to_access/__init__.py
new file mode 100644
index 0000000..38a47de
--- /dev/null
+++ b/traffic_control/clients/python/to_access/__init__.py
@@ -0,0 +1,454 @@
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+"""
+.. _toaccess:
+
+.. program:: toaccess
+
+``toaccess``
+============
+This module provides a set of functions meant to provide ease-of-use functionality for interacting
+with the Traffic Ops API. It provides scripts named :file:`to{method}` where `method` is the name of
+an HTTP method (in lowercase). Collectively they are referred to as :program:`toaccess` Implemented
+methods thus far are:
+
+- delete
+- head
+- get
+- options
+- patch
+- post
+- put
+
+Arguments and Flags
+-------------------
+.. option:: PATH
+
+ This is the request path. By default, whatever is passed is considered to be relative to
+ :file:`/api/{api-version}/` where ``api-version`` is :option:`--api-version`. This behavior can
+ be disabled by using :option:`--raw-path`.
+
+.. option:: DATA
+
+ An optional positional argument that is a data payload to pass to the Traffic Ops server in the
+ request body. If this is the absolute or relative path to a file, the contents of the file will
+ instead be read and used as the request payload.
+
+.. option:: -h, --help
+
+ Print usage information and exit
+
+.. option:: -a API_VERSION, --api-version API_VERSION
+
+ Specifies the version of the Traffic Ops API that will be used for the request. Has no effect if
+ :option:`--raw-path` is used. (Default: 1.3)
+
+.. option:: -f, --full
+
+ Output the full HTTP exchange including request method line, request headers, request body (if
+ any), response status line, and response headers (as well as the response body, if any). This is
+ equivalent to using :option:`--request-headers`, :option:`--request-payload`, and
+ :option:`--response-headers` at the same time, and those options will have no effect if given.
+ (Default: false)
+
+.. option:: -k, --insecure
+
+ Do not verify SSL certificates - typically useful for making requests to development or testing
+ servers as they frequently have self-signed certificates. (Default: false)
+
+.. option:: -p, --pretty
+
+ Pretty-print any payloads that are output as formatted JSON. Has no effect on plaintext payloads.
+ Uses tab characters for indentation. (Default: false)
+
+.. option:: -r, --raw-path
+
+ Request exactly :option:`PATH`; do not preface the request path with :file:`/api/{api_version}`.
+ This effectively means that :option:`--api-version` will have no effect. (Default: false)
+
+.. option:: --request-headers
+
+ Output the request method line and any and all request headers. (Default: false)
+
+.. option:: --request-payload
+
+ Output the request body if any was sent. Will attempt to pretty-print the body as JSON if
+ :option:`--pretty` is used. (Default: false)
+
+.. option:: --response-headers
+
+ Output the response status line and any and all response headers. (Default: false)
+
+.. option:: --to-url URL
+
+ The :abbr:`FQDN (Fully Qualified Domain Name)` and optionally the port and scheme of the Traffic
+ Ops server. This will override :envvar:`TO_URL`. The format is the same as for :envvar:`TO_URL`.
+ (Default: uses the value of :envvar:`TO_URL`)
+
+.. option:: --to-password PASSWORD
+
+ The password to use when authenticating to Traffic Ops. Overrides :envvar:`TO_PASSWORD`.
+ (Default: uses the value of :envvar:`TO_PASSWORD`)
+
+.. option:: --to-user USERNAME
+
+ The username to use when connecting to Traffic Ops. Overrides :envvar:`TO_USER`. (Default: uses
+ the value of :envvar:`TO_USER`)
+
+Environment Variables
+---------------------
+If defined, :program:`toaccess` scripts will use these environment variables to define their
+connection to and authentication with the Traffic Ops server. Typically, setting these is easier
+than using the long options :option:`--to-url`, :option:`--to-user`, and :option:`--to-password` on
+every invocation.
+
+.. envvar:: TO_PASSWORD
+
+ Will be used to authenticate the user defined by either :option:`--to-user` or :envvar:`TO_USER`.
+
+.. envvar:: TO_URL
+
+ The :abbr:`FQDN (Fully Qualified Domain Name)` of the Traffic Ops server to which the script
+ will connect. The format of this should be :file:`[{http or https}://]{hostname}[:{port}]`. Note
+ that this may optionally start with ``http://`` or ``https://`` (case insensitive), but
+ typically this is unnecessary. Also notice that the port number may be specified, though again
+ this isn't usually required. All :program:`toaccess` scripts will assume that port 443 should be
+ used unless otherwise specified. They will further assume that the protocol is HTTPS unless
+ :envvar:`TO_URL` (or :option:`--to-url`) starts with ``http://``, in which case the default port
+ will also be set to 80 unless otherwise specified in the URL.
+
+.. envvar:: TO_USER
+
+ The name of the user as whom to connect to the Traffic Ops server. Overriden by
+ :option:`--to-user`.
+
+Exit Codes
+----------
+The exit code of a :program:`toaccess` script can sometimes be used by the caller to determine what
+the result of calling the script was without needing to parse the output. The exit codes used are:
+
+0
+ The command executed successfully, and the result is on STDOUT.
+1
+ Typically this exit code means that an error was encountered when parsing positional command
+ line arguments. However, this is also the exit code used by most Python interpreters to signal
+ an unhandled exception.
+2
+ Signifies a runtime error that caused the request to fail - this is **not** generally indicative
+ of an HTTP client or server error, but rather an underlying issue connecting to or
+ authenticating with Traffic Ops. This is distinct from an exit code of ``32`` in that the
+ *format* of the arguments was correct, but there was some problem with the *value*. For example,
+ passing ``https://test:`` to :option:`--to-url` will cause an exit code of ``2``, not ``32``.
+4
+ An HTTP client error occurred. The HTTP stack will be printed to stdout as indicated by other
+ options - meaning by default it will only print the response payload if one was given, but will
+ respect options like e.g. :option:`--request-payload` as well as
+ :option:`-p`/:option:`--pretty`.
+5
+ An HTTP server error occurred. The HTTP stack will be printed to stdout as indicated by other
+ options - meaning by default it will only print the response payload if one was given, but will
+ respect options like e.g. :option:`--request-payload` as well as
+ :option:`-p`/:option:`--pretty`.
+32
+ This is the error code emitted by Python's :mod:`argparse` module when the passed arguments
+ could not be parsed successfully.
+
+.. note:: The way exit codes ``4`` and ``5`` are implemented is by returning the status code of the
+ HTTP request divided by 100 whenever it is at least 400. This means that if the Traffic Ops
+ server ever started returning e.g. 700 status codes, the exit code of the script would be 7.
+
+
+Module Reference
+================
+
+"""
+from __future__ import print_function
+
+import json
+import logging
+import os
+import sys
+from urllib.parse import urlparse
+
+from future.utils import raise_from
+
+from trafficops.restapi import LoginError, OperationError, InvalidJSONError
+from trafficops.tosession import TOSession
+
+from requests.exceptions import RequestException
+
+l = logging.getLogger()
+l.disabled = True
+logging.basicConfig(level=logging.CRITICAL+1)
+
+def output(r, pretty, request_header, response_header, request_payload, indent = '\t'):
+ """
+ Prints the passed response object in a format consistent with the other parameters.
+
+ :param r: The :mod:`requests` response object being printed
+ :param pretty: If :const:`True`, attempt to pretty-print payloads as JSON
+ :param request_header: If :const:`True`, print request line and request headers
+ :param response_header: If :const:`True`, print response line and response headers
+ :param request_payload: If :const:`True`, print the request payload
+ :param indent: An optional number of spaces for pretty-printing indentation (default is the tab character)
+ """
+ if request_header:
+ print(r.request.method, r.request.path_url, "HTTP/1.1")
+ for h,v in r.request.headers.items():
+ print("%s:" % h, v)
+ print()
+
+ if request_payload and r.request.body:
+ try:
+ result = r.request.body if not pretty else json.dumps(json.loads(r.request.body))
+ except ValueError:
+ result = r.request.body
+ print(result, end="\n\n")
+
+ if response_header:
+ print("HTTP/1.1", r.status_code, end="")
+ print(" "+r.reason if r.reason else "")
+ for h,v in r.headers.items():
+ print("%s:" % h, v)
+ print()
+
+ try:
+ result = r.text if not pretty else json.dumps(r.json(), indent=indent)
+ except ValueError:
+ result = r.text
+ print(result)
+
+def parse_arguments(program):
+ """
+ A common-use function that parses the command line arguments.
+
+ :param program: The name of the program being run - used for usage informational output
+ :returns: The Traffic Ops HTTP session object, the requested path, any data to be sent, an output
+ format specification, whether or not the path is raw, and whether or not output should
+ be prettified
+ """
+ from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter
+ parser = ArgumentParser(prog=program,
+ formatter_class=ArgumentDefaultsHelpFormatter,
+ description="A helper program for interfacing with the Traffic Ops API",
+ epilog=("Typically, one will want to connect and authenticate by defining "
+ "the 'TO_URL', 'TO_USER' and 'TO_PASSWORD' environment variables "
+ "rather than (respectively) the '--to-url', '--to-user' and "
+ "'--to-password' command-line flags. Those flags are only "
+ "required when said environment variables are not defined.\n"
+ "%(prog)s will exit with a success provided a response was "
+ "received and the status code of said response was less than 400. "
+ "The exit code will be 1 if command line arguments cannot be "
+ "parsed or authentication with the Traffic Ops server fails. "
+ "In the event of some unknown error occurring when waiting for a "
+ "response, the exit code will be 2. If the server responds with "
+ "a status code indicating a client or server error, that status "
+ "code will be used as the exit code."))
+
+ parser.add_argument("--to-url",
+ type=str,
+ help=("The fully qualified domain name of the Traffic Ops server. Overrides "
+ "'$TO_URL'. The format for both the environment variable and the flag "
+ "is '[scheme]hostname[:port]'. That is, ports should be specified here, "
+ "and they need not start with 'http://' or 'https://'. HTTPS is the "
+ "assumed protocol unless the scheme _is_ provided and is 'http://'."))
+ parser.add_argument("--to-user",
+ type=str,
+ help="The username to use when connecting to Traffic Ops. Overrides '$TO_USER")
+ parser.add_argument("--to-password",
+ type=str,
+ help="The password to use when authenticating to Traffic Ops. Overrides '$TO_PASSWORD'")
+ parser.add_argument("-k", "--insecure", action="store_true", help="Do not verify SSL certificates")
+ parser.add_argument("-f", "--full",
+ action="store_true",
+ help=("Also output HTTP request/response lines and headers, and request payload. "
+ "This is equivalent to using '--request-headers', '--response-headers' "
+ "and '--request-payload' at the same time."))
+ parser.add_argument("--request-headers",
+ action="store_true",
+ help="Output request method line and headers")
+ parser.add_argument("--response-headers",
+ action="store_true",
+ help="Output response status line and headers")
+ parser.add_argument("--request-payload",
+ action="store_true",
+ help="Output request payload (will try to pretty-print if '--pretty' is given)")
+ parser.add_argument("-r", "--raw-path",
+ action="store_true",
+ help="Request exactly PATH; it won't be prefaced with '/api/{{api-version}}/")
+ parser.add_argument("-a", "--api-version",
+ type=float,
+ default=1.3,
+ help="Specify the API version to request against")
+ parser.add_argument("-p", "--pretty",
+ action="store_true",
+ help=("Pretty-print payloads as JSON. "
+ "Note that this will make Content-Type headers \"wrong\", in general"))
+ parser.add_argument("PATH", help="The path to the resource being requested - omit '/api/1.x'")
+ parser.add_argument("DATA",
+ help=("An optional data string to pass with the request. If this is a "
+ "filename, the contents of the file will be sent instead."),
+ nargs='?')
+
+
+ args = parser.parse_args()
+
+ try:
+ to_host = args.to_url if args.to_url else os.environ["TO_URL"]
+ except KeyError as e:
+ raise KeyError("Traffic Ops hostname not set! Set the TO_URL environment variable or use "\
+ "'--to-url'.") from e
+
+ original_to_host = to_host
+ to_host = urlparse(to_host, scheme="https")
+ useSSL = to_host.scheme.lower() == "https"
+ to_port = to_host.port
+ if to_port is None:
+ if useSSL:
+ to_port = 443
+ else:
+ to_port = 80
+
+ to_host = to_host.hostname
+ if not to_host:
+ raise KeyError("Invalid URL/host for Traffic Ops: '%s'" % original_to_host)
+
+ s = TOSession(to_host,
+ host_port=to_port,
+ ssl=useSSL,
+ api_version=str(args.api_version),
+ verify_cert=not args.insecure)
+
+ data = args.DATA
+ if data and os.path.isfile(data):
+ with open(data) as f:
+ data = f.read()
+
+ if isinstance(data, str):
+ data = data.encode()
+
+ try:
+ to_user = args.to_user if args.to_user else os.environ["TO_USER"]
+ except KeyError as e:
+ raise KeyError("Traffic Ops user not set! Set the TO_USER environment variable or use "\
+ "'--to-user'.") from e
+
+ try:
+ to_passwd = args.to_password if args.to_password else os.environ["TO_PASSWORD"]
+ except KeyError as e:
+ raise KeyError("Traffic Ops password not set! Set the TO_PASSWORD environment variable or "\
+ "use '--to-password'") from e
+
+ try:
+ s.login(to_user, to_passwd)
+ except (OperationError, InvalidJSONError, LoginError) as e:
+ raise PermissionError from e
+
+ return (s,
+ args.PATH,
+ data,
+ (
+ args.request_headers or args.full,
+ args.response_headers or args.full,
+ args.request_payload or args.full
+ ),
+ args.raw_path,
+ args.pretty)
+
+def request(method):
+ """
+ All of the scripts wind up calling this function to handle their common functionality.
+
+ :param method: The name of the request method to use (case-insensitive)
+ :returns: The program's exit code
+ """
+ try:
+ s, path, data, full, raw, pretty = parse_arguments("to%s" % method)
+ except (PermissionError, KeyError) as e:
+ print(e, file=sys.stderr)
+ return 1
+
+ if raw:
+ path = '/'.join((s.to_url.rstrip('/'), path.lstrip('/')))
+ else:
+ path = '/'.join((s.base_url.rstrip('/'), path.lstrip('/')))
+
+ try:
+ if data is not None:
+ r = s._session.request(method, path, data=data)
+ else:
+ r = s._session.request(method, path)
+ except (RequestException, ValueError) as e:
+ print("Error occurred: ", e, file=sys.stderr)
+ return 2
+
+ output(r, pretty, *full)
+ return 0 if r.status_code < 400 else r.status_code // 100
+
+def get():
+ """
+ Entry point for :program:`toget`
+
+ :returns: The program's exit code
+ """
+ return request("get")
+
+def put():
+ """
+ Entry point for :program:`toput`
+
+ :returns: The program's exit code
+ """
+ return request("put")
+
+def post():
+ """
+ Entry point for :program:`topost`
+
+ :returns: The program's exit code
+ """
+ return request("post")
+
+def delete():
+ """
+ Entry point for :program:`todelete`
+
+ :returns: The program's exit code
+ """
+ return request("delete")
+
+def options():
+ """
+ Entry point for :program:`tooptions`
+
+ :returns: The program's exit code
+ """
+ return request("options")
+
+def head():
+ """
+ Entry point for :program:`tohead`
+
+ :returns: The program's exit code
+ """
+ return request("head")
+
+def patch():
+ """
+ Entry point for :program:`topatch`
+
+ :returns: The program's exit code
+ """
+ return request("patch")
diff --git a/traffic_control/clients/python/trafficops/restapi.py b/traffic_control/clients/python/trafficops/restapi.py
index cacfe83..4189af4 100644
--- a/traffic_control/clients/python/trafficops/restapi.py
+++ b/traffic_control/clients/python/trafficops/restapi.py
@@ -61,16 +61,22 @@ class OperationError(IOError):
This class represents a generic error, indicating something went wrong with the request or on
the server.
"""
- def __init__(self, *args):
+ #: Contains the response object that generated the error
+ resp = None
+ def __init__(self, *args, resp=None):
IOError.__init__(self, *args)
+ self.resp = resp
class InvalidJSONError(ValueError):
"""
An error that occurs when an invalid JSON payload is passed to an endpoint.
"""
- def __init__(self, *args):
+ #: Contains the response object that generated the error
+ resp = None
+ def __init__(self, *args, resp=None):
ValueError.__init__(self, *args)
+ self.resp = resp
# Miscellaneous Constants and/or Variables
DEFAULT_HEADERS = {u'Content-Type': u'application/json; charset=UTF-8'}
@@ -410,13 +416,13 @@ class RestApiSession(object):
msg = msg.format(response.status_code, endpoint, e)
if debug_response:
log_with_debug_info(logging.ERROR, msg + u' Data: [' + str(response.text) + u']')
- raise InvalidJSONError(msg)
+ raise InvalidJSONError(msg, resp=response)
msg = u'{0} request to RESTful API at [{1}] expected status(s) {2}; failed: {3} {4};'\
u' Response: {5}'
msg = msg.format(operation.upper(), endpoint, expected_status_codes,
response.status_code, response.reason, retdata)
log_with_debug_info(logging.ERROR, msg)
- raise OperationError(msg)
+ raise OperationError(msg, resp=response)
try:
if response.status_code in ('204',):
@@ -432,7 +438,7 @@ class RestApiSession(object):
msg = msg.format(response.status_code, endpoint, e)
if debug_response:
log_with_debug_info(logging.ERROR, msg + u' Data: [' + str(response.text) + u']')
- raise InvalidJSONError(msg)
+ raise InvalidJSONError(msg, resp=response)
retdata = munch.munchify(retdata) if munchify else retdata
return (retdata[u'response'] if u'response' in retdata else retdata), response
@@ -495,3 +501,75 @@ class RestApiSession(object):
"""
return self._do_operation(u'delete', api_path, *args, **kwargs)
+
+ def head(self, api_path, *args, **kwargs):
+ """
+ Perform HTTP HEAD requests
+ :param api_path: The path to the API end-point that you want to call which does not include
+ the base URL e.g. ``user/login``, ``servers``, etc. This string can contain substitution
+ parameters as denoted by a valid field_name replacement field specification as per
+ :meth:`str.format` e.g. ``cachegroups/{id}`` or ``cachegroups/{id:d}``
+
+ :type api_path: str
+ :param kwargs: Passed Keyword Parameters. If you need to send JSON data to the endpoint pass
+ the keyword parameter ``data`` with the Python data structure. This method will convert
+ it to JSON before sending it to the API endpoint. Use ``query_params`` to pass a
+ dictionary of query parameters
+
+ :type kwargs: Dict[str, Any]
+ :return: Python data structure distilled from JSON from the API request.
+ :rtype: Tuple[Union[Dict[str, Any], List[Dict[str, Any]], munch.Munch, List[munch.Munch]],
+ requests.Response]
+
+ :raises: Union[LoginError, OperationError]
+ """
+
+ return self._do_operation(u'head', api_path, *args, **kwargs)
+
+ def options(self, api_path, *args, **kwargs):
+ """
+ Perform HTTP OPTIONS requests
+ :param api_path: The path to the API end-point that you want to call which does not include
+ the base URL e.g. ``user/login``, ``servers``, etc. This string can contain substitution
+ parameters as denoted by a valid field_name replacement field specification as per
+ :meth:`str.format` e.g. ``cachegroups/{id}`` or ``cachegroups/{id:d}``
+
+ :type api_path: str
+ :param kwargs: Passed Keyword Parameters. If you need to send JSON data to the endpoint pass
+ the keyword parameter ``data`` with the Python data structure. This method will convert
+ it to JSON before sending it to the API endpoint. Use ``query_params`` to pass a
+ dictionary of query parameters
+
+ :type kwargs: Dict[str, Any]
+ :return: Python data structure distilled from JSON from the API request.
+ :rtype: Tuple[Union[Dict[str, Any], List[Dict[str, Any]], munch.Munch, List[munch.Munch]],
+ requests.Response]
+
+ :raises: Union[LoginError, OperationError]
+ """
+
+ return self._do_operation(u'options', api_path, *args, **kwargs)
+
+ def patch(self, api_path, *args, **kwargs):
+ """
+ Perform HTTP PATCH requests
+ :param api_path: The path to the API end-point that you want to call which does not include
+ the base URL e.g. ``user/login``, ``servers``, etc. This string can contain substitution
+ parameters as denoted by a valid field_name replacement field specification as per
+ :meth:`str.format` e.g. ``cachegroups/{id}`` or ``cachegroups/{id:d}``
+
+ :type api_path: str
+ :param kwargs: Passed Keyword Parameters. If you need to send JSON data to the endpoint pass
+ the keyword parameter ``data`` with the Python data structure. This method will convert
+ it to JSON before sending it to the API endpoint. Use ``query_params`` to pass a
+ dictionary of query parameters
+
+ :type kwargs: Dict[str, Any]
+ :return: Python data structure distilled from JSON from the API request.
+ :rtype: Tuple[Union[Dict[str, Any], List[Dict[str, Any]], munch.Munch, List[munch.Munch]],
+ requests.Response]
+
+ :raises: Union[LoginError, OperationError]
+ """
+
+ return self._do_operation(u'patch', api_path, *args, **kwargs)