Created
May 16, 2024 16:34
-
-
Save chbrandt/87b36edd9b7ed3650ff60f387adc9eb3 to your computer and use it in GitHub Desktop.
Recurse Center Database Server code sample
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
RC Database Server code sample | |
> Before your interview, write a program that runs a server that is accessible | |
> on `http://localhost:4000/`. When your server receives a request | |
> on `http://localhost:4000/set?somekey=somevalue` it should store | |
> the passed key and value in memory. When it receives a request | |
> on `http://localhost:4000/get?key=somekey` it should return | |
> the value stored at `somekey`. | |
> During your interview, you will pair on saving the data to a file. | |
> You can start with simply appending each write to the file, | |
> and work on making it more efficient if you have time. | |
How to Run | |
---------- | |
To run this server, all you need is Python 3 installed (developed with Python 3.12). | |
Save this file/code as 'myserver.py' (for example), and run it with | |
``` | |
$ python myserver.py | |
``` | |
The "myserver" will accept requests at `http://localhost:4000`. | |
You can then use paths `/set` to set a key-value pair in the internal database, | |
and `/get` to retrieve a value from the database. For example: | |
- `http://localhost:4000/set?thekey=thevalue` will set the pair `thekey`, `thevalue`; | |
- `http://localhost:4000/get?key=thekey` will get the value (`thevalue`) for database. | |
Implementation | |
-------------- | |
Python provides an API for implementing a simple HTTP server in its standard library: | |
- https://docs.python.org/3/library/http.server.html | |
If you are not well versed in client-server workflow (like me), this doc may be useful: | |
- https://developer.mozilla.org/en-US/docs/Learn/Server-side/First_steps/Client-Server_overview | |
We are going to use two classes from Python's `http.server` library: | |
- HTTPServer, responsible for listening for http requests; | |
- BaseHTTPRequestHandler, responsible for handling the requests. | |
BaseHTTPRequestHandler is a base class, needs to be customized to answer for | |
specific methods. All requests we're handling here are GET requests. According | |
to the path in the URI request, database will be written or read: | |
- `set` : request server to store a *key-value* pair. Only one pair at a time. | |
- `get` : request server to retrieve a *value* given a *key*. | |
The *query* component is necessarily composed by an attribute `key` | |
indicating the *key* (value) to retrieve. | |
Success/Error outputs | |
--------------------- | |
Errors may happen in different situation, the status codes and situations covered: | |
- `400`, "bad request" | |
* when (URL) path is different than `/get` or `/set` | |
* when query is not correct (e.g. "somekey=somevalue" (set), or "key=somekey" (get)) | |
- `404`, "not found" | |
* when `/get` key does not exist in database | |
- `200`, success | |
* `/set` or `/get` were executed successfully. | |
""" | |
from http.server import HTTPServer, BaseHTTPRequestHandler | |
from urllib.parse import urlparse, parse_qs | |
from typing import Tuple | |
# Database | |
_DB = {} | |
class MyHandler(BaseHTTPRequestHandler): | |
""" | |
Handles all requests of this server | |
""" | |
def do_GET(self): | |
""" | |
Handles GET requests, send fail/success responses | |
""" | |
_actions = { | |
'/set': set_db, | |
'/get': get_db | |
} | |
# Our (valid) requests are always GET queries, ie. URLs with the | |
# structure: /{get,set}?key=value | |
# So, we can use and trust urllib's `urlparse` and `parse_qs` | |
# to decide if this is a valid request and how to answer. | |
_parts = urlparse(self.path) | |
path = _parts.path | |
query = _parts.query | |
# Return "bad request" if not '/get' nor '/set' in URL's path | |
if not (path and path in _actions.keys()): | |
code = 400 | |
text = "Invalid path, try '/set' or '/get'" | |
self.log_error('%s', text) | |
# Return "bad request" if there is no query component in URI | |
elif not query: | |
code = 400 | |
text = "Invalid query, try '/set?somekey=somevalue' or '/get?key=somekey'" | |
self.log_error('%s', text) | |
else: | |
code, text = _actions[path](_DB, query) | |
if code >= 400: | |
self.log_error('%s', text) | |
self.log_request(code) | |
# Send request status code (in header) | |
self.send_response(code) | |
self.end_headers() | |
# Write data value (or message error) in response body | |
if isinstance(text, str): | |
self.wfile.write(bytes(text, encoding='utf8')) | |
else: | |
assert isinstance(text, bytes), type(text) | |
self.wfile.write(text) | |
def set_db(db:dict, query:str) -> Tuple[int,str]: | |
""" | |
Set key-value in db if 'query' is valid, return (code, message) | |
A valid query for us contains ONE pair of key-value, no blanks (eg, "key=value"). | |
Input: | |
- db: database | |
- query: URI query (string) component (e.g. "key=value") | |
Output: | |
- `(code,message)`: tuple containing request status-code and status message | |
Possible codes are "200" or "400" in case of exception parsing 'query' | |
""" | |
try: | |
kv = parse_qs(query, | |
keep_blank_values=False, | |
strict_parsing=True, | |
max_num_fields=1) | |
except Exception as err: | |
return (400, str(err)) | |
assert len(kv) == 1 | |
for k,v in kv.items(): | |
db[k] = v[0] | |
return (200, F"Key-Value pair successfully set") | |
def get_db(db:dict, query:str) -> Tuple[int,str]: | |
""" | |
Get value associate to key in 'db', key is given in 'query' string | |
A valid query for us contains ONE pair as "key=somekey", "key" is mandatory. | |
Input: | |
- db : database | |
- query: URI query (string) component (e.g. "key=somekey") | |
Output: | |
- `(code,message)`: tuple containing request status-code and status message | |
Possible codes are "200" or "400" in case of exception parsing 'query', | |
and "404" in case the requested key (eg, "somekey") is not found in Db. | |
""" | |
try: | |
kv = parse_qs(query, | |
keep_blank_values=False, | |
strict_parsing=True, | |
max_num_fields=1) | |
assert 'key' in kv | |
except Exception as err: | |
return (400, str(err)) | |
key = kv['key'][0] | |
if not key in db: | |
return (404, F"Key {key} not found") | |
return (200, db[key]) | |
if __name__ == '__main__': | |
host = 'localhost' | |
port = 4000 | |
server_address = (host, port) | |
httpd = HTTPServer(server_address, MyHandler) | |
print(F"Server started at http://{host}:{port}") | |
print(F"- Use '/set?somekey=somevalue' to set a key-value pair in the database.") | |
print(F"- Use '/get?key=somekey' to get the value associated to 'somekey' in the database.") | |
try: | |
httpd.serve_forever() | |
except KeyboardInterrupt: | |
print() | |
httpd.server_close() | |
print("Server stopped.") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment