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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,12 @@ urlpatterns = winter.django.create_urls(app)
`winter.openapi.serve_spec(app, spec, url='openapi.json')` and
`winter.openapi.serve_swagger_ui(app, spec_url='openapi.json', url='docs/')` add the routes that serve them;
the served spec and Swagger UI work under whatever prefix the application is included at.
- OpenAPI: `@winter.openapi.type_inspector(Answer)` replaces `register_type_inspector` and is a decorator only; it
also describes the subclasses of the type. `register_global_exception` is gone: `@winter.openapi.global_exception`
remains. The parameters of a route come from its path, query and `PagePosition` arguments only:
`register_route_parameters_inspector`, `get_route_parameters_inspectors` and `winter.openapi.initialization` are
gone, and the inspectors and `inspect_page` are no longer exported. A parameter that no argument stands for, such
as a header read by a request-scoped provider, is added to the spec that `generate_openapi` returns.

## Upgrading to Winter 33

Expand Down
26 changes: 9 additions & 17 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,12 @@ at least two of them.
subpackage can later ship as its own distribution from its own repository without any
import path changing.
2. **Framework behaviour is never process-global.** Every behavioural registry (exception
handlers, interceptors, throttling configuration, OpenAPI inspectors)
belongs to the application object an adapter builds from, such as `winter.web.WebApplication`,
and the values that its methods receive come from its injector, so two applications can live
in one process.
Declarative metadata about types (component annotations, JSON encoders for a type) may be
global, like the classes it describes.
handlers, interceptors, throttling configuration) belongs to the application object an adapter
builds from, such as `winter.web.WebApplication`, and the values that its methods receive come
from its injector, so two applications can live in one process. What belongs to an adapter, such
as the response types of its framework, the adapter passes to `winter.web.handle_request` itself.
Declarative metadata about types (component annotations, JSON encoders and OpenAPI type
inspectors for a type) may be global, like the classes it describes.
3. **Glue between models, or between adapters, is application code.** Mapping a domain
`NotFoundException` to HTTP 404, or mounting the Swagger UI route, is done by the
application that composes the pieces, not by the framework.
Expand All @@ -82,17 +82,9 @@ at least two of them.

## Current deviations

- **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; 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.sqlalchemy`, `winter.ddd` and `winter.messaging` take the injector from a service
locator,** `winter.application.get_injector`, instead of being given the injector of an
application.
- **`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
13 changes: 0 additions & 13 deletions tests/openapi/inspectors/test_route_parameter_inspector.py

This file was deleted.

10 changes: 1 addition & 9 deletions winter/openapi/__init__.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,7 @@
from .annotations import global_exception
from .annotations import register_global_exception
from .generator import generate_openapi
from .inspection.inspection import inspect_type
from .inspection.inspection import register_type_inspector
from .inspectors import PagePositionArgumentsInspector
from .inspectors import PathParametersInspector
from .inspectors import QueryParametersInspector
from .inspectors import RouteParametersInspector
from .inspectors import get_route_parameters_inspectors
from .inspectors import inspect_page
from .inspectors import register_route_parameters_inspector
from .inspection.inspection import type_inspector
from .serving import serve_spec
from .serving import serve_swagger_ui
from .swagger_ui import get_swagger_ui_html
Expand Down
1 change: 0 additions & 1 deletion winter/openapi/annotations/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
from .global_exception import GlobalExceptionAnnotation
from .global_exception import global_exception
from .global_exception import register_global_exception
5 changes: 1 addition & 4 deletions winter/openapi/annotations/global_exception.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,7 @@ class GlobalExceptionAnnotation:


def global_exception(exception_class: Type[Exception]) -> Type[Exception]:
return register_global_exception(exception_class)


def register_global_exception(exception_class: Type[Exception]) -> Type[Exception]:
"""The exception may be raised by any API method, so the check of @raises does not require it to be declared."""
assert issubclass(exception_class, Exception), f'Class "{exception_class}" must be a subclass of Exception'
annotation = GlobalExceptionAnnotation(exception_cls=exception_class)
annotation_decorator = annotate(annotation, unique=True)
Expand Down
18 changes: 13 additions & 5 deletions winter/openapi/generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
from winter.web.exception_handlers import find_exception_handlers
from winter.web.exceptions import ExceptionHandlers
from winter.web.exceptions.raises import get_raises
from winter.web.pagination.page_position_argument_resolver import PagePositionArgumentResolver
from winter.web.request_body_annotation import RequestBodyAnnotation
from winter.web.routing import Route
from winter.web.routing import RouteAnnotation
Expand All @@ -45,8 +46,9 @@
from .inspection import DataTypes
from .inspection import TypeInfo
from .inspection.inspection import InspectorNotFound
from .initialization import initialize
from .inspectors import get_route_parameters_inspectors
from .inspectors import PagePositionArgumentsInspector
from .inspectors import PathParametersInspector
from .inspectors import QueryParametersInspector
from .validators import validate_missing_raises_annotations


Expand Down Expand Up @@ -152,7 +154,6 @@ def generate_openapi(
allow_missing_raises_annotation: bool = False,
) -> Dict[str, Any]:
"""OpenAPI specification of the routes of an application, as a JSON-compatible dict."""
initialize()
if not allow_missing_raises_annotation:
validate_missing_raises_annotations(app)
return generate_openapi_for_routes(
Expand All @@ -178,7 +179,6 @@ def generate_openapi_for_routes(
add_url_segment_as_tag: bool = True,
) -> Dict[str, Any]:
"""The responses of the raised exceptions come from the exception handlers, the built-in ones by default."""
initialize()
if exception_handlers is None:
exception_handlers = built_in_exception_handlers()
routes = list(routes)
Expand Down Expand Up @@ -329,9 +329,17 @@ def get_url_path_tag(url_path: str, path_prefix: str) -> Optional[str]:
return url_path_tag


# The parameters of a route mirror the arguments that winter.web maps from the request
_route_parameters_inspectors = (
PathParametersInspector(),
QueryParametersInspector(),
PagePositionArgumentsInspector(PagePositionArgumentResolver()),
)


def get_route_parameters(route: Route, schema_registry: SchemaRegistry) -> List[Parameter]:
parameters = []
for inspector in get_route_parameters_inspectors():
for inspector in _route_parameters_inspectors:
parameters += inspector.inspect_parameters(route, schema_registry)
return parameters

Expand Down
34 changes: 0 additions & 34 deletions winter/openapi/initialization.py

This file was deleted.

2 changes: 1 addition & 1 deletion winter/openapi/inspection/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from .data_formats import DataFormat
from .data_types import DataTypes
from .inspection import inspect_type
from .inspection import register_type_inspector
from .inspection import type_inspector
from .type_info import TypeInfo
18 changes: 10 additions & 8 deletions winter/openapi/inspection/inspection.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,16 @@
] = {}


def register_type_inspector(*types_: Type, checker: Callable = None, func: Callable = None):
if func is None:
return lambda func: register_type_inspector(*types_, checker=checker, func=func)

for type_ in types_:
callables = _inspectors_by_type.setdefault(type_, [])
callables.append((func, checker))
return func
def type_inspector(*types_: Type, checker: Callable = None):
"""
The decorated function describes the types, and their subclasses, in OpenAPI schemas. A checker limits it to
the type hints for which it returns true.
"""
def decorator(func: Callable) -> Callable:
for type_ in types_:
_inspectors_by_type.setdefault(type_, []).append((func, checker))
return func
return decorator


class InspectorNotFound(Exception):
Expand Down
3 changes: 0 additions & 3 deletions winter/openapi/inspectors/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,3 @@
from .path_parameters_inspector import PathParametersInspector
from .query_parameters_inspector import QueryParametersInspector
from .route_parameters_inspector import RouteParametersInspector
from .route_parameters_inspector import get_route_parameters_inspectors
from .route_parameters_inspector import get_route_parameters_inspectors
from .route_parameters_inspector import register_route_parameters_inspector
4 changes: 2 additions & 2 deletions winter/openapi/inspectors/page_inspector.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from winter.pagination import Page
from winter.openapi.inspection.type_info import TypeInfo
from winter.openapi.inspectors.standard_types_inspectors import inspect_type
from winter.openapi.inspectors.standard_types_inspectors import register_type_inspector
from winter.openapi.inspectors.standard_types_inspectors import type_inspector


def create_dataclass(page_type: Type) -> Type:
Expand Down Expand Up @@ -52,7 +52,7 @@ def create_dataclass(page_type: Type) -> Type:


# noinspection PyUnusedLocal
@register_type_inspector(Page)
@type_inspector(Page)
def inspect_page(hint_class) -> TypeInfo:
if hint_class not in page_to_dataclass_map:
page_to_dataclass_map[hint_class] = create_dataclass(hint_class)
Expand Down
18 changes: 0 additions & 18 deletions winter/openapi/inspectors/route_parameters_inspector.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import abc
import logging
from typing import List
from typing import TYPE_CHECKING

Expand All @@ -16,20 +15,3 @@ class RouteParametersInspector(abc.ABC): # pragma: no cover
@abc.abstractmethod
def inspect_parameters(self, route: 'Route', schema_registry: 'SchemaRegistry') -> List[Parameter]:
return []


_route_parameters_inspectors: List[RouteParametersInspector] = []


def register_route_parameters_inspector(inspector: RouteParametersInspector):
inspector_classes = [inspector.__class__ for inspector in get_route_parameters_inspectors()]

if inspector.__class__ in inspector_classes:
logging.warning(f'{inspector.__class__.__name__} already registered')
return

_route_parameters_inspectors.append(inspector)


def get_route_parameters_inspectors() -> List[RouteParametersInspector]:
return _route_parameters_inspectors
Loading
Loading