Skip to content

Web Framework for Python based on Spring Framework

License

Notifications You must be signed in to change notification settings

LeonidKrutovsky/winter

 
 

Repository files navigation

Winter

Build Status codecov Maintainability PyPI version Gitter

Web Framework for Python inspired by Spring Framework

Main features

  • Declarative API
  • Built around python type annotations
  • Automatic OpenAPI (swagger) documentation generation
  • Suitable for DDD
  • Handling exception without boilerplate in accordance with RFC 7807

How to use

Installation

pip install winter

Hello world

import winter

@winter.controller
class HelloWorldController:
    @winter.route_get('/hello/')
    def hello(self):
        return f'Hello, world!'

To use it with Django:

import winter_django

urlpatterns = [
    *winter_django.create_django_urls(HelloWorldController),
]

Todo list CRUD example:

from http import HTTPStatus
from typing import List
from typing import Optional

import winter
import winter.web
from dataclasses import dataclass
from winter.data.pagination import Page
from winter.data.pagination import PagePosition


@dataclass
class NewTodoDTO:
    todo: str


@dataclass
class TodoUpdateDTO:
    todo: str


@dataclass
class TodoDTO:
    todo_index: int
    todo: str


@winter.web.problem(status=HTTPStatus.NOT_FOUND)
class NotFoundException(Exception):
    def __init__(self, todo_index: int):
        self.index = todo_index


todo_list: List[str] = []


@winter.web.controller
@winter.route('todo/')
class TodoController:
    @winter.route_post('')
    @winter.request_body(argument_name='new_todo_dto')
    def create_todo(self, new_todo_dto: NewTodoDTO) -> TodoDTO:
        todo_list.append(new_todo_dto.todo)
        return self._build_todo_dto(len(todo_list) - 1)

    @winter.route_get('{todo_index}/')
    def get_todo(self, todo_index: int) -> TodoDTO:
        self._check_index(todo_index)
        return self._build_todo_dto(todo_index)

    @winter.route_get('{?q}')
    def get_todo_list(self, page_position: PagePosition, q: Optional[str] = None) -> Page[TodoDTO]:
        q = q if q is None else q.lower()
        dto_list = [
            TodoDTO(todo_index=todo_index, todo=todo)
            for todo_index, todo in enumerate(todo_list)
            if q is None or q in todo.lower()
        ]
        limit = page_position.limit
        offset = page_position.offset
        paginated_dto_list = dto_list[offset: offset + limit]
        return Page(total_count=len(dto_list), items=paginated_dto_list, position=page_position)

    @winter.route_get('{todo_index}/')
    @winter.request_body(argument_name='todo_update_dto')
    def update_todo(self, todo_index: int, todo_update_dto: TodoUpdateDTO):
        self._check_index(todo_index)
        todo_list[todo_index] = todo_update_dto.todo

    @winter.route_get('{todo_index}/')
    def delete_todo(self, todo_index: int):
        self._check_index(todo_index)
        del todo_list[todo_index]

    def _check_index(self, todo_index: int):
        if todo_index < 0 or todo_index >= len(todo_list):
            raise NotFoundException(todo_index=todo_index)

    def _build_todo_dto(self, todo_index: int):
        return TodoDTO(todo_index=todo_index, todo=todo_list[todo_index])

Extending Page class

import winter
import winter.web
from dataclasses import dataclass
from winter.data.pagination import Page
from winter.data.pagination import PagePosition
from typing import TypeVar
from typing import Generic


T = TypeVar('T')

@dataclass(frozen=True)
class CustomPage(Page, Generic[T]):
    extra_field: str  # The field will go to meta JSON response field


@winter.web.controller
class ExampleController:
    @winter.route_get('/')
    def create_todo(self, page_position: PagePosition) -> CustomPage[int]:
        return CustomPage(
            # Standard Page fields
            total_count=3,
            items=[1, 2, 3],
            position=page_position,
            # Custom fields
            extra_field=456,
        )

Exception handling

from dataclasses import dataclass
from http import HTTPStatus
from typing import List

from rest_framework.request import Request
import winter
import winter.web


# Minimalist approach. Pointed status and that this exception will be handling automatically. Expected output below:
# {'status': 404, 'type': 'urn:problem-type:todo-not-found', 'title': 'Todo not found', 'detail': 'Incorrect index: 1'}
@winter.web.problem(status=HTTPStatus.NOT_FOUND)
class TodoNotFoundException(Exception):
    def __init__(self, invalid_index: int):
        self.invalid_index = invalid_index

    def __str__(self):
        return f'Incorrect index: {self.invalid_index}'

# Extending output using dataclass. Dataclass fields will be added to response body. Expected output below:
# {'status': 404, 'type': 'urn:problem-type:todo-not-found', 'title': 'Todo not found', 'detail': '', 'invalid_index': 1}
@winter.web.problem(status=HTTPStatus.NOT_FOUND)
@dataclass
class TodoNotFoundException(Exception):
    invalid_index: int

# When we want to override global handler and customize response body. Expected output below:
# {index: 1, 'message': 'Access denied'}
@dataclass
class ErrorDTO:
    index: int
    message: str


class TodoNotFoundExceptionCustomHandler(winter.web.ExceptionHandler):
    @winter.response_status(HTTPStatus.NOT_FOUND)
    def handle(self, request: Request, exception: TodoNotFoundException) -> ErrorDTO:
        return ErrorDTO(index=exception.invalid_index, message='Access denied')


todo_list: List[str] = []


@winter.web.controller
class TodoProblemExistsController:
    @winter.route_get('global/{todo_index}/')
    def get_todo_with_global_handling(self, todo_index: int):
        raise TodoNotFoundException(invalid_index=todo_index)

    @winter.route_get('custom/{todo_index}/')
    @winter.raises(TodoNotFoundException, handler_cls=TodoNotFoundExceptionCustomHandler)
    def get_todo_with_custom_handling(self, todo_index: int):
        raise TodoNotFoundException(invalid_index=todo_index)

Interceptors

You can define interceptors to pre-handle a web request before it gets to a controller. The pre_handle method arguments will be injected the same way as it's done in controllers. It's not supported to return any response from interceptors. However, the exceptions thrown from within an interceptor will be handled automatically.

from rest_framework.request import Request
import winter
from winter.core import ComponentMethod
from winter.web import Interceptor
from winter.web import ResponseHeader


class HelloWorldInterceptor(Interceptor):
    @winter.response_header('x-hello-world', 'hello_world_header')
    def pre_handle(self, method: ComponentMethod, request: Request, hello_world_header: ResponseHeader[str]):
        print(f'Controller method: {method.name}')
        if 'hello_world' in request.query_params:
            hello_world_header.set('Hello, World!')

The only way now to register an interceptor is to define a configurer (don't forget to import during app initialization) and implement the add_interceptors method.

from winter.web import Configurer
from winter.web import InterceptorRegistry

from .interceptors import HelloWorldInterceptor


class HelloWorldConfigurer(Configurer):
    def add_interceptors(self, registry: InterceptorRegistry):
        registry.add_interceptor(HelloWorldInterceptor())

About

Web Framework for Python based on Spring Framework

Resources

License

Code of conduct

Stars

Watchers

Forks

Packages

No packages published

Languages

  • Python 100.0%