Now that you have had some experience interacting with SOAP-based APIs, you can turn
your attention to the other type of web service: REST. In order to send
REST-based requests using Python, this lab will utilize the Python
Requests
library. To make this re-usable, a REST class will be implemented as a basic building block to send and
receive the various request types (GET, POST, PUT, and DELETE) and to manage session
re-use, so that each REST API, such as CUPI and CMS, can extend the class by adding the API-specific
headers and parsing of the responses.
While you won't be able to test this code individually here, all subsequent REST modules
will leverage it.
Basic REST Request
Response Checking
Step 1 - Basic REST Request
The REST class will use the following parameters:
host: The host name / IP address of the server
port: The server port for API access (default: 443)
username and password: The credentials for basic authentication
base_url: The base URL, such as "/api/v1" for a given API. This is simply to
avoid having to send, for example, "/api/v1" with every request.
headers: A dictionary of header settings that we need to apply. These are
specific to the API you are interacting with, such as a specific Accept header for CUPI.
tls_verify: Whether or not server certificate validation will be enforced.
Within this class, besides the __init__ method to initialize the class, there will be two methods:
_send_request() send the request to the server--regardless of what verb
is required and return the response. This method should return a Python dictionary indicating
'success', the raw 'response', and a 'message', which can be used to relay error information
which may have occurred.
_check_response() used to check if the response
was in the 200-299 range, indicating a success from the HTTP perspective.
This will only help identify basic HTTP-type errors. More API-specific
error evaluation will occur later. For example, CUPI may return a 400 HTTP
response to a query and then put more information about the specific error
in the payload, which we can further process and return to the user.
Start implementing these methods using Visual Studio Code.
In your VS Code
tab, open up flaskr/rest/v1/rest.py using the
Explorer
The __init__ method initializes the REST object with parameters mentioned above.
In order for your application to minimize its impact on the server, add
a Session() instance. This object, also part of the Python Requests library, allows certain
parameters to persist from one request to the next (meaning, that it adds and re-uses a session ID
in the request header, if the server supplied one).
The server, therefore, does not need to instantiate a new client session for every request
made, potentially using up limited server resources--especially in the case of Unity Connection
and Communications Manager--as their individual sessions do not time out for 30 minutes,
by default. Notice that the Session() instance is created with credentials and the TLS
verification setting. The headers are also updated with the API-specific headers that are
passed to the object.
Add the following to the __init__() method:
def__init__(self, host, username=None, password=None, base_url=None, headers={}, port=443, tls_verify=False):"""
Initialize an object with the host, port, and base_url using the parameters passed in.
"""
self.host = host
self.port =str(port)
self.base_url = base_url
self.session = Session()
self.session.auth = HTTPBasicAuth(username, password)
self.session.verify = tls_verify
self.session.headers.update(headers)
Currently the _send_request method only
sets a result variable. You add in functionality by first
properly assigning the url variable using the settings
obtained from when the class was initialized (in __init__). This
is, in effect, just a concatenation of the strings using the host, port, base_url and
api_method in the correct place to form a correct URL (as we had used with Postman).
Insert the highlighted line below to assign the url variable:
# Set the URL using the parameters of the object and the api_method requested# in a format such as: https//host:port/api/v1/api_method
url ="https://{}:{}{}/{}".format(self.host, self.port, self.base_url, api_method)
Now add the code to send the individual request types. As we noted with Postman,
the POST and PUT often have both parameters and a payload, while GET can only have parameters and
DELETE will not have anything aside from the URL.
These requests should be placed inside a try block so that if a RequestExcept
exception occurs, it can be returned in the 'message' instead
of crashing your program. There will be a different request for each of the supported
types. Only PUT and POST will need to send a payload, and a DELETE doesn't require any parameters
other than the URL to send to, which will include and object ID of some kind built into the URL.
If a response is received, you simply place it in 'response'
and set 'success' to True. You do not need to do any further interpretation of whether things
were successful yet.
Insert the following below the # Send the request comment line:
# Send the request and handle RequestException that may occurtry:if http_method in['GET','POST','PUT','DELETE']:if http_method =='GET':
raw_response = self.session.get(url, params=parameters)elif http_method =='POST':
raw_response = self.session.post(url, data=payload, params=parameters)elif http_method =='PUT':
raw_response = self.session.put(url, data=payload, params=parameters)elif http_method =='DELETE':
raw_response = self.session.delete(url)
result ={'success':True,'response': raw_response,'message':'Successful {} request to: {}'.format(http_method, url)}except RequestException as e:
result ={'success':False,'message':'RequestException {} request to: {} :: {}'.format(http_method, url, e)}return result
Step 2 - Response Checking
In most cases you will want to have some checks in place to see if a HTTP response code other than the 200-299
range was received. This can indicate congestion or other transient or permanent failure on the server side.
You just need a method that you can call to check for that simple condition.
In this lab, you won't implement any retries or other handling, but it could be added for specific
cases.
In flaskr/rest/v1/rest.py, we have created a method called
_check_response. Currently it looks like this. Remove
the highlighted pass line:
def_check_response(self, raw_response):"""
Evaluates a response status. If it has a non-2XX value, then set the 'success' result
to false and place the exception in the 'message'
:param raw_response: The raw response from a _send_request.
:returns: Returns a the same dictionary passed to it, with the 'success' and 'message'
keys modified, if needed.
:rtype: Dict
"""pass
Add the following code:
def_check_response(self, raw_response):"""
Evaluates a response status. If it has a non-2XX value, then set the 'success' result
to false and place the exception in the 'message'
:param raw_response: The raw response from a _send_request.
:returns: Returns a the same dictionary passed to it, with the 'success' and 'message'
keys modified, if needed.
:rtype: Dict
"""try:# Raise HTTPError error for non-2XX responses
raw_response['response'].raise_for_status()except HTTPError as e:
raw_response['success']=False
raw_response['message']='HTTPError Exception: {}'.format(e)return raw_response
Save this file now, as you have finished configured it.
Now you have a method to send the requests and one to check the results for obvious return
failures. Next you can implement each individual API to utilize these methods and parse
the results according to their own requirements.
from requests import get, post, put, delete, packages, request
from requests.exceptions import RequestException, HTTPError
from requests.auth import HTTPBasicAuth
from requests import Session
classREST:"""
The REST Server class
Use this class to connect and make API calls to an most REST-based devices.
:param host: The Hostname / IP Address of the server
:param username: The username of user with REST/API access
:param password: The password for user
:param port: (optional) The server port for API access (default: 443)
:param base_url: (optional) The base URL, such as "/api/v1" for a given API
:param headers: (optional) A dictionary of header key/value pairs
:param tls_verify: (optional) Whether certificate validation will be performed.
:type host: String
:type username: String
:type password: String
:type port: Integer
:type base_url: String
:type headers: Dict
:type tls_verify: Bool
:returns: return an REST object
:rtype: REST
"""def__init__(self, host, username=None, password=None, base_url=None, headers={}, port=443, tls_verify=False):"""
Initialize an object with the host, port, and base_url using the parameters passed in.
"""
self.host = host
self.port =str(port)
self.base_url = base_url
self.session = Session()
self.session.auth = HTTPBasicAuth(username, password)
self.session.verify = tls_verify
self.session.headers.update(headers)def_send_request(self, api_method, parameters={}, payload=None, http_method='GET'):"""
Used to send a REST request using the desired http_method (GET, PUT, POST, DELETE) to the
specified base_url + api_method. Any specified parameters and payload, where applicable,
will be included.
:param api_method: API method, such as "users" requested. Will be combined with base_url
:param parameters: (optional) Any URL parameters, such as {'filter': 'blah'}
:param payload: (optional) A body/payload to be included in the request.
:param http_method: (optional) The type of method (GET, PUT, POST, DELETE)
:type api_method: String
:type parameters: Dict
:type payload: String (usually JSON-encoded)
:type http_method: String (GET, PUT, POST, DELETE)
:returns: returns a dictionary indicating success with the raw response or
a message indicating the error encountered.
:returns: return a dictionary with the following keys:
'success' :rtype:Bool: Whether the response received from the server is deemed a success
'message' :rtype:String: A message indicating success or details of any exception encountered
'response' :rtype:requests.models.Response: The raw response from the Python requests library
:rtype: Dict
"""
result ={'success':False,'message':'','response':''}# Set the URL using the parameters of the object and the api_method requested# in a format such as: https//host:port/api/v1/api_method
url ="https://{}:{}{}/{}".format(self.host, self.port, self.base_url, api_method)# Send the request and handle RequestException that may occurtry:if http_method in['GET','POST','PUT','DELETE']:if http_method =='GET':
raw_response = self.session.get(url, params=parameters)elif http_method =='POST':
raw_response = self.session.post(url, data=payload, params=parameters)elif http_method =='PUT':
raw_response = self.session.put(url, data=payload, params=parameters)elif http_method =='DELETE':
raw_response = self.session.delete(url)
result ={'success':True,'response': raw_response,'message':'Successful {} request to: {}'.format(http_method, url)}except RequestException as e:
result ={'success':False,'message':'RequestException {} request to: {} :: {}'.format(http_method, url, e)}return result
def_check_response(self, raw_response):"""
Evaluates a response status. If it has a non-2XX value, then set the 'success' result
to false and place the exception in the 'message'
:param raw_response: The raw response from a _send_request.
:returns: Returns a the same dictionary passed to it, with the 'success' and 'message'
keys modified, if needed.
:rtype: Dict
"""try:# Raise HTTPError error for non-2XX responses
raw_response['response'].raise_for_status()except HTTPError as e:
raw_response['success']=False
raw_response['message']='HTTPError Exception: {}'.format(e)return raw_response