PyMicroHTTP is a lightweight, flexible HTTP framework built from scratch in Python. It provides a simple way to create HTTP services without heavy external dependencies, making it ideal for learning purposes or small projects.
NOTE: this is a toy project and not production ready.
- PyMicroHTTP
- What is PyMicroHTTP? (Introduction)
- Features (List of functionalities)
- Installation (How to install)
- Quick Start (Getting started example)
- Running the example
- Routing (Defining routes)
- Basic Routing Syntax
- Path Parameters (Accessing dynamic parts of the URL)
- Query Parameters (Accessing data after the '?' in the URL)
- Request Object (Understanding the request data)
- Available properties in the request object
- Accessing headers, path parameters, query parameters, body and verb
- Response Handling (Sending data back to the client)
- Returning dictionaries (converted to JSON)
- Returning strings
- Returning custom status codes and headers
- Middleware (Adding custom logic before request handling)
- Creating middleware functions
- Before all (Running middleware before every request)
- Middleware chaining
- Running the Server (Starting the server)
- Contributing (How to contribute to the project)
- Built on raw TCP sockets
- Routing with HTTP verb and path matching
- Middleware support with easy chaining
- JSON response handling
- Zero external dependencies
You can install the package via pip:
$ pip install pymicrohttp
Here's a simple example to get you started:
from pymicrohttp.server import Server
s = Server()
@s.register('GET /hello')
def hello(request):
return {"message": "Hello, World!"}
@s.register('GET /hello/:name')
def hello_name(request):
name = request['params'].get('name')
return {"message": f"Hello, {name}!"}
if __name__ == "__main__":
s.start_server(port=8080)
Run this script, and you'll have a server running on http://localhost:8080
. Access it with:
curl http://localhost:8080/hello
Routes are defined using the @s.register
decorator:
@s.register('GET /ping')
def ping_handler(request):
return "pong"
Following this syntax:
VERB /<PATH>
*
: to match any verb- GET
- POST
- PUT
- PATCH
- DELETE
- HEAD
- OPTIONS
With a signle space separating between the verb and the request path.
Example:
@s.register('POST /login')
def login_handler(request):
try:
body = json.loads(request['body'])
if 'username' not in body or 'password' not in body:
# do somthing
except:
return { 'error': 'invalid data' }
You can declare dynamic path params using a colon, for example:
GET /users/:group/:channel
To read these params you can access them via the request object:
@s.register('GET /users/:group/:channel')
def handler(request):
...
group = request['params']['group']
channel = request['params']['channel']
...
You can read query parameters via the request obejct:
@s.register('GET /products')
def handler(request):
...
name = request['query'].get('name', '')
category = request['query'].get('category', 'shoes')
...
Note that it is better to use .get(key, default_value)
because query params are optional and may not exist, and accessing them without the .get()
method may result in key errors.
The request object is a dict containing these key and value:
{
'verb': ...
'path': ...
'body': ...
'headers': ... # { 'key': 'value' }
'params': ... # { 'key': 'value' }
'query': ... # { 'key': 'value' }
}
You can access it via the handler:
@s.register('* /ping')
def ping_handler(request):
# accessing request headers
if 'double' in request['headers']:
return "pong-pong"
return "pong"
Examples:
-
Accessing headers:
# say hello s.register('GET /hello/:name') def hello(request): name = request['params']['name'] return "Hello " + name
-
Accessing dynamic path params:
# say hello `n` times s.register('GET /hello/:name/:n') def hello(request): name, n = request['params']['name'], request['params']['n'] return "Hello " * int(n) + name
-
Accessing query params:
# say hello `n` times # read n from query params # with default value of 3 s.register('GET /hello/:name') def hello(request): name = request['params']['name'] n = request['query'].get('n', 3) return "Hello " * n + name
The framework supports different types of responses:
-
Dictionary (automatically converted to JSON):
return {"key": "value"}
-
String:
return "Hello, World!"
-
Tuple for custom status codes and headers:
return "Not Found", 404 # or return "Created", 201, {"Location": "/resource/1"}
Middleware functions can be used to add functionality to your routes:
def log_middleware(next):
def handler(request):
print(f"Request: {request['verb']} {request['path']}")
return next(request)
return handler
@s.register('GET /logged', log_middleware)
def logged_route(request):
return {"message": "This is a logged route"}
If you want to run a middleware before every single request you can use the s.beforeAll()
decorator:
@s.beforeAll()
def logger(next):
def handler(request):
verb, path = request['verb'], request['path']
print(f'{datetime.datetime.now()} {verb} {path}')
return next(request)
return handler
You can chain multiple middlwares together
def log_middleware(next):
def handler(request):
# do your logging logic here
return next(request)
return handler
def auth_middleware(next):
def handler(request):
# do your auth logic here
return next(request)
return handler
@s.register('GET /protected', [log_middleware, auth_middleware])
def protected_route(request):
return {"message": "This is a protected route"}
To run the server:
if __name__ == "__main__":
s = Server()
# Register your routes here
s.start_server(port=8080)
Contributions are welcome! Please feel free to submit a Pull Request.