diff --git a/docs/async_worker.md b/docs/async_worker.md index 621968d7..a40ffff2 100644 --- a/docs/async_worker.md +++ b/docs/async_worker.md @@ -8,7 +8,7 @@ You can leverage the async worker without needing to know anything specific about the worker implementation. -The generic [Service](reference/starlite_saqlalchemy/service/#starlite_saqlalchemy.service.Service) +The generic [Service](../reference/starlite_saqlalchemy/service/#starlite_saqlalchemy.service.Service) object includes a method that allows you to enqueue a background task. ### Example diff --git a/docs/config.md b/docs/config.md new file mode 100644 index 00000000..b8c53336 --- /dev/null +++ b/docs/config.md @@ -0,0 +1,7 @@ +# Configuring the application + +Configuration is via environment. Here's an example `.env`: + +```dotenv title="Example .env" +--8<-- ".env.example" +``` diff --git a/docs/dto.md b/docs/dto.md new file mode 100644 index 00000000..e268e3a9 --- /dev/null +++ b/docs/dto.md @@ -0,0 +1,117 @@ +# DTOs + +- pydantic models generated from SQLAlchemy declarative models. +- DTOs have a purpose, read or write. +- Model attributes can have a mode, read-only or private. + +## What are DTOs? + +DTO stands for "Data Transfer Object". They are the filter through which data is accepted into, and +output from the application. + +## DTO Factory + +`starlite-saqlalchemy` includes +[`dto.factory()`](../reference/starlite_saqlalchemy/dto/#starlite_saqlalchemy.dto.factory) +which automatically creates [pydantic](https://pydantic-docs.helpmanual.io/) models from +[SQLAlchemy 2.0 ORM](https://docs.sqlalchemy.org/en/20/orm/) models. + +### Creating a SQLAlchemy ORM model + +If you are new to SQLAlchemy, I cannot recommend their docs enough. Start at +[the beginning](https://docs.sqlalchemy.org/en/20/orm/quickstart.html), and follow along until you +are comfortable. I won't even try to compete with the quality and depth of information that can be +found there - a credit to everyone who has contributed to that project over the years. + +### Configuring generated DTOs + +#### DTO Purpose + +The `dto.Purpose` enum tells the factory if the purpose of the DTO is parse data submitted by the +client for updating or "writing" to a resource, or if it is to serialize data to be transmitted back +to, or "read" by the client. + +For example, the DTO objects generated by the factory may differ due to the intended purpose of the +DTO: + +```python +from starlite_saqlalchemy import dto + +from domain.users import User + + +ReadDTO = dto.factory("UserReadDTO", model=User, purpose=dto.Purpose.READ) +WriteDTO = dto.factory("UserWriteDTO", model=User, purpose=dto.Purpose.WRITE) +``` + +#### DTO Mode + +We use the `info` parameter to `mapped_column()` to guide `dto.factory()`. + +The [dto.Mode](../reference/starlite_saqlalchemy/dto/#Mode) enumeration is used to indicate on the +SQLAlchemy ORM model, whether properties should always be private, or read-only. + +Take this model, for example: + +```python +from datetime import datetime + +from sqlalchemy.orm import mapped_column +from starlite_saqlalchemy import dto, orm + + +class User(orm.Base): + name: str + password_hash: str = mapped_column(info={"dto": dto.Mode.PRIVATE}) + updated_at: datetime = mapped_column(info={"dto": dto.Mode.READ_ONLY}) + + +ReadDTO = dto.factory("UserReadDTO", model=User, purpose=dto.Purpose.READ) +WriteDTO = dto.factory("UserWriteDTO", model=User, purpose=dto.Purpose.WRITE) +``` + +Both `ReadDTO` and `WriteDTO` are pydantic models that have a `name` attribute. + +Neither `ReadDTO` or `WriteDTO` have a `password_hash` attribute - this is the side effect of +marking the column with `dto.Mode.PRIVATE`. Columns that are marked private will never be included +in any generated DTO model, meaning that in the context of the application, they are unable to be +read or modified by the client. + +`ReadDTO` has an `updated_at` field, while `WriteDTO` does not. This is the side effect of marking +the column with `dto.Mode.READ_ONLY` - these fields will only be included in DTOs generated for +`dto.Purpose.READ` and make sense for fields that have internally generated values. + +The following class is pretty much the same as +[`orm.Base`](../reference/starlite_saqlalchemy/orm/#starlite_saqlalchemy.orm.Base) - the bundled +SQLAlchemy base class that comes with `starlite-saqlalchemy`. + +```python +from datetime import datetime +from uuid import UUID, uuid4 + +from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column + +from starlite_saqlalchemy import dto + + +class Base(DeclarativeBase): + id: Mapped[UUID] = mapped_column( + default=uuid4, primary_key=True, info={"dto": dto.Mode.READ_ONLY} + ) + """Primary key column.""" + created: Mapped[datetime] = mapped_column( + default=datetime.now, info={"dto": dto.Mode.READ_ONLY} + ) + """Date/time of instance creation.""" + updated: Mapped[datetime] = mapped_column( + default=datetime.now, info={"dto": dto.Mode.READ_ONLY} + ) +``` + +Notice that all these fields are marked as `dto.Mode.READ_ONLY`. This means that they are unable to +be modified by clients, even if they include values for them in the payloads to `POST`/`PUT`/`PATCH` +routes. + +You can inherit from `orm.Base` to create your SQLAlchemy models, but you don't have to. You can +choose to subclass `orm.Base` or roll your own base class altogether. `dto.factory()` will still +work as advertised. diff --git a/docs/index.md b/docs/index.md index 0154ec1c..9212361e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,10 +1,27 @@ # starlite-saqlalchemy -Starlite, SQLAlchemy 2.0 and SAQ configuration plugin. +An API application pattern standing on the shoulders of: + +- [Starlite](https://starlite-api.github.io/starlite/): "...a light, opinionated and flexible ASGI + API framework built on top of pydantic". +- [SQLAlchemy 2.0](https://docs.sqlalchemy.org/en/20/): "The Python SQL Toolkit and Object + Relational Mapper". +- [SAQ](https://github.com/tobymao/saq): "...a simple and performant job queueing framework built on + top of asyncio and redis". +- [Structlog](https://www.structlog.org/en/stable/): "...makes logging in Python faster, less + painful, and more powerful". + +## Usage Example + +```py title="Simple Example" +--8<-- "examples/basic_example.py" +``` + +Check out the [Usage](config/) section to see everything that is enabled by the framework! ## Pattern -This is the pattern that this application encourages. +This is the pattern encouraged by this framework: ``` mermaid sequenceDiagram @@ -28,14 +45,27 @@ sequenceDiagram Depending on architecture, this may not be the same instance of the application that handled the request. -## Usage Example +## Motivation -```py title="Simple Example" ---8<-- "examples/basic_example.py" -``` +A modern, production-ready API application has a lot of components. Starlite, the backbone of this +library, exposes a plethora of features and functionality that requires some amount of boilerplate +and configuration that must be carried from one application implementation to the next. -Configuration via environment. +`starlite-saqlalchemy` is an example of how Starlite's `on_app_init` hook can be utilized to build +application configuration libraries that support streamlining the application development process. -```dotenv title="Example .env" ---8<-- ".env.example" -``` +However, this library intends to be not only an example, but also an opinionated resource to support +the efficient, and consistent rollout of production ready API applications built on top of Starlite. + +Use this library if the stack and design decisions suit your taste. If there are improvements or +generalizations that could be made to the library to support your use case, we'd love to hear about +them. Open [an issue](https://github.com/topsport-com-au/starlite-saqlalchemy/issues) or start +[a discussion](https://github.com/topsport-com-au/starlite-saqlalchemy/discussions). + +## Backward compatibility and releases + +This project follows semantic versioning, and we use +[semantic releases](https://python-semantic-release.readthedocs.io/en/latest/) in our toolchain. +This means that bug fixes and new features will find there way into a release as soon they hit the +main branch, and if we break something, we'll bump the major version number. However, until we hit +v1.0, there will be breaking changes between minor versions, but v1.0 is close! diff --git a/docs/logging.md b/docs/logging.md index 17b16461..b5b8aff6 100644 --- a/docs/logging.md +++ b/docs/logging.md @@ -107,7 +107,7 @@ as [PII](https://en.wikipedia.org/wiki/Personal_data) and secrets. Thankfully, we have mechanisms to ensure that this type of data is excluded from our logs! Our -[LogSettings](/reference/starlite_saqlalchemy/settings/#starlite_saqlalchemy.settings.LogSettings) +[LogSettings](../reference/starlite_saqlalchemy/settings/#starlite_saqlalchemy.settings.LogSettings) object provides a host of options that allow you to customize log output. This exposes the following environment variables: @@ -218,7 +218,8 @@ contextvars for the job. If logging configuration is enabled, we use this SAQ `Worker` hook to extract the configured `Job` attributes and inject them into the log, and emit the log event. The attributes that are logged for each `Job` can be configured in -[`LogSettings`](/reference/starlite_saqlalchemy/settings/#starlite_saqlalchemy.settings.LogSettings.JOB_FIELDS). +[`LogSettings`](../reference/starlite_saqlalchemy/settings/#starlite_saqlalchemy.settings. +LogSettings.JOB_FIELDS). If the `Job.error` attribute is truthy, we log at `ERROR` severity, otherwise log at `INFO`. diff --git a/mkdocs.yml b/mkdocs.yml index 8b12672b..a501f7cf 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -2,8 +2,11 @@ site_name: starlite-saqlalchemy repo_url: https://github.com/topsport-com-au/starlite-saqlalchemy nav: - index.md - - Async Worker: async_worker.md - - Logging: logging.md + - Usage: + - Configuration: config.md + - DTOs: dto.md + - Async Worker: async_worker.md + - Logging: logging.md - Reference: reference/ watch: - src/starlite_saqlalchemy