Giter VIP home page Giter VIP logo

litestar-org / litestar Goto Github PK

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

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

Home Page: https://litestar.dev/

License: MIT License

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

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

📖

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

litestar's Issues

[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.

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

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.

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.

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)

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?

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

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

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

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 🤷

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.

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.

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.

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)

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.

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)

[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.

[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.

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.

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])

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}

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

[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.

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] 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.

`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.

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 🙏

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?

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.

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.

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

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.

[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

[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.

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.

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)

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.

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.

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.

[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

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.

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.