Type Annotations#
Restly is typed for Pyright, VS Code with Pylance, and mypy. Its views and its reference types are generic classes, and they take type parameters in two places:
Reference fields name the model they point to, as in
fr.MustExist[int, User],fr.IDRef[User]andfr.IDSchema[User]. Restly also reads this parameter at runtime, to find the row. A type checker sees the type of the value that your code gets.Views take optional type parameters, as in
fr.AsyncRestView[User, UserSchema, UserCreate, UserUpdate, int]. They change nothing at runtime. They give exact types to the methods that you override and call.
A view without type parameters works, and it is the best place to start. Add parameters where you want your editor to know the model and the schemas.
Restly also rewrites the signatures of the endpoint methods when it registers a view. Type checkers do not see that step; see What a type checker cannot see.
Start without type parameters#
For normal CRUD, a bare view class is enough:
import fastapi_restly as fr
from fastapi import FastAPI
from sqlalchemy.orm import DeclarativeBase, Mapped
app = FastAPI()
class Base(fr.ModelDefaults, DeclarativeBase):
pass
class User(fr.IDMixin, Base):
name: Mapped[str]
email: Mapped[str]
@fr.include_view(app)
class UserView(fr.AsyncRestView):
prefix = "/users"
model = User
Type the schema and its reference fields#
The view’s schema usually subclasses
fr.IDSchema; see
Choose a base class. You can also give the base class
the model, as in fr.IDSchema[User]:
class UserSchema(fr.IDSchema[User]):
name: str
email: str
This form records the model for type checkers. It has no effect at runtime,
and the bare fr.IDSchema works just as well.
A reference field has the model as a type parameter. The type checker then
knows what your code receives in data.<field>, for example in a business
method:
Field |
What the type checker sees |
|---|---|
|
|
|
|
|
|
|
a list of |
fr.MustExist takes the type of
the primary key first, then the model: fr.MustExist[UUID, Account] for a
UUID key. When to use which form, and what each one does at runtime, is on
Relationships.
Type the methods that you override#
AsyncRestView and
RestView take five type parameters,
in this order:
Parameter |
What it types |
Default |
|---|---|---|
the model |
the objects that the methods load, build and return |
|
the response type |
the return value of |
|
the create body |
|
|
the update body |
|
|
the id type |
|
|
Each parameter has a default, so you can stop after the ones you need. The
starter project names the model and the schema
(app/users/views.py):
class UserView(fr.AsyncRestView[User, UserSchema]):
prefix = "/users"
model = User
schema = UserSchema
The parameters help most where you override
a view method, usually a business method:
create, update or delete. Here is a view with all five parameters and
typed overrides:
from sqlalchemy import ColumnElement
class UserSchema(fr.IDSchema[User]):
name: str
email: str
class UserCreate(fr.BaseSchema):
name: str
email: str
class UserUpdate(fr.BaseSchema):
name: str
email: str
@fr.include_view(app)
class UserView(
fr.AsyncRestView[User, UserSchema, UserCreate, UserUpdate, int]
):
prefix = "/users"
model = User
schema = UserSchema
schema_create = UserCreate
schema_update = UserUpdate
def where_id(self, id: int) -> ColumnElement[bool]:
return User.id == id
async def create(self, data: UserCreate) -> User:
return await super().create(data)
async def update(self, obj: User, data: UserUpdate) -> User:
return await super().update(obj, data)
With these parameters, data in create is a UserCreate, obj in
update is a User, and the type checker checks the return types too. Note
these points:
createtakes the create body and returns the model.updatetakes the loadedobjand the update body. The handlerhandle_updateloads the row, and returns 404 if it does not exist, beforeupdateruns.where_idgets the id from the path, so its type is the id type, the last parameter. It returns a SQL condition, aColumnElement[bool]; see Look a row up by another key.get_oneandget_manyare final, so you call them and do not override them. A type checker reports an override, as it does for a handler. Their return types follow the model parameter:await self.get_one(id)is aUser, and so is each row ofself.select().For the response type, give the view’s schema: the response class that Restly builds is a subclass of it. A view that sets its own
schema_responsegives that class instead, as infr.AsyncRestView[User, UserResponse].
Details
A project base view can stay generic in the model, so each view that
subclasses it names its own model. The SaaS example does this
(app/views.py):
from typing import TypeVar
ModelT = TypeVar("ModelT")
class AuthenticatedView(fr.AsyncRestView[ModelT]):
...
class ProjectView(AuthenticatedView[Project]):
prefix = "/projects"
model = Project
Type a custom route#
A custom route needs no view type parameters. Normal Python annotations are enough:
@fr.include_view(app)
class UserView(fr.AsyncRestView):
prefix = "/users"
model = User
@fr.get("/health")
async def health(self) -> dict[str, str]:
return {"status": "ok"}
When a custom route calls the view’s methods, the parameters type those
calls too: get_one(id)
returns the model, and
handle_update(id, data)
takes the update body.
The classes that
derive_schema_response()
returns cannot be used in an annotation. In a route, use the markers of
fastapi_restly.views instead, such as ViewResponse: a type checker reads
each marker as Any. See
Name the view’s classes in a custom route.
What a type checker cannot see#
Type checkers work well on the public API: models, schemas, reference types, class attributes, and the view methods such as
get_one,createandhandle_update.They help less with the endpoint methods (
*_endpoint), whose signatures Restly fills in at registration.mypy gives a setting that a base view sets without an annotation the type of its value. So after
id_type = UUIDon a base view,id_type = intin a subclass is an error. This holds for every setting, such astagsandinclude_filters. Annotate the setting on the base view with the type that Restly declares for it, such asid_type: ClassVar[type[Any] | None] = UUID. The View Reference lists these types. Pyright needs no annotation.
Check your project#
To check your use of Restly, run Pyright or mypy on your project. Restly
keeps a set of small example apps in
tests/typing/.
CI checks them with Pyright and with mypy, both in strict mode. They show each typed form on this page, and they are a good
place to look when your type checker complains.
Internals#
Restly does some work at runtime when it registers a view. It copies the inherited endpoint methods, and it changes their signatures for FastAPI; see view registration. Static type checkers do not see this.
See also#
View Reference: the methods that the type parameters type, sorted by kind.
Request Lifecycle & Hooks: which method to override for each change.
Relationships: what
fr.MustExist,fr.IDRefandfr.IDSchema[Model]do at runtime.Schemas & Validation: the schema base classes and the response class.