Goal
In the previous project A simple API: Implementation, the API accepts a string ("hello") and returns a string ("hello").
However, in practice, we would probably want to only accept and return data as JSON-formatted strings.
Goal: Reimplement the API, processing input and output as JSON.
Contents
- Goal
- Contents
- Project Log
Project Log
Using JSON-formatted data allows us to think of the input and output as language-agnostic objects, to which properties can be attached.
Example additional properties for the input:
- the identity that made the request
- the time at which the request was received by the server
- a data package (e.g. the data from a POST request)
Example additional properties for the output:
- the validity of the request (perhaps the request was incorrectly formatted)
- an error (with an error code and an error message e.g. "You aren't authorised to view this item")
- a data package (e.g. an reasonably large file)
The functionality from the previous project can be preserved. The input object can still have the original URI (e.g. "/api/v1/hello") as a property. The output can still have the original result as a property (e.g. "world").
API = Application Programming Interface
URI = Uniform Resource Identifier
System details:
- Name: Shovel
- Specifications: HP 6005 Pro SFF. 3 GHz x86_64 processor (AMD II x4 B95 Quad Core), 4 GB RAM, 1 TB hard drive. Running CentOS 7.6.1810 (Core).
- More information: New computer: Shovel
- Installed items: GCC 4.8.5, Make 3.82, Vim 7.4, Python 2.7.5, Python 3.3.2, Gnome 3.28.2, gedit 3.28.1.
I'm working in the Gnome GUI.
I'm using:
- gedit for writing this log and the code
- Python 2.7.5 as the code language
Create a new project directory named "a_simple_api_json_inputoutput". Within it, create a new work directory named "work".
Open a terminal and change directory to the work directory.
Let's design the API implementation. We're planning for multiple versions of the API over time, so the main API processor will only look at the URI and transfer the input to the relevant API version.
API implementation:
- work [directory] [contains project]
-- app.py [file] [main program, creates input and sends it to the API]
-- api.py [file] [processes first section of URI and removes it, routes input to the right API version, waits for output, returns output to app.py]
-- api_v1 [directory] [contains version 1 of the API]
--- api.py [file] [processes URI, routes input to the right function, waits for output, returns output to ../api.py]
--- functions.py [file] [the actual core functionality of this API. in this case, return "world" in response to the URI "hello"]
- work [directory] [contains project]
-- app.py [file] [main program, creates input and sends it to the API]
-- api.py [file] [processes first section of URI and removes it, routes input to the right API version, waits for output, returns output to app.py]
-- api_v1 [directory] [contains version 1 of the API]
--- api.py [file] [processes URI, routes input to the right function, waits for output, returns output to ../api.py]
--- functions.py [file] [the actual core functionality of this API. in this case, return "world" in response to the URI "hello"]
Let's design some test cases.
Case 1:
- Input: "/api/v1/hello"
- Output: "world"
Case 2:
- Input: "/foo"
- Output: "Error: No resource found at URI {/foo}."
Case 3:
- Input: "/api/v1/hola"
- Output: "Error: No resource found at URI {/api/v1/hola}."
Case 4:
- Input: "/api/v1/foo%20bar"
- Output: "Error: URIs cannot contain the character {%}."
Case 5:
- Input: "bar"
- Output: "Error: URIs must start with {/}."
[development occurs here]
Finished.
[spiano@localhost work]$ python app.py
uri: /api/v1/hello
API input (JSON): "/api/v1/hello"
API output (JSON): {"message": "world", "type": "result"}
output message: world
uri: /foo
API input (JSON): "/foo"
API output (JSON): {"message": "Error: No resource found at URI {/foo}.", "code": 0, "type": "error"}
output message: Error: No resource found at URI {/foo}.
uri: /api/v1/hola
API input (JSON): "/api/v1/hola"
API output (JSON): {"message": "Error: No resource found at URI {/api/v1/hola}.", "code": 0, "type": "error"}
output message: Error: No resource found at URI {/api/v1/hola}.
uri: /api/v1/foo%20bar
API input (JSON): "/api/v1/foo%20bar"
API output (JSON): {"message": "Error: URIs cannot contain the character {%}.", "code": 1, "type": "error"}
output message: Error: URIs cannot contain the character {%}.
uri: bar
API input (JSON): "bar"
API output (JSON): {"message": "Error: URIs must start with {/}. URI: {bar}.", "code": 1, "type": "error"}
output message: Error: URIs must start with {/}. URI: {bar}.
Good.
[spiano@localhost work]$ ls -1
api.py
api.pyc
api_v1
app.py
[spiano@localhost work]$ ls -1 api_v1
api.py
api.pyc
functions.py
functions.pyc
__init__.py
__init__.pyc
The rest of this article contains the code.
In the work directory, we have app.py and api.py.
app.py
import json | |
import api | |
def main(): | |
uris = [ | |
"/api/v1/hello", | |
"/foo", | |
"/api/v1/hola", | |
"/api/v1/foo%20bar", | |
"bar", | |
] | |
for uri in uris: | |
input1 = json.dumps(uri) | |
output1 = api.get_response(uri) | |
output_obj = json.loads(output1) | |
message = output_obj["message"] | |
print "" | |
print "uri:", uri | |
print "API input (JSON):", input1 | |
print "API output (JSON):", output1 | |
print "output message:", message | |
print "" | |
if __name__ == "__main__": main() |
api.py
import json | |
import api_v1.api as api_v1 | |
def get_response(uri): | |
try: | |
output = process_uri(uri) | |
except Exception as e: | |
output = json.dumps({ | |
"type": "error", | |
"code": 1, | |
"message": str(e), | |
}) | |
#import traceback, sys | |
#exc_type, exc_value, exc_tb = sys.exc_info() | |
#traceback.print_exception(exc_type, exc_value, exc_tb) | |
return output | |
def process_uri(uri): | |
# default output | |
default_output = json.dumps({ | |
"type": "error", | |
"code": 0, | |
"message": "Error: No resource found at URI {%s}." % uri, | |
}) | |
validate_uri(uri) | |
# remove first character {/} | |
uri = uri[1:] | |
sections = uri.split("/") | |
if sections[0] == "api" and sections[1] == "v1": | |
sections2 = sections[2:] | |
uri2 = "/" + "/".join(sections2) | |
output = api_v1.get_response(uri2) | |
output_obj = json.loads(output) | |
if output_obj["type"] == "error": | |
if output_obj["code"] == 0: | |
output = default_output | |
return output | |
return default_output | |
def validate_uri(uri): | |
# first character must be / | |
if uri[0] != "/": | |
raise ValueError("Error: URIs must start with {/}. URI: {%s}." % uri) | |
alphabet_lower = "abcdefghijklmnopqrstuvwxyz" | |
alphabet_upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" | |
digits = "01234567890" | |
symbols = "/.-_?=" | |
permitted_characters = alphabet_lower + alphabet_upper + digits + symbols | |
for c in uri: # c = character | |
if c not in permitted_characters: | |
if not (33 <= ord(c) <= 126): # visible ascii glyphs | |
c = "[byte ordinal value %d]" % ord(c) | |
raise ValueError("Error: URIs cannot contain the character {%s}." % c) |
In the api_v1 directory, we have api.py, functions.py, and __init__.py.
api.py
import json | |
import functions as ff | |
def get_response(uri): | |
try: | |
output = process_uri(uri) | |
except Exception as e: | |
output = json.dumps({ | |
"type": "error", | |
"error_code": 1, | |
"error_message": str(e), | |
}) | |
return output | |
def process_uri(uri): | |
# default output | |
default_output = json.dumps({ | |
"type": "error", | |
"code": 0, | |
"message": "Error: No resource found at URI {%s}." % uri, | |
}) | |
# first character must be / | |
if uri[0] != "/": | |
raise ValueError("URIs must start with {/}. URI: {%s}." % uri) | |
# remove first character | |
uri = uri[1:] | |
sections = uri.split("/") | |
if sections[0] == "hello": | |
if len(sections) == 1: | |
result = ff.hello() | |
output = json.dumps({ | |
"type": "result", | |
"message": result, | |
}) | |
return output | |
return default_output |
functions.py
def hello(): | |
return "world" |
__init__.py