Giter Club home page Giter Club logo

litestar-org / litestar Goto Github PK

View Code? Open in Web Editor NEW
4.5K 38.0 322.0 98.14 MB

Production-ready, Light, Flexible and Extensible ASGI API framework | Effortlessly Build Performant APIs

Home Page: https://litestar.dev/

License: MIT License

Python 99.61% HTML 0.06% JavaScript 0.03% CSS 0.06% Makefile 0.21% Lua 0.03% Dockerfile 0.02%
python api rest pydantic asgi openapi redoc starlite starlite-api swagger litestar litestar-framework litestar-api asyncio hacktoberfest msgspec rapidoc

litestar's Introduction

Litestar Logo - Light Litestar Logo - Dark

Project Status
CI/CD Latest Release ci Documentation Building
Quality Coverage Quality Gate Status Maintainability Rating Reliability Rating Security Rating
Package PyPI - Version PyPI - Support Python Versions Starlite PyPI - Downloads Litestar PyPI - Downloads
Community Reddit Discord Matrix Medium Twitter Blog
Meta Litestar Project types - Mypy License - MIT Litestar Sponsors linting - Ruff code style - Ruff All Contributors

Litestar is a powerful, flexible yet opinionated ASGI framework, focused on building APIs, and offers high-performance data validation and parsing, dependency injection, first-class ORM integration, authorization primitives, and much more that's needed to get applications up and running.

Check out the documentation πŸ“š for a detailed overview of its features!

Additionally, the Litestar fullstack repository can give you a good impression how a fully fledged Litestar application may look.

Table of Contents

Installation

pip install litestar

Quick Start

from litestar import Litestar, get


@get("/")
def hello_world() -> dict[str, str]:
    """Keeping the tradition alive with hello world."""
    return {"hello": "world"}


app = Litestar(route_handlers=[hello_world])

Core Features

Example Applications

Pre-built Example Apps
  • litestar-pg-redis-docker: In addition to Litestar, this demonstrates a pattern of application modularity, SQLAlchemy 2.0 ORM, Redis cache connectivity, and more. Like all Litestar projects, this application is open to contributions, big and small.
  • litestar-fullstack: A reference application that contains most of the boilerplate required for a web application. It features a Litestar app configured with best practices, SQLAlchemy 2.0 and SAQ, a frontend integrated with Vitejs and Jinja2 templates, Docker, and more.
  • litestar-hello-world: A bare-minimum application setup. Great for testing and POC work.

Sponsors

Litestar is an open-source project, and we enjoy the support of our sponsors to help fund the exciting work we do.

A huge thanks to our sponsors:

Scalar.com Telemetry Sports Stok

Check out our sponsors in the docs

If you would like to support the work that we do please consider becoming a sponsor via Polar.sh (preferred), GitHub or Open Collective.

Also, exclusively with Polar, you can engage in pledge-based sponsorships.

Features

Class-based Controllers

While supporting function-based route handlers, Litestar also supports and promotes python OOP using class based controllers:

Example for class-based controllers
from typing import List, Optional
from datetime import datetime

from litestar import Controller, get, post, put, patch, delete
from litestar.dto import DTOData
from pydantic import UUID4

from my_app.models import User, PartialUserDTO


class UserController(Controller):
    path = "/users"

    @post()
    async def create_user(self, data: User) -> User: ...

    @get()
    async def list_users(self) -> List[User]: ...

    @get(path="/{date:int}")
    async def list_new_users(self, date: datetime) -> List[User]: ...

    @patch(path="/{user_id:uuid}", dto=PartialUserDTO)
    async def partial_update_user(
        self, user_id: UUID4, data: DTOData[PartialUserDTO]
    ) -> User: ...

    @put(path="/{user_id:uuid}")
    async def update_user(self, user_id: UUID4, data: User) -> User: ...

    @get(path="/{user_name:str}")
    async def get_user_by_name(self, user_name: str) -> Optional[User]: ...

    @get(path="/{user_id:uuid}")
    async def get_user(self, user_id: UUID4) -> User: ...

    @delete(path="/{user_id:uuid}")
    async def delete_user(self, user_id: UUID4) -> None: ...

Data Parsing, Type Hints, and Msgspec

Litestar is rigorously typed, and it enforces typing. For example, if you forget to type a return value for a route handler, an exception will be raised. The reason for this is that Litestar uses typing data to generate OpenAPI specs, as well as to validate and parse data. Thus, typing is essential to the framework.

Furthermore, Litestar allows extending its support using plugins.

Plugin System, ORM support, and DTOs

Litestar has a plugin system that allows the user to extend serialization/deserialization, OpenAPI generation, and other features.

It ships with a builtin plugin for SQL Alchemy, which allows the user to use SQLAlchemy declarative classes "natively" i.e., as type parameters that will be serialized/deserialized and to return them as values from route handlers.

Litestar also supports the programmatic creation of DTOs with a DTOFactory class, which also supports the use of plugins.

OpenAPI

Litestar has custom logic to generate OpenAPI 3.1.0 schema, include optional generation of examples using the polyfactory library.

ReDoc, Swagger-UI and Stoplight Elements API Documentation

Litestar serves the documentation from the generated OpenAPI schema with:

All these are available and enabled by default.

Dependency Injection

Litestar has a simple but powerful DI system inspired by pytest. You can define named dependencies - sync or async - at different levels of the application, and then selective use or overwrite them.

Example for DI
from litestar import Litestar, get
from litestar.di import Provide


async def my_dependency() -> str: ...


@get("/")
async def index(injected: str) -> str:
    return injected


app = Litestar([index], dependencies={"injected": Provide(my_dependency)})

Middleware

Litestar supports typical ASGI middleware and ships with middlewares to handle things such as

  • CORS
  • CSRF
  • Rate limiting
  • GZip and Brotli compression
  • Client- and server-side sessions

Route Guards

Litestar has an authorization mechanism called guards, which allows the user to define guard functions at different level of the application (app, router, controller etc.) and validate the request before hitting the route handler function.

Example for route guards
from litestar import Litestar, get

from litestar.connection import ASGIConnection
from litestar.handlers.base import BaseRouteHandler
from litestar.exceptions import NotAuthorizedException


async def is_authorized(connection: ASGIConnection, handler: BaseRouteHandler) -> None:
    # validate authorization
    # if not authorized, raise NotAuthorizedException
    raise NotAuthorizedException()


@get("/", guards=[is_authorized])
async def index() -> None: ...


app = Litestar([index])

Request Life Cycle Hooks

Litestar supports request life cycle hooks, similarly to Flask - i.e. before_request and after_request

Performance

Litestar is fast. It is on par with, or significantly faster than comparable ASGI frameworks.

You can see and run the benchmarks here, or read more about it here in our documentation.

Contributing

Litestar is open to contributions big and small. You can always join our discord server or join our Matrix space to discuss contributions and project maintenance. For guidelines on how to contribute, please see the contribution guide.

Contributors ✨

Thanks goes to these wonderful people: Emoji Key
Na'aman Hirschfeld
Na'aman Hirschfeld

🚧 πŸ’» πŸ“– ⚠️ πŸ€” πŸ’‘ πŸ›
Peter Schutt
Peter Schutt

🚧 πŸ’» πŸ“– ⚠️ πŸ€” πŸ’‘ πŸ›
Ashwin Vinod
Ashwin Vinod

πŸ’» πŸ“–
Damian
Damian

πŸ“–
Vincent Sarago
Vincent Sarago

πŸ’»
Jonas KrΓΌger Svensson
Jonas KrΓΌger Svensson

πŸ“¦
Sondre LillebΓΈ Gundersen
Sondre LillebΓΈ Gundersen

πŸ“¦
Lev
Lev

πŸ’» πŸ€”
Tim Wedde
Tim Wedde

πŸ’»
Tory Clasen
Tory Clasen

πŸ’»
Arseny Boykov
Arseny Boykov

πŸ’» πŸ€”
Jacob Rodgers
Jacob Rodgers

πŸ’‘
Dane Solberg
Dane Solberg

πŸ’»
madlad33
madlad33

πŸ’»
Matthew Aylward
Matthew Aylward

πŸ’»
Jan Klima
Jan Klima

πŸ’»
C2D
C2D

⚠️
to-ph
to-ph

πŸ’»
imbev
imbev

πŸ“–
cătălin
cătălin

πŸ’»
Seon82
Seon82

πŸ“–
Slava
Slava

πŸ’»
Harry
Harry

πŸ’» πŸ“–
Cody Fincher
Cody Fincher

🚧 πŸ’» πŸ“– ⚠️ πŸ€” πŸ’‘ πŸ›
Christian Clauss
Christian Clauss

πŸ“–
josepdaniel
josepdaniel

πŸ’»
devtud
devtud

πŸ›
Nicholas Ramos
Nicholas Ramos

πŸ’»
seladb
seladb

πŸ“– πŸ’»
Simon WienhΓΆfer
Simon WienhΓΆfer

πŸ’»
MobiusXS
MobiusXS

πŸ’»
Aidan Simard
Aidan Simard

πŸ“–
wweber
wweber

πŸ’»
Samuel Colvin
Samuel Colvin

πŸ’»
Mateusz MikoΕ‚ajczyk
Mateusz MikoΕ‚ajczyk

πŸ’»
Alex
Alex

πŸ’»
Odiseo
Odiseo

πŸ“–
Javier  Pinilla
Javier Pinilla

πŸ’»
Chaoying
Chaoying

πŸ“–
infohash
infohash

πŸ’»
John Ingles
John Ingles

πŸ’»
Eugene
Eugene

⚠️ πŸ’»
Jon Daly
Jon Daly

πŸ“– πŸ’»
Harshal Laheri
Harshal Laheri

πŸ’» πŸ“–
TΓ©va KRIEF
TΓ©va KRIEF

πŸ’»
Konstantin Mikhailov
Konstantin Mikhailov

🚧 πŸ’» πŸ“– ⚠️ πŸ€” πŸ’‘ πŸ›
Mitchell Henry
Mitchell Henry

πŸ“–
chbndrhnns
chbndrhnns

πŸ“–
nielsvanhooy
nielsvanhooy

πŸ’» πŸ› ⚠️
provinzkraut
provinzkraut

🚧 πŸ’» πŸ“– ⚠️ πŸ€” πŸ’‘ πŸ› 🎨
Joshua Bronson
Joshua Bronson

πŸ“–
Roman Reznikov
Roman Reznikov

πŸ“–
mookrs
mookrs

πŸ“–
Mike DePalatis
Mike DePalatis

πŸ“–
Carlos Alberto PΓ©rez-Molano
Carlos Alberto PΓ©rez-Molano

πŸ“–
ThinksFast
ThinksFast

⚠️ πŸ“–
Christopher Krause
Christopher Krause

πŸ’»
Kyle Smith
Kyle Smith

πŸ’» πŸ“– πŸ›
Scott Bradley
Scott Bradley

πŸ›
Srikanth Chekuri
Srikanth Chekuri

⚠️ πŸ“–
Michael Bosch
Michael Bosch

πŸ“–
sssssss340
sssssss340

πŸ›
ste-pool
ste-pool

πŸ’» πŸš‡
Alc-Alc
Alc-Alc

πŸ“– πŸ’» ⚠️
asomethings
asomethings

πŸ’»
Garry Bullock
Garry Bullock

πŸ“–
Niclas Haderer
Niclas Haderer

πŸ’»
Diego Alvarez
Diego Alvarez

πŸ“– πŸ’» ⚠️
Jason Nance
Jason Nance

πŸ“–
Igor Kapadze
Igor Kapadze

πŸ“–
Somraj Saha
Somraj Saha

πŸ“–
Magnús Ágúst Skúlason
Magnús Ágúst Skúlason

πŸ’» πŸ“–
Alessio Parma
Alessio Parma

πŸ“–
Peter Brunner
Peter Brunner

πŸ’»
Jacob Coffee
Jacob Coffee

πŸ“– πŸ’» ⚠️ πŸš‡ πŸ€” 🚧 πŸ’Ό 🎨
Gamazic
Gamazic

πŸ’»
Kareem Mahlees
Kareem Mahlees

πŸ’»
Abdulhaq Emhemmed
Abdulhaq Emhemmed

πŸ’» πŸ“–
Jenish
Jenish

πŸ’» πŸ“–
chris-telemetry
chris-telemetry

πŸ’»
Ward
Ward

πŸ›
Stephan Fitzpatrick
Stephan Fitzpatrick

πŸ›
Eric Kennedy
Eric Kennedy

πŸ“–
wassaf shahzad
wassaf shahzad

πŸ’»
Nils Olsson
Nils Olsson

πŸ’» πŸ›
Riley Chase
Riley Chase

πŸ’»
arl
arl

🚧
Antoine van der Horst
Antoine van der Horst

πŸ“–
Nick Groenen
Nick Groenen

πŸ“–
Giorgio Vilardo
Giorgio Vilardo

πŸ“–
Nicholas Bollweg
Nicholas Bollweg

πŸ’»
Tomas Jonsson
Tomas Jonsson

⚠️ πŸ’»
Khiem Doan
Khiem Doan

πŸ“–
kedod
kedod

πŸ“– πŸ’» ⚠️
sonpro1296
sonpro1296

πŸ’» ⚠️ πŸš‡ πŸ“–
Patrick Armengol
Patrick Armengol

πŸ“–
Sander
Sander

πŸ“–
η–―δΊΊι™’δΈ»δ»»
η–―δΊΊι™’δΈ»δ»»

πŸ“–
aviral-nayya
aviral-nayya

πŸ’»
whiskeyriver
whiskeyriver

πŸ’»
Phyo Arkar Lwin
Phyo Arkar Lwin

πŸ’»
MatthewNewland
MatthewNewland

πŸ› πŸ’» ⚠️
Tom Kuo
Tom Kuo

πŸ›
LeckerenSirupwaffeln
LeckerenSirupwaffeln

πŸ›
Daniel GonzΓ‘lez FernΓ‘ndez
Daniel GonzΓ‘lez FernΓ‘ndez

πŸ“–
01EK98
01EK98

πŸ“–
Sarbo Roy
Sarbo Roy

πŸ’»
Ryan Seeley
Ryan Seeley

πŸ’»
Felix
Felix

πŸ“– πŸ›
George Sakkis
George Sakkis

πŸ’»
Huba Tuba
Huba Tuba

πŸ“–
Stefane Fermigier
Stefane Fermigier

πŸ“–
r4ge
r4ge

πŸ’» πŸ“–
Jay
Jay

πŸ’»
sinisaos
sinisaos

πŸ“–
Tharuka Devendra
Tharuka Devendra

πŸ’»
euri10
euri10

πŸ’» πŸ“– πŸ›
Shubham
Shubham

πŸ“–
Erik Hasse
Erik Hasse

πŸ› πŸ’»
Nikita Sobolev
Nikita Sobolev

πŸš‡ πŸ’»
Nguyα»…n HoΓ ng Đức
Nguyα»…n HoΓ ng Đức

πŸ›
RavanaBhrama
RavanaBhrama

πŸ“–
Marcel Johannesmann
Marcel Johannesmann

πŸ“–
Matthew
Matthew

πŸ“–
Mattwmaster58
Mattwmaster58

πŸ› πŸ’» ⚠️
Manuel Sanchez Pinar
Manuel Sanchez Pinar

πŸ“–
Juan Riveros
Juan Riveros

πŸ“–
David Brochart
David Brochart

πŸ“–
Sean Donoghue
Sean Donoghue

πŸ“–
P.C. Shyamshankar
P.C. Shyamshankar

πŸ› πŸ’» ⚠️
William Evonosky
William Evonosky

πŸ’»
geeshta
geeshta

πŸ“– πŸ’» πŸ›
Robert Rosca
Robert Rosca

πŸ“–
DICE_Lab
DICE_Lab

πŸ’»
Luis San Pablo
Luis San Pablo

πŸ’» ⚠️ πŸ“–
Pastukhov Nikita
Pastukhov Nikita

πŸ“–
James O'Claire
James O'Claire

πŸ“–
Pete
Pete

πŸ“–
Alexandre Richonnier
Alexandre Richonnier

πŸ’» πŸ“–
betaboon
betaboon

πŸ’»
Dennis Brakhane
Dennis Brakhane

πŸ’» πŸ›
Pragy Agarwal
Pragy Agarwal

πŸ“–
Piotr Dybowski
Piotr Dybowski

πŸ“–
Konrad Szczurek
Konrad Szczurek

πŸ“– ⚠️
Orell Garten
Orell Garten

πŸ’» πŸ“– ⚠️
Julien
Julien

πŸ“–
Leejay Hsu
Leejay Hsu

🚧 πŸš‡ πŸ“–
Michiel W. Beijen
Michiel W. Beijen

πŸ“–
L. Bao
L. Bao

πŸ“–
Jarred Glaser
Jarred Glaser

πŸ“–
Hunter Boyd
Hunter Boyd

πŸ“–
Cesar Giulietti
Cesar Giulietti

πŸ“–
Marcus Lim
Marcus Lim

πŸ“–
Henry Zhou
Henry Zhou

πŸ› πŸ’»
William Stam
William Stam

πŸ“–
andrew do
andrew do

πŸ’» ⚠️ πŸ“–
Boseong Choi
Boseong Choi

πŸ’» ⚠️
Kim Minki
Kim Minki

πŸ’» πŸ“–
Jeongseop Lim
Jeongseop Lim

πŸ“–
FergusMok
FergusMok

πŸ“– πŸ’» ⚠️
Manu Singhal
Manu Singhal

πŸ“–
Jerry Wu
Jerry Wu

πŸ“–
horo
horo

πŸ›
Ross Titmarsh
Ross Titmarsh

πŸ’»
Mike Korneev
Mike Korneev

πŸ“–
Patrick Neise
Patrick Neise

πŸ’»
Jean Arhancet
Jean Arhancet

πŸ›
Leo Alekseyev
Leo Alekseyev

πŸ’»
aranvir
aranvir

πŸ“–
bunny-therapist
bunny-therapist

πŸ’»
Ben Luo
Ben Luo

πŸ“–
Hugo van Kemenade
Hugo van Kemenade

πŸ“–
Michael Gerbig
Michael Gerbig

πŸ“–
CrisOG
CrisOG

πŸ› πŸ’» ⚠️
harryle
harryle

πŸ’» ⚠️
James Bennett
James Bennett

πŸ›
sherbang
sherbang

πŸ“–
Carl Smedstad
Carl Smedstad

⚠️
Taein Min
Taein Min

πŸ“–
Stanislav Lyu.
Stanislav Lyu.

πŸ›

This project follows the all-contributors specification. Contributions of any kind welcome!

litestar's People

Contributors

abdulhaq-e avatar alc-alc avatar allcontributors[bot] avatar ashwinvin avatar bobronium avatar cbscsm avatar cofin avatar danesolberg avatar dependabot[bot] avatar dkress59 avatar euri10 avatar goldziher avatar gsakkis avatar guacs avatar infohash avatar jacobcoffee avatar jonadaly avatar jtraub avatar kedod avatar odiseo0 avatar peterschutt avatar provinzkraut avatar seladb avatar smithk86 avatar sobolevn avatar tclasen avatar timwedde avatar vincentsarago avatar vrslev avatar yudjinn avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

litestar's Issues

new user issue with path parameter type definition

I didn't realize that path parameters must have a type, so I declared one like @get(path="/{dataset_id}").

(1) I think this is a common mistake people will make with the minimal example, since it only uses uuid. Maybe adding an example of using str to the minimal example and a comment that it's required would help.

(2) I was confused at first by the error message starlite.exceptions.ImproperlyConfiguredException: path parameter must declare a type: '{parameter_name:type}' because I had added the type hint to to the method signature. This would be more useful if it actually printed the parameter name without the type and the method it was associated with. I think it should also suggest that you change it to "str" since that's what most people who make this mistake will intend.

(3) I'm usually an advocate for explicit typing, but it seems that the most common case where path parameters are just strings shouldn't require the :str addition, but I think more clearly documenting this for new users should be sufficient.

import fails because of missing sqlalchemy

I guess #27 introduced a bug which result in MissingDependencyException: sqlalchemy is not installed when I try to import some Starlite submodule

In [1]: from starlite.controller import Controller
---------------------------------------------------------------------------
ImportError                               Traceback (most recent call last)
~/Dev/DevSeed/starlite/starlite/plugins/sql_alchemy.py in <module>
     28     )
---> 29     from sqlalchemy.orm import DeclarativeMeta, Mapper
     30     from sqlalchemy.sql.type_api import TypeEngine

ImportError: cannot import name 'DeclarativeMeta' from 'sqlalchemy.orm' (/Users/vincentsarago/Dev/venv/python38/lib/python3.8/site-packages/sqlalchemy/orm/__init__.py)

The above exception was the direct cause of the following exception:

MissingDependencyException                Traceback (most recent call last)
<ipython-input-1-fceda90b0bd9> in <module>
----> 1 from starlite.controller import Controller

~/Dev/DevSeed/starlite/starlite/__init__.py in <module>
      1 # flake8: noqa
----> 2 from .app import Starlite
      3 from .config import CORSConfig, OpenAPIConfig
      4 from .controller import Controller
      5 from .dto import DTOFactory

~/Dev/DevSeed/starlite/starlite/app.py in <module>
     18 from typing_extensions import Type
     19 
---> 20 from starlite.config import CORSConfig, OpenAPIConfig
     21 from starlite.enums import MediaType
     22 from starlite.exceptions import HTTPException

~/Dev/DevSeed/starlite/starlite/config.py in <module>
     16 from typing_extensions import Type
     17 
---> 18 from starlite.openapi.controller import OpenAPIController
     19 
     20 

~/Dev/DevSeed/starlite/starlite/openapi/controller.py in <module>
      2 from orjson import OPT_INDENT_2, dumps
      3 
----> 4 from starlite.controller import Controller
      5 from starlite.enums import MediaType, OpenAPIMediaType
      6 from starlite.exceptions import ImproperlyConfiguredException

~/Dev/DevSeed/starlite/starlite/controller.py in <module>
      6 from starlite.response import Response
      7 from starlite.types import Guard, ResponseHeader
----> 8 from starlite.utils import normalize_path
      9 
     10 if TYPE_CHECKING:  # pragma: no cover

~/Dev/DevSeed/starlite/starlite/utils/__init__.py in <module>
      2 from .model import convert_dataclass_to_model, create_parsed_model_field
      3 from .sequence import find_index, unique
----> 4 from .signature import SignatureModel, create_function_signature_model
      5 from .url import join_paths, normalize_path
      6 

~/Dev/DevSeed/starlite/starlite/utils/signature.py in <module>
      8 
      9 from starlite.exceptions import ImproperlyConfiguredException
---> 10 from starlite.plugins.base import PluginMapping, PluginProtocol, get_plugin_for_value
     11 
     12 

~/Dev/DevSeed/starlite/starlite/plugins/__init__.py in <module>
      1 from .base import PluginMapping, PluginProtocol, get_plugin_for_value
----> 2 from .sql_alchemy import SQLAlchemyPlugin
      3 
      4 __all__ = [
      5     "PluginMapping",

~/Dev/DevSeed/starlite/starlite/plugins/sql_alchemy.py in <module>
     30     from sqlalchemy.sql.type_api import TypeEngine
     31 except ImportError as exc:  # pragma: no cover
---> 32     raise MissingDependencyException("sqlalchemy is not installed") from exc
     33 
     34 

MissingDependencyException: sqlalchemy is not installed

I believe sqlalchemy is only a plugin 🀷

Inherited Pydantic's GenericModel or dataclasses as dependencies

Dependency that outputs instance of a class that inherits from a class that inherits from pydantic.generics.GenericModel or dataclasses.dataclass (I know πŸ˜ƒ) are reverted back to their base classes.

from typing import Generic, Optional, TypeVar, cast

from pydantic import BaseModel
from pydantic.generics import GenericModel
from starlite import Provide, Starlite, get

T = TypeVar("T")


class Store(GenericModel, Generic[T]):
    """Abstract store"""

    model: type[T]

    def get(self, id: str) -> Optional[T]:
        raise NotImplementedError


class DictStore(Store[T]):
    """In-memory store implementation"""

    def get(self, id: str) -> Optional[T]:
        return None


class Item(BaseModel):
    name: str


@get()
def root(store: Store[Item]) -> Optional[Item]:
    # Expected DictStore[Item], got Store[Item].
    # That's why the line below will fail.
    assert type(store) is DictStore
    return cast(DictStore[Item], store).get("0")  # Unreachable


def get_item_store() -> Store[Item]:
    return DictStore(model=Item)


deps = {"store": Provide(get_item_store, use_cache=True)}
app = Starlite(route_handlers=[root], debug=True, dependencies=deps)

As you can see, there are Store class that inherits from GenericModel and DictStore that implements Store. We set up dependencies in such a way that store argument corresponds with DictStore. We expect DictStore to be injected in route handler but it fails because Pydantic (right?) tries to give the most appropriate type for what we asked for.

I'm not sure if this is a Starlite issue. Also I don't think dependencies should be validatedβ€”so strictly or at all.


Funding

  • If you would like to see an issue prioritized, make a pledge towards it!
  • We receive the pledge once the issue is completed & verified
Fund with Polar

README discord link seems to be broken?

It just links to the image, seemingly.


Note

While we are open for sponsoring on GitHub Sponsors and
OpenCollective, we also utilize Polar.sh to engage in pledge-based sponsorship.

Check out all issues funded or available for funding on our Polar.sh dashboard

  • If you would like to see an issue prioritized, make a pledge towards it!
  • We receive the pledge once the issue is completed & verified
  • This, along with engagement in the community, helps us know which features are a priority to our users.
Fund with Polar

middleware help

How do I achieve the same result as per below using starlites MiddlewareProtocol ?

The documentation doesn't provide enough information how to go about it, well for me atleast :/

class CustomHeaderMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request, call_next):
        start_time = time.time()
        response = await call_next(request)
        process_time = time.time() - start_time
        response.headers["X-Process-Time"] = str(process_time)

        return response
class CustomHeaderMiddleware2(MiddlewareProtocol):
    def __init__(self, app: ASGIApp):
        self.app = app

    async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
        if scope["type"] == "http":
            request = Request(scope)
        await self.app(scope, receive, send)

List[User] in openapi is displayed as Any

code

from typing import List

from starlite import Starlite, Controller, Partial, get, post, delete, patch, put, OpenAPIConfig

from pydantic import BaseModel, UUID4


class User(BaseModel):
    first_name: str
    last_name: str
    id: UUID4


class UserController(Controller):
    path = "/users"

    @post()
    async def create_user(self, data: User) -> User:
        ...

    @get()
    async def list_users(self) -> List[User]:
        ...

    @patch(path="/{user_id:uuid}")
    async def partially_update_user(self, user_id: UUID4, data: Partial[User]) -> User:
        ...

    @put(path="/{user_id:uuid}")
    async def update_user(self, user_id: UUID4, data: User) -> User:
        ...

    @get(path="/{user_id:uuid}")
    async def get_user(self, user_id: UUID4) -> User:
        ...

    @delete(path="/{user_id:uuid}")
    async def delete_user(self, user_id: UUID4) -> User:
        ...

app = Starlite(route_handlers=[UserController])

result

image
And how to access the interactive openapi documentation fastAPI is used in /docs
Can you tell me what to do?

Mark that a dependency should never be a query parameter.

This is a side-effect of the change in 1.3.9 and the way that I've structured the consumption of dependencies in the example app.

The __init__() method of the repository looks like this:

    def __init__(
        self,
        limit_offset: LimitOffset | None = None,
        updated_filter: BeforeAfter | None = None,
    ) -> None:
        ...
        if limit_offset is not None:
            self.apply_limit_offset_pagination(limit_offset)
        if updated_filter is not None:
            self.filter_on_datetime_field(updated_filter)

If the repository is included as a dependency and if the the limit_offset and updated_filter dependencies are available those dependencies get consumed by the repository, and their dependencies end up documented as query params. However, for routes where the limit_offset and updated_filter dependencies aren't available to the route, they themselves are shown as query params:

image

The reason I use this pattern is that without it, every list route, e.g., /users or /users/{user_id}/items has to do something like this:

    @get(
        dependencies={
            "limit_offset": Provide(limit_offset_pagination),
            "updated_filter": Provide(filter_for_updated),
        }
    )
    async def get(
        self,
        repository: UserRepository,
        is_active: bool = Parameter(query="is-active", default=True),
        limit_offset: LimitOffset,
        updated_filter: BeforeAfter,
    ) -> list[UserModel]:
        repository.apply_limit_offset_pagination(limit_offset)
        repository.filter_on_datetime_field(updated_filter)
        return await repository.get_many(is_active=is_active)

which reduces down to this if the repository can be left to optionally consume the dependency if it is available:

    @get(
        dependencies={
            "limit_offset": Provide(limit_offset_pagination),
            "updated_filter": Provide(filter_for_updated),
        }
    )
    async def get(
        self,
        repository: UserRepository,
        is_active: bool = Parameter(query="is-active", default=True),
    ) -> list[UserModel]:
        return await repository.get_many(is_active=is_active)

[refactor]Β the tests suite and add more tests

You can never have enough tests - some parts of the tests are not great, it would be very nice to have more tests / better tests etc. Also it would be very good to find more bugs.

This ticket is meant to stay open - anyone can add PRs that point to it. Its also a place to aggregate discussions regarding the test suite.

Data validation error response is not usable on front end side

For data validation in request handler parameters Starlite returns JSON
detail: "Validation failed for POST http://localhost:8000/api/a/brand/:\n\ndata -> age\n field required (type=value_error.missing)" extra: null
It is hard to use such response for form validation on front end side.

Besides validation errors breaks CORS middleware which makes response body unavailable for client app in browser.
I think it would be more convenient instead raising Starlite ValidationException leave Pydantic ValidationError. It would make it possible to handle it on middleware level and fix CORS.

`Provide` and `@classmethod`

Provide(Something.new) where new() is an @classmethod doesn't work.

repro:

asyncio REPL 3.10.2 (main, Jan 26 2022, 20:19:57) [GCC 10.2.1 20210110] on linux
Use "await" directly instead of "asyncio.run()".
Type "help", "copyright", "credits" or "license" for more information.
>>> import asyncio
>>> from app.domain.providers.entities.service import Service
>>> Service.new
<bound method Service.new of <class 'app.domain.providers.entities.service.Service'>>
>>> await Service.new()
3
>>> from starlite import Provide
>>> await Provide(Service.new).dependency()
Traceback (most recent call last):
  File "/usr/local/lib/python3.10/concurrent/futures/_base.py", line 446, in result
    return self.__get_result()
  File "/usr/local/lib/python3.10/concurrent/futures/_base.py", line 391, in __get_result
    raise self._exception
  File "<console>", line 1, in <module>
TypeError: Service.new() takes 1 positional argument but 2 were given

Stems from here:
https://github.com/starlite-api/starlite/blob/4c411b4e260c1db0d1e8f9beba3e888e7f1e7a89/starlite/provide.py#L22-L24

I think the current pattern is to prevent the callable becoming an instance method of the Provide instance, which perhaps would be better done with self.dependency = staticmethod(dependency)... but I haven't tested it or anything yet.

Incorrect middleware call order

So I have an app with configured trusted host and CORS and some middlewares

app = Starlite(
    debug=True,
    cors_config=cors_conf,
    allowed_hosts=['127.0.0.1', 'localhost'],
    dependencies={'edb': Provide(init_edgedb)},
    on_startup=[logging_config.configure],
    on_shutdown=[disconnect_edgedb],
    route_handlers=[health_check, admin_router],
    middleware=[SomeMiddleware, SomeMiddleware2],
)

class SomeMiddleware(MiddlewareProtocol):
    def __init__(self, app: ASGIApp):
        self.app = app

    async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
        print("///call some middleware")
        await self.app(scope, receive, send)
        print("///some middleware AWAITED")

There are two problems here:

  1. SomeMiddleware2 called before SomeMiddleware. Normally we expect that middlewares will be called in order they were passed.
  2. cors_config and allowed_hosts settings can't be used with other middlewares, as the other middlewares will be called before host check which in most cases does not make sense. See log below, my middlewares called even for preflight OPTIONS request.
INFO - 2022-04-19 00:22:24,513 - uvicorn.error - on - Application startup complete.
///call some middleware2
///call some middleware
///call cors middleware
INFO:     127.0.0.1:38354 - "OPTIONS /api/a/brand HTTP/1.1" 200 OK
///cors middleware AWAITED OPTIONS
///some middleware AWAITED
///some middleware2 AWAITED
# here OPTIONS req finishes and POST req starts
///call some middleware2
///call some middleware
///call cors middleware
///call trusted host middleware
INFO:     127.0.0.1:38354 - "POST /api/a/brand HTTP/1.1" 201 Created
///trusted host middleware AWAITED
///cors middleware AWAITED
///some middleware AWAITED
///some middleware2 AWAITED

router with path already registered

Thought I'd have a little play with Starlite as it seems nice.

https://starlite-api.github.io/starlite/usage/1-routers-and-controllers/#registering-routes

# controller
from starlite.controller import Controller
from starlite.handlers import get


class HomeController(Controller):
    path = "/"

    @get()
    async def home(self) -> None:
        return {"message": "hello"}
# route handler
from controllers.home_controller import HomeController
from starlite import Router

base_router = Router(path="/base", route_handlers=[HomeController])
# main
from starlite import Starlite

from routers.base import base_router

app = Starlite(route_handlers=[base_router])

From what I am reading on the docs this should work? However it fails with

  File "/home/champ/projects/v7-starlite/__pypackages__/3.10/lib/starlite/router.py", line 133, in validate_registration_value
    raise ImproperlyConfiguredException(f"Router with path {value.path} has already been registered")
starlite.exceptions.ImproperlyConfiguredException: Router with path /base has already been registered

Pytest failure: ValidationError

________________________________________________________ test_conversion_from_model_instance[VanillaDataClassPerson-exclude1-field_mapping1-plugins1] ________________________________________________________

model = <class 'tests.VanillaDataClassPerson'>, exclude = ['id'], field_mapping = {'complex': 'ultra'}, plugins = []

    @pytest.mark.parametrize(  # type: ignore[misc]
        "model, exclude, field_mapping, plugins",
        [
            [Person, ["id"], {"complex": "ultra"}, []],
            [VanillaDataClassPerson, ["id"], {"complex": "ultra"}, []],
            [Pet, ["age"], {"species": "kind"}, [SQLAlchemyPlugin()]],
        ],
    )
    def test_conversion_from_model_instance(model: Any, exclude: list, field_mapping: dict, plugins: list) -> None:
        DTO = DTOFactory(plugins=plugins)("MyDTO", model, exclude=exclude, field_mapping=field_mapping)
    
        if issubclass(model, (Person, VanillaDataClassPerson)):
            model_instance = model(
                first_name="moishe",
                last_name="zuchmir",
                id=1,
                optional="some-value",
                complex={"key": [{"key": "value"}]},
                pets=None,
            )
        else:
            model_instance = cast(Type[Pet], model)(  # type: ignore
                id=1,
                species=Species.MONKEY,
                name="Mike",
                age=3,
                owner_id=1,
            )
>       dto_instance = DTO.from_model_instance(model_instance=model_instance)

tests/test_dto_factory.py:180: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
starlite/dto.py:51: in from_model_instance
    return cls(**values)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

>   ???
E   pydantic.error_wrappers.ValidationError: 1 validation error for MyDTO
E   pets
E     none is not an allowed value (type=type_error.none.not_allowed)

pydantic/main.py:331: ValidationError

Occurs after #116.

[enhancement] logging setup to be non-blocking

The default logging config, which is just a simple abstraction on top of DictConfig, creates a standard python logger. The issue with this setup is that python logging is sync and blocking. This means that the logging config needs to be updated to setup a QueueHandler.

Here are some resources regarding this:

  1. https://rob-blackbourn.medium.com/how-to-use-python-logging-queuehandler-with-dictconfig-1e8b1284e27a
  2. https://stackoverflow.com/questions/25585518/python-logging-logutils-with-queuehandler-and-queuelistener
  3. https://betterprogramming.pub/power-up-your-python-logging-6dd3ed38f322

[infra] github workflow for forks

The github action doesn't run correctly for forks - we need to fix it so tests run also for forks.

A second level of this would be to add coverage reporting in github, so it becomes clear whats what with coverage.

[Question] Mount Piccolo Admin to Starlite app

Hello,
First I want to thanks all for this great new framework.
I would like to implement a Starlite asgi template for Piccolo ORM. Piccolo ORM is an easy-to-use async orm with an integrated web admin. Piccolo ORM currently support Starlette, FastAPI, BlackSheep and Xpresso (all this frameworks support mounting asgi apps) and it would be nice if we could add Starlite support. I have already implemented a basic CRUD for Piccolo tables, but the main problem is mounting Piccolo Admin (which is already an asgi application) on the Starlite app.

Starlite routing has a different design and you can only run the another asgi application through middleware which work,

# some other import
from piccolo_admin.endpoints import create_admin
from starlite import Starlite
from home.piccolo_app import APP_CONFIG

# Piccolo Admin asgi app
admin_app = create_admin(tables=APP_CONFIG.table_classes)

class AdminMiddleware(MiddlewareProtocol):
    def __init__(self, app: ASGIApp):
        self.app = admin_app

    async def __call__(
        self, scope: Scope, receive: Receive, send: Send
    ) -> None:
        if scope["type"] == "http":
            request = Request(scope)
        await self.app(scope, receive, send)

app = Starlite(
    middleware=[AdminMiddleware],
   ...
)

but that didn't solve the main problem because admin app is mounted to root path and any other routes from route_handlers is not accesible (e.g /tasks). Router class also does not work because route_handlers accept standalone function or controller but not asgi app.
Is it possible to mount Piccolo Admin on the /admin route (other frameworks have Mount or app.mount), and the rest of the Starlite app is on the root path?
Sorry for long comment (I hope I have explained the problem well) but any help appriciated.
Thanks.

DTOFactory.__call__() unnecessarily modifies FieldInfo

The field is neither excluded, nor mapped so the least surprising thing would be that the field stays the same. This can be achieved as create_model() accepts FieldInfo for attributes.

>>> class Example(BaseModel):
...     not_optional: str
... 
>>> ExampleDTO = DTOFactory(plugins=[])("ExampleDTO", Example, exclude=[], field_mapping={}, field_definitions={})
>>> Example.__fields__["not_optional"].field_info.default
PydanticUndefined
>>> ExampleDTO.__fields__["not_optional"].field_info.default
Ellipsis

create_model doc:

   :param field_definitions: fields of the model (or extra fields if a base is supplied)
    in the format `<name>=(<type>, <default default>)` or `<name>=<default value>, e.g.
    `foobar=(str, ...)` or `foobar=123`, or, for complex use-cases, in the format
    `<name>=<FieldInfo>`, e.g. `foo=Field(default_factory=datetime.utcnow, alias='bar')`

But also see: pydantic/pydantic#4041 as turns out it still needs to be passed as (<type>, <FieldInfo>) tuple.

Can't set query parameter with default value

I'm trying to set default values for query parameters. I've tried with and without the Parameter class but it gives the same result:

from starlite import get, Parameter
from starlite import controller
from starlite import create_test_client


class Endpoints(controller.Controller):
    """TileMatrixSets endpoints."""

    path = ""

    @get(path="/ya")
    def ya(
        self,
        id: str = Parameter(
            default="something",
            required=False,
            description="some id",
        ),
    ) -> str:   
        return id

    @get(path="/ye")
    def ye(self, id: str = "something") -> str:   
        return id

    
with create_test_client(Endpoints) as client:
    print(client.get("/ya?id=vincent").text)
    print(client.get("/ya").text)
    print()
    print(client.get("/ye?id=vincent").text)
    print(client.get("/ye").text)

>> "vincent"
>> {"detail":"Validation failed for GET http://testserver/ya:\n\nid\n  none is not an allowed value (type=type_error.none.not_allowed)","extra":null}

>> "vincent"
>> {"detail":"Validation failed for GET http://testserver/ye:\n\nid\n  none is not an allowed value (type=type_error.none.not_allowed)","extra":null}

TestClient() ignores Starlite(on_startup=[...])

For my_app/main.py

import sys

from starlite import MediaType, Starlite, State, get


def on_startup(state: State):
    state.value = 1


@get(path="/health-check", media_type=MediaType.TEXT)
def health_check(state: State) -> str:
    try:
        print(f'{state.value=}')
        return "healthy"
    except Exception as exc:
        print(exc, file=sys.stderr)


app = Starlite(route_handlers=[health_check], on_startup=[on_startup])

This test will fail. tests/test_health_check.py

from starlette.status import HTTP_200_OK
from starlite import TestClient

from my_app.main import app

client = TestClient(app=app)


def test_health_check():
    response = client.get("/health-check")
    assert response.status_code == HTTP_200_OK
    assert response.text == "healthy"

The error 'State' object has no attribute 'value' indicates, on_startup() were not executed prior to the test execution. It ran as expected when using uvicorn.

Enhance multipart handling

Starlite currently uses the Starlette built-in multipart handling, which is somewhat limited. @adriangb has proposed to enhance this by supporting different encodings and data.

Static files with path="/"

Hi Guys,

I have a question about serving static files with path="/".
I have found that the problem is here, in line 54.
https://github.com/starlite-api/starlite/blob/e9372128d490667402db2893e47f5cfda0917bab/starlite/asgi.py#L52-L55

Consider this static file configuration:

[StaticFilesConfig(
    directories=[Path(pkg_resources.resource_filename(__name__, 'view_dist'))],
    path='/',
    html_mode=True
)]

and following view_dist subdirectories:

+--- view_dist
    +--- index.html
    +--- diagnostic
        +--- index.html

Requests and its reponses:

URL=http://192.168.200.100:8000

1. wget -O- $URL/                          --> 200 OK
2. wget -O- $URL/index.html                --> 200 OK
3. wget -O- $URL/diagnostic                --> 307 Temporary Redirect to new location (bad): http://192.168.200.100:8000diagnostic/
4. wget -O- $URL/diagnostic/               --> 307 like above
5. wget -O- $URL/diagnostic/index.html     --> 404 Not Found

The problems in points 3 and 4 come from the fact that line 54 in asgi.py file removed every occurrences of / from scope['path'] and then the same scope is used in the starlette/staticfiles.py file to build the URL for redirecting purpose, but unfortunately scope['path'] is lack of forward slashes.
https://github.com/encode/starlette/blob/d7cbe2a4887ad6b15fe7523ed62e28a426b7697d/starlette/staticfiles.py#L135-L139

After guarding line 54 (removing every static_path='/' from scope['path']) with extra if it works as expected.

if static_path != '/':
    scope["path"] = scope["path"].replace(static_path, "") 
1. wget -O- $URL/                          --> 200 OK
2. wget -O- $URL/index.html                --> 200 OK
3. wget -O- $URL/diagnostic                --> 307 Temporary Redirect to new location (ok): http://192.168.200.100:8000/diagnostic/ --> 200 OK
4. wget -O- $URL/diagnostic/               --> 200 OK
5. wget -O- $URL/diagnostic/index.html     --> 200 OK

I have just started my journey with starlite so I do not know if and where are the side effects of commented out this line in asgi.py. Do you have any suggestions on what I should do to make all the previously mentioned requests work?

DTO Factory changing not nullable fields to Optional

SQLAlchemy 1.4.36, Starlite 1.3.3, pydantic 1.9.1

from pydantic import create_model
from sqlalchemy import Column, String
from sqlalchemy.orm import declarative_base
from starlite.dto import DTOFactory
from starlite.plugins.sql_alchemy import SQLAlchemyPlugin

Base = declarative_base()


class MyModel(Base):
    __tablename__ = "whatever"
    id = Column(String, nullable=False, primary_key=True)


FromPlugin = SQLAlchemyPlugin().to_pydantic_model_class(MyModel)
print(repr(FromPlugin.__fields__["id"]))
# ModelField(name='id', type=str, required=True)

FromDTO = DTOFactory(plugins=[SQLAlchemyPlugin()])("FromDTO", MyModel)
print(repr(FromDTO.__fields__["id"]))
# ModelField(name="id", type=Optional[str], required=False, default=None)

print(FromPlugin.__fields__["id"].field_info.default)
# Ellipsis
print(FromPlugin.__fields__["id"].default)
# None

print(create_model("Test", id=(str, None)).__fields__)
# {'id': ModelField(name='id', type=Optional[str], required=False, default=None)}

https://github.com/starlite-api/starlite/blob/b95519c6c325074d9209f36119d749532d506ae1/starlite/dto.py#L140-L145

The model field hits the first line of the if statement, if model_field.field_info.default is not Undefined:, and as you can see in the above script, for the FromPlugin model that is Ellipsis, so it enters the block and returns (field_type, None), where None is model_field.default. The 2nd element of the tuple being None makes create_model() make the type Optional.

Enhancement: Add a CLI

We should consider adding a CLI that will allow users to quickly scaffold a project with a default folder / file structure, readme, tooling etc.

A good example is the NestJS CLI.

I'd like to discuss this proposal in this issue.

Fund with Polar

Having body causes duplicate log entries

Take the example and run it with uvicorn:

from pydantic import BaseModel
from starlite import Starlite, post


class RouteData(BaseModel):
    id: str


@post()
def route(value: RouteData) -> None:
    pass


app = Starlite(route_handlers=[route])

You will notice that all log entries are duplicated:

INFO:     Started server process [75249]
INFO:uvicorn.error:Started server process [75249]
INFO:     Waiting for application startup.
INFO:uvicorn.error:Waiting for application startup.
INFO:     Application startup complete.
INFO:uvicorn.error:Application startup complete.
INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO:uvicorn.error:Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)

If you change value type to string i. e. it would change to be query parameter:

@post()
def route(value: str) -> None:
    pass

then duplicate entires disappear:

INFO:     Started server process [75353]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)

[documentation]Β Continous update

The documentation is an ongoing project - we would like to ask the community to contribute. PRs including any of the following will be welcome:

  1. edits and rewrites to improve the documentation in terms of grammar, syntax, structure
  2. improvements to examples - better code or cleaner code, fixes, additional examples
  3. additional documentation
  4. improvements to the documentation website (using mkdocs metarial theme)

You are also welcome to discuss in this thread your opinion regarding required additions or improvements to the documentation.

OpenAPI: duplicate query parameters when multiple class dependencies with query param defined in common base class

This is from the example app after updating to 1.3.9.

I have a nested router to extend the /users route to represent user items as /users/{user_id}/items.

user_router.register(item_router)
v1_router = Router(path=Paths.V1, route_handlers=[user_router])

This is how the schema is representing for the nested items list route:

image

Haven't had time to dig in to find source of the issue yet, but should do later tonight.

[infra] update mypy setup

Currently mypy ignores the tests suite. Its a lot of busy work, but it would be awesome to have mypy run against the tests because we would be able to find typing issues this way quicker.

Note: Mypy is ran using pre-commit. To run it locally you need to have pre-commit installed, then install the pre-commit hooks with pre-commit install, and then run it with pre-commit run --all-files.

The mypy config for it can be found in pre-commit-config.yaml in the root of the repository.

Documentation: Add API documentation using Doctstring Annotations.

I really miss having API docs generally available.. this isn't just a starlite issue, but in general I notice that developer docs are more tailored to the beginner by being mostly prose and metaphor.

I actually agree with that though, ease of uptake is important, but that doesn't mean api docs can't be buried somewhere via some tiny obscure link at the bottom of the page somewhere.

https://github.com/mkdocstrings/mkdocstrings looks like it might be a good plugin to use.

Version >0.6.0 breaks query parameters

I haven't done extensive testing yet, but it seems like version >0.6.0 breaks query parameters as described in the documentation (https://starlite-api.github.io/starlite/usage/3-parameters/#query-parameters). It gives 404 without every executing the function.

class ApiV1Controller(Controller):
    path = "/api/v1"

    @get(path="/product/{isin:str}/rawdata")
    async def product_rawdata(self, isin: str, dataType:  Literal['profile', 'etc']) -> Any:
        raise KeyError("Test")

with create_test_client(ApiV1Controller) as client:
        print(client.get("/api/v1/product/test/rawdata").text)
        print(client.get("/api/v1/product/test/rawdata?dataType=profile").text)

Output (1.2.0):

{"detail":"Missing required parameter dataType for url http://testserver/api/v1/product/test/rawdata","extra":null}
{"detail":"Not Found","extra":null}

Output (0.6.0):

{"detail":"Validation failed for GET http://testserver/api/v1/product/test/rawdata:\n\ndataType\n  none is not an allowed value (type=type_error.none.not_allowed)","extra":null}
Traceback (most recent call last):
...
    raise KeyError("test")
KeyError: 'test'

UPDATE: seems like it does run the function, but fails with 404 if there is a KeyError; I've updated the example. This might be intended behavior but it's very confusing since it also doesn't log anything on the server-side.

Move `requests` to `testing` extra

I was inspecting starlight dependencies and was confused to see requests being required without any obvious reason.

I found #13 then, but I disagree with the resolution since I believe libs required for testing purposes should not be installed for normal use. Now it only one lib (with several dependencies), but imagine if more dependencies would be added for testing.

What I propose:

  1. Move requests from required dependencies to testing extra (pip install starlight[testing])
  2. Remove import of starlite.testing from starlite package
  3. When starlight is imported explicitly (from starlight import testint), check for requests installed. if not, raise RuntimeError("To access starlight.testing install starlight with [testing] extra")

How would pyproject.toml of end user look like:

[tool.poetry.dependencies]
python = "^3.10"
starlite = "^1.3.9"

[tool.poetry.dev-dependencies]
starlite = {extras = ["testing"], version = "*"}  # whatever version is installed + testing dependencies
pytest = "^5.2"

I can send a PR if changes are welcomed.

[enhancement] allow the exclusion of some keys or None in output response

In other Framework (e.g FastAPI), users can set the output model but also exclude specific keys or unset values without the need of re-writting new classes:

With FastAPI

class User(BaseModel):
    name: str
    password: str
    age: Optional[int]

@app.get(
    "/user", 
    response_model=User,
    response_model_exclude_none=True,
    response_model_exclude={"password"},
)
def get_user() -> User:
    return User(name="vincent", password="starlite")

With Starlite

class User(BaseModel):
    name: str
    password: str
    age: Optional[int]

class UserWithoutPassword(BaseModel):
    name: str
    age: Optional[int]

@get(path="/user")
def get_user() -> UserWithoutPassword:
    return UserWithoutPassword(**User(name="vincent", password="starlite").dict())

Note: in Starlite, we don't need the response_model=User because it's defined in the model signature.

pydantic-factories testing examples

Testing page in docs has a tip that pydantic-factories package is bundled with Starlite. The problem is that there’s no example why it is useful. As I can see Starlite uses the package mostly for generating OpenAPI examples. I think we should either don’t mention pydantic-factories on testing page or add an example how to use it.

Any response type for `Respose.serializer()`

Response.serializer() passed to orjson.dumps() in Response.render():

    def render(self, content: Any) -> bytes:
        """Renders content into bytes"""
        try:
            if self.media_type == MediaType.JSON:
                return dumps(content, default=self.serializer, option=OPT_SERIALIZE_NUMPY | OPT_OMIT_MICROSECONDS)

Response.serializer() return type is dict[str, Any] but should be Any as overriding serializer() is the way to extend orjson for any type. Currently I need a #type: ignore for a subclass:

class Response(_Response):
    @staticmethod
    def serializer(value: Any) -> Any:  # type:ignore[override]
        if isinstance(value, pgproto.UUID):
            return str(value)
        return _Response.serializer(value)

Otherwise mypy gives us:

src/app/core/response.py:8: error: Signature of "serializer" incompatible with supertype "Response"  [override]
src/app/core/response.py:8: note:      Superclass:
src/app/core/response.py:8: note:          def serializer(value: Any) -> Dict[str, Any]
src/app/core/response.py:8: note:      Subclass:
src/app/core/response.py:8: note:          def serializer(value) -> Any

allow empty `""` path in Controller ?

https://github.com/starlite-api/starlite/blob/965d97620774991bd302a3a227bd3ebe6236ae96/starlite/controller.py#L27-L28

Context

In some of our project (built with FastAPI) we use Factory classes that helps us creating bunch of endpoints for given user inputs and dependencies (https://developmentseed.org/titiler/advanced/tiler_factories/ https://developmentseed.org/titiler/api/titiler/core/factory/#tilerfactory). This is why I was really interested in the Controller class to replace our Factories.

One problem I'm facing right now is that Controller Path has to be non null.

from starlite import get, Starlite
from starlite.controller import Controller
from starlette.requests import Request


class Endpoints(Controller):
    """TileMatrixSets endpoints."""

    path = "/"

    @get(path="/yo")
    def hey(self, request: Request) -> str:
        return "yipi"

    @get(path="ye")
    def hey2(self, request: Request) -> str:
        return "what"


app = Starlite(route_handlers=[Endpoints])

schema

{
  "openapi": "3.1.0",
  "info": {
    "title": "Starlite API",
    "version": "1.0.0"
  },
  "servers": [
    {
      "url": "/"
    }
  ],
  "paths": {
    "//yo": {
      "get": {
        "operationId": "hey",
        "responses": {
          "200": {
            "description": "Request fulfilled, document follows",
            "headers": {},
            "content": {
              "application/json": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "deprecated": false
      }
    },
    "//ye": {
      "get": {
        "operationId": "hey2",
        "responses": {
          "200": {
            "description": "Request fulfilled, document follows",
            "headers": {},
            "content": {
              "application/json": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "deprecated": false
      }
    }
  }
}

as we can see in the schema ☝️, both endpoints are // prefixed because Starlite is normalizing path (I think). I can agree that I want to use the Controller class for something it wasn't designed for, but it could be nice if the path could be set to "".

Potential fix

By allowing path to be all but not None it seem to fix my issue, but I'm not quite sure what it means for the rest of the application.

class Endpoints(Controller):
    """TileMatrixSets endpoints."""

    path = ""

    def __init__(self, owner: "Router"):
        if not hasattr(self, "path") or self.path is None:
            raise ImproperlyConfiguredException("Controller subclasses must set a path attribute")

        for key in ["dependencies", "response_headers", "response_class", "guards"]:
            if not hasattr(self, key):
                setattr(self, key, None)

        self.path = self.path
        self.owner = owner
        for route_handler in self.get_route_handlers():
            route_handler.owner = self

    @get(path="/yo")
    def hey(self, request: Request) -> str:
        print(request)
        return "yipi"

    @get(path="/ye")
    def hey2(self, request: Request) -> str:
        print(request)
        return "what"

again, I will understand that I want to use the Controller class outside of its scope πŸ™

Detect where `__call__` is `async def` in context of `iscoroutinefunction()`

My use-case is basically a guard that I create with some state:

@put(guards=[CheckPayloadMismatch("id", "user_id").__call__])

Without passing through the __call__() method, starlite tries to call the instance sync.

As of 0.20.1, starlette themselves include this utility:

import asyncio
import functools
import typing


def is_async_callable(obj: typing.Any) -> bool:
    while isinstance(obj, functools.partial):
        obj = obj.func

    return asyncio.iscoroutinefunction(obj) or (
        callable(obj) and asyncio.iscoroutinefunction(obj.__call__)
    )

And a handful of test cases: https://github.com/encode/starlette/blob/master/tests/test__utils.py

Based on a quick search, there are 6 separate usages of iscoroutinefunction(): https://github.com/starlite-api/starlite/search?q=iscoroutinefunction. The most complex pattern of use is:

https://github.com/starlite-api/starlite/blob/f9893f28bc210e032e0527a61a6b695fc9e5c801/starlite/handlers/asgi.py#L43

Which would collapse down to a single call to is_async_callable() if we adopt such a function.

I'm happy to do the work if you give it the green light.

[documentation] how to set cookies.

Currently, it is not mention in docs on how to set cookies although it is possible by leveraging after_request hook.
example:

async def set_auth_status(response: Response) -> Response:
    response.set_cookie("X-AUTH-STATUS", "1")
    return response

header Parameter throws pydantic error in 0.7.1

These docs:
https://starlite-api.github.io/starlite/usage/3-parameters/#header-and-cookie-parameters

have the example for using a header parameter as:

@get(path="/users/{user_id:uuid}/")
async def get_user(
    user_id: UUID4,
    token: Parameter(header="X-API-KEY"),
) -> User:
    ...

when I try the (slightly modified) resource (since I don't have User in my app):

@get(path="/users/{user_id:uuid}/")
async def get_user(
    user_id: UUID4,
    token: Parameter(header="X-API-KEY"),
) -> Request:
    ...

I get the error

RuntimeError: error checking inheritance of FieldInfo(default=PydanticUndefined, alias='', extra={'header': 'X-API-KEY', 'cookie': None, 'query': None, 'required': True, 'examples': None, 'external_docs': None, 'content_encoding': None}) (type: FieldInfo)

This is on python v3.9.7 and pydantic 1.9.0 (which I'm not installing explicitly, and I think got installed as a dependency of starlite)

Minimal example doesn't run because of incorrect import

In the minimal example: https://starlite-api.github.io/starlite/#minimal-example

The Controller (my_app/controllers/user.py) imports the model with

from my_app.models import User

This fails to startup with:

ImportError: cannot import name 'User' from 'my_app.models' (unknown location)

because the User class is defined in:

my_app/models/user.py

This would work if there were a init.py file in my_app/models that imports all the model classes (as there is for the backend-starlite-postgres), but this isn't in the minimal example.

To fix, the import should be:

from my_app.models.user import User

It would also be nice if this code were in a standalone template repo to clone and use, instead of cutting and pasting it out of the docs.

Add redoc section to top level readme or documentation page

When migrating from FastAPI or any other application framework where schemas are a big deal, it is very important to developers to know where to go to access the automatic schema (in this cases, ReDoc). While the README.md and documentation page specifically mentions that StarLite uses redoc, the URL it is hosted under is only mentioned as a side note in the openapi section for how to move it to a new URL.

Ideally, the example code in the README.md should be immediately followed up with a url of the ReDoc to see your new applications schema in live action. Maybe also mentioned in the usage or overview section of the documentation page.

Requests module not found

File ".\main.py", line 1, in <module>
  from starlite import Starlite, get
File ".\venv\lib\site-packages\starlite\__init__.py", line 24, in <module>
  from .testing import TestClient, create_test_client, create_test_request
File ".\venv\lib\site-packages\starlite\testing.py", line 7, in <module>
  from requests.models import RequestEncodingMixin
ModuleNotFoundError: No module named 'requests'

I'm getting this error when I just do pip install starlite and then run the app using uvicorn.

The code I'm trying out is the one from the documentation,

from starlite import Starlite, get


@get("/")
def health_check() -> str:
    return "healthy"


app = Starlite(route_handlers=[health_check])

Enhancement: Use asgiref for typing

Starlette's ASGI types use MutableMapping[str, Any] which is not too different from just typing them dict. This package is fully spec compliant and would help in implementing additional ASGI functionality, as it would allow us to see exactly which keys are available/required to support a feature. There is currently a PR for Starlette that addresses this but it doesn't seem like it would be merged soon.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.