-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapp.py
More file actions
148 lines (134 loc) · 5.54 KB
/
Copy pathapp.py
File metadata and controls
148 lines (134 loc) · 5.54 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
import json
import threading
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import unquote, urlsplit
MAX_BODY_SIZE = 1_000_000
class AppHandler(BaseHTTPRequestHandler):
def do_GET(self):
path = urlsplit(self.path).path
if path == "/health":
self._send(200, {"status": "ok"})
return
if path == "/tasks":
with self.server.lock:
tasks = list(self.server.tasks.values())
self._send(200, tasks)
return
task_id = self._task_id(path)
if task_id is None:
self._send(404, {"error": "not found"})
return
with self.server.lock:
task = self.server.tasks.get(task_id)
self._send(200, task) if task else self._send(404, {"error": "task not found"})
def do_POST(self):
if urlsplit(self.path).path != "/tasks":
self._unsupported_or_missing()
return
try:
body = self._read_json()
except ValueError as error:
self._send(400, {"error": str(error)})
return
title = body.get("title")
if not isinstance(title, str) or not title.strip():
self._send(400, {"error": "title is required"})
return
with self.server.lock:
task_id = str(self.server.next_id)
self.server.next_id += 1
task = {"id": task_id, "title": title.strip(), "completed": False}
self.server.tasks[task_id] = task
self._send(201, task)
def do_PATCH(self):
task_id = self._task_id(urlsplit(self.path).path)
if task_id is None:
self._unsupported_or_missing()
return
try:
body = self._read_json()
except ValueError as error:
self._send(400, {"error": str(error)})
return
if "title" not in body and "completed" not in body:
self._send(400, {"error": "title or completed is required"})
return
if "title" in body and (not isinstance(body["title"], str) or not body["title"].strip()):
self._send(400, {"error": "title must be a non-empty string"})
return
if "completed" in body and not isinstance(body["completed"], bool):
self._send(400, {"error": "completed must be a boolean"})
return
with self.server.lock:
task = self.server.tasks.get(task_id)
if task:
if "title" in body:
task["title"] = body["title"].strip()
if "completed" in body:
task["completed"] = body["completed"]
self._send(200, task) if task else self._send(404, {"error": "task not found"})
def do_DELETE(self):
task_id = self._task_id(urlsplit(self.path).path)
if task_id is None:
self._unsupported_or_missing()
return
with self.server.lock:
task = self.server.tasks.pop(task_id, None)
self._send(204) if task else self._send(404, {"error": "task not found"})
def do_PUT(self):
self._unsupported_or_missing()
def _unsupported_or_missing(self):
path = urlsplit(self.path).path
if path == "/tasks":
self._send(405, {"error": "method not allowed"}, {"Allow": "GET, POST"})
elif self._task_id(path) is not None:
self._send(405, {"error": "method not allowed"}, {"Allow": "GET, PATCH, DELETE"})
else:
self._send(404, {"error": "not found"})
def _read_json(self):
if self.headers.get_content_type() != "application/json":
raise ValueError("content-type must be application/json")
try:
length = int(self.headers.get("content-length", "0"))
except ValueError as error:
raise ValueError("invalid content-length") from error
if length < 0:
raise ValueError("invalid content-length")
if length > MAX_BODY_SIZE:
remaining = length
while remaining:
chunk = self.rfile.read(min(remaining, 64 * 1024))
if not chunk:
break
remaining -= len(chunk)
raise ValueError("request body exceeds 1 MB")
try:
body = json.loads(self.rfile.read(length))
except (json.JSONDecodeError, UnicodeDecodeError) as error:
detail = error.msg if isinstance(error, json.JSONDecodeError) else str(error)
raise ValueError(f"invalid JSON: {detail}") from error
if not isinstance(body, dict):
raise ValueError("request body must be a JSON object")
return body
@staticmethod
def _task_id(path):
parts = path.split("/")
return unquote(parts[2]) if len(parts) == 3 and parts[1] == "tasks" and parts[2] else None
def _send(self, status, body=None, headers=None):
data = b"" if body is None else json.dumps(body, separators=(",", ":")).encode()
self.send_response(status)
if body is not None:
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(data)))
for name, value in (headers or {}).items():
self.send_header(name, value)
self.end_headers()
self.wfile.write(data)
def log_message(self, *_):
pass
def create_server(host="127.0.0.1", port=3000):
server = ThreadingHTTPServer((host, port), AppHandler)
server.tasks = {}
server.next_id = 1
server.lock = threading.Lock()
return server