Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ urlpatterns = winter.django.create_urls(app)
- `winter.web.throttling.reset(request, scope)` is gone: an API method with `@throttling` declares a
`throttle: winter.web.Throttle` argument, and `throttle.reset()` forgets what the scope has counted for the sender:
for the identity that the request was counted for, even if the method has logged the user in since.
- `register_native_response_type` is gone: an adapter passes the response types of its framework to `handle_request`.
An API method may still return a Django response.
- OpenAPI: `generate_openapi(app, title=..., version=...)` returns the spec and checks `@raises` annotations
of the application (`allow_missing_raises_annotation=True` skips the check).
`winter.openapi.serve_spec(app, spec, url='openapi.json')` and
Expand Down
19 changes: 8 additions & 11 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,17 +85,14 @@ at least two of them.
- **Framework behaviour is still process-global.** A `winter.web.WebApplication` holds the code of
a web application, its injector and its interceptors. The adapters derive its exception handlers
from its definitions, handle each of its requests in a child of its injector and throttle in the
storage that its injector binds. The other behavioural registries of `winter.web` and
`winter.openapi` (native response types, OpenAPI inspectors) are module singletons, and
`winter.sqlalchemy`, `winter.ddd` and `winter.messaging` take the injector from the service
locator `winter.application.get_injector`.
- **Models are initialised once per process.** The Django adapter's `_initialize_process` and
`winter.openapi.initialization.initialize` fill those module singletons with the Django response type
and the standard inspectors on first use. In the
target, a `WebApplication` is given these registries like its interceptors, and they start with the
standard entries. Building from an application, an adapter adds
its own entries (the native response type) to that application only. The
`initialize` functions then disappear.
storage that its injector binds; what belongs to an adapter, such as the response types of its
framework, it passes to `handle_request` itself. The OpenAPI inspectors and global exceptions of
`winter.openapi` are module singletons, and `winter.sqlalchemy`, `winter.ddd` and
`winter.messaging` take the injector from the service locator `winter.application.get_injector`.
- **`winter.openapi` is initialised once per process.** `winter.openapi.initialization.initialize`
fills those module singletons with the standard inspectors and global exceptions on first use. In
the target, `generate_openapi` is given them, starting with the standard entries, and `initialize`
disappears.
- **`winter.wsgi`, `winter.asgi` and `winter.rabbitmq` do not exist yet.** HTTP is served through
`winter.django` only, and `winter.messaging` has no transport adapter.
- **All dependencies are mandatory.** Django, SQLAlchemy, Redis and pydantic are installed with
Expand Down
23 changes: 9 additions & 14 deletions winter/django/view.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
from winter.web import Response
from winter.web import WebApplication
from winter.web import handle_request
from winter.web import register_native_response_type
from winter.web.autodiscovery import find_routes
from winter.web.exception_handlers import find_exception_handlers
from winter.web.exceptions import ExceptionHandlers
Expand All @@ -27,25 +26,13 @@

def create_urls(app: WebApplication) -> List[URLPattern]:
"""URL patterns of the routes of an application, relative to where they are included in the urlpatterns."""
_initialize_process()
routes = find_routes(app)
exception_handlers = find_exception_handlers(app)
# Each request binds the Django request, see _create_dispatch_function
check_arguments(app, routes, exception_handlers, request_types=[django.http.HttpRequest])
return _create_urls_from_routes(app, routes, exception_handlers)


_process_initialized = False


def _initialize_process():
"""Process-global until native response types belong to an application: docs/architecture.md, Current deviations."""
global _process_initialized
if not _process_initialized:
register_native_response_type(HttpResponseBase)
_process_initialized = True


def _create_urls_from_routes(
app: WebApplication,
routes: List[Route],
Expand Down Expand Up @@ -96,7 +83,15 @@ def _create_dispatch_function(app: WebApplication, route: Route, exception_handl
def dispatch(winter_view, request: django.http.HttpRequest, **path_parameters):
winter_request = DjangoRequest(request, path_parameters)
request_values = {django.http.HttpRequest: request}
response = handle_request(app, route, winter_request, exception_handlers, request_values, django_user_identity)
response = handle_request(
app,
route,
winter_request,
exception_handlers,
request_values,
throttling_identity=django_user_identity,
native_response_types=(HttpResponseBase,),
)
return to_django_response(response)

return dispatch
Expand Down
1 change: 0 additions & 1 deletion winter/web/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
from .request_body_annotation import request_body
from .request import Request
from .request_handler import handle_request
from .request_handler import register_native_response_type
from .request_scope import request_scope
from .request_header_annotation import request_header
from .response import Response
Expand Down
32 changes: 12 additions & 20 deletions winter/web/request_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ def handle_request(
exception_handlers: ExceptionHandlers,
request_values: Mapping[type, object],
throttling_identity: IdentityResolver,
native_response_types: tuple[type, ...],
) -> Response:
"""
Run the whole handling pipeline for a matched route of the application: throttling, its interceptors, argument
Expand All @@ -90,7 +91,8 @@ def handle_request(
The request is handled in an injector of its own, a child of the application's one, that binds the Request
and the request values of the adapter, such as its own request object. Request-scoped values live in it.
Throttling counts the request in the storage of the application's injector for the identity that the adapter
resolves, since the adapter knows the users of its framework.
resolves, since the adapter knows the users of its framework. An API method or an exception handler may return
an object of the native response types of the adapter, which reaches the adapter untouched.
"""
method = route.method
response_headers: dict[str, str] = {}
Expand Down Expand Up @@ -121,7 +123,7 @@ def handle_request(
method = exception_handlers.get(Exception)
result = _handle_exception(method, handler_exception, request, response_headers, injector)

return _to_response(request, result, method, response_headers)
return _to_response(request, result, method, response_headers, native_response_types)


def _handle_exception(
Expand All @@ -137,7 +139,13 @@ def _handle_exception(
return method(handler, **arguments)


def _to_response(request: Request, result: Any, method: ComponentMethod, headers: dict[str, str]) -> Response:
def _to_response(
request: Request,
result: Any,
method: ComponentMethod,
headers: dict[str, str],
native_response_types: tuple[type, ...],
) -> Response:
if isinstance(result, Response):
return Response(status_code=result.status_code, body=result.body, headers={**headers, **result.headers})

Expand All @@ -148,25 +156,9 @@ def _to_response(request: Request, result: Any, method: ComponentMethod, headers
body = result
status_code = get_response_status(request.method, method)

if _is_native(body):
if isinstance(body, native_response_types):
return Response(status_code=status_code, body=body, headers=headers)

headers.setdefault('content-type', 'application/json')
content = b'' if body is None else json.dumps(body, cls=JSONEncoder).encode()
return Response(status_code=status_code, body=content, headers=headers)


_native_response_types = ()


def register_native_response_type(type_: type):
"""
Adapters register the response types of their framework here. An API method may return such an object,
and it reaches the adapter untouched.
"""
global _native_response_types
_native_response_types = _native_response_types + (type_,)


def _is_native(body: Any) -> bool:
return isinstance(body, _native_response_types)
6 changes: 3 additions & 3 deletions winter/web/response.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
class Response:
"""
What winter.web hands to an adapter after handling a request, and what an API method or
exception handler returns to bypass output processing and JSON encoding.
exception handler returns to bypass JSON encoding.

`body` is the final payload as bytes, or an object native to the adapter in use
(a Django HttpResponse, for example), which the adapter sends as is.
`body` is the final payload as bytes, or an object of the native response types of the adapter
in use (a Django HttpResponse, for example), which the adapter sends as is.
"""
status_code: int
body: Any = b''
Expand Down
Loading