litestar-org / litestar Goto Github PK
View Code? Open in Web Editor NEWProduction-ready, Light, Flexible and Extensible ASGI API framework | Effortlessly Build Performant APIs
Home Page: https://litestar.dev/
License: MIT License
Production-ready, Light, Flexible and Extensible ASGI API framework | Effortlessly Build Performant APIs
Home Page: https://litestar.dev/
License: MIT License
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 ""
.
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 π
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.
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")
SomeMiddleware2
called before SomeMiddleware
. Normally we expect that middlewares will be called in order they were passed.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
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.
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:
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")
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.
We need to add all contributors. Its a CLI tool or github bot that automatically updates the readme with all the contributors and their various contributions.
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])
And how to access the interactive openapi documentation fastAPI is used in /docs
Can you tell me what to do?
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.
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.
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])
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.
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
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.
________________________________________________________ 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.
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.
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.
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:
I sometimes want to use optional path
parameter. For this I need to be able to set more than one path per route:
@get(path="/image")
@get(path="/image.{format:str}")
def get_image(format: str = "jpeg"):
...
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:
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)
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.
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}
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.
The documentation is an ongoing project - we would like to ask the community to contribute. PRs including any of the following will be welcome:
You are also welcome to discuss in this thread your opinion regarding required additions or improvements to the documentation.
My controller:
class BrandController(Controller):
path = '/brand'
@post()
async def create(self, data: BrandNew, edb: AsyncIOClient) -> str:
await edb.query("INSERT Brand { name := <str>$name }", name=data.name)
return 'ok'
On preflight request it returns access-control-allow-methods header equal to "DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT" but only "OPTIONS, POST" should be there.
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.
Within a controller, I don't think you can find a url
for another handler using starlette url_path_for.
A working example of it can be found over https://github.com/developmentseed/pg_mvt/blob/10fa599928fc96ea30fad63289e40e39e564845d/pg_mvt/factory.py#L66-L96
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)
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.
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.
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.
A plugin is a class the fulfills the starlite PluginProtocol interface. You can see the source code for this here.
It would be nice to have a plugin for the Elastic Search DSL library --> https://elasticsearch-dsl.readthedocs.io/en/latest/
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
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:
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.
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:
Haven't had time to dig in to find source of the issue yet, but should do later tonight.
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.
testing
extra (pip install starlight[testing]
)starlite.testing
from starlite
packagefrom 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.
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.
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.
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
Hi, Guys, I want to know, how to implement socketio with this.
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)
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?
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)
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.
We should update redoc to the latest version.
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 π€·
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)}
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.
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
A declarative, efficient, and flexible JavaScript library for building user interfaces.
π Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. πππ
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google β€οΈ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.