Migration Guide: v0.9x → v0.93¶
Cullinan Framework — Comprehensive migration guide for upgrading from v0.9x to v0.93.
Upgrade-only page: keep this page for version transition work, not for first-time onboarding.
Overview of Changes¶
Cullinan v0.93 is a major architectural rewrite introducing:
| Feature | v0.9x | v0.93 |
|---|---|---|
| HTTP Engine | Tornado only | Tornado or ASGI (configurable) |
| Request Handling | One Handler per URL (Servlet-per-URL) | Single Dispatcher (Spring-style) |
| Request Object | tornado.httputil.HTTPServerRequest |
WebRequest (transport-agnostic) |
| Response Object | HttpResponse + self.write() |
WebResponse (factory methods) |
| Routing | Dynamic type('Servlet'...) classes |
Prefix-tree Router |
| Middleware | Init-only, not in request pipeline | Onion-model pipeline for every request |
| Tornado Dependency | Required | Optional (pip install cullinan[tornado]) |
| ASGI Support | None | Full ASGI 3.0 (uvicorn/hypercorn) |
| WebSocket | Tornado only | Tornado + ASGI |
| OpenAPI | None | Auto-generated from routes |
Installation¶
# Tornado mode
pip install cullinan[tornado]
# ASGI mode (uvicorn)
pip install cullinan[asgi]
# Full install
pip install cullinan[full]
Step-by-Step Migration¶
1. Import Path Changes¶
Keep existing controller/service imports as-is, but use the new Web entry names:
# v0.9x — still works in v0.93
from cullinan.web.controller import controller, get_api, post_api
from cullinan.core.services import service, Service
from cullinan.core import Inject
# v0.93 — new semantic imports available
from cullinan import (
WebRequest, WebResponse, # Unified request/response
Router, Dispatcher, # Gateway layer
GatewayMiddleware, CORSMiddleware,# Middleware pipeline
OpenAPIGenerator, # OpenAPI spec
get_asgi_app, # ASGI convenience
)
# Advanced transport integration stays explicit
from cullinan.transport.adapter import ASGIAdapter, TornadoAdapter, WebAdapter
2. Controller Changes¶
v0.9x: Controller methods used Tornado self.write()¶
# v0.9x — handler operates on tornado internals
@controller(url='/api/users')
class UserController:
@get_api(url='/{id}')
async def get_user(self, url_params=None):
user_id = url_params.get('id')
# ... business logic ...
return HttpResponse(body=user_data, status=200)
v0.93: Controller methods return WebResponse¶
# v0.93 — transport-agnostic, returns WebResponse
from cullinan import WebResponse
@controller(url='/api/users')
class UserController:
user_service: UserService = Inject()
@get_api(url='/{id}')
async def get_user(self, id):
user = self.user_service.get_by_id(id)
if not user:
return WebResponse.error(404, "User not found")
return WebResponse.json(user)
Backward compatibility: The old HttpResponse return type still works — the Dispatcher auto-converts it.
3. Response Object Migration¶
| v0.9x | v0.93 Equivalent |
|---|---|
HttpResponse(body=data, status=200) |
WebResponse.json(data) |
HttpResponse(body="text", status=200) |
WebResponse.text("text") |
HttpResponse(body=data, status=404) |
WebResponse.error(404, "msg") |
return {"key": "value"} (dict) |
return {"key": "value"} (auto-JSON, still works) |
return None |
return None (auto 204 No Content) |
4. Running the Application¶
v0.9x: Tornado only¶
from cullinan import run
run() # starts Tornado on port 4080
v0.93: Let Cullinan resolve the backend, or choose explicitly¶
from cullinan import run
# Recommended default path
from cullinan import application, configure
@configure(user_packages=["myapp"])
@application
def main(): ...
main()
# For an external ASGI server, prefer the entry-method helper
app = main.get_asgi_app()
# Then: uvicorn myapp:app
Environment variable: CULLINAN_ENGINE=asgi, or config: server_engine='auto' / 'asgi' / 'tornado'.
5. Configuration Changes¶
from cullinan import configure
configure(
user_packages=['myapp'],
# New v0.93 options:
# server_engine='auto', # 'auto', 'tornado', or 'asgi'
# asgi_server='uvicorn', # 'uvicorn' or 'hypercorn'
# route_trailing_slash=False,
# route_case_sensitive=True,
# debug=False,
)
6. Middleware Migration¶
v0.9x: Middleware registered but not in request pipeline¶
from cullinan.web.middleware import Middleware, middleware
@middleware(order=1)
class AuthMiddleware(Middleware):
def process_request(self, request):
# was initialized at startup, but not called per-request
pass
v0.93: Middleware in the onion pipeline¶
from cullinan import GatewayMiddleware, get_pipeline
class AuthMiddleware(GatewayMiddleware):
async def __call__(self, request, call_next):
token = request.get_header('Authorization')
if not token:
return WebResponse.error(401, "Unauthorized")
response = await call_next(request)
return response
# Register
get_pipeline().add(AuthMiddleware())
Backward compatibility: Old @middleware classes are auto-bridged via LegacyMiddlewareBridge.
7. OpenAPI Integration¶
from cullinan import OpenAPIGenerator, get_router
# Auto-generate spec from all registered routes
gen = OpenAPIGenerator(
title='My API',
version='1.0.0',
description='My awesome API',
)
# Register /openapi.json and /openapi.yaml endpoints
gen.register_spec_routes()
# Or get the spec programmatically
spec_dict = gen.to_dict()
json_str = gen.to_json()
8. WebSocket Changes¶
v0.9x: Tornado WebSocket only¶
@websocket_handler(url='/ws/chat')
class ChatHandler:
def on_open(self):
pass
def on_message(self, message):
self.write_message(f"echo: {message}")
def on_close(self):
pass
v0.93: Same API, works with both Tornado and ASGI¶
The @websocket_handler API is unchanged. When running in ASGI mode, WebSocket connections are handled natively via the ASGI WebSocket protocol.
9. Parameter System¶
The cullinan.web.params system (Path, Query, Body, Header) is unchanged and works identically in v0.93. Parameters are now resolved by the Dispatcher instead of Tornado handlers.
Architecture Comparison¶
v0.9x Architecture¶
Request → Tornado → Dynamic Handler(per-URL) → Controller method → self.write()
v0.93 Architecture¶
Request → Adapter(Tornado/ASGI) → WebRequest → Middleware Pipeline
→ Dispatcher → Router → Controller method → WebResponse
→ Adapter → Native Response
Breaking Changes¶
tornadois no longer a required dependency — install withpip install cullinan[tornado]if you need it.Handlerbase class is deprecated — controllers no longer inherit fromtornado.web.RequestHandler.- Direct Tornado API usage (
self.set_status(),self.write(),self.finish()) in controllers is deprecated — useWebResponseinstead.
Deprecation Timeline¶
| Item | v0.93 Status | Planned Removal |
|---|---|---|
Handler class |
Deprecated (warning) | v3.0 |
HttpResponse |
Deprecated (auto-bridged) | v3.0 |
EncapsulationHandler.add_url() |
Internal only | v3.0 |
self.write() in controllers |
Deprecated | v3.0 |
Old middleware (Middleware base) |
Auto-bridged | v3.0 |