You can run the examples from the section below, simply by running the server given in the ./examples
directory:
python3 examples/main.py
You can then send HTTP queries to the server with the command curl
:
curl -X $HTTP_METHOD -d "$BODY" -w "\n\nStatus code:%{http_code}\n" "$URL"
For development purposes, you can execute the tests with :
deno task --cwd examples/ test
To create a new HTTP server, just call the function startHTTPServer()
:
import asyncio
from VSHS import startHTTPServer, rootDir
# Put the bytecode-cache in the same directory to keep the project clean
import sys
sys.pycache_prefix = rootDir() + "/__pycache__"
asyncio.run( startHTTPServer(hostname="localhost", port=8080, routes="/routes") )
The routes
parameter is a directory containing the differents routes your HTTP server will answer to. In this directory, each subdirectory corresponds to a route, and each files, to a supported HTTP method for this route.
For example, the file ./routes/hello-world/GET.ts
defines how your server will answer to a GET /hello-world
HTTP query. In order to do so, GET.py
default exports an asynchronous function whose return value is the answer to the received HTTP query.
async def default(url, body, route):
return {"message": "Hello World"}
curl -w "\n" -X GET http://localhost:8080/hello-world
Output:
{
"message": "Hello World"
}
In fact, the handler function takes 3 parameters:
from VSHS import get_query
async def default(
url, # yarl.URL: the requested URL
body, # any|None: json.loads( body ) or None if empty body.
route # Route: cf next section
):
return {
"urlParams" : get_query(url),
"bodyParams": body,
"pathParams": route.vars # dict[string, string]
}
curl -w "\n" -X POST -d '{"body": "A"}' http://localhost:8080/params/C?url=B
Output:
{
"urlParams": {
"url": "B"
},
"bodyParams": {
"body": "A"
},
"pathParams": {
"route": "C"
}
}
⚠ Some brower might forbid to add body to GET queries.
The route
parameter has two components:
-
path
is the route path, e.g./params/{name}/GET.ts
. Letters in between braces represents a variable, corresponding to set of letters (except/
). Hence a single route path can match several URL, e.g.:/params/faa
/params/fuu
-
vars
is an object whose keys are the path variables names and whose values their values in the current URL, e.g.:{name: "faa"}
{name: "fuu"}
If an exception is thrown inside an handlers, the server will automatically send an HTTP 500 status code (Internal Server Error).
async def default(url, body, route):
raise Exception('Oups...')
curl -w "\n\nStatus code: %{http_code}\n" -X GET http://localhost:8080/exception
Output:
Oups...
Status code: 500
You can send other HTTP status code, by throwing an instance of HTTPError
:
from VSHS import HTTPError
async def default(url, body, route):
raise HTTPError(403, "Forbidden Access")
curl -w "\n\nStatus code: %{http_code}\n" -X GET http://localhost:8080/http-error
Output:
Forbidden Access
Status code: 403
💡 If it exists, errors are redirected to the /errors/{error_code}
route, with body
containing the error message.
We infer the mime type from the handler return value :
Return | Mime |
---|---|
str |
text/plain |
MultiDict |
application/x-www-form-urlencoded |
bytes |
application/octet-stream |
Blob |
blob.type or application/octet-stream |
any |
application/json |
SSEResponse |
text/event-stream |
💡 We provide a Blob
class to mimic JS Blob
:
blob = Blob(content, type="text/plain")
await blob.text()
await blob.bytes()
We automatically perform the following conversions on the query body:
Mime | Result |
---|---|
No body | None |
text/plain |
str or dict |
application/x-www-form-urlencoded |
dict |
application/json |
dict |
application/octet-stream |
bytes |
others | Blob |
⚠ For text/plain
and application/x-www-form-urlencoded
, we first try to parse it with JSON.parse()
.
💡 The default mime-types set by the client are :
Source (JS) | Mime-type |
---|---|
string |
text/plain |
URLSearchParams |
application/x-www-form-urlencoded |
FormData |
application/x-www-form-urlencoded |
Uint8Array |
None |
Blob |
blob.type or none |
curl -d |
application/x-www-form-urlencoded |
💡 To provide an explicit mime-type in the query :
fetch('...', {body: ..., headers: {"Content-Type", "..."})
curl -d "..." -H "Content-Type: ..."
You can also provide a directory containing static files
import asyncio
from VSHS import startHTTPServer, rootDir
# Put the bytecode-cache in the same directory to keep the project clean
import sys
sys.pycache_prefix = rootDir() + "/__pycache__"
asyncio.run( startHTTPServer(hostname="localhost",
port=8080,
routes="/routes",
static="/assets") )
curl -w "\n\nType: %{content_type}\n" -X GET http://localhost:8080/
Output:
<b>Hello world</b>
Type: text/html
If you want to return Server-Sent Events, you just have to return an instance of SSEResponse
:
import asyncio
import traceback
from VSHS import SSEResponse
async def run(self):
try:
i = 0
while True:
await asyncio.sleep(1)
i += 1
await self.send(data={"count": i}, event="event_name")
except Exception as e:
print("Connection closed")
traceback.print_exc()
async def default(url, body, route):
return SSEResponse( run )
The method send(message: any, event?: str)
sends a new event to the client. Once the client closes the connection, an exception is raised:
curl -X GET http://localhost:8080/server-sent-events
Output:
event: event_name
data: {"count":0}
event: event_name
data: {"count":1}
event: event_name
data: {"count":2}
We also provide an additionnal demonstration in ./examples/demo/
.
This webpage sends two HTTP queries :
GET /demo/website
to receive Server-Sent Events at each modification of./examples/messages.txt
.POST /demo/website
to append a new line into./examples/messages.txt
at each submission of the formular.