Releases: VxidDev/Nebula
Release list
Nebula - 3.4.1
What's Changed
- normal worker support and router without regex by @amogus-gggy in #10
Full Changelog: 3.3.0...3.4.1
Nebula - 3.3.0
Nebula 3.3.0 - Release Notes
What’s New
⚠️ Error Dispatcher & Injection System (Major)
Nebula introduces a flexible injection system for error handlers along with a fully redesigned error dispatch pipeline.
Error handlers can now declare exactly what they need, and Nebula will inject it automatically.
Error Handler Injection
Error handlers now support explicit parameter injection.
Supported parameters:
request- current request objectscope- raw ASGI scopereceive- ASGI receive callablesend- ASGI send callablecode- HTTP status code
Example
@app.error_handler(404)
async def not_found(request, code):
return f"<h1>{code} - {request.path} not found</h1>"Only declared parameters are passed, nothing extra.
⚠️ Important Behavior
- Injection is name-based, not positional
- Only explicitly declared parameters are provided
- Works for both sync and async handlers
- No runtime introspection, everything is cached at registration
Error Dispatcher
Nebula’s internal _dispatch_error system has been completely redesigned.
Key Improvements
-
⚡ Zero overhead at runtime
- No
inspectcalls during request handling
- No
-
🧠 Precomputed handler metadata
- Accepted parameters cached
- Async/sync status cached
-
🔁 Unified execution path
- Same logic for all error handlers
How It Works
- Error occurs (404 / 405 / 500 or
HTTPException) - Matching handler is resolved
- Required parameters are injected
- Handler is executed (sync or async)
- Return value is normalized into a
Response
Strict Signature Validation
Error handlers are now validated at registration time.
Allowed parameters:
{"scope", "receive", "send", "request", "code"}Example (invalid)
@app.error_handler(404)
def handler(foo): # ❌ not allowed
...➡️ Raises:
ExtraArgumentsDetected⚠️ Behavior Note
- Invalid signatures fail early (at startup)
- Prevents runtime bugs and undefined behavior
- Ensures consistent handler contracts
Automatic Response Handling
Error handlers can return any supported type:
-
Response→ used directly -
dict/list→JSONResponse -
str→ auto-detected:- HTML →
HTMLResponse - URL →
RedirectResponse - otherwise →
PlainTextResponse
- HTML →
Example
@app.error_handler(500)
def internal_error(code):
return {"error": "Internal Server Error", "status": code}Request Context Access
Nebula now provides a global request proxy:
from nebula import request
@app.error_handler(500)
def handler():
return f"Error on {request.path}"⚠️ Important Behavior
- Powered by
ContextVar(async-safe) - Only available inside request lifecycle
- Access outside request raises:
RuntimeErrorDefault Error Handlers
Built-in handlers (404, 405, 500) now:
- Use the same injection system
- Follow the same response normalization rules
- Are fully replaceable via
@app.error_handler(...)
Internal Improvements
- Added
_error_handler_paramscache - Added
_error_handler_is_asynccache - Removed runtime
inspectusage from hot path - Improved separation of dispatch and execution
- Optimized error handling performance
⚠️ Breaking Changes
1. Restricted Error Handler Signatures
Old:
def handler(foo, bar): ...New:
def handler(request, code): ...Only supported parameters are allowed.
Nebula - 3.2.0
Nebula 3.2.0 - Release Notes
What’s New
⚠️ Middleware System Update (Major)
Nebula now supports composable middleware at two levels:
- Global ASGI Middleware - wraps the entire application
- Route-level Middleware - applied per route or
RouteGroup
This enables fine-grained control over request handling while maintaining full ASGI compatibility.
Middleware
Middleware in Nebula follows the ASGI pattern:
class BaseMiddleware:
def __init__(self, app):
self.app = app
async def __call__(self, scope, receive, send):
await self.app(scope, receive, send)Middleware is applied using the Middleware wrapper:
class Middleware:
def __init__(self, middleware_cls: type, **options):
self.middleware_cls = middleware_cls
self.options = options
def build(self, app):
return self.middleware_cls(app, **self.options)🌍 Global Middleware
Global middleware wraps the entire application:
app = Nebula(
middlewares=[
Middleware(LoggingMiddleware)
]
)🧩 Route & Group Middleware
Middleware can now be applied per route or route group:
api = app.group(
"/api",
middlewares=[
Middleware(APILoggerMiddleware)
]
)Route-level middleware can also be passed directly:
@api.get("/greet/{name}", route_middlewares=[...])
async def greet(name: str):
return {"greeting": f"Hi, {name}!"}⚠️ Important Behavior
- Middleware is applied in stacked order
- Global middleware runs before group middleware
- Route-specific middleware runs last
- Middleware is composed per request, not statically wrapped at the app level
RouteGroup
RouteGroup is now a first-class abstraction for organizing routes and applying scoped middleware.
It allows you to:
- Group routes under a shared prefix
- Attach middleware to a logical section of your app
- Keep code modular and maintainable
Example
api = app.group(
"/api",
middlewares=[
Middleware(APILoggerMiddleware)
]
)
@api.get("/greet/{name}")
async def greet(name: str):
return {"greeting": f"Hi, {name}!"}⚠️ Behavior Note
-
RouteGroupdoes not wrap the ASGI app directly -
Instead, its middleware is composed into the request pipeline
-
This ensures:
- correct routing behavior
- full compatibility with ASGI
- no unintended global side effects
Middleware Execution Order
For a request, middleware is executed in this order:
- Global middleware (outermost)
- Group middleware
- Route middleware
- Route handler
This creates a predictable and composable execution pipeline.
Nebula - 3.1.4
Nebula 3.1.4 - Release Notes
What’s New
Added RouteGroup, enabling cleaner and more organized route definitions, and laying the foundation for future middleware support and route isolation.
RouteGroup
RouteGroup is a utility that helps simplify and organize route definitions. It also introduces the groundwork for future middleware isolation between the main Nebula application and grouped routes.
Examples
api: RouteGroup = app.group("/api")
@api.get("/weather")
async def weather():
return {"temperature": 27}RouteGroup acts as a structural wrapper.
New Behavior
Previously, you could explicitly pass a response class:
@app.get("/", return_class=HTMLResponse)
async def root() -> None:
return "<h1>Welcome to Nebula!</h1>"Now, Nebula also inspects the route’s return type annotation. If a valid class that inherits from Response is provided, and the user has not explicitly passed return_class, the return type annotation will be used automatically.
Examples
# returns HTMLResponse
@app.get("/", return_class=HTMLResponse)
async def root() -> None:
return "<h1>Welcome to Nebula!</h1>"
# also returns HTMLResponse
@app.get("/a")
async def something() -> HTMLResponse:
return "<h1>Welcome to Nebula!</h1>"
# returns PlainTextResponse
@app.get("/b", return_class=PlainTextResponse)
async def something_again() -> HTMLResponse:
return "<h1>Welcome to Nebula!</h1>"Nebula - 3.1.2
Nebula 3.1.2 - Release Notes
What’s New
Route return values are now automatically converted into the appropriate response type, removing the need to explicitly specify return_class in most cases.
New Behavior
Previously, you had to define a response class explicitly:
@app.get("/", return_class=HTMLResponse)
async def root() -> None:
return "<h1>Welcome to Nebula!</h1>"Now, Nebula automatically detects the response type:
@app.get("/")
async def root() -> None:
return "<h1>Welcome to Nebula!</h1>"Auto-Detection Rules
Nebula will now automatically wrap return values using the following rules:
-
dictorlist→JSONResponse -
str:- Starts with
<→HTMLResponse - Otherwise →
PlainTextResponse
- Starts with
-
Other types → converted to
strand wrapped inPlainTextResponse
Examples
✅ JSON Response (automatic)
@app.get("/json")
async def json_route():
return {"message": "Hello, world!"}✅ HTML Response (automatic)
@app.get("/html")
async def html_route():
return "<h1>Hello, world!</h1>"✅ Plain Text Response (automatic)
@app.get("/text")
async def text_route():
return "Hello, world!"❌ Old behavior (no longer required)
@app.get("/", return_class=HTMLResponse)
async def root() -> None:
return "<h1>Welcome to Nebula!</h1>"Migration Guide
If your routes previously relied on return_class, you can safely:
- ❌ Remove
return_classfrom your route definitions - ✅ Keep your return values unchanged
Before
@app.get("/", return_class=HTMLResponse)
async def root() -> None:
return "<h1>Welcome to Nebula!</h1>"After
@app.get("/")
async def root() -> None:
return "<h1>Welcome to Nebula!</h1>"Notes
- This change simplifies route definitions and reduces boilerplate.
- Response behavior is now inferred automatically based on return type.
- You can still explicitly return a
Responseinstance to bypass auto-detection.
Nebula - 3.1.1
Nebula 3.1.1 - Release Notes
What’s New
request object is no longer always injected automatically into route handlers.
Request is now only injected when one of the following conditions is met:
a) The parameter is named request
b) The parameter is type-annotated with Request (or a compatible class)
If neither condition is met, the request can still be accessed via nebula.get_request().
Examples
❌ No request injection
from nebula.response import HTMLResponse
@app.get("/", return_class=HTMLResponse)
async def root() -> HTMLResponse:
return "<h1>Welcome to Nebula!</h1>"❌ No request injection (other parameters)
@app.get("/{something}", return_class=HTMLResponse)
async def something(something) -> HTMLResponse:
return f"<h1>Welcome to {something}!</h1>"✅ Request injected via type annotation
from nebula.request import Request
from nebula.response import HTMLResponse
@app.get("/route", return_class=HTMLResponse)
async def route(request: Request) -> HTMLResponse:
return f"<h1>You're currently on: http://{app.host}:{app.port}{request.path}</h1>"✅ Request injected regardless of parameter name
@app.get("/route2", return_class=HTMLResponse)
async def route2(req: Request) -> HTMLResponse:
return f"<h1>You're currently on: http://{app.host}:{app.port}{req.path}</h1>"✅ Access request via get_request()
from nebula import get_request
from nebula.response import HTMLResponse
@app.get("/route3", return_class=HTMLResponse)
async def route3() -> HTMLResponse:
return f"<h1>You're currently on: http://{app.host}:{app.port}{get_request().path}</h1>"Migration Guide
If your existing routes relied on implicit request injection, update them by:
- Adding request:
Requestto your route handler, or - Using
nebula.get_request()inside your function
Notes
- The parameter name does not matter if it is annotated with Request.
- This change makes request handling more explicit and predictable.
Nebula - 3.1.0
Release notes for Nebula 3.1.0
Whats new?
- Overall optimization of Nebula
- Added Nebula.get , Nebula.post, Nebula.put, Nebula.delete
- Added return_class to Nebula.route and methods above
- Added Nebula.run (uses run_dev)
- Added render_template (sync) and render_template_async
- Added render_template_string (sync) and render_template_async
Example Code
from nebula import Nebula
from nebula.response import HTMLResponse
app = Nebula(__file__, "localhost", 8000)
@app.get("/", return_class=HTMLResponse)
async def root(request) -> None:
return "<h1>Welcome to Nebula!</h1>"
if __name__ == "__main__":
app.run()Summary
This update introduces optimizations and some new functions, while keeping the old code compatible.
Nebula - 3.0.0
Nebula 3.0.0 Release Notes
Nebula 3.0.0 is a major update that transitions the framework from WSGI to ASGI, bringing native async support and a more modern architecture.
Highlights
-
Full migration to ASGI
-
Nebula no longer depends on eventlet or werkzeug
-
Built for modern async Python
-
Native async support
-
Route handlers now support async def
-
Better performance and scalability
-
New development runner
-
Introduced run_dev() for quick local development
-
No need for external WSGI servers like Gunicorn
-
Simplified API
-
Cleaner routing
-
More intuitive request handling
Breaking Changes
-
WSGI removed
-
app.wsgi_app is no longer available
-
Gunicorn + eventlet setup is no longer supported
-
Routes are now async-first
-
Handlers can use async def
-
request object is now passed to route functions
-
Routing signature simplified
-
endpoint argument is no longer required in most cases
Migration Guide
Before (WSGI)
# tiny.py
from nebula import Nebula
from nebula.utils import htmlify
app = Nebula(__file__, "localhost", 8000)
@app.route("/", endpoint="main")
def root() -> None:
return htmlify("<h1>Welcome to Nebula!</h1>")
main = app.wsgi_app # gunicorn -w 1 -k eventlet tiny:mainAfter (ASGI)
from nebula import Nebula, run_dev
from nebula.utils import htmlify
app = Nebula(__file__, "localhost", 8000)
@app.route("/")
async def root(request) -> None:
return htmlify("<h1>Welcome to Nebula!</h1>")
run_dev(app)What This Means
- More aligned with modern Python frameworks
- Ready for high-performance async workloads
- Easier to use for modern web development
Final Notes
This is a breaking release, but it sets the foundation for future features and performance improvements.
If you were using Nebula before, migrating is straightforward and worth it for the async benefits.
Nebula - 2.2.0
Nebula Session Management Feature - Release Notes
New feature just arrived! Now you can easily manage user sessions and authentication in your Nebula applications. This update introduces a secure, cookie-based session management system that integrates seamlessly with both standard HTTP routes and Socket.IO event handlers.
Key features include:
- Cookie-Based Sessions: Securely store session data on the client using HMAC-signed cookies.
- User Authentication: A simple API to log users in, log them out, and check their authentication status.
- Route Protection: A decorator to easily protect routes and require user login.
- WebSocket Integration: Authenticate users for your real-time WebSocket connections.
- Flexible User Model: Integrates with your existing user model via a simple mixin.
How to Use
1. Enable Sessions
First, enable session management by calling app.setup_sessions() with a secret key. This is crucial for signing the session cookies securely.
from nebula import Nebula
app = Nebula(__file__, "0.0.0.0", 5000)
app.init_all()
# Enable sessions with a secret key
app.setup_sessions(secret_key="a-super-secret-key-that-you-should-change")2. Create a User Model
Your user class should inherit from UserMixin to be compatible with Nebula's authentication system. This mixin provides default properties like is_authenticated.
from nebula import UserMixin
class User(UserMixin):
def __init__(self, user_id, username):
self.id = user_id
self.username = username3. Implement a User Loader
Nebula needs a way to load a user object from the ID stored in the session. Create a function for this and decorate it with @app.user_loader.
# A dummy user database
USERS = {"1": User("1", "alice"), "2": User("2", "bob")}
@app.user_loader
def load_user(user_id: str):
return USERS.get(user_id)4. Log Users In and Out
Use the login_user and logout_user functions to manage user authentication state.
from nebula import login_user, logout_user, current_user
from werkzeug.utils import redirect
@app.route("/login", methods=["POST"])
def login():
# In a real app, you'd validate a username and password
user = USERS.get("1") # Example: logging in user "alice"
if user:
login_user(user)
return redirect("/dashboard")
@app.route("/logout")
def logout():
logout_user()
return redirect("/login")5. Protect Routes
Use the @login_required() decorator to protect routes that require authentication. Unauthenticated users will be redirected to the specified login page.
from nebula import login_required
@app.route("/dashboard")
@login_required(redirect_to="/login")
def dashboard():
return f"Welcome, {current_user.username}!"6. Authenticate WebSocket Connections
You can also authenticate users for your Socket.IO connections. Use app.get_session_from_environ() in your on_connect handler to access the session and verify the user.
@app.on_connect()
def on_connect(sid, environ):
session = app.get_session_from_environ(environ)
user_id = session.get("_user_id")
if not user_id:
return False # Reject connection
user = load_user(user_id)
if not user:
return False # Reject connection
print(f"{user.username} connected to WebSocket")
# You can now associate the user with the connection (sid)New Example: Authenticated Chat
To see this feature in action, check out the new chat_auth.py example in the examples/ directory. It demonstrates how to build a real-time chat application with session-based authentication.
Nebula - 2.1.1
This release features an useful addition of HTTPS support.
Usage
app.run(ssl_context=("localhost+2.pem", "localhost+2-key.pem")) # example for localhost Support is still not flawless, but will get polished in future.