Web Framework for Python inspired by Spring Framework
- Declarative API
- Built around python type annotations
- Automatic OpenAPI (swagger) documentation generation
- Suitable for DDD
- Handling exception without boilerplate in accordance with RFC 7807
pip install winter
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),
]
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, auto_handle=True)
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])
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,
)
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, auto_handle=True)
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, auto_handle=True)
@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.throws(TodoNotFoundException, handler_cls=TodoNotFoundExceptionCustomHandler)
def get_todo_with_custom_handling(self, todo_index: int):
raise TodoNotFoundException(invalid_index=todo_index)