-
Notifications
You must be signed in to change notification settings - Fork 8
Expand file tree
/
Copy pathserver.py
More file actions
167 lines (141 loc) · 4.97 KB
/
server.py
File metadata and controls
167 lines (141 loc) · 4.97 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
from BaseHTTPServer import HTTPServer
from SimpleHTTPServer import SimpleHTTPRequestHandler
from urlparse import urlparse, parse_qs
import urllib, urllib2
import shutil
import json
import uuid
import re
# in-memory entries database for simplicity
entries = []
class ExtendedHTTPHandler(SimpleHTTPRequestHandler):
"""
Serves files in the local directory. Allows CRUD operations on entries.
"""
PUT_PATH_REGEX = re.compile(r'/entries/([^/]+)$')
DELETE_PATH_REGEX = re.compile(r'/entries/([^/]+)/delete$')
STATUS_OK = 200
STATUS_USER_ERROR = 422
TYPE_PLAIN = 'text/plain'
TYPE_JSON = 'application/json'
def __init__(self, *args, **kwargs):
"""
Creates an ExtendedHTTPHandler that serves entries.
"""
SimpleHTTPRequestHandler.__init__(self, *args, **kwargs)
def respond(self, status_code, content_type, body):
"""
Sets the status code and content-type based on the given parameters. Sends
the given body to the client, adding an appropriate content-length header.
"""
self.send_response(status_code)
self.send_header('Content-type', content_type)
self.send_header('Content-length', len(body))
self.end_headers()
self.wfile.write(body)
def do_GET(self):
"""
Handles GET requests.
"""
if self.path == '/entries':
self.respond(self.STATUS_OK, self.TYPE_JSON, json.dumps(entries))
else:
# let SimpleHTTPRequestHandler serve static files
return SimpleHTTPRequestHandler.do_GET(self)
def entry_index_by_id(self, entry_id):
"""
Returns the entry that has the given id, or None if no such entry exists.
"""
for index, entry in enumerate(entries):
if entry['id'] == entry_id:
return index
return None
def validate_entry_data(self, data):
"""
Validates that the given data is in the form of an entry.
"""
name = data.get('name')
address = data.get('address')
description = data.get('description')
# name, address, and description must be non-empty strings
if (not isinstance(name, basestring) or
not isinstance(address, basestring) or
not isinstance(description, basestring) or
name == '' or address == '' or description == ''):
self.respond(self.STATUS_USER_ERROR, self.TYPE_PLAIN,
'Must provide a valid name, address, and description.')
return False
return True
def validate_entry_id(self, entry_id):
"""
Validates that the given entry_id is valid.
"""
# id must correspond to an entry
if (not isinstance(entry_id, basestring) or
self.entry_index_by_id(entry_id) is None):
self.respond(self.STATUS_USER_ERROR, self.TYPE_PLAIN,
'Must provide a valid string id that refers to an entry.')
return False
return True
def do_POST(self):
"""
Handles POST requests.
"""
is_post = self.path == '/entries'
is_put = bool(self.PUT_PATH_REGEX.match(self.path))
is_delete = bool(self.DELETE_PATH_REGEX.match(self.path))
if is_post or is_put or is_delete:
valid = True
if is_post or is_put:
# read entry from request body
length = int(self.headers.getheader('content-length', 0))
try:
data = json.loads(self.rfile.read(length))
except ValueError:
self.respond(self.STATUS_USER_ERROR, self.TYPE_PLAIN,
'Must provide a valid JSON body.')
return
# must validate all entry fields
valid = valid and self.validate_entry_data(data)
# for put and delete, must validate id
if is_put:
entry_id = self.PUT_PATH_REGEX.match(self.path).group(1)
valid = valid and self.validate_entry_id(entry_id)
if is_delete:
entry_id = self.DELETE_PATH_REGEX.match(self.path).group(1)
valid = valid and self.validate_entry_id(entry_id)
if valid:
# perform appropriate action
if is_post:
entry = {
'id': str(uuid.uuid4()),
'name': data['name'],
'address': data['address'],
'description': data['description'],
}
entries.append(entry)
self.respond(self.STATUS_OK, self.TYPE_JSON, json.dumps(entry))
elif is_put:
entry = {
'id': entry_id,
'name': data['name'],
'address': data['address'],
'description': data['description'],
}
index = self.entry_index_by_id(entry_id)
entries[index] = entry
self.respond(self.STATUS_OK, self.TYPE_JSON, json.dumps(entry))
else: # delete path
index = self.entry_index_by_id(entry_id)
entries.pop(index)
success = {'success': True}
self.respond(self.STATUS_OK, self.TYPE_JSON, json.dumps(success))
else:
return SimpleHTTPRequestHandler.do_POST(self)
# server on 0.0.0.0:8000
try:
server = HTTPServer(('0.0.0.0', 8000), ExtendedHTTPHandler)
print 'Serving on 127.0.0.1:8000'
server.serve_forever()
except KeyboardInterrupt:
server.socket.close()