https://github.com/lsfratel/webspark
A lightweight and minimalist Python web framework for building fast WSGI applications and APIs.
https://github.com/lsfratel/webspark
bottlepy django django-rest-framework flask python web-framework
Last synced: about 1 month ago
JSON representation
A lightweight and minimalist Python web framework for building fast WSGI applications and APIs.
- Host: GitHub
- URL: https://github.com/lsfratel/webspark
- Owner: lsfratel
- License: mit
- Created: 2025-08-17T20:58:38.000Z (10 months ago)
- Default Branch: main
- Last Pushed: 2025-08-26T18:19:51.000Z (10 months ago)
- Last Synced: 2025-09-01T17:56:25.030Z (10 months ago)
- Topics: bottlepy, django, django-rest-framework, flask, python, web-framework
- Language: Python
- Homepage:
- Size: 216 KB
- Stars: 1
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# WebSpark ✨
**A lightweight, minimalist Python web framework for building WSGI applications and APIs.**
[](https://codecov.io/gh/lsfratel/webspark)
[](https://github.com/lsfratel/webspark/blob/main/LICENSE)
WebSpark provides a simple yet powerful architecture for handling HTTP requests and responses, featuring a robust routing system, class-based views, middleware support, and a powerful data validation engine. It's designed for developers who want speed and flexibility without the overhead of a larger framework.
## Features
- **Zero Runtime Dependencies**: A pure WSGI framework with no external dependencies required.
- **Powerful Routing**: Flexible, parameterized routing with support for nesting and wildcards.
- **Class-Based Views**: Intuitive request handling with automatic dispatching based on HTTP methods.
- **Data Validation**: A declarative schema system for validating request bodies and query parameters.
- **Middleware via Plugins**: A simple plugin system for request/response processing and cross-cutting concerns.
- **Method-Level Plugin Application**: Apply plugins directly to view methods using the `apply` helper.
- **Modern HTTP Toolkit**: Clean abstractions for Requests, Responses, and Cookies.
- **Optimized JSON Handling**: Automatically uses the fastest available JSON library (`orjson`, `ujson`, or `json`).
- **Built-in File Uploads**: Seamlessly handle multipart form data and file uploads.
- **Comprehensive Error Handling**: A simple `HTTPException` system for clear and consistent error responses.
- **Proxy Support**: Built-in support for running behind a reverse proxy.
- **Environment Configuration**: Helper utilities for managing configuration via environment variables.
- **Extensive Testing**: 90% test coverage ensuring reliability and stability.
## Installation
Install WebSpark directly from GitHub using `pip`:
```bash
pip install git+https://github.com/lsfratel/webspark.git
```
Or, if you are developing locally with [PDM](https://pdm-project.org/):
```bash
# Clone the repository
git clone https://github.com/lsfratel/webspark.git
cd webspark
# Install dependencies with PDM
pdm install
```
## Quick Start
Create a file named `app.py` and get a server running in under a minute.
```python
# app.py
from webspark.core import WebSpark, View, path
from webspark.http import Context
# 1. Define a View
class HelloView(View):
"""A view to handle requests to the root URL."""
def handle_get(self, ctx: Context):
# The `handle_get` method is automatically called for GET requests.
ctx.json({"message": "Hello, from WebSpark! ✨"})
# 2. Create the application instance
app = WebSpark()
# 3. Add the route to the application
app.add_paths([
path("/", view=HelloView.as_view())
])
# 4. To run, use a WSGI server like Gunicorn:
# gunicorn app:app
```
Now, run your application with a WSGI server:
```bash
gunicorn app:app
```
Open your browser to `http://127.0.0.1:8000`, and you should see the JSON response!
## More Examples
Check out the [examples](examples/) directory for more comprehensive examples:
1. **[Basic API](examples/basic_api.py)** - A simple REST API with CRUD operations
2. **[Schema Validation](examples/schema_example.py)** - Data validation with schemas
3. **[Plugins/Middleware](examples/plugins_example.py)** - Middleware and exception handling
4. **[Cookies](examples/cookies_example.py)** - Session management with cookies
5. **[Configuration](examples/config_example.py)** - Proxy configuration and security
6. **[File Uploads](examples/file_upload_example.py)** - Handling multipart form data
7. **[Database Integration](examples/database_example.py)** - Working with databases
8. **[CORS](examples/cors_example.py)** - Cross-Origin Resource Sharing configuration
9. **[Token Auth](examples/auth_example.py)** - Token-based authentication
10. **[Schema Plugin](examples/schema_plugin_example.py)** - Schema validation with the SchemaPlugin
---
## Core Concepts
### 1. Routing
WebSpark's router is powerful and flexible. Use the `path` helper to define routes and add them to your application with `app.add_paths()`.
#### Parameterized Routes
Capture parts of the URL by defining parameters with a colon (`:`).
```python
# Route for /users/123
path("/users/:id", view=UserDetailView.as_view())
# Wildcard route (matches /files/path/to/your/file.txt)
path("/files/*path", view=FileView.as_view())
```
#### Nested Routes
Organize your routes by nesting `path` objects. The parent path's pattern is automatically prefixed to its children.
```python
app.add_paths([
path("/api/v1", children=[
path("/users", view=UsersView.as_view()),
path("/posts", view=PostsView.as_view()),
])
])
# Registers /api/v1/users and /api/v1/posts
```
### 2. Class-Based Views
Views handle the logic for your routes. They are classes that inherit from `webspark.core.views.View`.
- **Method Dispatch**: Requests are automatically routed to `handle_` methods (e.g., `handle_get`, `handle_post`).
- **Context Object**: Each handler method receives a `Context` object with all the request details.
```python
from webspark.core import View
from webspark.http import Context
class UserView(View):
def handle_get(self, ctx: Context):
"""Handles GET /users/:id"""
user_id = ctx.path_params.get('id')
page = ctx.query_params.get('page', 1) # Access query params with a default
ctx.json({"user_id": user_id, "page": page})
def handle_post(self, ctx):
"""Handles POST /users"""
data = ctx.body # Access parsed JSON body
# ... create a new user ...
ctx.json({"created": True, "data": data}, status=201)
```
### 3. Schema Validation
Ensure your incoming data is valid by defining schemas. If validation fails, WebSpark automatically returns a `400 Bad Request` response.
Define a schema by inheriting from `Schema` and adding fields.
```python
from webspark.validation import Schema, StringField, IntegerField, EmailField
class UserSchema(Schema):
name = StringField(required=True, max_length=100)
age = IntegerField(min_value=18)
email = EmailField(required=True)
class CreateUserView(View):
def handle_post(self, ctx: Context):
# This method is only called if the body is valid.
# Access the validated data:
schema_instance = UserSchema(data=ctx.body)
if not schema_instance.is_valid():
raise HTTPException("Validation error.", schema_instance.errors, status_code=400)
# ... process the data ...
ctx.json({"user": schema_instance.validated_data})
```
#### Available Fields
WebSpark offers a rich set of fields for comprehensive validation:
- `StringField` - String validation with min/max length
- `IntegerField` - Integer validation with min/max value
- `FloatField` - Float validation with min/max value
- `BooleanField` - Boolean value validation
- `ListField` - List validation with item type specification
- `SerializerField` - Nested object validation
- `DateTimeField` - ISO format datetime validation
- `UUIDField` - UUID validation
- `EmailField` - Email format validation
- `URLField` - URL format validation
- `EnumField` - Enumeration value validation
- `DecimalField` - Decimal number validation
- `MethodField` - Custom validation method
- `RegexField` - Regular expression validation
### 4. Responses
WebSpark provides convenient `Context` that simplifies request handling and response generation.
```python
from webspark.http import Context
# JSON response (most common for APIs)
ctx.json({"message": "Success"})
# HTML response
ctx.html("
Hello, World!
")
# Plain text response
ctx.text("OK")
# Stream a large file without loading it all into memory
ctx.stream("/path/to/large/video.mp4")
# Redirect response
ctx.redirect("/new-url")
```
### 5. Cookies
Easily set and delete cookies on the `Context` object.
```python
class AuthView(View):
def handle_post(self, ctx: Context):
# Set a cookie on login
ctx.set_cookie("session_id", "abc123", path="/", max_age=3600, httponly=True, secure=True)
ctx.json({"logged_in": True})
def handle_delete(self, ctx: Context):
# Delete a cookie on logout
ctx.delete_cookie("session_id")
ctx.json({"logged_out": True})
```
### 6. Middleware (Plugins)
Plugins allow you to hook into the request-response lifecycle. A plugin is a class that implements an `apply` method, which wraps a view handler.
Here is a simple logging plugin:
```python
from webspark.core import Plugin
class LoggingPlugin(Plugin):
def apply(self, handler):
# This method returns a new handler that wraps the original one.
def wrapped_handler(ctx):
print(f"Request: {ctx.method} {ctx.path}")
handler(ctx) # The original view handler is called here
print(f"Response: {ctx.status}")
return wrapped_handler
# Register the plugin globally
app = WebSpark(plugins=[LoggingPlugin()])
# Or apply it to a specific path
app.add_paths([
path("/admin", view=AdminView.as_view(), plugins=[AuthPlugin()])
])
```
#### CORS Plugin
WebSpark includes a CORS (Cross-Origin Resource Sharing) plugin that implements the full CORS specification. It supports both simple and preflighted requests with configurable origins, methods, headers, and credentials.
```python
from webspark.contrib.plugins import CORSPlugin
# Create a CORS plugin with a specific configuration
cors_plugin = CORSPlugin(
allow_origins=["https://mydomain.com", "https://api.mydomain.com"],
allow_methods=["GET", "POST", "PUT", "DELETE"],
allow_headers=["Content-Type", "Authorization", "X-Requested-With"],
allow_credentials=True,
max_age=86400, # 24 hours
expose_headers=["X-Custom-Header"]
)
# Register the plugin globally
app = WebSpark(plugins=[cors_plugin])
# Or apply it to specific paths
app.add_paths([
path("/api/", view=APIView.as_view(), plugins=[cors_plugin])
])
```
The CORS plugin supports the following configuration options:
- `allow_origins` - List of allowed origins. Use `["*"]` to allow all origins (not recommended for production with credentials).
- `allow_methods` - List of allowed HTTP methods.
- `allow_headers` - List of allowed headers.
- `allow_credentials` - Whether to allow credentials (cookies, authorization headers).
- `max_age` - How long the preflight response should be cached (in seconds).
- `expose_headers` - List of headers that browsers are allowed to access.
- `vary_header` - Whether to add Vary header for preflight requests.
#### AllowedHosts Plugin
To prevent HTTP Host header attacks, WebSpark provides an `AllowedHostsPlugin`. This plugin checks the request's `Host` header against a list of allowed hostnames.
```python
from webspark.contrib.plugins import AllowedHostsPlugin
# Allow requests only to "mydomain.com" and any subdomain of "api.mydomain.com"
allowed_hosts_plugin = AllowedHostsPlugin(
allowed_hosts=["mydomain.com", ".api.mydomain.com"]
)
# Register the plugin globally
app = WebSpark(plugins=[allowed_hosts_plugin])
```
- **Behavior**:
- If `allowed_hosts` is not set, all requests will be rejected with a `400 Bad Request` error, ensuring that only requests from specified hosts are processed.
- **Matching**:
- `"mydomain.com"`: Matches the exact domain.
- `".mydomain.com"`: Matches `mydomain.com` and any subdomain (e.g., `api.mydomain.com`).
- `"*"`: Matches any host.
#### TokenAuth Plugin
WebSpark provides a `TokenAuthPlugin` for implementing token-based authentication, typically used for APIs.
This plugin can check for a token either in the **Authorization header** or in a **cookie**, and then validates it using a provided function.
```python
from webspark.contrib.plugins import TokenAuthPlugin
# A simple function to validate a token and return a user object.
# In a real application, this would check a database or cache.
def get_user_from_token(token: str):
if token == "secret-token":
return {"username": "admin"}
return None
# Create a token auth plugin (header-based by default)
token_auth_plugin = TokenAuthPlugin(token_loader=get_user_from_token)
# Or configure it to read from a cookie instead:
cookie_auth_plugin = TokenAuthPlugin(
token_loader=get_user_from_token,
cookie_name="auth_token", # will look for Cookie: auth_token=
)
# Apply the plugin to a protected view
app.add_paths([
path("/protected", view=ProtectedView.as_view(), plugins=[token_auth_plugin])
])
```
- **Behavior**:
- By default, the plugin expects an **Authorization** header in the format `Authorization: Token `, the scheme (`Token`) can be customized when instantiating the plugin.
- If `cookie_name` is provided, the plugin will first check for a cookie with that name `Cookie: auth_token=`, if not found, it falls back to the Authorization header.
- If no valid token is found, the plugin returns a `401 Unauthorized` response with a `WWW-Authenticate` header.
- If the token is successfully validated by the `token_loader` function, the returned user object is attached to the context as `ctx.state["user"]` and the request proceeds to the view.
#### Schema Validation Plugin
WebSpark provides a `SchemaPlugin` for validating data from the request context using an `Schema`. This plugin can validate any data accessible through the context object, such as the request body, query parameters, or path parameters.
The plugin reads a value from the view context using `prop`. If that value is callable, it raises `ValueError`. The data is then validated using the provided `schema`. If validation succeeds, the validated data is injected into the handler's keyword arguments. If validation fails, an `HTTPException` with status code 400 is raised.
You can apply the SchemaPlugin using the `@apply` decorator from `webspark.utils.decorators`:
```python
from webspark.contrib.plugins import SchemaPlugin
from webspark.core import View
from webspark.http import Context
from webspark.validation import Schema, StringField, IntegerField
from webspark.utils import apply
# Define a schema for validation
class UserSchema(Schema):
name = StringField(required=True, max_length=100)
age = IntegerField(min_value=1, max_value=120)
class UserView(View):
@apply(
SchemaPlugin(UserSchema, prop="body", param="validated_body"),
)
def handle_post(self, ctx: Context, validated_body: dict):
# The validated_body parameter contains the validated data
ctx.json({"received": validated_body}, status=201)
```
In this example:
- `prop="body"` tells the plugin to read data from `ctx.body`
- `param="validated_body"` specifies that the validated data should be passed to the handler as the `validated_body` keyword argument
- If validation fails, an HTTP 400 error is automatically returned with details about the validation errors
You can also apply the plugin to a specific path when registering routes:
```python
app.add_paths([
path("/users", view=UserView.as_view(), plugins=[
SchemaPlugin(UserSchema, prop="body", param="validated_body")
])
])
```
#### The `apply` Helper
WebSpark provides an `apply` helper function in `webspark.utils.decorators` that makes it easy to apply plugins directly to view methods. This is particularly useful for applying plugins to specific handlers rather than entire views or routes.
The `apply` function takes one or more plugin instances and returns a decorator that wraps the target function with those plugins:
```python
from webspark.utils import apply
from webspark.contrib.plugins import SchemaPlugin, CORSPlugin
# Apply a single plugin
@apply(SchemaPlugin(UserSchema, prop="body"))
def handle_post(self, ctx: Context, body: dict):
# Handler logic here
pass
# Apply multiple plugins
@apply(
CORSPlugin(allow_origins=["https://mydomain.com"]),
SchemaPlugin(UserSchema, prop="body")
)
def handle_post(self, ctx: Context, body: dict):
# Handler logic here
pass
```
The `apply` helper is especially useful when you want to:
- Apply validation to specific handlers rather than entire views
- Combine multiple plugins on a single method
- Keep plugin configuration close to the code it affects
When using `apply`, the plugins are applied in the order they are passed to the function, with the first plugin wrapping the original function, the second plugin wrapping the first, and so on.
```
### 7. Error Handling
Gracefully handle errors using `HTTPException`. When raised, the framework will catch it and generate a standardized JSON error response.
```python
from webspark.utils import HTTPException
class UserView(View):
def handle_get(self, ctx):
user_id = ctx.path_params.get('id')
user = find_user_by_id(user_id) # Your database logic
if not user:
# This will generate a 404 Not Found response
raise HTTPException("User not found", status_code=404)
ctx.json({"user": user.serialize()})
```
### 8. Custom Exception Handlers
WebSpark allows you to define custom handlers for specific HTTP status codes using the `@app.handle_exception(status_code)` decorator. This is useful for overriding the default JSON error response and providing custom error pages or formats.
The handler function receives the `request` and the `exception` object and must return a `Response` object.
```python
from webspark.http import Context
app = WebSpark(debug=True)
@app.handle_exception(404)
def handle_not_found(ctx: Context, exc: Exception):
"""Custom handler for 404 Not Found errors."""
ctx.html("
Oops! Page not found.
", status=404)
@app.handle_exception(500)
def handle_server_error(ctx: Context, exc: Exception):
"""Custom handler for 500 Internal Server Error."""
if app.debug:
# In debug mode, show the full exception
ctx.text(str(exc), status=500)
else:
ctx.html("
A server error occurred.
", status=500)
```
### 9. Proxy Configuration
If your WebSpark application is running behind a reverse proxy (like Nginx or a load balancer), you'll need to configure it to correctly handle headers like `X-Forwarded-For` and `X-Forwarded-Proto`. This ensures that `request.ip`, `request.scheme`, and `request.host` reflect the original client information, not the proxy's.
Proxy support is configured on the `WebSpark` application instance via a configuration object.
```python
class AppConfig:
TRUST_PROXY = True
# For more granular control, you can also specify:
# TRUSTED_PROXY_LIST = ["192.168.1.1", "10.0.0.1"] # List of trusted proxy IPs
# TRUSTED_PROXY_COUNT = 1 # Number of trusted proxies in the chain
app = WebSpark(config=AppConfig())
```
- **`TRUST_PROXY`**: (bool) Set to `True` to enable proxy header processing. Defaults to `False`.
- **`TRUSTED_PROXY_LIST`**: (list) A list of trusted proxy IP addresses. If set, only requests from these IPs will have their proxy headers processed.
- **`TRUSTED_PROXY_COUNT`**: (int) The number of reverse proxies that are trusted in the chain. This is useful when you have a known number of proxies.
The framework checks for the following headers when `TRUST_PROXY` is enabled:
- `X-Forwarded-For` and `X-Real-IP` for the client's IP address.
- `X-Forwarded-Proto` for the request scheme (`http` or `https`).
- `X-Forwarded-Host` for the original host.
### 10. Environment Variable Helper
WebSpark includes a convenient `env` helper function in `webspark.utils` to simplify reading and parsing environment variables.
```python
from webspark.utils import env
# Get a string value with a default
DATABASE_URL = env("DATABASE_URL", default="sqlite:///default.db")
# Get an integer, raising an error if not set
PORT = env("PORT", parser=int, raise_exception=True)
# Get a boolean value (handles "true", "1", "yes", "on")
DEBUG = env("DEBUG", default=False, parser=bool)
```
This helper streamlines configuration management, making it easy to handle different data types and required settings.
### 11. File Uploads
WebSpark makes handling file uploads simple with built-in multipart form data parsing. Uploaded files are accessible through the `ctx.files` attribute.
```python
class FileUploadView(View):
def handle_post(self, ctx: Context):
# Check if request has files
if not ctx.files:
raise HTTPException("No files uploaded", status_code=400)
# Process each uploaded file
for field_name, file_list in ctx.files.items():
# file_list can be a single file dict or list of file dicts
files = file_list if isinstance(file_list, list) else [file_list]
for file_info in files:
# file_info contains:
# - filename: original filename
# - content_type: MIME type
# - file: file-like object with read() method
# Save the file
with open(f"/uploads/{file_info['filename']}", "wb") as f:
f.write(file_info["file"].read())
```
---
## Development
### Running Tests
This project uses `pdm` and `pytest`.
```bash
# Run all tests
pdm run pytest
# Run tests with coverage
pdm run tests
```
### Code Quality
We use `ruff` for linting and formatting.
```bash
# Check for linting errors
pdm run ruff check .
# Format the code
pdm run ruff format .
```
---
## Project Structure
```
webspark/
├── core/ # Core components (WSGI app, router, views, schemas)
├── contrib/ # Optional plugins (CORS, AllowedHosts)
├── http/ # HTTP abstractions (request, response, cookies)
├── schema/ # Data validation schemas and fields
├── utils/ # Utilities (exceptions, JSON handling, env vars)
├── examples/ # Comprehensive usage examples
├── tests/ # Test suite for all components
├── constants.py # HTTP constants and error codes
└── __init__.py # Package metadata
```
### Core Modules
- **core/** - Contains the fundamental building blocks:
- `WebSpark` - The main WSGI application class
- `View` - Base class for request handlers
- `path` - Routing helper function
- `Plugin` - Base class for middleware
- **contrib/plugins/** - Optional plugins:
- `CORSPlugin` - Handles Cross-Origin Resource Sharing.
- `AllowedHostsPlugin` - Validates incoming Host headers.
- `TokenAuthPlugin` - Token-based authentication middleware for securing endpoints.
- `SchemaPlugin` - Validates data from the request context using Schema.
- **http/** - HTTP abstractions:
- `Context` - Request/response context object
- `Cookie` - Cookie handling utilities
- `multipart` - File upload handling
- **schema/** - Data validation:
- `Schema` - Base class for validation schemas
- `fields` - Validation field types
- **utils/** - Utility functions:
- `HTTPException` - Standardized error handling
- `env` - Environment variable helper
- `json` - Optimized JSON handling
- `decorators` - Helper decorators including `apply` and `cached_property`
## Contributing
Contributions are welcome! Please feel free to fork the repository, make your changes, and submit a pull request.
1. Fork the repository.
2. Create your feature branch (`git checkout -b feature/AmazingFeature`).
3. Commit your changes (`git commit -m 'Add some AmazingFeature'`).
4. Push to the branch (`git push origin feature/AmazingFeature`).
5. Open a pull request.
Please ensure your code follows the project's style guidelines and includes appropriate tests.
## License
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.