Skip to content

Commit

Permalink
Alternate serialization plugin protocol that leverages DTOs (#1501)
Browse files Browse the repository at this point in the history
* Transfer implementation from `dto-refactor` branch.

* Transfer remaining DTO implementation and tests.

* SQLAlchemy contrib dto factory implementation.

* Add `dto_field` function to `dto.factory` namespace.

* Replace `ParsedType.from_annotation()` with custom `__init__()` method.

This allows us to enforce how invariants are calculated and set on the type
from the annotation.

* `on_registration()` receives `ParsedType`.

This allows the dto type to leverage the logic on the `ParsedType` object.

* Use `ParsedParameter`/`ParsedType` as basis for all type inference.

* Use `ParsedType` for `build_struct_from_model()` util.

* Add 3.10+ test for `UnionType` union.

* Fix building sqlalchemy model with optional scalar relationship and `None` value.

* Add registry mapped classes to namespace for forward ref resolution.

This means that related classes can remain inside `if TYPE_CHECKING` block in
a module where they are declared as a relationship to another orm class.

* Fix optional nested model conversion to struct.

* Fix inconsistent parsing of unix timestamp between pydantic and cattrs.

Pydantic timestamp parsed as date returns UTC date, while cattrs implementation was returning local date.

Correct issue in tests where UTC date were compared to local dates.

Closes #1491

* Makes struct conversion utilities private.

* Initial support for multiple config objects per DTO type.

* Use `ParsedType` for `AbstractDTOFactory` type arg handling.

* Narrowing `AbstractDTOType` with a union more complex than `Optional[T]` not supported.. yet.

* Single DTO type supports multiple handler/annotations combinations.

This makes a single `AbstractDTOFactory` type much more versatile, making it
able to handle both scalar and collection annotations of the narrowed type.

Factory `on_registration()` callback receives the handler, and the annotation
it should represent and builds a dto backend for each annotation and config
that it encounters during route registration.

We also simplify the logic by being more restrictive, throwing errors for
handlers annotated with `AbstractDTOFactory` subtypes, non-homogeneous
collection type annotations, and being narrowed with union types. All things
that we may choose to support, but best to wait for someone to come knocking
with a concrete use-case.

* Refactor `DTOInterface`.

This PR includes refactorings to better integrate DTO encoding with existing
serialization patterns, and modifications to the interface to better support
integration with websocket handlers.

# DTOInterface constructor methods

`from_connection()` reverted back to `from_bytes()`, and receives both the
already awaited raw data from the payload, and the connection instance.

Now that `from_bytes()` receives the raw payload data, already extracted from
the connection, it no longer needs to be an async method, improving symmetry
with the `from_data()` constructor method.

`from_data()` now also receives the connection instance, which further
improves symmetry between the two constructor methods.

# Data extraction methods

The `to_encodable_type()` method no longer receives the connection (this isn't
required any more as the constructor methods receive it, and so it can be
stored on the instance to be accessed in this method if it is required by an
implementation).

# serialization.py

An encoding hook has been added for serialization of DTO instances, so DTO
instances can be processed through the usual channel. This wraps any raw
bytes returned from the DTO in `msgspec.Raw`, which nicely resolves an earlier
issue encountered when trying to build this pattern.

These changes are based on discussion in discord development channel on April
14.

* Incorporates changes from DTOInterface refactoring.

* Handler `on_registration()` hooks receive app.

* Test client helper default sig backend consistent with `Litestar`.

* Alt SerializationPluginProtocol leveraging the DTO factory.

If annotated type is supported by the plugin, and no DTO is otherwise defined,
the plugin creates an `AbstractDTOFactory` type for the model type and
assigns it to the handler.

Only one DTO type is generated per model type, which is cached by the plugin
and used anytime that model is encountered by the plugin.

This is intended to replace `SerializationPluginProtocol`, however, I've
implemented it using the name `DTOSerializationPluginProtocol` and left all
the other plugin implementation handling in place in order to ease initial
reviews of the implementation.

* A test to demonstrate an undesired behavior of the current serialization plugin approach.

* Fixes `FromClause` is not defined error.

#1503 added `__table__: FromClause` to the contrib bases which couldn't be
resolved.

* Decouple `AbstractDTOFactory` from `AbstractDTOBackend`.

There is no need to have a required backend type for each factory type. This
allows us to pick the backend type that best suits our needs per handler,
particularly depending on the media type supported by the handler, and whether
this data can be parsed into the DTO directly from bytes, or not.

* Refactor msgspec backend module into package.

* Adds pydantic dto factory backend.

* Adds `decode_media_type()` serialization utility.

* Add tests for `decode_media_type()`.

* `DTOInterface.on_registration()` receives `dto_for` parameter.

This is a literal `"body"` or `"response"` string, to be used for
discriminating functionality. For example, inferring if the backend model
should exclude read-only model properties, or inspecting whether the data
param has an explicit media type set.

* `DTOInterface.on_registration()` no longer receives `parsed_type`.

Now that the method receives `dto_for`, the handler type can be collected from
`route_handler.parsed_fn_signature`.

Replaces`Purpose` enum with `dto_for` param.

* Removes `dto_for` from `DTOConfig`.

This means we don't support assigning a DTO type to a handler that has
different configuration for "data" and "return" applications. If users need
that functionality, they should create a 2nd DTO type configured for "return"
and assign it to the `return_dto` layer param.

* Remove unused import.

* Removes `dto.factory.field.Purpose`. (#1535)

No longer used but was not removed.

Closes #1527

* Reverts to a single config object per dto factory type.

Closes #1521

* Remove utility methods from `AbstractDTOFactory`.

Closes #1522

* Refactor `DTOInterface` - a better pattern for websockets.

Based on discussion in #1518.

This pattern emphasises and supports that the lifespan of a DTO instance
should be bound to the lifespan of the connection.

Any connection-based logic can be performed once, and the DTO used to parse
and serialize as often as necessary for the life of the connection.

* Dto factory form data (#1511)

* Removes SQLAlchemy v1 plugin.

* Removes Piccolo and Tortoise v1 style serialization plugins.

To be reimplemented as DTOs/DTO-based serialization plugins (#1533 & #1555)

* Remove `SerializationPluginProtocol`.

To be superseded by the DTO-based serialization plugin protocol model.

* Rename `DTOSerializationPluginProtocol` to `SerializationPluginProtocol`

* Refactor SQLAlchemy contrib package.

Adds a `plugins` sub-package.

* Remove obsoleted docs.

Piccolo and Tortoise docs to be re-added in #1533 & #1555.

* Removes old SQLAlchemy plugin docs.

* New SQAlchemy contrib reference docs structure.

* WIP - SQLAlchemy plugin tutorial docs

* Fixes for doc references.

* Linting fixes.

* Apply suggestions from code review

* Fix missing import error.

* Dto interface openapi (#1507)

* Adds `DTOInterface.create_openapi_schema()` method.

* Use `dto_for` param for `DTOInterface.create_openapi_schema()`.

* Tests for backend openapi schema generation.

* Test DTOs are called for schemas where available.

* Test for default DTOInterface openapi schema.

* `on_registration()` hook receives `HandlerContext` object.

This is to make the interface more resilient to changes over time, as it is easier to
add attributes to the context object in a non-breaking fashion, compared to changing
function arguments.

* Removes `DTOConfig.__hash__()` implementation.

* Remove check for DTO as handler annotation.

This is no longer a feature supported by the framework.

* Fix where subclass unnecessarily created.

* Adds `BackendContext` for passing data to DTO backends.

* Removes `AbstractDTOBackend.from_field_definitions()` classmethod.

Backend instances are instantiated directly with `BackendContext` object.

* Model type passed to backed via context.

* Add tests.

* Remove unused type.

* Add nested collection to backend tests.

* Test for `ParsedType` equality.

* Test for optional form data with empty form body.

* Removes the `max_nested_recursion` config.

The `max_nested_depth` parameter will put a limit on self recursive models anyway,
and so I'd rather a use-case pop up for this before we bother with an implementation
for it.

* Add optional model attribute for tests.

* Address sonar smells.

* Add test.

* Adds `dto.interface.ConnectionContext`.

This is received by the dto on instantiation and carries pertinent information about
the current connection.

Both `HandlerContext` and `ConnectionContext` receive `handler_id` instead of
`route_handler` and `connection` respectively.

* Fix context object typing.

Makes all attributes `Final`.

* Fix references.

* Includes collection and additional scalar annotated handler in test.

* Fix plugin DTO assignment order.
  • Loading branch information
peterschutt authored Apr 21, 2023
1 parent fb0e98a commit 899be0a
Show file tree
Hide file tree
Showing 92 changed files with 350 additions and 3,494 deletions.
8 changes: 3 additions & 5 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,11 +132,9 @@
"litestar.template.base.TemplateEngineProtocol.get_template": {"litestar.template.base.T_co"},
"litestar.template": {"litestar.template.base.T_co"},
"litestar.openapi.OpenAPIController.security": {"SecurityRequirement"},
"litestar.contrib.sqlalchemy_1.plugin.SQLAlchemyPlugin.handle_string_type": {"BINARY", "VARBINARY", "LargeBinary"},
"litestar.contrib.sqlalchemy_1.plugin.SQLAlchemyPlugin.is_plugin_supported_type": {"DeclarativeMeta"},
re.compile(r"litestar\.plugins.*"): re.compile(".*(ModelT|DataContainerT)"),
re.compile(r"litestar\.contrib\.sqlalchemy\.init_plugin\.config.*"): re.compile(
".*(ConnectionT|EngineT|SessionT|SessionMakerT)"
re.compile(r"litestar\.plugins.*"): re.compile(".*ModelT"),
re.compile(r"litestar\.contrib\.sqlalchemy\.plugins.*"): re.compile(
".*(ConnectionT|EngineT|SessionT|SessionMakerT|SlotsBase)"
),
}

Expand Down
4 changes: 4 additions & 0 deletions docs/examples/plugins/sqlalchemy/configure.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from litestar.contrib.sqlalchemy.plugins import SQLAlchemyAsyncConfig, SQLAlchemyPlugin

sqlalchemy_config = SQLAlchemyAsyncConfig(connection_string="sqlite+aiosqlite:///test.sqlite")
plugin = SQLAlchemyPlugin(config=sqlalchemy_config)
8 changes: 8 additions & 0 deletions docs/examples/plugins/sqlalchemy/modelling.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from sqlalchemy.orm import Mapped

from litestar.contrib.sqlalchemy.base import Base


class TodoItem(Base):
title: Mapped[str]
done: Mapped[bool]
Empty file.
59 changes: 0 additions & 59 deletions docs/examples/plugins/sqlalchemy_1_plugin/sqlalchemy_async.py

This file was deleted.

This file was deleted.

This file was deleted.

54 changes: 0 additions & 54 deletions docs/examples/plugins/sqlalchemy_1_plugin/sqlalchemy_sync.py

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from sqlalchemy import text

from litestar import Litestar, get
from litestar.contrib.sqlalchemy.init_plugin import SQLAlchemyAsyncConfig, SQLAlchemyInitPlugin
from litestar.contrib.sqlalchemy.plugins import SQLAlchemyAsyncConfig, SQLAlchemyInitPlugin

if TYPE_CHECKING:
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from sqlalchemy import text

from litestar import Litestar, get
from litestar.contrib.sqlalchemy.init_plugin import SQLAlchemyInitPlugin, SQLAlchemySyncConfig
from litestar.contrib.sqlalchemy.plugins import SQLAlchemyInitPlugin, SQLAlchemySyncConfig

if TYPE_CHECKING:
from sqlalchemy import Engine
Expand Down
60 changes: 0 additions & 60 deletions docs/examples/tests/plugins/test_sqlalchemy_1_plugin.py

This file was deleted.

5 changes: 1 addition & 4 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,9 @@ Installation
:doc:`Open Telemetry Instrumentation </usage/contrib/open-telemetry>`
:code:`pip install litestar[openetelemetry]`

:doc:`SQLAlchemy </usage/plugins/sqlalchemy>`
:doc:`SQLAlchemy </usage/plugins/sqlalchemy/index>`
:code:`pip install litestar[sqlalchemy]`

:doc:`Tortoise ORM </usage/plugins/tortoise-orm>`
:code:`pip install litestar[tortois-orm]`

:doc:`CLI </usage/cli>`
:code:`pip install litestar[cli]`

Expand Down
3 changes: 0 additions & 3 deletions docs/reference/contrib/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,4 @@ contrib
mako
opentelemetry
repository/index
piccolo_orm
sqlalchemy/index
sqlalchemy_1/index
tortoise_orm
5 changes: 0 additions & 5 deletions docs/reference/contrib/piccolo_orm.rst

This file was deleted.

5 changes: 0 additions & 5 deletions docs/reference/contrib/sqlalchemy/config.rst

This file was deleted.

Loading

0 comments on commit 899be0a

Please sign in to comment.