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)