', methods=['PUT'])
154 | @auth.login_required
155 | def get_image_dimensions(img_id):
156 | img = [img for img in images if img['id'] == img_id]
157 | if len(img) == 0:
158 | abort(404)
159 | url = img[0]['url']
160 | img[0]['size'] = get_image_dims(url)
161 | return jsonify({'img': make_public_img(img[0])}), 200
162 |
163 | def get_image_dims(imgURL):
164 | imagePath, headers = urllib.request.urlretrieve(imgURL)
165 | img=Image.open(imagePath)
166 | width, height = img.size
167 | size = {
168 | 'height' : height,
169 | 'width': width
170 | }
171 | return size
172 |
173 |
174 | ### Model and Labels files for TensorFlow
175 | modelFullPath = './static/output_graph.pb'
176 | labelsFullPath = './static/output_labels.txt'
177 | # ### pythonanywhere handles paths differently, uncomment in production
178 | # modelFullPath = '/home/10la/restful-api-flask/static/output_graph.pb'
179 | # labelsFullPath = '/home/10la/restful-api-flask/static/output_labels.txt'
180 |
181 | def create_graph():
182 | """Creates a graph from saved GraphDef file and returns a saver."""
183 | # Creates graph from saved graph_def.pb.
184 | with tf.gfile.FastGFile(modelFullPath, 'rb') as f:
185 | graph_def = tf.GraphDef()
186 | graph_def.ParseFromString(f.read())
187 | _ = tf.import_graph_def(graph_def, name='')
188 |
189 | def run_inference_on_image(imgURL):
190 | # answer = None
191 | results_dict = {}
192 | results = []
193 | results_name = []
194 | results_score = []
195 | imagePath, headers = urllib.request.urlretrieve(imgURL)
196 | if not tf.gfile.Exists(imagePath):
197 | tf.logging.fatal('File does not exist %s', imagePath)
198 | return answer
199 |
200 | image_data = tf.gfile.FastGFile(imagePath, 'rb').read()
201 |
202 | # Creates graph from saved GraphDef.
203 | create_graph()
204 |
205 | with tf.Session() as sess:
206 |
207 | softmax_tensor = sess.graph.get_tensor_by_name('final_result:0')
208 | predictions = sess.run(softmax_tensor,
209 | {'DecodeJpeg/contents:0': image_data})
210 | predictions = np.squeeze(predictions)
211 |
212 | top_k = predictions.argsort()[-5:][::-1] # Getting top 5 predictions
213 | f = open(labelsFullPath, 'rb')
214 | lines = f.readlines()
215 | labels = [str(w).replace("\n", "") for w in lines]
216 | for node_id in top_k:
217 | human_string = labels[node_id]
218 | score = predictions[node_id]
219 | results_name.append(human_string)
220 | results_score.append(score)
221 | # print('%s (score = %.5f)' % (human_string, score))
222 | # answer = labels[top_k[0]]
223 | # results = zip(results_name, results_score)
224 | results_dict = {
225 | "results_name_1": results_name[0],
226 | "results_score_1": json.JSONEncoder().encode(format(results_score[0], '.4f')),
227 | "results_name_2": results_name[1],
228 | "results_score_2": json.JSONEncoder().encode(format(results_score[1], '.4f')),
229 | "results_name_3": results_name[2],
230 | "results_score_3": json.JSONEncoder().encode(format(results_score[2], '.4f'))
231 | }
232 |
233 | results_dict2={}
234 | i = 0
235 | for item in results_name:
236 | results_dict2[i] = {"results_score": format(results_score[i], '.4f'), "results_name": results_name[i]}
237 | i += 1
238 | # print results_dict2
239 | # return results_dict2
240 | return results_dict2
241 |
242 |
243 | if __name__ == '__main__':
244 |
245 | print("* Loading TensorFlow Model...")
246 | t = Thread(target=create_graph, args=())
247 | t.daemon = True
248 | t.start()
249 | print("** Model loaded, starting webapp")
250 | # Start main webapp
251 | app.run()
--------------------------------------------------------------------------------
/images/AltraDifficult01.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chriskaschner/restful-api-flask/ab58218341f1aedd6195424c8c50d7ee09869597/images/AltraDifficult01.jpg
--------------------------------------------------------------------------------
/images/AltraExample.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chriskaschner/restful-api-flask/ab58218341f1aedd6195424c8c50d7ee09869597/images/AltraExample.jpg
--------------------------------------------------------------------------------
/images/AltraExample02.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chriskaschner/restful-api-flask/ab58218341f1aedd6195424c8c50d7ee09869597/images/AltraExample02.jpg
--------------------------------------------------------------------------------
/images/AltraOccluded.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chriskaschner/restful-api-flask/ab58218341f1aedd6195424c8c50d7ee09869597/images/AltraOccluded.jpg
--------------------------------------------------------------------------------
/images/Neither.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chriskaschner/restful-api-flask/ab58218341f1aedd6195424c8c50d7ee09869597/images/Neither.jpg
--------------------------------------------------------------------------------
/images/NikeDifficult01.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chriskaschner/restful-api-flask/ab58218341f1aedd6195424c8c50d7ee09869597/images/NikeDifficult01.jpg
--------------------------------------------------------------------------------
/images/NikeExample.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chriskaschner/restful-api-flask/ab58218341f1aedd6195424c8c50d7ee09869597/images/NikeExample.jpg
--------------------------------------------------------------------------------
/images/NikePreCrop.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chriskaschner/restful-api-flask/ab58218341f1aedd6195424c8c50d7ee09869597/images/NikePreCrop.jpg
--------------------------------------------------------------------------------
/keras_server.py:
--------------------------------------------------------------------------------
1 | # USAGE
2 | # Start the server:
3 | # python keras_server.py
4 | # Submit a request via cURL:
5 | # curl -X POST -F image=@jemma.png 'http://localhost:5000/predict'
6 | # Submit a request via Python:
7 | # python simple_request.py
8 |
9 | # import the necessary packages
10 | from keras.applications.inception_v3 import InceptionV3
11 | from keras.preprocessing.image import img_to_array
12 | from keras.applications import imagenet_utils
13 | from threading import Thread
14 | from PIL import Image
15 | import numpy as np
16 | import base64
17 | import flask
18 | import redis
19 | import uuid
20 | import time
21 | import json
22 | import sys
23 | import io
24 |
25 | # initialize constants used to control image spatial dimensions and
26 | # data type
27 | IMAGE_WIDTH = 224
28 | IMAGE_HEIGHT = 224
29 | IMAGE_CHANS = 3
30 | IMAGE_DTYPE = "float32"
31 |
32 | # initialize constants used for server queuing
33 | IMAGE_QUEUE = "image_queue"
34 | BATCH_SIZE = 32
35 | # time the server and client will pause before polling Redis again
36 | SERVER_SLEEP = 0.25
37 | CLIENT_SLEEP = 0.25
38 |
39 |
40 | # initialize our Flask application, Redis server, and Keras model
41 | app = flask.Flask(__name__)
42 | db = redis.StrictRedis(host="localhost", port=6379, db=0)
43 | model = None
44 |
45 | def base64_encode_image(a):
46 | # base64 encode the input NumPy array
47 | return base64.b64encode(a).decode("utf-8")
48 |
49 | def base64_decode_image(a, dtype, shape):
50 | # if this is Python 3, we need the extra step of encoding the
51 | # serialized NumPy string as a byte object
52 | if sys.version_info.major == 3:
53 | a = bytes(a, encoding="utf-8")
54 |
55 | # convert the string to a NumPy array using the supplied data
56 | # type and target shape
57 | a = np.frombuffer(base64.decodebytes(a), dtype=dtype)
58 | a = a.reshape(shape)
59 |
60 | # return the decoded image
61 | return a
62 |
63 |
64 | def prepare_image(image, target):
65 | # if the image mode is not RGB, convert it
66 | if image.mode != "RGB":
67 | image = image.convert("RGB")
68 |
69 | # resize the input image and preprocess it
70 | image = image.resize(target)
71 | image = img_to_array(image)
72 | image = np.expand_dims(image, axis=0)
73 | image = imagenet_utils.preprocess_input(image)
74 |
75 | # return the processed image
76 | return image
77 |
78 |
79 | def classify_process():
80 | # load the pre-trained Keras model (here we are using a model
81 | # pre-trained on ImageNet and provided by Keras, but you can
82 | # substitute in your own networks just as easily)
83 | print("* Loading model...")
84 | model = InceptionV3(weights='imagenet')
85 | print("* Model loaded")
86 |
87 | # continually pool for new images to classify
88 | while True:
89 | # attempt to grab a batch of images from the database, then
90 | # initialize the image IDs and batch of images themselves
91 | queue = db.lrange(IMAGE_QUEUE, 0, BATCH_SIZE - 1)
92 | imageIDs = []
93 | batch = None
94 |
95 | # loop over the queue
96 | for q in queue:
97 | # deserialize the object and obtain the input image
98 | q = json.loads(q.decode("utf-8"))
99 | image = base64_decode_image(q["image"], IMAGE_DTYPE,
100 | (1, IMAGE_HEIGHT, IMAGE_WIDTH, IMAGE_CHANS))
101 |
102 | # check to see if the batch list is None
103 | if batch is None:
104 | batch = image
105 |
106 | # otherwise, stack the data
107 | else:
108 | batch = np.vstack([batch, image])
109 |
110 | # update the list of image IDs
111 | imageIDs.append(q["id"])
112 |
113 | # check to see if we need to process the batch
114 | if len(imageIDs) > 0:
115 | # classify the batch
116 | print("* Batch size: {}".format(batch.shape))
117 | preds = model.predict(batch)
118 | results = imagenet_utils.decode_predictions(preds)
119 |
120 | # loop over the image IDs and their corresponding set of
121 | # results from our model
122 | for (imageID, resultSet) in zip(imageIDs, results):
123 | # initialize the list of output predictions
124 | output = []
125 |
126 | # loop over the results and add them to the list of
127 | # output predictions
128 | for (imagenetID, label, prob) in resultSet:
129 | r = {"label": label, "probability": float(prob)}
130 | output.append(r)
131 |
132 | # store the output predictions in the database, using
133 | # the image ID as the key so we can fetch the results
134 | db.set(imageID, json.dumps(output))
135 |
136 | # remove the set of images from our queue
137 | db.ltrim(IMAGE_QUEUE, len(imageIDs), -1)
138 |
139 | # sleep for a small amount
140 | time.sleep(SERVER_SLEEP)
141 |
142 | # @app.route("/predict", methods=["POST"])
143 | def predict():
144 | # initialize the data dictionary that will be returned from the
145 | # view
146 | data = {"success": False}
147 |
148 | # ensure an image was properly uploaded to our endpoint
149 | if flask.request.method == "POST":
150 | if flask.request.files.get("image"):
151 | # read the image in PIL format and prepare it for
152 | # classification
153 | image = flask.request.files["image"].read()
154 | image = Image.open(io.BytesIO(image))
155 | image = prepare_image(image, (IMAGE_WIDTH, IMAGE_HEIGHT))
156 |
157 | # ensure our NumPy array is C-contiguous as well,
158 | # otherwise we won't be able to serialize it
159 | image = image.copy(order="C")
160 |
161 | # generate an ID for the classification then add the
162 | # classification ID + image to the queue
163 | k = str(uuid.uuid4())
164 | d = {"id": k, "image": base64_encode_image(image)}
165 | db.rpush(IMAGE_QUEUE, json.dumps(d))
166 |
167 | # keep looping until our model server returns the output
168 | # predictions
169 | while True:
170 | # attempt to grab the output predictions
171 | output = db.get(k)
172 |
173 | # check to see if our model has classified the input
174 | # image
175 | if output is not None:
176 | # add the output predictions to our data
177 | # dictionary so we can return it to the client
178 | output = output.decode("utf-8")
179 | data["predictions"] = json.loads(output)
180 |
181 | # delete the result from the database and break
182 | # from the polling loop
183 | db.delete(k)
184 | break
185 |
186 | # sleep for a small amount to give the model a chance
187 | # to classify the input image
188 | time.sleep(CLIENT_SLEEP)
189 |
190 | # indicate that the request was a success
191 | data["success"] = True
192 |
193 | # return the data dictionary as a JSON response
194 | return flask.jsonify(data)
195 |
196 | # if this is the main thread of execution first load the model and
197 | # then start the server
198 | if __name__ == "__main__":
199 | # load the function used to classify input images in a *separate*
200 | # thread than the one used for main classification
201 | print("* Starting model service...")
202 | t = Thread(target=classify_process, args=())
203 | t.daemon = True
204 | t.start()
205 |
206 | # start the web server
207 | print("* Starting web service...")
208 | app.run()
--------------------------------------------------------------------------------
/report/DesignDocument.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chriskaschner/restful-api-flask/ab58218341f1aedd6195424c8c50d7ee09869597/report/DesignDocument.pdf
--------------------------------------------------------------------------------
/report/report.md:
--------------------------------------------------------------------------------
1 |
2 | ### Purpose
3 |
4 | Can you teach a computer to recognize the brand logos in an unlabeled feed of images from Instagram?
5 |
6 | In a [previous project](chriskaschner.com/retraining), I created a Convolutional Neural Network (CNN) that can identify brand logos (specifically Nike and Altra) in untagged/ unlabeled photos from a social media feed. The model I used implemented a [previously trained](https://github.com/tensorflow/models/tree/master/inception) network and [transfer learning](https://en.wikipedia.org/wiki/Inductive_transfer).
7 |
8 | For this project I wanted to build a REST API that allowed me to send requests to that neural network and store its results in JSON.
9 |
10 | Fortunately, I also had the opportunity to develop a solution for the GapJumpers Challenge [available here](https://www.gapjumpers.me/questions/return-path/qs-323/).
11 |
12 | The challenge specifies the following for a successful submission:
13 |
14 | - Write a simple Rest API with PHP, Node.js, Ruby, Python or Go.
15 | - Your API must have:
16 | - at least three endpoints
17 | - all endpoints must be linked in some way
18 | - Pay careful attention to REST best practices
19 | - Describe the application in detail in a design document including why you chose the approach you did.
20 | - Include tests that validate success.
21 |
22 | ### Deliverables
23 | - Using Python and Flask, I created this website- [a single page](https://chriskaschner.github.io/restful-api-flask/) that allows for retrieval of data from my endpoints. The 3 endpoints are described in detail in the API Documentation section below.
24 | - In order to share my application and provide links where it can be tested, links and curl embeds are included throughout this document that allow for interacting with the API directly. The API is currently live at [http://10la.pythonanywhere.com/img/api/v1.0/images](http://10la.pythonanywhere.com/img/api/v1.0/images) and all references to a [hostname] in this documentation refers to `http://10la.pythonanywhere.com`. The [application](https://github.com/chriskaschner/restful-api-flask/blob/master/app.py), it's [unit tests](https://github.com/chriskaschner/restful-api-flask/blob/master/test_app.py), and all supporting documentation can be found at the [project's GitHub](https://github.com/chriskaschner/restful-api-flask).
25 | - This webpage represents the design document. In particular the sections dedicated to the REST Architecture and API Documentation discuss the design process in detail.
26 | - Unit tests, including those that validate success, are available in a number of ways. Basic functionality can be verified by executing each curl command in the order it appears in the the API Documentation section. A copy of the output of the unit test application is included in the Unit Tests section below. Finally, the [test application](https://github.com/chriskaschner/restful-api-flask/blob/master/test_app.py) can be downloaded from GitHub and run locally.
27 |
28 | ### REST Architecture
29 |
30 | Representational state transfer (REST) was originally specified in the [5th chapter](http://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm) of Roy Fielding's PhD thesis "Architectural Styles and
31 | the Design of Network-based Software Architectures" which specifies 6 constraints for the REST architecture.
32 |
33 | 1. Client-Server - API is separated from a client, although this could represent different processes within a single computer. The objective is to allow for scalability- mulitple clients and multiple types of clients can thus connect with a single API.
34 | 1. Stateless - [No](http://programmers.stackexchange.com/questions/141019/should-cookies-be-used-in-a-restful-api) [cookies](http://stackoverflow.com/questions/319530/restful-authentication) and no sessions. Clients must authenticate upon every request.
35 | 1. Cache - Server should provide caching directives so that in the event that a similar request arrives multiple times, a response can be provided without requiring a repeated request to the server. There are a variety of caches, including inside of the browser.
36 | 1. Layered System - Both the client and the API may or may not be communicating directly with each the other. Likely there's a layer in between (such as a load balancer). This prevents a server from identifying requests by the client. In the case of a load-balancer, a server may receive *ALL* of its requests from a single client. The benefit of this is that in the event that a load balancer is placed in between the server and the client, you now have the ability to scale to a huge number of clients and servers without requiring fundamental changes.
37 | 1. Code-On-Demand - In practice this is an optional REST principle. Clients can receive executable code to run as a response to a request. How would an API know what types of code that a client could run?
38 | 1. Uniform Interface -
39 | 1. Identification of resources - Resources represent every entity in the domain of the application. Each resource gets its own unique identifier URI such as: `/img/api/v1.0/images/3`. Collections of resources can also have identifiers such as `/img/api/v1.0/images`.
40 | 1. Resource Representations - Clients only access representations of a resource, never the resource itself. Additionally, clients only operate on representations of resources. The server may provide a number of different content types, like JSON or XML.
41 | 1. Self-Descriptive Message - Clients send and receive HTTP. Request method defines the operation; target is represented in the URI of the request, headers provide authentication credentials and content types, the representation of the resource is in the body, and the result of an operation is in the response status code.
42 | 1. HATEOAS [Hypermedia As The Engine Of Application State](https://en.wikipedia.org/wiki/HATEOAS) - Clients don't know any resource URIs except for the root of the API and everything else can be discovered through links in resource representations.
43 |
44 | Because REST defines an architectural style and is not a specification, there is room for interpretation (or [arguments](http://stackoverflow.com/questions/37151257/can-rest-clients-be-not-restful) on [StackOverflow](http://stackoverflow.com/questions/9055197/splitting-hairs-with-rest-does-a-standard-json-rest-api-violate-hateoas) as [the](http://stackoverflow.com/questions/1950764/why-isnt-soap-based-web-service-restful) case [may](http://stackoverflow.com/questions/1164154/is-that-rest-api-really-rpc-roy-fielding-seems-to-think-so?rq=1) be).
45 |
46 | ### My project's interpretation of REST constraints
47 |
48 | 1. Client-Server - Yes, the API is hosted on a server both physically and logically separated from the clients.
49 | 1. Stateless - Yes, outside of the root URI for the API clients must authenticate upon every request. Although as listed in the improvements section, adding rate limiting would potentially remove this constraint. In a stateless system how would you identify an un-authenticated user in order to limit their number of requests?
50 | 1. Cache - Yes, this is being met when the API is consumed in a browser. However, as discussed in the improvements section, adding etags would improve the existing solution.
51 | 1. Layered System - Yes, Similar to Client-Server above, all requests are authenticated separately, and there are no systems in place that would attempt to identify a client merely by its requests. I use HTTP Basic Auth and discuss some ways this could be improved in the improvements section.
52 | 1. Code-On-Demand - No, I do not meet this [optional requirement](https://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm#sec_5_1_7) of REST. One potential application that I could see this constraint being used for is to download and execute JavaScript in the browser.
53 | 1. Uniform Interface -
54 | 1. Identification of resources - Yes, each resource gets its own unique identifier URI such as: `/img/api/v1.0/images/3` and each collections of resources has an udentifier `/img/api/v1.0/images`. I also include versioning information in this category. And while there is some argument about *where* to place the version information for your API, there is none as to *if* you should version it. I place versioning info in the URI. The other alternative is in the header.
55 | 1. Resource Representations - Yes, clients can only operate on representations of resources. In this case, the server only provides JSON content. I use JSON because it's ubiquitous, human readable, and easy to work with.
56 | 1. Self-Descriptive Message - Yes, the target is represented by the URI of the request, headers provide authentication credentials and content types, the representation of the resource is in the body, and the result of an operation is in the response status code. Request methods define the operation being performed as follows:
57 | - GET requests get data
58 | - POST requests create new data
59 | - PUT requests update existing data - although [there](http://stackoverflow.com/questions/630453/put-vs-post-in-rest) are differing [opinions](https://stormpath.com/blog/fundamentals-rest-api-design) that says you should use POST for everything.
60 | - DELETE requests delete data
61 |
62 | 1. HATEOAS [Hypermedia As The Engine Of Application State](https://en.wikipedia.org/wiki/HATEOAS) - Yes, Every resource request returns a URI property (except DELETE) that is the full URI of where a resource resides. Potential improvements for this feature are discussed below.
63 |
64 | Overall I would say that my implementation is a level 2/3 on the [Richardson REST Maturity Model](http://martinfowler.com/articles/richardsonMaturityModel.html).
65 |
66 | ### API Documentation
67 |
68 | Below I describe and give examples for all available API resources. By following along and executing the embedded curl commands in order one can effectively test the API.
69 |
70 | ### Endpoint 1. Images File Store
71 |
72 | The first endpoint provides a means to store and retrieve information that relates to images. It handles all of the portions of our API related to creating, updating, retrieving and deleting individual image records as well as lists of images.
73 |
74 | | HTTP Method | URI | Action |
75 | |--------------|------------------------------------------------|------------------------------|
76 | | GET | [hostname]/img/api/v1.0/imgs | Retrieve list of images |
77 | | POST | [hostname]/img/api/v1.0/imgs | Create new image |
78 | | GET | [hostname]/img/api/v1.0/imgs/[img_id] | Retrieve an individual image |
79 | | PUT | [hostname]/img/api/v1.0/imgs/[img_id] | Update an existing image |
80 | | DELETE | [hostname]/img/api/v1.0/imgs/[img_id] | Delete an existing image |
81 |
82 | Each image record will have a number of different fields of JSON data:
83 | - `id` and `uri`: Unique identifier for images. Although id integers are returned as a full URI that controls the image. This means a client does not need to construct a URI from information it receives from the API but rather receives a usable full path. An example- `'id'=1` and `'uri'= "http://10la.pythonanywhere.com/img/api/v1.0/images/1"`.
84 | - `title` : A short description of the image.
85 | - `url`: A location of the image as stored on AWS S3, such as `http://imgdirect.s3-website-us-west-2.amazonaws.com/nike.jpg`
86 | - `results`: The probabilities and labels for each potential class an image can belong to. The below sample shows that there is a 79% confidence that the image in question is a Nike.
87 |
88 | ```
89 | "results": {
90 | "results_name_1": "nike",
91 | "results_score_1": "\"0.7914\""
92 | }
93 | ```
94 | - `resize`: Boolean used for determining if the image size has been processed to the appropriate size.
95 | - `size` : Height and width in pixels of an image as returned from the `resize` resource.
96 |
97 | #### GET [hostname]/img/api/v1.0/imgs
98 | curl -i http://10la.pythonanywhere.com/img/api/v1.0/images
99 |
100 | #### Results:
101 | ```
102 | HTTP/1.1 200 OK
103 | Server: openresty/1.9.15.1
104 | Date: Thu, 25 Aug 2016 11:14:44 GMT
105 | Content-Type: application/json
106 | Content-Length: 509
107 | Connection: keep-alive
108 | Vary: Accept-Encoding
109 | X-Clacks-Overhead: GNU Terry Pratchett
110 |
111 | {
112 | "images": [
113 | {
114 | "title": "Nikes",
115 | "url": "http://imgdirect.s3-website-us-west-2.amazonaws.com/nike.jpg",
116 | "uri": "http://10la.pythonanywhere.com/img/api/v1.0/images/1",
117 | "results": "",
118 | "resize": false,
119 | "size": ""
120 | },
121 | {
122 | "title": "Altra",
123 | "url": "http://imgdirect.s3-website-us-west-2.amazonaws.com/altra.jpg",
124 | "uri": "http://10la.pythonanywhere.com/img/api/v1.0/images/2",
125 | "results": "",
126 | "resize": false,
127 | "size": ""
128 | }
129 | ]
130 | }
131 | ```
132 |
133 | #### POST [hostname]/img/api/v1.0/imgs
134 |
135 | curl -u ReturnPath:python -i -H "Content-Type: application/json" -X POST -d '{"url":"http://imgdirect.s3-website-us-west-2.amazonaws.com/neither.jpg"}' http://10la.pythonanywhere.com/img/api/v1.0/images
136 |
137 | #### Results:
138 |
139 | ```
140 | HTTP/1.1 201 CREATED
141 | Server: openresty/1.9.15.1
142 | Date: Thu, 25 Aug 2016 11:16:34 GMT
143 | Content-Type: application/json
144 | Content-Length: 237
145 | Connection: keep-alive
146 | X-Clacks-Overhead: GNU Terry Pratchett
147 |
148 | {
149 | "image": {
150 | "title": "",
151 | "url": "http://imgdirect.s3-website-us-west-2.amazonaws.com/neither.jpg",
152 | "uri": "http://10la.pythonanywhere.com/img/api/v1.0/images/3",
153 | "results": "",
154 | "resize": false,
155 | "size": ""
156 | }
157 | }
158 | ```
159 |
160 | #### GET [hostname]/img/api/v1.0/imgs/[img_id]
161 |
162 | curl -u ReturnPath:python -i http://10la.pythonanywhere.com/img/api/v1.0/images/3
163 |
164 |
165 | #### Results:
166 |
167 | ```
168 | HTTP/1.1 200 OK
169 | Server: openresty/1.9.15.1
170 | Date: Thu, 25 Aug 2016 11:18:26 GMT
171 | Content-Type: application/json
172 | Content-Length: 181
173 | Connection: keep-alive
174 | X-Clacks-Overhead: GNU Terry Pratchett
175 |
176 | {
177 | "img": {
178 | "title": "",
179 | "url": "http://imgdirect.s3-website-us-west-2.amazonaws.com/neither.jpg",
180 | "results": "",
181 | "id": 3,
182 | "resize": false,
183 | "size": ""
184 | }
185 | }
186 | ```
187 |
188 |
189 |
190 | |
191 | #### PUT [hostname]/img/api/v1.0/imgs/[img_id]
192 |
193 |
194 | curl -u ReturnPath:python -i -H "Content-Type: application/json" -X PUT -d '{"title":"C-ron-ron"}' http://10la.pythonanywhere.com/img/api/v1.0/images/2
195 |
196 | #### Results:
197 |
198 | ```
199 | HTTP/1.1 200 OK
200 | Server: openresty/1.9.15.1
201 | Date: Thu, 25 Aug 2016 11:58:11 GMT
202 | Content-Type: application/json
203 | Content-Length: 181
204 | Connection: keep-alive
205 | X-Clacks-Overhead: GNU Terry Pratchett
206 |
207 | {
208 | "img": {
209 | "title": "C-ron-ron",
210 | "url": "https://s3-us-west-2.amazonaws.com/imgdirect/altra.jpg",
211 | "results": "",
212 | "id": 2,
213 | "resize": false,
214 | "size": ""
215 | }
216 | }
217 | ```
218 |
219 | #### DELETE [hostname]/img/api/v1.0/imgs/[img_id]
220 |
221 | curl -u ReturnPath:python -i -H "Content-Type: application/json" -X DELETE http://10la.pythonanywhere.com/img/api/v1.0/images/3
222 |
223 | #### Results:
224 |
225 | ```
226 | HTTP/1.1 200 OK
227 | Server: openresty/1.9.15.1
228 | Date: Thu, 25 Aug 2016 11:22:52 GMT
229 | Content-Type: application/json
230 | Content-Length: 20
231 | Connection: keep-alive
232 | X-Clacks-Overhead: GNU Terry Pratchett
233 |
234 | {
235 | "result": true
236 | }
237 | ```
238 |
239 | #### Endpoint 2. Image Inference
240 |
241 | The image inference endpoint supplies a resource for interacting with the TensorFlow model I use for determining whether a given image contains and brands of interest. Results are returned as a probability for each of 3 possible outcomes- Altra, Nike, or Neither and are appended to the JSON associated with a given image along with the name for each outcome.
242 |
243 | | HTTP Method | URI | Action |
244 | |--------------|------------------------------------------------|------------------------------|
245 | | PUT | [hostname]/img/api/v1.0/inference/[img_id] | Measure image dimensions and add height and width to field `"size"` in JSON |
246 |
247 | #### PUT [hostname]/img/api/v1.0/inference/[img_id]
248 |
249 | curl -u ReturnPath:python -X PUT -i -H "Content-Type: application/json" -d '{"id":1}' http://10la.pythonanywhere.com/img/api/v1.0/inference/1
250 |
251 | ##### Results:
252 | ```
253 | HTTP/1.1 200 OK
254 | Server: openresty/1.9.15.1
255 | Date: Thu, 25 Aug 2016 11:24:20 GMT
256 | Content-Type: application/json
257 | Content-Length: 458
258 | Connection: keep-alive
259 | Vary: Accept-Encoding
260 | X-Clacks-Overhead: GNU Terry Pratchett
261 |
262 | {
263 | "img": {
264 | "title": "Nikes",
265 | "url": "http://imgdirect.s3-website-us-west-2.amazonaws.com/nike.jpg",
266 | "uri": "http://10la.pythonanywhere.com/img/api/v1.0/images/1",
267 | "results": {
268 | "results_name_3": "altra",
269 | "results_name_2": "neither",
270 | "results_name_1": "nike",
271 | "results_score_3": "\"0.0008\"",
272 | "results_score_2": "\"0.2078\"",
273 | "results_score_1": "\"0.7914\""
274 | },
275 | "resize": false,
276 | "size": ""
277 | }
278 | }C
279 | ```
280 |
281 | #### Endpoint 3. Image Resize
282 |
283 | The third endpoint begins the implementation of resizing images. Because I will eventually extend this API out to be able to ingest a photo, resize it, then upload it to AWS S3, this endpoint lays the groundwork for that function.
284 |
285 | | HTTP Method | URI | Action |
286 | |--------------|------------------------------------------------|------------------------------|
287 | | PUT | [hostname]/img/api/v1.0/resize/[img_id] | Measure image dimensions and add height and width to field `"size"` in JSON |
288 |
289 | #### PUT [hostname]/img/api/v1.0/resize/[img_id]
290 |
291 | curl -u ReturnPath:python -i -H "Content-Type: application/json" -X PUT http://10la.pythonanywhere.com/img/api/v1.0/resize/2
292 |
293 | #### Results:
294 | ```
295 | HTTP/1.1 200 OK
296 | Server: openresty/1.9.15.1
297 | Date: Thu, 25 Aug 2016 11:25:27 GMT
298 | Content-Type: application/json
299 | Content-Length: 504
300 | Connection: keep-alive
301 | Vary: Accept-Encoding
302 | X-Clacks-Overhead: GNU Terry Pratchett
303 |
304 | {
305 | "img": {
306 | "title": "Altra",
307 | "url": "http://imgdirect.s3-website-us-west-2.amazonaws.com/altra.jpg",
308 | "uri": "http://10la.pythonanywhere.com/img/api/v1.0/images/2",
309 | "results": {
310 | "results_name_3": "nike",
311 | "results_name_2": "altra",
312 | "results_name_1": "neither",
313 | "results_score_3": "\"0.0316\"",
314 | "results_score_2": "\"0.2004\"",
315 | "results_score_1": "\"0.7680\""
316 | },
317 | "resize": false,
318 | "size": {
319 | "width": 480,
320 | "height": 480
321 | }
322 | }
323 | }
324 | ```
325 |
326 | #### Unit Tests
327 |
328 | The unit tests in `test_app.py` run a number of different tests against the API.
329 |
330 | They test:
331 | 1. A root address exists and responds appropriately
332 | 1. A nonexistent URL returns a 404 error
333 | 1. Individual images have their own URI that responds correctly
334 | 1. Bad image URLs return a 404 error
335 | 1. Creating a new image functions and returns the appropriate response
336 | 1. JSON is used throughout the requests and responses
337 | 1. Authentication is used correctly on all requests
338 | 1. The inference resource works on an image and returns the correct response
339 | 1. The resize resource functions correctly and returns the correct response
340 | 1. Deleting an image functions and returns the correct response
341 |
342 | Below is sample output from running the unit tests from the file `test_app.py`. Note that there is a deprecation warning from TensorFlow [that is expected](https://github.com/tensorflow/tensorflow/issues/1528).
343 | ```
344 | $ python test_app.py
345 | ...tensorflow/core/framework/op_def_util.cc:332] Op BatchNormWithGlobalNormalization is deprecated. It will cease to work in GraphDef version 9. Use tf.nn.batch_normalization().
346 | ...
347 | ----------------------------------------------------------------------
348 | Ran 13 tests in 4.186s
349 |
350 | OK
351 | ```
352 |
353 |
354 | #### Improvements
355 | - Better authentication beyond HTTP Basic
356 | - [HTTPS for every request](http://flask.pocoo.org/snippets/111/)
357 | - Add a database
358 | - Better [HATEOAS compliance](http://flask-restful-cn.readthedocs.io/en/0.3.5/)
359 | - [Improved caching](http://werkzeug.pocoo.org/docs/0.11/wrappers/#werkzeug.wrappers.ETagResponseMixin.make_conditional) via etags.
360 | - Better type checking for images
361 | - Add pagination
362 | - Add rate limiting
363 | - Add direct upload to AWS S3 with [IAM roles](http://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-api.html)
364 |
365 |
366 | ### References
367 | - [Best Practices for a Pragmatic RESTful API](http://www.vinaysahni.com/best-practices-for-a-pragmatic-restful-api)
368 | - [Is Your REST API RESTful? - PyCon 2015](https://www.youtube.com/watch?v=pZYRC8IbCwk)
369 | - [Designing a RESTful API with Python and Flask](http://blog.miguelgrinberg.com/post/designing-a-restful-api-with-python-and-flask)
370 | - [The Flask Mega-Tutorial](http://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-i-hello-world)
371 | - [What Exactly is REST Programming?](http://stackoverflow.com/questions/671118/what-exactly-is-restful-programming)
372 | - [How to design a REST API](http://blog.octo.com/en/design-a-rest-api/)
373 | - [REST API Design Guidelines](https://developer.atlassian.com/docs/atlassian-platform-common-components/rest-api-development/atlassian-rest-api-design-guidelines-version-1)
374 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | Flask==0.12.2
2 | Flask-HTTPAuth==3.2.3
3 | numpy==1.13.1
4 | Pillow==4.2.1
5 | pytest==3.2.1
6 | requests==2.18.3
7 | tensorflow==1.2.1
8 |
--------------------------------------------------------------------------------
/static/output_graph.pb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chriskaschner/restful-api-flask/ab58218341f1aedd6195424c8c50d7ee09869597/static/output_graph.pb
--------------------------------------------------------------------------------
/static/output_labels.txt:
--------------------------------------------------------------------------------
1 | neither
2 | nike
3 | altra
4 |
--------------------------------------------------------------------------------
/templates/base.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | {% if title %}
4 | {{ title }} - RESTful API
5 | {% else %}
6 | Welcome to RESTful API
7 | {% endif %}
8 |
9 |
10 |
11 |
12 |
13 |
14 | {% block content %}{% endblock %}
15 |
16 |
17 |
--------------------------------------------------------------------------------
/templates/index.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% block content %}
3 | Hello, {{ user.nickname }}!
4 | {% for image in images %}
5 | {{ image.title}} located here: {{ image.url }}
6 |
7 | Get JSON data
8 |
9 | delete JSON data
10 |
11 | {% endfor %}
12 |
15 | JS Bin on jsbin.com
16 | {% endblock %}
17 |
--------------------------------------------------------------------------------
/templates/markdown.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Markdown Snippet
4 |
5 |
6 | {{ content }}
7 | here's the first call to our images api
8 | curl -i http://chriskaschner.pythonanywhere.com/img/api/v1.0/images
9 |
10 | you could also use javascript, but we run into issues with the following embed because of CORS
11 | JS Bin on jsbin.com
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------