diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml
new file mode 100644
index 0000000..cc44f46
--- /dev/null
+++ b/.github/workflows/doc.yml
@@ -0,0 +1,45 @@
+name: Build & Publish Doc
+
+on:
+ push:
+ branches:
+ - main
+ - docs/**
+
+jobs:
+
+ Build-Doc:
+ runs-on: ubuntu-latest
+ concurrency: release
+ permissions:
+ contents: write
+ id-token: write
+ steps:
+ - name: 📥 checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ - name: 🔧 setup uv
+ uses: ./.github/uv
+ - name: ⚙️ install deps
+ run: uv sync --extra docs
+ - name: 📙 mkdocs build
+ run: uv run mkdocs build
+ - name: 📦 Upload artifacts
+ uses: actions/upload-pages-artifact@v3
+ with:
+ path: site
+
+ Publish-Doc:
+ needs: Build-Doc
+ runs-on: ubuntu-latest
+ permissions:
+ pages: write
+ id-token: write
+ steps:
+ - name: Deploy to GitHub Pages
+ id: deployment
+ uses: actions/deploy-pages@v4
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
\ No newline at end of file
diff --git a/README.md b/README.md
index 85c29bd..b2e920a 100644
--- a/README.md
+++ b/README.md
@@ -1,74 +1,119 @@
-# 🚀 FastSQLA
+# FastSQLA
+
+_Async SQLAlchemy 2 for FastAPI — boilerplate, pagination, and seamless session management._
[](https://pypi.org/project/FastSQLA/)
+[](https://github.com/hadrien/FastSQLA/actions?query=branch%3Amain+event%3Apush)
+[](https://codecov.io/gh/hadrien/FastSQLA)
[](https://conventionalcommits.org)
-[](https://codecov.io/gh/hadrien/fastsqla)
+[](https://github.com/hadrien/FastSQLA/blob/main/LICENSE)
+
+
+**Documentation**: [https://hadrien.github.io/FastSQLA/](https://hadrien.github.io/FastSQLA/)
+
+**Github Repo:** [https://github.com/hadrien/fastsqla](https://github.com/hadrien/fastsqla)
+
+-----------------------------------------------------------------------------------------
+
+`FastSQLA` is an [`SQLAlchemy 2`](https://docs.sqlalchemy.org/en/20/) extension for
+[`FastAPI`](https://fastapi.tiangolo.com/).
+It streamlines the configuration and asynchronous connection to relational databases by
+providing boilerplate and intuitive helpers. Additionally, it offers built-in
+customizable pagination and automatically manages the `SQLAlchemy` session lifecycle
+following [`SQLAlchemy`'s best practices](https://docs.sqlalchemy.org/en/20/orm/session_basics.html#when-do-i-construct-a-session-when-do-i-commit-it-and-when-do-i-close-it).
-`FastSQLA` is an [`SQLAlchemy`] extension for [`FastAPI`].
-It supports asynchronous `SQLAlchemy` sessions and includes built-in custimizable
-pagination.
## Features
-
- Automatic SQLAlchemy configuration at app startup.
+* Easy setup at app startup using
+ [`FastAPI` Lifespan](https://fastapi.tiangolo.com/advanced/events/#lifespan):
- Using [`FastAPI` Lifespan](https://fastapi.tiangolo.com/advanced/events/#lifespan):
-```python
-from fastapi import FastAPI
-from fastsqla import lifespan
+ ```python
+ from fastapi import FastAPI
+ from fastsqla import lifespan
-app = FastAPI(lifespan=lifespan)
-```
-
-
- Async SQLAlchemy session as a FastAPI dependency.
+ app = FastAPI(lifespan=lifespan)
+ ```
-```python
-...
-from fastsqla import Session
-from sqlalchemy import select
-...
+* `SQLAlchemy` async session dependency:
-@app.get("/heros")
-async def get_heros(session:Session):
- stmt = select(...)
- result = await session.execute(stmt)
+ ```python
+ ...
+ from fastsqla import Session
+ from sqlalchemy import select
...
-```
-
-
- Built-in pagination.
-```python
-...
-from fastsqla import Page, Paginate
-from sqlalchemy import select
-...
+ @app.get("/heros")
+ async def get_heros(session:Session):
+ stmt = select(...)
+ result = await session.execute(stmt)
+ ...
+ ```
-@app.get("/heros", response_model=Page[HeroModel])
-async def get_heros(paginate:Paginate):
- return paginate(select(Hero))
-```
-
-
- Allows pagination customization.
+* `SQLAlchemy` async session with an async context manager:
-```python
-...
-from fastapi import Page, new_pagination
-...
+ ```python
+ from fastsqla import open_session
-Paginate = new_pagination(min_page_size=5, max_page_size=500)
+ async def background_job():
+ async with open_session() as session:
+ stmt = select(...)
+ result = await session.execute(stmt)
+ ...
+ ```
-@app.get("/heros", response_model=Page[HeroModel])
-async def get_heros(paginate:Paginate):
- return paginate(select(Hero))
-```
-
+* Built-in pagination:
+
+ ```python
+ ...
+ from fastsqla import Page, Paginate
+ from sqlalchemy import select
+ ...
+
+ @app.get("/heros", response_model=Page[HeroModel])
+ async def get_heros(paginate:Paginate):
+ return await paginate(select(Hero))
+ ```
+
👇👇👇
+ ```json
+ // /heros?offset=10&limit=10
+ {
+ "data": [
+ {
+ "name": "The Flash",
+ "secret_identity": "Barry Allen",
+ "id": 11
+ },
+ {
+ "name": "Green Lantern",
+ "secret_identity": "Hal Jordan",
+ "id": 12
+ }
+ ],
+ "meta": {
+ "offset": 10,
+ "total_items": 12,
+ "total_pages": 2,
+ "page_number": 2
+ }
+ }
+ ```
+
+* Pagination customization:
+ ```python
+ ...
+ from fastapi import Page, new_pagination
+ ...
+
+ Paginate = new_pagination(min_page_size=5, max_page_size=500)
+
+ @app.get("/heros", response_model=Page[HeroModel])
+ async def get_heros(paginate:Paginate):
+ return paginate(select(Hero))
+ ```
+* Session lifecycle management: session is commited on request success or rollback on
+ failure.
-And more ...
-
## Installing
@@ -84,17 +129,21 @@ pip install fastsqla
## Quick Example
+### `example.py`
+
+Let's write some tiny app in `example.py`:
+
```python
# example.py
from http import HTTPStatus
from fastapi import FastAPI, HTTPException
+from fastsqla import Base, Item, Page, Paginate, Session, lifespan
from pydantic import BaseModel, ConfigDict
from sqlalchemy import select
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Mapped, mapped_column
-from fastsqla import Base, Item, Page, Paginate, Session, lifespan
app = FastAPI(lifespan=lifespan)
@@ -104,11 +153,13 @@ class Hero(Base):
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(unique=True)
secret_identity: Mapped[str]
+ age: Mapped[int]
class HeroBase(BaseModel):
name: str
secret_identity: str
+ age: int
class HeroModel(HeroBase):
@@ -117,12 +168,13 @@ class HeroModel(HeroBase):
@app.get("/heros", response_model=Page[HeroModel])
-async def list_users(paginate: Paginate):
- return await paginate(select(Hero))
+async def list_heros(paginate: Paginate):
+ stmt = select(Hero)
+ return await paginate(stmt)
@app.get("/heros/{hero_id}", response_model=Item[HeroModel])
-async def get_user(hero_id: int, session: Session):
+async def get_hero(hero_id: int, session: Session):
hero = await session.get(Hero, hero_id)
if hero is None:
raise HTTPException(HTTPStatus.NOT_FOUND, "Hero not found")
@@ -130,7 +182,7 @@ async def get_user(hero_id: int, session: Session):
@app.post("/heros", response_model=Item[HeroModel])
-async def create_user(new_hero: HeroBase, session: Session):
+async def create_hero(new_hero: HeroBase, session: Session):
hero = Hero(**new_hero.model_dump())
session.add(hero)
try:
@@ -140,55 +192,56 @@ async def create_user(new_hero: HeroBase, session: Session):
return {"data": hero}
```
-> [!NOTE]
-> Sqlite is used for the sake of the example.
-> FastSQLA is compatible with all async db drivers that SQLAlchemy is compatible with.
+### Database
+
+💡 This example uses an `SQLite` database for simplicity: `FastSQLA` is compatible with
+all asynchronous db drivers that `SQLAlchemy` is compatible with.
-
- Create an sqlite3 db:
+Let's create an `SQLite` database using `sqlite3` and insert 12 rows in the `hero` table:
```bash
sqlite3 db.sqlite <
-
-
- Install dependencies & run the app
+### Run the app
+Let's install required dependencies:
```bash
pip install uvicorn aiosqlite fastsqla
-sqlalchemy_url=sqlite+aiosqlite:///db.sqlite?check_same_thread=false uvicorn example:app
+```
+Let's run the app:
+```
+sqlalchemy_url=sqlite+aiosqlite:///db.sqlite?check_same_thread=false \
+ uvicorn example:app
```
-
-
-Execute `GET /heros?offset=10`:
+### Check the result
+Execute `GET /heros?offset=10&limit=10` using `curl`:
```bash
-curl -X 'GET' \
-'http://127.0.0.1:8000/heros?offset=10&limit=10' \
--H 'accept: application/json'
+curl -X 'GET' -H 'accept: application/json' 'http://127.0.0.1:8000/heros?offset=10&limit=10'
```
Returns:
```json
@@ -214,5 +267,11 @@ Returns:
}
```
-[`FastAPI`]: https://fastapi.tiangolo.com/
-[`SQLAlchemy`]: http://sqlalchemy.org/
+You can also check the generated openapi doc by opening your browser to
+[http://127.0.0.1:8000/docs](http://127.0.0.1:8000/docs).
+
+
+
+## License
+
+This project is licensed under the terms of the [MIT license](https://github.com/hadrien/FastSQLA/blob/main/LICENSE).
diff --git a/CHANGELOG.md b/docs/changelog.md
similarity index 99%
rename from CHANGELOG.md
rename to docs/changelog.md
index 82e1f68..55703c1 100644
--- a/CHANGELOG.md
+++ b/docs/changelog.md
@@ -1,4 +1,4 @@
-# CHANGELOG
+# Changelog
## v0.2.4 (2025-01-27)
diff --git a/docs/images/example-openapi-generated-doc.png b/docs/images/example-openapi-generated-doc.png
new file mode 100644
index 0000000..2067e77
Binary files /dev/null and b/docs/images/example-openapi-generated-doc.png differ
diff --git a/docs/images/favicon.png b/docs/images/favicon.png
new file mode 100644
index 0000000..4e3200c
Binary files /dev/null and b/docs/images/favicon.png differ
diff --git a/docs/index.md b/docs/index.md
new file mode 120000
index 0000000..32d46ee
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1 @@
+../README.md
\ No newline at end of file
diff --git a/docs/orm.md b/docs/orm.md
new file mode 100644
index 0000000..f4c9e4f
--- /dev/null
+++ b/docs/orm.md
@@ -0,0 +1,9 @@
+# Object-relational mapping
+
+## `fastsqla.Base`
+
+::: fastsqla.Base
+ options:
+ heading_level: false
+ show_source: false
+ show_bases: false
diff --git a/docs/pagination.md b/docs/pagination.md
new file mode 100644
index 0000000..38fcf45
--- /dev/null
+++ b/docs/pagination.md
@@ -0,0 +1,8 @@
+# Pagination
+
+## `fastsqla.Paginate[T]`
+
+::: fastsqla.Paginate
+ options:
+ heading_level: false
+ show_source: false
diff --git a/docs/session.md b/docs/session.md
new file mode 100644
index 0000000..de3b621
--- /dev/null
+++ b/docs/session.md
@@ -0,0 +1,39 @@
+# `SQLAlchemy` Session
+
+## Lifecycle
+
+[`SQLAlchemy` documentation](https://docs.sqlalchemy.org/en/20/orm/session_basics.html#when-do-i-construct-a-session-when-do-i-commit-it-and-when-do-i-close-it)
+recommends the following:
+
+* Keep the lifecycle of the session **separate and external** from functions and
+ objects that access and/or manipulate database data.
+* Make sure you have a clear notion of where transactions begin and end, and keep
+ transactions **short**, meaning, they end at the series of a sequence of operations,
+ instead of being held open indefinitely.
+
+`FastSQLA` automatically manages the session lifecycle:
+
+* If the request is successful, the session is committed.
+* If the request fails, the session is rolled back.
+* In all cases, at the end of the request, the session is closed and the associated
+ connection is returned to the connection pool.
+
+
+To learn more about `SQLAlchemy` sessions:
+
+* [Session Basics](https://docs.sqlalchemy.org/en/20/orm/session_basics.html#)
+
+
+## `fastsqla.Session` dependency
+
+::: fastsqla.Session
+ options:
+ heading_level: false
+ show_source: false
+
+## `fastsqla.open_session` context manager
+
+::: fastsqla.open_session
+ options:
+ heading_level: false
+ show_source: false
diff --git a/docs/setup.md b/docs/setup.md
new file mode 100644
index 0000000..0f0b2ec
--- /dev/null
+++ b/docs/setup.md
@@ -0,0 +1,55 @@
+# Setup
+
+## `fastsqla.lifespan`
+
+::: fastsqla.lifespan
+ options:
+ heading_level: false
+ show_source: false
+
+## Configuration
+
+Configuration is done exclusively via environment variables, adhering to the
+[**Twelve-Factor App methodology**](https://12factor.net/config).
+
+The only required key is **`SQLALCHEMY_URL`**, which defines the database URL. It
+specifies the database driver in the URL's scheme and allows embedding driver parameters
+in the query string. Example:
+
+ sqlite+aiosqlite:////tmp/test.db?check_same_thread=false
+
+All parameters of [`sqlalchemy.create_engine`][] can be configured by setting environment
+variables, with each parameter name prefixed by **`SQLALCHEMY_`**.
+
+!!! note
+
+ FastSQLA is **case-insensitive** when reading environment variables, so parameter
+ names prefixed with **`SQLALCHEMY_`** can be provided in any letter case.
+
+### Examples
+
+1. :simple-postgresql: PostgreSQL url using
+ [`asyncpg`][sqlalchemy.dialects.postgresql.asyncpg] driver with a
+ [`pool_recycle`][sqlalchemy.create_engine.params.pool_recycle] of 30 minutes:
+
+ ```bash
+ export SQLALCHEMY_URL=postgresql+asyncpg://postgres@localhost/postgres
+ export SQLALCHEMY_POOL_RECYCLE=1800
+ ```
+
+2. :simple-sqlite: SQLite db file using
+ [`aiosqlite`][sqlalchemy.dialects.sqlite.aiosqlite] driver with a
+ [`pool_size`][sqlalchemy.create_engine.params.pool_size] of 50:
+
+ ```bash
+ export sqlalchemy_url=sqlite+aiosqlite:///tmp/test.db?check_same_thread=false
+ export sqlalchemy_pool_size=10
+ ```
+
+3. :simple-mariadb: MariaDB url using [`aiomysql`][sqlalchemy.dialects.mysql.aiomysql]
+ driver with [`echo`][sqlalchemy.create_engine.params.echo] parameter set to `True`
+
+ ```bash
+ export sqlalchemy_url=mysql+aiomysql://bob:password!@db.example.com/app
+ export sqlalchemy_echo=true
+ ```
diff --git a/mkdocs.yml b/mkdocs.yml
new file mode 100644
index 0000000..ce8f493
--- /dev/null
+++ b/mkdocs.yml
@@ -0,0 +1,90 @@
+site_name: FastSQLA
+site_url: https://mydomain.org/FastSQLA
+repo_name: hadrien/FastSQLA
+repo_url: https://github.com/hadrien/fastsqla
+edit_uri: edit/main/docs/
+
+nav:
+- Get Started:
+ - Welcome to FastSQLA: index.md
+- Usage:
+ - Setup: setup.md
+ - Object-relational mapping: orm.md
+ - SQLAlchemy Session: session.md
+ - Pagination: pagination.md
+- Changelog: changelog.md
+
+theme:
+ favicon: images/favicon.png
+ icon:
+ logo: material/database
+ name: material
+ features:
+ - announce.dismiss
+ - content.code.annotate
+ - content.code.copy
+ - content.tabs.link
+ - navigation.instant
+ - navigation.instant.prefetch
+ - navigation.instant.progress
+ - navigation.path
+ - navigation.sections
+ - navigation.tabs
+ - navigation.top
+ - navigation.tracking
+ - search.suggest
+ - toc.follow
+ palette:
+ - media: "(prefers-color-scheme)"
+ toggle:
+ icon: material/brightness-auto
+ name: Switch to light mode
+
+ - media: "(prefers-color-scheme: light)"
+ scheme: default
+ toggle:
+ icon: material/weather-sunny
+ name: Switch to dark mode
+
+ - media: "(prefers-color-scheme: dark)"
+ scheme: slate
+ toggle:
+ icon: material/weather-night
+ name: Switch to system preference
+
+plugins:
+ - autorefs
+ - glightbox
+ - mkdocstrings:
+ enable_inventory: true
+ handlers:
+ python:
+ import:
+ - https://docs.python.org/3/objects.inv
+ - https://docs.sqlalchemy.org/en/20/objects.inv
+ - https://fastapi.tiangolo.com/objects.inv
+ - search
+
+markdown_extensions:
+ - abbr
+ - admonition
+ - attr_list
+ - md_in_html
+ - pymdownx.blocks.caption
+ - pymdownx.details
+ - pymdownx.emoji:
+ emoji_index: !!python/name:material.extensions.emoji.twemoji
+ emoji_generator: !!python/name:material.extensions.emoji.to_svg
+ - pymdownx.highlight:
+ anchor_linenums: true
+ line_spans: __span
+ pygments_lang_class: true
+ - pymdownx.inlinehilite
+ - pymdownx.snippets
+ - pymdownx.superfences
+
+ - toc:
+ permalink: true
+watch:
+ - docs
+ - src
diff --git a/pyproject.toml b/pyproject.toml
index 7584534..cb07363 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -42,6 +42,13 @@ Repository = "https://github.com/hadrien/fastsqla"
Issues = "https://github.com/hadrien/fastsqla/issues"
Changelog = "https://github.com/hadrien/fastsqla/releases"
+[project.optional-dependencies]
+docs = [
+ "mkdocs-glightbox>=0.4.0",
+ "mkdocs-material>=9.5.50",
+ "mkdocstrings[python]>=0.27.0",
+]
+
[tool.uv]
package = true
dev-dependencies = [
@@ -76,3 +83,6 @@ env = "GH_TOKEN"
[tool.semantic_release]
version_toml = ["pyproject.toml:project.version"]
+
+[tool.semantic_release.changelog.default_templates]
+changelog_file = "./docs/changelog.md"
diff --git a/src/fastsqla.py b/src/fastsqla.py
index e8e6269..636afd4 100644
--- a/src/fastsqla.py
+++ b/src/fastsqla.py
@@ -4,7 +4,7 @@
from contextlib import asynccontextmanager
from typing import Annotated, Generic, TypeVar, TypedDict
-from fastapi import Depends, Query
+from fastapi import Depends, FastAPI, Query
from pydantic import BaseModel, Field
from sqlalchemy import Result, Select, func, select
from sqlalchemy.ext.asyncio import (
@@ -36,6 +36,28 @@
class Base(DeclarativeBase, DeferredReflection):
+ """Inherit from `Base` to declare an `SQLAlchemy` model.
+
+ Example:
+ ```py
+ from fastsqla import Base
+ from sqlalchemy.orm import Mapped, mapped_column
+
+
+ class Hero(Base):
+ __tablename__ = "hero"
+ id: Mapped[int] = mapped_column(primary_key=True)
+ name: Mapped[str] = mapped_column(unique=True)
+ secret_identity: Mapped[str]
+ age: Mapped[int]
+ ```
+
+ To learn more on `SQLAlchemy` ORM & Declarative mapping:
+
+ * [ORM Quick Start](https://docs.sqlalchemy.org/en/20/orm/quickstart.html)
+ * [Declarative Mapping](https://docs.sqlalchemy.org/en/20/orm/mapping_styles.html#declarative-mapping)
+ """
+
__abstract__ = True
@@ -44,7 +66,57 @@ class State(TypedDict):
@asynccontextmanager
-async def lifespan(_) -> AsyncGenerator[State, None]:
+async def lifespan(app: FastAPI) -> AsyncGenerator[State, None]:
+ """Use `fastsqla.lifespan` to set up SQLAlchemy.
+
+ In an ASGI application, [lifespan events](https://asgi.readthedocs.io/en/latest/specs/lifespan.html)
+ are used to communicate startup & shutdown events.
+
+ The [`lifespan`](https://fastapi.tiangolo.com/advanced/events/#lifespan) parameter of
+ the `FastAPI` app can be assigned to a context manager, which is opened when the app
+ starts and closed when the app stops.
+
+ In order for `FastSQLA` to setup `SQLAlchemy` before the app is started, set
+ `lifespan` parameter to `fastsqla.lifespan`:
+
+ ```python
+ from fastapi import FastAPI
+ from fastsqla import lifespan
+
+
+ app = FastAPI(lifespan=lifespan)
+ ```
+
+ If multiple lifespan contexts are required, create an async context manager function
+ to handle them and set it as the app's lifespan:
+
+ ```python
+ from collections.abc import AsyncGenerator
+ from contextlib import asynccontextmanager
+
+ from fastapi import FastAPI
+ from fastsqla import lifespan as fastsqla_lifespan
+ from this_other_library import another_lifespan
+
+
+ @asynccontextmanager
+ async def lifespan(app:FastAPI) -> AsyncGenerator[dict, None]:
+ async with AsyncExitStack() as stack:
+ yield {
+ **stack.enter_async_context(lifespan(app)),
+ **stack.enter_async_context(another_lifespan(app)),
+ }
+
+
+ app = FastAPI(lifespan=lifespan)
+ ```
+
+ To learn more about lifespan protocol:
+
+ * [Lifespan Protocol](https://asgi.readthedocs.io/en/latest/specs/lifespan.html)
+ * [Use Lifespan State instead of `app.state`](https://github.com/Kludex/fastapi-tips?tab=readme-ov-file#6-use-lifespan-state-instead-of-appstate)
+ * [FastAPI lifespan documentation](https://fastapi.tiangolo.com/advanced/events/)
+ """
prefix = "sqlalchemy_"
sqla_config = {k.lower(): v for k, v in os.environ.items()}
try:
@@ -70,6 +142,25 @@ async def lifespan(_) -> AsyncGenerator[State, None]:
@asynccontextmanager
async def open_session() -> AsyncGenerator[AsyncSession, None]:
+ """An asynchronous context manager that opens a new `SQLAlchemy` async session.
+
+ To the contrary of the [`Session`][fastsqla.Session] dependency which can only be
+ used in endpoints, `open_session` can be used anywhere such as in background tasks.
+
+ On exit, it automatically commits the session if no errors occur inside the context,
+ or rolls back when an exception is raised.
+ In all cases, it closes the session and returns the associated connection to the
+ connection pool.
+
+ ```python
+ from fastsqla import open_session
+
+ async def example():
+ async with open_session() as session:
+ await session.execute(...)
+ ```
+
+ """
session = SessionFactory()
try:
yield session
@@ -100,6 +191,54 @@ async def new_session() -> AsyncGenerator[AsyncSession, None]:
Session = Annotated[AsyncSession, Depends(new_session)]
+"""A dependency used exclusively in endpoints to get an `SQLAlchemy` session.
+
+`Session` is a [`FastAPI` dependency](https://fastapi.tiangolo.com/tutorial/dependencies/)
+that provides an asynchronous `SQLAlchemy` session.
+By defining an argument with type `Session` in an endpoint, `FastAPI` will automatically
+inject an `SQLAlchemy` async session into the endpoint.
+
+At the end of request handling:
+
+* If no exceptions are raised, the session is automatically committed.
+* If an exception is raised, the session is automatically rolled back.
+* In alls cases, the session is closed and the associated connection is returned to the
+ connection pool.
+
+Example:
+
+``` py title="example.py" hl_lines="3"
+@app.get("/heros/{hero_id}", response_model=Item[HeroItem])
+async def get_items(
+ session: Session, # (1)!
+ item_id: int,
+):
+ hero = await session.get(Hero, hero_id)
+ return {"data": hero}
+```
+
+1. Just define an argument with type `Session` to get an async session injected
+ in your endpoint.
+
+---
+
+**Recommendation**: Unless there is a good reason to do so, avoid committing the session
+manually, as `FastSQLA` handles it automatically.
+
+If you need data generated by the database server, such as auto-incremented IDs, flush
+the session instead:
+
+```python
+@app.post("/heros", response_model=Item[HeroItem])
+async def create_item(session: Session, new_hero: HeroBase):
+ hero = Hero(**new_hero.model_dump())
+ session.add(hero)
+ await session.flush()
+ return {"data": hero}
+```
+
+Or use the [session context manager][fastsqla.open_session] instead.
+"""
class Meta(BaseModel):
@@ -193,4 +332,74 @@ async def paginate(stmt: Select) -> Page:
type PaginateType[T] = Callable[[Select], Awaitable[Page[T]]]
+
Paginate = Annotated[PaginateType[T], Depends(new_pagination())]
+"""A dependency used in endpoints to paginate `SQLAlchemy` select queries.
+
+It adds `offset`and `limit` query parameters to the endpoint, which are used to paginate.
+The model returned by the endpoint is a `Page` model. It contains a page of data and
+metadata:
+
+```json
+{
+ "data": List[T],
+ "meta": {
+ "offset": int,
+ "total_items": int,
+ "total_pages": int,
+ "page_number": int,
+ }
+}
+```
+
+-----
+
+Example:
+``` py title="example.py" hl_lines="22 23 25"
+from fastsqla import Base, Paginate, Page
+from pydantic import BaseModel
+
+
+class Hero(Base):
+ __tablename__ = "hero"
+
+
+class Hero(Base):
+ __tablename__ = "hero"
+ id: Mapped[int] = mapped_column(primary_key=True)
+ name: Mapped[str] = mapped_column(unique=True)
+ secret_identity: Mapped[str]
+ age: Mapped[int]
+
+
+class HeroModel(HeroBase):
+ model_config = ConfigDict(from_attributes=True)
+ id: int
+
+
+@app.get("/heros", response_model=Page[HeroModel]) # (1)!
+async def list_heros(paginate: Paginate): # (2)!
+ stmt = select(Hero)
+ return await paginate(stmt) # (3)!
+```
+
+1. The endpoint returns a `Page` model of `HeroModel`.
+2. Just define an argument with type `Paginate` to get an async `paginate` function
+ injected in your endpoint function.
+3. Await the `paginate` function with the `SQLAlchemy` select statement to get the
+ paginated result.
+
+To add filtering, just add whatever query parameters you need to the endpoint:
+
+```python
+
+from fastsqla import Paginate, Page
+
+@app.get("/heros", response_model=Page[HeroModel])
+async def list_heros(paginate: Paginate, age:int | None = None):
+ stmt = select(Hero)
+ if age:
+ stmt = stmt.where(Hero.age == age)
+ return await paginate(stmt)
+```
+"""
diff --git a/uv.lock b/uv.lock
index fa1d7ae..3976e36 100644
--- a/uv.lock
+++ b/uv.lock
@@ -51,6 +51,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/2f/f5/c36551e93acba41a59939ae6a0fb77ddb3f2e8e8caa716410c65f7341f72/asgi_lifespan-2.1.0-py3-none-any.whl", hash = "sha256:ed840706680e28428c01e14afb3875d7d76d3206f3d5b2f2294e059b5c23804f", size = 10895 },
]
+[[package]]
+name = "babel"
+version = "2.16.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/2a/74/f1bc80f23eeba13393b7222b11d95ca3af2c1e28edca18af487137eefed9/babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316", size = 9348104 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ed/20/bc79bc575ba2e2a7f70e8a1155618bb1301eaa5132a8271373a6903f73f8/babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b", size = 9587599 },
+]
+
[[package]]
name = "certifi"
version = "2024.8.30"
@@ -255,7 +264,7 @@ wheels = [
[[package]]
name = "fastsqla"
-version = "0.2.3"
+version = "0.2.4"
source = { editable = "." }
dependencies = [
{ name = "fastapi" },
@@ -263,6 +272,13 @@ dependencies = [
{ name = "structlog" },
]
+[package.optional-dependencies]
+docs = [
+ { name = "mkdocs-glightbox" },
+ { name = "mkdocs-material" },
+ { name = "mkdocstrings", extra = ["python"] },
+]
+
[package.dev-dependencies]
dev = [
{ name = "aiosqlite" },
@@ -283,6 +299,9 @@ dev = [
[package.metadata]
requires-dist = [
{ name = "fastapi", specifier = ">=0.115.6" },
+ { name = "mkdocs-glightbox", marker = "extra == 'docs'", specifier = ">=0.4.0" },
+ { name = "mkdocs-material", marker = "extra == 'docs'", specifier = ">=9.5.50" },
+ { name = "mkdocstrings", extras = ["python"], marker = "extra == 'docs'", specifier = ">=0.27.0" },
{ name = "sqlalchemy", extras = ["asyncio"], specifier = ">=2.0.37" },
{ name = "structlog", specifier = ">=24.4.0" },
]
@@ -304,6 +323,18 @@ dev = [
{ name = "twine", specifier = ">=5.1.1" },
]
+[[package]]
+name = "ghp-import"
+version = "2.1.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "python-dateutil" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034 },
+]
+
[[package]]
name = "gitdb"
version = "4.0.11"
@@ -361,6 +392,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ac/38/08cc303ddddc4b3d7c628c3039a61a3aae36c241ed01393d00c2fd663473/greenlet-3.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:411f015496fec93c1c8cd4e5238da364e1da7a124bcb293f085bf2860c32c6f6", size = 1142112 },
]
+[[package]]
+name = "griffe"
+version = "1.5.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/5c/74/cd35a98cb11f79de0581e8e1e6fbd738aeeed1f2d90e9b5106728b63f5f7/griffe-1.5.5.tar.gz", hash = "sha256:35ee5b38b93d6a839098aad0f92207e6ad6b70c3e8866c08ca669275b8cba585", size = 391124 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1f/88/52c9422bc853cd7c2b6122090e887d17b5fad29b67f930e4277c9c557357/griffe-1.5.5-py3-none-any.whl", hash = "sha256:2761b1e8876c6f1f9ab1af274df93ea6bbadd65090de5f38f4cb5cc84897c7dd", size = 128221 },
+]
+
[[package]]
name = "h11"
version = "0.14.0"
@@ -509,6 +552,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/63/42/ea8c9726e5ee5ff0731978aaf7cd5fa16674cf549c46279b279d7167c2b4/keyring-25.3.0-py3-none-any.whl", hash = "sha256:8d963da00ccdf06e356acd9bf3b743208878751032d8599c6cc89eb51310ffae", size = 38742 },
]
+[[package]]
+name = "markdown"
+version = "3.7"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/54/28/3af612670f82f4c056911fbbbb42760255801b3068c48de792d354ff4472/markdown-3.7.tar.gz", hash = "sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2", size = 357086 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3f/08/83871f3c50fc983b88547c196d11cf8c3340e37c32d2e9d6152abe2c61f7/Markdown-3.7-py3-none-any.whl", hash = "sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803", size = 106349 },
+]
+
[[package]]
name = "markdown-it-py"
version = "3.0.0"
@@ -548,6 +600,145 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 },
]
+[[package]]
+name = "mergedeep"
+version = "1.3.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354 },
+]
+
+[[package]]
+name = "mkdocs"
+version = "1.6.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "click" },
+ { name = "colorama", marker = "platform_system == 'Windows'" },
+ { name = "ghp-import" },
+ { name = "jinja2" },
+ { name = "markdown" },
+ { name = "markupsafe" },
+ { name = "mergedeep" },
+ { name = "mkdocs-get-deps" },
+ { name = "packaging" },
+ { name = "pathspec" },
+ { name = "pyyaml" },
+ { name = "pyyaml-env-tag" },
+ { name = "watchdog" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451 },
+]
+
+[[package]]
+name = "mkdocs-autorefs"
+version = "1.3.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "markdown" },
+ { name = "markupsafe" },
+ { name = "mkdocs" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/fe/18/fb1e17fb705228b51bf7b2f791adaf83c0fa708e51bbc003411ba48ae21e/mkdocs_autorefs-1.3.0.tar.gz", hash = "sha256:6867764c099ace9025d6ac24fd07b85a98335fbd30107ef01053697c8f46db61", size = 42597 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f4/4a/960c441950f98becfa5dd419adab20274939fd575ab848aee2c87e3599ac/mkdocs_autorefs-1.3.0-py3-none-any.whl", hash = "sha256:d180f9778a04e78b7134e31418f238bba56f56d6a8af97873946ff661befffb3", size = 17642 },
+]
+
+[[package]]
+name = "mkdocs-get-deps"
+version = "0.2.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "mergedeep" },
+ { name = "platformdirs" },
+ { name = "pyyaml" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/98/f5/ed29cd50067784976f25ed0ed6fcd3c2ce9eb90650aa3b2796ddf7b6870b/mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c", size = 10239 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134", size = 9521 },
+]
+
+[[package]]
+name = "mkdocs-glightbox"
+version = "0.4.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/86/5a/0bc456397ba0acc684b5b1daa4ca232ed717938fd37198251d8bcc4053bf/mkdocs-glightbox-0.4.0.tar.gz", hash = "sha256:392b34207bf95991071a16d5f8916d1d2f2cd5d5bb59ae2997485ccd778c70d9", size = 32010 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c1/72/b0c2128bb569c732c11ae8e49a777089e77d83c05946062caa19b841e6fb/mkdocs_glightbox-0.4.0-py3-none-any.whl", hash = "sha256:e0107beee75d3eb7380ac06ea2d6eac94c999eaa49f8c3cbab0e7be2ac006ccf", size = 31154 },
+]
+
+[[package]]
+name = "mkdocs-material"
+version = "9.5.50"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "babel" },
+ { name = "colorama" },
+ { name = "jinja2" },
+ { name = "markdown" },
+ { name = "mkdocs" },
+ { name = "mkdocs-material-extensions" },
+ { name = "paginate" },
+ { name = "pygments" },
+ { name = "pymdown-extensions" },
+ { name = "regex" },
+ { name = "requests" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c7/16/c48d5a28bc4a67c49808180b6009d4d1b4c0753739ffee3cc37046ab29d7/mkdocs_material-9.5.50.tar.gz", hash = "sha256:ae5fe16f3d7c9ccd05bb6916a7da7420cf99a9ce5e33debd9d40403a090d5825", size = 3923354 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ee/b5/1bf29cd744896ae83bd38c72970782c843ba13e0240b1a85277bd3928637/mkdocs_material-9.5.50-py3-none-any.whl", hash = "sha256:f24100f234741f4d423a9d672a909d859668a4f404796be3cf035f10d6050385", size = 8645274 },
+]
+
+[[package]]
+name = "mkdocs-material-extensions"
+version = "1.3.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728 },
+]
+
+[[package]]
+name = "mkdocstrings"
+version = "0.27.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "click" },
+ { name = "jinja2" },
+ { name = "markdown" },
+ { name = "markupsafe" },
+ { name = "mkdocs" },
+ { name = "mkdocs-autorefs" },
+ { name = "platformdirs" },
+ { name = "pymdown-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/e2/5a/5de70538c2cefae7ac3a15b5601e306ef3717290cb2aab11d51cbbc2d1c0/mkdocstrings-0.27.0.tar.gz", hash = "sha256:16adca6d6b0a1f9e0c07ff0b02ced8e16f228a9d65a37c063ec4c14d7b76a657", size = 94830 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cd/10/4c27c3063c2b3681a4b7942f8dbdeb4fa34fecb2c19b594e7345ebf4f86f/mkdocstrings-0.27.0-py3-none-any.whl", hash = "sha256:6ceaa7ea830770959b55a16203ac63da24badd71325b96af950e59fd37366332", size = 30658 },
+]
+
+[package.optional-dependencies]
+python = [
+ { name = "mkdocstrings-python" },
+]
+
+[[package]]
+name = "mkdocstrings-python"
+version = "1.13.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "griffe" },
+ { name = "mkdocs-autorefs" },
+ { name = "mkdocstrings" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ab/ae/32703e35d74040051c672400fd9f5f2b48a6ea094f5071dd8a0e3be35322/mkdocstrings_python-1.13.0.tar.gz", hash = "sha256:2dbd5757e8375b9720e81db16f52f1856bf59905428fd7ef88005d1370e2f64c", size = 185697 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/51/23/d02d86553327296c3bf369d444194ea83410cce8f0e690565264f37f3261/mkdocstrings_python-1.13.0-py3-none-any.whl", hash = "sha256:b88bbb207bab4086434743849f8e796788b373bd32e7bfefbf8560ac45d88f97", size = 112254 },
+]
+
[[package]]
name = "more-itertools"
version = "10.5.0"
@@ -589,6 +780,24 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/08/aa/cc0199a5f0ad350994d660967a8efb233fe0416e4639146c089643407ce6/packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124", size = 53985 },
]
+[[package]]
+name = "paginate"
+version = "0.5.7"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746 },
+]
+
+[[package]]
+name = "pathspec"
+version = "0.12.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 },
+]
+
[[package]]
name = "pkginfo"
version = "1.10.0"
@@ -598,6 +807,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/56/09/054aea9b7534a15ad38a363a2bd974c20646ab1582a387a95b8df1bfea1c/pkginfo-1.10.0-py3-none-any.whl", hash = "sha256:889a6da2ed7ffc58ab5b900d888ddce90bce912f2d2de1dc1c26f4cb9fe65097", size = 30392 },
]
+[[package]]
+name = "platformdirs"
+version = "4.3.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 },
+]
+
[[package]]
name = "pluggy"
version = "1.5.0"
@@ -675,6 +893,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f7/3f/01c8b82017c199075f8f788d0d906b9ffbbc5a47dc9918a945e13d5a2bda/pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a", size = 1205513 },
]
+[[package]]
+name = "pymdown-extensions"
+version = "10.14.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "markdown" },
+ { name = "pyyaml" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/e7/24/f7a412dc1630b1a6d7b288e7c736215ce878ee4aad24359f7f67b53bbaa9/pymdown_extensions-10.14.1.tar.gz", hash = "sha256:b65801996a0cd4f42a3110810c306c45b7313c09b0610a6f773730f2a9e3c96b", size = 845243 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/09/fb/79a8d27966e90feeeb686395c8b1bff8221727abcbd80d2485841393a955/pymdown_extensions-10.14.1-py3-none-any.whl", hash = "sha256:637951cbfbe9874ba28134fb3ce4b8bcadd6aca89ac4998ec29dcbafd554ae08", size = 264283 },
+]
+
[[package]]
name = "pytest"
version = "8.3.2"
@@ -783,6 +1014,44 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756 },
]
+[[package]]
+name = "pyyaml"
+version = "6.0.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 },
+ { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 },
+ { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 },
+ { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 },
+ { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 },
+ { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 },
+ { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 },
+ { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 },
+ { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 },
+ { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 },
+ { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 },
+ { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 },
+ { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 },
+ { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 },
+ { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 },
+ { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 },
+ { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 },
+ { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 },
+]
+
+[[package]]
+name = "pyyaml-env-tag"
+version = "0.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pyyaml" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/fb/8e/da1c6c58f751b70f8ceb1eb25bc25d524e8f14fe16edcce3f4e3ba08629c/pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb", size = 5631 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5a/66/bbb1dd374f5c870f59c5bb1db0e18cbe7fa739415a24cbd95b2d1f5ae0c4/pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069", size = 3911 },
+]
+
[[package]]
name = "readme-renderer"
version = "44.0"
@@ -797,6 +1066,44 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e1/67/921ec3024056483db83953ae8e48079ad62b92db7880013ca77632921dd0/readme_renderer-44.0-py3-none-any.whl", hash = "sha256:2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151", size = 13310 },
]
+[[package]]
+name = "regex"
+version = "2024.11.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/8e/5f/bd69653fbfb76cf8604468d3b4ec4c403197144c7bfe0e6a5fc9e02a07cb/regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519", size = 399494 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ba/30/9a87ce8336b172cc232a0db89a3af97929d06c11ceaa19d97d84fa90a8f8/regex-2024.11.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:52fb28f528778f184f870b7cf8f225f5eef0a8f6e3778529bdd40c7b3920796a", size = 483781 },
+ { url = "https://files.pythonhosted.org/packages/01/e8/00008ad4ff4be8b1844786ba6636035f7ef926db5686e4c0f98093612add/regex-2024.11.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdd6028445d2460f33136c55eeb1f601ab06d74cb3347132e1c24250187500d9", size = 288455 },
+ { url = "https://files.pythonhosted.org/packages/60/85/cebcc0aff603ea0a201667b203f13ba75d9fc8668fab917ac5b2de3967bc/regex-2024.11.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805e6b60c54bf766b251e94526ebad60b7de0c70f70a4e6210ee2891acb70bf2", size = 284759 },
+ { url = "https://files.pythonhosted.org/packages/94/2b/701a4b0585cb05472a4da28ee28fdfe155f3638f5e1ec92306d924e5faf0/regex-2024.11.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b85c2530be953a890eaffde05485238f07029600e8f098cdf1848d414a8b45e4", size = 794976 },
+ { url = "https://files.pythonhosted.org/packages/4b/bf/fa87e563bf5fee75db8915f7352e1887b1249126a1be4813837f5dbec965/regex-2024.11.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb26437975da7dc36b7efad18aa9dd4ea569d2357ae6b783bf1118dabd9ea577", size = 833077 },
+ { url = "https://files.pythonhosted.org/packages/a1/56/7295e6bad94b047f4d0834e4779491b81216583c00c288252ef625c01d23/regex-2024.11.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abfa5080c374a76a251ba60683242bc17eeb2c9818d0d30117b4486be10c59d3", size = 823160 },
+ { url = "https://files.pythonhosted.org/packages/fb/13/e3b075031a738c9598c51cfbc4c7879e26729c53aa9cca59211c44235314/regex-2024.11.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b7fa6606c2881c1db9479b0eaa11ed5dfa11c8d60a474ff0e095099f39d98e", size = 796896 },
+ { url = "https://files.pythonhosted.org/packages/24/56/0b3f1b66d592be6efec23a795b37732682520b47c53da5a32c33ed7d84e3/regex-2024.11.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c32f75920cf99fe6b6c539c399a4a128452eaf1af27f39bce8909c9a3fd8cbe", size = 783997 },
+ { url = "https://files.pythonhosted.org/packages/f9/a1/eb378dada8b91c0e4c5f08ffb56f25fcae47bf52ad18f9b2f33b83e6d498/regex-2024.11.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:982e6d21414e78e1f51cf595d7f321dcd14de1f2881c5dc6a6e23bbbbd68435e", size = 781725 },
+ { url = "https://files.pythonhosted.org/packages/83/f2/033e7dec0cfd6dda93390089864732a3409246ffe8b042e9554afa9bff4e/regex-2024.11.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a7c2155f790e2fb448faed6dd241386719802296ec588a8b9051c1f5c481bc29", size = 789481 },
+ { url = "https://files.pythonhosted.org/packages/83/23/15d4552ea28990a74e7696780c438aadd73a20318c47e527b47a4a5a596d/regex-2024.11.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:149f5008d286636e48cd0b1dd65018548944e495b0265b45e1bffecce1ef7f39", size = 852896 },
+ { url = "https://files.pythonhosted.org/packages/e3/39/ed4416bc90deedbfdada2568b2cb0bc1fdb98efe11f5378d9892b2a88f8f/regex-2024.11.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e5364a4502efca094731680e80009632ad6624084aff9a23ce8c8c6820de3e51", size = 860138 },
+ { url = "https://files.pythonhosted.org/packages/93/2d/dd56bb76bd8e95bbce684326302f287455b56242a4f9c61f1bc76e28360e/regex-2024.11.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0a86e7eeca091c09e021db8eb72d54751e527fa47b8d5787caf96d9831bd02ad", size = 787692 },
+ { url = "https://files.pythonhosted.org/packages/0b/55/31877a249ab7a5156758246b9c59539abbeba22461b7d8adc9e8475ff73e/regex-2024.11.6-cp312-cp312-win32.whl", hash = "sha256:32f9a4c643baad4efa81d549c2aadefaeba12249b2adc5af541759237eee1c54", size = 262135 },
+ { url = "https://files.pythonhosted.org/packages/38/ec/ad2d7de49a600cdb8dd78434a1aeffe28b9d6fc42eb36afab4a27ad23384/regex-2024.11.6-cp312-cp312-win_amd64.whl", hash = "sha256:a93c194e2df18f7d264092dc8539b8ffb86b45b899ab976aa15d48214138e81b", size = 273567 },
+ { url = "https://files.pythonhosted.org/packages/90/73/bcb0e36614601016552fa9344544a3a2ae1809dc1401b100eab02e772e1f/regex-2024.11.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a6ba92c0bcdf96cbf43a12c717eae4bc98325ca3730f6b130ffa2e3c3c723d84", size = 483525 },
+ { url = "https://files.pythonhosted.org/packages/0f/3f/f1a082a46b31e25291d830b369b6b0c5576a6f7fb89d3053a354c24b8a83/regex-2024.11.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:525eab0b789891ac3be914d36893bdf972d483fe66551f79d3e27146191a37d4", size = 288324 },
+ { url = "https://files.pythonhosted.org/packages/09/c9/4e68181a4a652fb3ef5099e077faf4fd2a694ea6e0f806a7737aff9e758a/regex-2024.11.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:086a27a0b4ca227941700e0b31425e7a28ef1ae8e5e05a33826e17e47fbfdba0", size = 284617 },
+ { url = "https://files.pythonhosted.org/packages/fc/fd/37868b75eaf63843165f1d2122ca6cb94bfc0271e4428cf58c0616786dce/regex-2024.11.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bde01f35767c4a7899b7eb6e823b125a64de314a8ee9791367c9a34d56af18d0", size = 795023 },
+ { url = "https://files.pythonhosted.org/packages/c4/7c/d4cd9c528502a3dedb5c13c146e7a7a539a3853dc20209c8e75d9ba9d1b2/regex-2024.11.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b583904576650166b3d920d2bcce13971f6f9e9a396c673187f49811b2769dc7", size = 833072 },
+ { url = "https://files.pythonhosted.org/packages/4f/db/46f563a08f969159c5a0f0e722260568425363bea43bb7ae370becb66a67/regex-2024.11.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c4de13f06a0d54fa0d5ab1b7138bfa0d883220965a29616e3ea61b35d5f5fc7", size = 823130 },
+ { url = "https://files.pythonhosted.org/packages/db/60/1eeca2074f5b87df394fccaa432ae3fc06c9c9bfa97c5051aed70e6e00c2/regex-2024.11.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cde6e9f2580eb1665965ce9bf17ff4952f34f5b126beb509fee8f4e994f143c", size = 796857 },
+ { url = "https://files.pythonhosted.org/packages/10/db/ac718a08fcee981554d2f7bb8402f1faa7e868c1345c16ab1ebec54b0d7b/regex-2024.11.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d7f453dca13f40a02b79636a339c5b62b670141e63efd511d3f8f73fba162b3", size = 784006 },
+ { url = "https://files.pythonhosted.org/packages/c2/41/7da3fe70216cea93144bf12da2b87367590bcf07db97604edeea55dac9ad/regex-2024.11.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59dfe1ed21aea057a65c6b586afd2a945de04fc7db3de0a6e3ed5397ad491b07", size = 781650 },
+ { url = "https://files.pythonhosted.org/packages/a7/d5/880921ee4eec393a4752e6ab9f0fe28009435417c3102fc413f3fe81c4e5/regex-2024.11.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b97c1e0bd37c5cd7902e65f410779d39eeda155800b65fc4d04cc432efa9bc6e", size = 789545 },
+ { url = "https://files.pythonhosted.org/packages/dc/96/53770115e507081122beca8899ab7f5ae28ae790bfcc82b5e38976df6a77/regex-2024.11.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d1e379028e0fc2ae3654bac3cbbef81bf3fd571272a42d56c24007979bafb6", size = 853045 },
+ { url = "https://files.pythonhosted.org/packages/31/d3/1372add5251cc2d44b451bd94f43b2ec78e15a6e82bff6a290ef9fd8f00a/regex-2024.11.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:13291b39131e2d002a7940fb176e120bec5145f3aeb7621be6534e46251912c4", size = 860182 },
+ { url = "https://files.pythonhosted.org/packages/ed/e3/c446a64984ea9f69982ba1a69d4658d5014bc7a0ea468a07e1a1265db6e2/regex-2024.11.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f51f88c126370dcec4908576c5a627220da6c09d0bff31cfa89f2523843316d", size = 787733 },
+ { url = "https://files.pythonhosted.org/packages/2b/f1/e40c8373e3480e4f29f2692bd21b3e05f296d3afebc7e5dcf21b9756ca1c/regex-2024.11.6-cp313-cp313-win32.whl", hash = "sha256:63b13cfd72e9601125027202cad74995ab26921d8cd935c25f09c630436348ff", size = 262122 },
+ { url = "https://files.pythonhosted.org/packages/45/94/bc295babb3062a731f52621cdc992d123111282e291abaf23faa413443ea/regex-2024.11.6-cp313-cp313-win_amd64.whl", hash = "sha256:2b3361af3198667e99927da8b84c1b010752fa4b1115ee30beaa332cabc3ef1a", size = 273545 },
+]
+
[[package]]
name = "requests"
version = "2.32.3"