mirror of
https://github.com/grillazz/fastapi-sqlalchemy-asyncpg.git
synced 2025-08-26 16:40:40 +03:00
Compare commits
13 Commits
e9750107e7
...
41a8688aae
Author | SHA1 | Date | |
---|---|---|---|
|
41a8688aae | ||
|
ce40b7a6d9 | ||
|
0bb3576e90 | ||
|
3f4f1efb35 | ||
|
b594dee278 | ||
|
5ede530a4d | ||
|
2cc654e6c7 | ||
|
0260df4d09 | ||
|
8f96c8a472 | ||
|
e88f68e2bf | ||
|
21de7e2dfc | ||
|
2404c12542 | ||
|
87f8dab32f |
32
.github/workflows/build-and-test.yml
vendored
32
.github/workflows/build-and-test.yml
vendored
@ -7,20 +7,19 @@ on:
|
|||||||
- main
|
- main
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
python-version: [ "3.13" ]
|
python-version: [ "3.13" ]
|
||||||
poetry-version: [ "1.8.5" ]
|
|
||||||
|
|
||||||
env:
|
env:
|
||||||
PYTHONDONTWRITEBYTECODE: 1
|
PYTHONDONTWRITEBYTECODE: 1
|
||||||
PYTHONUNBUFFERED: 1
|
PYTHONUNBUFFERED: 1
|
||||||
POSTGRES_DB: testdb
|
POSTGRES_DB: testdb
|
||||||
POSTGRES_HOST: 127.0.0.1
|
POSTGRES_HOST: 127.0.0.1
|
||||||
POSTGRES_USER: app-user
|
POSTGRES_USER: panettone
|
||||||
POSTGRES_PASSWORD: secret
|
POSTGRES_PASSWORD: secret
|
||||||
PGPASSWORD: secret
|
PGPASSWORD: secret
|
||||||
REDIS_HOST: 127.0.0.1
|
REDIS_HOST: 127.0.0.1
|
||||||
@ -37,7 +36,7 @@ jobs:
|
|||||||
sqldb:
|
sqldb:
|
||||||
image: postgres:16
|
image: postgres:16
|
||||||
env:
|
env:
|
||||||
POSTGRES_USER: app-user
|
POSTGRES_USER: panettone
|
||||||
POSTGRES_PASSWORD: secret
|
POSTGRES_PASSWORD: secret
|
||||||
POSTGRES_DB: testdb
|
POSTGRES_DB: testdb
|
||||||
ports:
|
ports:
|
||||||
@ -48,18 +47,17 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Create database schema
|
- name: Create database schema
|
||||||
run: PGPASSWORD=secret psql -h 127.0.0.1 -d testdb -U app-user -c "CREATE SCHEMA shakespeare; CREATE SCHEMA happy_hog;"
|
run: PGPASSWORD=secret psql -h 127.0.0.1 -d testdb -U panettone -c "CREATE SCHEMA shakespeare; CREATE SCHEMA happy_hog;"
|
||||||
- uses: actions/setup-python@v5
|
|
||||||
|
- name: Install the latest version of uv and set the python version
|
||||||
|
uses: astral-sh/setup-uv@v5
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
- name: Install Poetry
|
|
||||||
uses: abatilo/actions-poetry@v3
|
- name: Lint with ruff
|
||||||
with:
|
run: uv run --frozen ruff check .
|
||||||
poetry-version: ${{ matrix.poetry-version }}
|
|
||||||
- name: Install dependencies
|
- name: Test with python ${{ matrix.python-version }}
|
||||||
if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
|
run: uv run --frozen pytest
|
||||||
run: poetry install --no-interaction --no-root
|
|
||||||
- name: Test Code
|
|
||||||
run: poetry run pytest tests/
|
|
||||||
- name: Lint Code
|
|
||||||
run: poetry run ruff check .
|
|
||||||
|
87
Dockerfile
87
Dockerfile
@ -1,43 +1,62 @@
|
|||||||
FROM python:3.13-slim-bookworm AS base
|
FROM ubuntu:oracular AS build
|
||||||
RUN apt-get update \
|
|
||||||
&& apt-get upgrade -y \
|
|
||||||
&& apt-get install -y --no-install-recommends curl git build-essential \
|
|
||||||
&& apt-get autoremove -y
|
|
||||||
ENV POETRY_HOME="/opt/poetry"
|
|
||||||
RUN curl -sSL https://install.python-poetry.org | python3 -
|
|
||||||
|
|
||||||
FROM base AS install
|
RUN apt-get update -qy && apt-get install -qyy \
|
||||||
WORKDIR /home/code
|
-o APT::Install-Recommends=false \
|
||||||
|
-o APT::Install-Suggests=false \
|
||||||
|
build-essential \
|
||||||
|
ca-certificates \
|
||||||
|
python3-setuptools \
|
||||||
|
python3.13-dev \
|
||||||
|
git
|
||||||
|
|
||||||
# allow controlling the poetry installation of dependencies via external args
|
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
|
||||||
ARG INSTALL_ARGS="--no-root --no-interaction --no-ansi"
|
|
||||||
ENV POETRY_HOME="/opt/poetry"
|
|
||||||
ENV PATH="$POETRY_HOME/bin:$PATH"
|
|
||||||
COPY pyproject.toml poetry.lock ./
|
|
||||||
|
|
||||||
# install without virtualenv, since we are inside a container
|
ENV UV_LINK_MODE=copy \
|
||||||
RUN poetry config virtualenvs.create false \
|
UV_COMPILE_BYTECODE=1 \
|
||||||
&& poetry install $INSTALL_ARGS
|
UV_PYTHON_DOWNLOADS=never \
|
||||||
|
UV_PYTHON=python3.13 \
|
||||||
|
UV_PROJECT_ENVIRONMENT=/panettone
|
||||||
|
|
||||||
# cleanup
|
COPY pyproject.toml /_lock/
|
||||||
RUN curl -sSL https://install.python-poetry.org | python3 - --uninstall
|
COPY uv.lock /_lock/
|
||||||
RUN apt-get purge -y curl git build-essential \
|
|
||||||
&& apt-get clean -y \
|
|
||||||
&& rm -rf /root/.cache \
|
|
||||||
&& rm -rf /var/apt/lists/* \
|
|
||||||
&& rm -rf /var/cache/apt/*
|
|
||||||
|
|
||||||
FROM install AS app-image
|
RUN --mount=type=cache,target=/root/.cache
|
||||||
|
RUN cd /_lock && uv sync \
|
||||||
|
--locked \
|
||||||
|
--no-dev \
|
||||||
|
--no-install-project
|
||||||
|
##########################################################################
|
||||||
|
FROM ubuntu:oracular
|
||||||
|
|
||||||
ENV PYTHONPATH=/home/code/ PYTHONHASHSEED=0 PYTHONASYNCIODEBUG=1
|
ENV PATH=/panettone/bin:$PATH
|
||||||
|
|
||||||
COPY tests/ tests/
|
RUN groupadd -r panettone
|
||||||
COPY app/ app/
|
RUN useradd -r -d /panettone -g panettone -N panettone
|
||||||
COPY alembic/ alembic/
|
|
||||||
COPY .env alembic.ini ./
|
|
||||||
|
|
||||||
# create a non-root user and switch to it, for security.
|
STOPSIGNAL SIGINT
|
||||||
RUN addgroup --system --gid 1001 "app-user"
|
|
||||||
RUN adduser --system --uid 1001 "app-user"
|
|
||||||
USER "app-user"
|
|
||||||
|
|
||||||
|
RUN apt-get update -qy && apt-get install -qyy \
|
||||||
|
-o APT::Install-Recommends=false \
|
||||||
|
-o APT::Install-Suggests=false \
|
||||||
|
python3.13 \
|
||||||
|
libpython3.13 \
|
||||||
|
libpcre3 \
|
||||||
|
libxml2
|
||||||
|
|
||||||
|
RUN apt-get clean
|
||||||
|
RUN rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||||
|
|
||||||
|
COPY --from=build --chown=panettone:panettone /panettone /panettone
|
||||||
|
|
||||||
|
USER panettone
|
||||||
|
WORKDIR /panettone
|
||||||
|
COPY /app/ app/
|
||||||
|
COPY /tests/ tests/
|
||||||
|
COPY .env app/
|
||||||
|
COPY alembic.ini app/
|
||||||
|
COPY alembic/ app/alembic/
|
||||||
|
COPY logging-uvicorn.json /panettone/logging-uvicorn.json
|
||||||
|
|
||||||
|
RUN python -V
|
||||||
|
RUN python -Im site
|
||||||
|
RUN python -Ic 'import uvicorn'
|
||||||
|
2
Makefile
2
Makefile
@ -39,7 +39,7 @@ safety: ## Check project and dependencies with safety https://github.com/pyupio/
|
|||||||
|
|
||||||
.PHONY: py-upgrade
|
.PHONY: py-upgrade
|
||||||
py-upgrade: ## Upgrade project py files with pyupgrade library for python version 3.10
|
py-upgrade: ## Upgrade project py files with pyupgrade library for python version 3.10
|
||||||
pyupgrade --py312-plus `find app -name "*.py"`
|
pyupgrade --py313-plus `find app -name "*.py"`
|
||||||
|
|
||||||
.PHONY: lint
|
.PHONY: lint
|
||||||
lint: ## Lint project code.
|
lint: ## Lint project code.
|
||||||
|
41
README.md
41
README.md
@ -26,10 +26,11 @@
|
|||||||
<li><a href="#how-to-feed-database">How to feed database</a></li>
|
<li><a href="#how-to-feed-database">How to feed database</a></li>
|
||||||
<li><a href="#rainbow-logs-with-rich">Rainbow logs with rich</a></li>
|
<li><a href="#rainbow-logs-with-rich">Rainbow logs with rich</a></li>
|
||||||
<li><a href="#setup-user-auth">Setup user auth</a></li>
|
<li><a href="#setup-user-auth">Setup user auth</a></li>
|
||||||
<li><a href="#local-development-with-poetry">Local development with poetry</a></li>
|
<li><a href="#setup-local-env-with-uv">Setup local development with uv</a></li>
|
||||||
<li><a href="#import-xlsx-files-with-polars-and-calamine">Import xlsx files with polars and calamine</a></li>
|
<li><a href="#import-xlsx-files-with-polars-and-calamine">Import xlsx files with polars and calamine</a></li>
|
||||||
<li><a href="#worker-aware-async-scheduler">Schedule jobs</a></li>
|
<li><a href="#worker-aware-async-scheduler">Schedule jobs</a></li>
|
||||||
<li><a href="#smtp-setup">Email Configuration</a></li>
|
<li><a href="#smtp-setup">Email Configuration</a></li>
|
||||||
|
<li><a href="#uv-knowledge-and-inspirations">UV knowledge and inspirations</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li><a href="#acknowledgments">Acknowledgments</a></li>
|
<li><a href="#acknowledgments">Acknowledgments</a></li>
|
||||||
@ -44,7 +45,7 @@
|
|||||||
This example demonstrates the seamless integration of [FastAPI](https://fastapi.tiangolo.com/), a modern, high-performance web framework,
|
This example demonstrates the seamless integration of [FastAPI](https://fastapi.tiangolo.com/), a modern, high-performance web framework,
|
||||||
with [Pydantic 2.0](https://github.com/pydantic/pydantic), a robust and powerful data validation library.
|
with [Pydantic 2.0](https://github.com/pydantic/pydantic), a robust and powerful data validation library.
|
||||||
The integration is further enhanced by the use of [SQLAlchemy ORM](https://www.sqlalchemy.org/), a popular and feature-rich Object-Relational Mapping tool,
|
The integration is further enhanced by the use of [SQLAlchemy ORM](https://www.sqlalchemy.org/), a popular and feature-rich Object-Relational Mapping tool,
|
||||||
and [PostgreSQL16](https://www.postgresql.org/about/news/postgresql-16-released-2715/) relational database.
|
and [PostgreSQL17](https://www.postgresql.org/docs/17/release.html) relational database.
|
||||||
|
|
||||||
The entire stack is connected using the [asyncpg](https://github.com/MagicStack/asyncpg) Database Client Library,
|
The entire stack is connected using the [asyncpg](https://github.com/MagicStack/asyncpg) Database Client Library,
|
||||||
which provides a robust and efficient way to interact with PostgreSQL databases in Python,
|
which provides a robust and efficient way to interact with PostgreSQL databases in Python,
|
||||||
@ -56,7 +57,7 @@ allowing for the rapid development of APIs with Python 3.8+.
|
|||||||
|
|
||||||
FastAPI has received significant recognition in the industry, including a review on thoughtworks Technology Radar in April 2021,
|
FastAPI has received significant recognition in the industry, including a review on thoughtworks Technology Radar in April 2021,
|
||||||
where it was classified as a Trial technology, with comments praising its performance, ease of use,
|
where it was classified as a Trial technology, with comments praising its performance, ease of use,
|
||||||
and features such as API documentation using OpenAPI. Additionally, FastAPI was recognized in the Python Developers Survey 2022 Results,
|
and features such as API documentation using OpenAPI. Additionally, FastAPI was recognized in the Python Developers Survey 2023 Results,
|
||||||
conducted by the Python Software Foundation and JetBrains, where it was reported that 1 in 4 Python developers use FastAPI,
|
conducted by the Python Software Foundation and JetBrains, where it was reported that 1 in 4 Python developers use FastAPI,
|
||||||
with a 4 percentage point increase from the previous year.
|
with a 4 percentage point increase from the previous year.
|
||||||
|
|
||||||
@ -86,7 +87,8 @@ To build , run and test and more ... use magic of make help to play with this pr
|
|||||||
4. make docker-feed-database
|
4. make docker-feed-database
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Adjust make with just
|
||||||
|
[//]: # (TODO: switch form make to just)
|
||||||
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
||||||
|
|
||||||
### How to feed database
|
### How to feed database
|
||||||
@ -126,15 +128,11 @@ he following steps were taken to integrate [rich](https://github.com/Textualize/
|
|||||||
|
|
||||||
Setup user authentication with JWT and Redis as token storage.
|
Setup user authentication with JWT and Redis as token storage.
|
||||||
|
|
||||||
### Local development with poetry
|
### Setup local env with uv
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
pyenv install 3.13 && pyenv local 3.13
|
uv sync
|
||||||
|
source .venv/bin/activate
|
||||||
```
|
```
|
||||||
```shell
|
|
||||||
poetry install --with dev
|
|
||||||
```
|
|
||||||
Hope you enjoy it.
|
|
||||||
|
|
||||||
### Import xlsx files with polars and calamine
|
### Import xlsx files with polars and calamine
|
||||||
Power of Polars Library in data manipulation and analysis.
|
Power of Polars Library in data manipulation and analysis.
|
||||||
@ -165,6 +163,16 @@ It is implemented as a singleton to ensure that only one SMTP connection is main
|
|||||||
throughout the application lifecycle, optimizing resource usage.
|
throughout the application lifecycle, optimizing resource usage.
|
||||||
|
|
||||||
|
|
||||||
|
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
||||||
|
|
||||||
|
### UV knowledge and inspirations
|
||||||
|
- https://docs.astral.sh/uv/
|
||||||
|
- https://hynek.me/articles/docker-uv/
|
||||||
|
- https://thedataquarry.com/posts/towards-a-unified-python-toolchain/
|
||||||
|
- https://www.youtube.com/watch?v=ifj-izwXKRA&t=760s > UV and Ruff: Next-gen Python Tooling
|
||||||
|
- https://www.youtube.com/watch?v=8UuW8o4bHbw&t=1s > uv IS the Future of Python Packaging! 🐍📦
|
||||||
|
|
||||||
|
|
||||||
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
||||||
|
|
||||||
## Acknowledgments
|
## Acknowledgments
|
||||||
@ -206,6 +214,7 @@ I've included a few of my favorites to kick things off!
|
|||||||
- **[OCT 16 2024]** apscheduler added to project :clock1:
|
- **[OCT 16 2024]** apscheduler added to project :clock1:
|
||||||
- **[DEC 16 2024]** bump project to Python 3.13 :fast_forward:
|
- **[DEC 16 2024]** bump project to Python 3.13 :fast_forward:
|
||||||
- **[JAN 28 2025]** add SMTP setup :email:
|
- **[JAN 28 2025]** add SMTP setup :email:
|
||||||
|
- **[MAR 8 2025]** switch from poetry to uv :fast_forward:
|
||||||
|
|
||||||
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
||||||
|
|
||||||
@ -225,19 +234,19 @@ I've included a few of my favorites to kick things off!
|
|||||||
[linkedin-shield]: https://img.shields.io/badge/-LinkedIn-black.svg?style=for-the-badge&logo=linkedin&colorB=555
|
[linkedin-shield]: https://img.shields.io/badge/-LinkedIn-black.svg?style=for-the-badge&logo=linkedin&colorB=555
|
||||||
[linkedin-url]: https://www.linkedin.com/in/python-has-powers/
|
[linkedin-url]: https://www.linkedin.com/in/python-has-powers/
|
||||||
|
|
||||||
[fastapi.tiangolo.com]: https://img.shields.io/badge/FastAPI-0.115.6-009485?style=for-the-badge&logo=fastapi&logoColor=white
|
[fastapi.tiangolo.com]: https://img.shields.io/badge/FastAPI-0.115.11-009485?style=for-the-badge&logo=fastapi&logoColor=white
|
||||||
[fastapi-url]: https://fastapi.tiangolo.com/
|
[fastapi-url]: https://fastapi.tiangolo.com/
|
||||||
[pydantic.com]: https://img.shields.io/badge/Pydantic-2.10.3-e92063?style=for-the-badge&logo=pydantic&logoColor=white
|
[pydantic.com]: https://img.shields.io/badge/Pydantic-2.10.6-e92063?style=for-the-badge&logo=pydantic&logoColor=white
|
||||||
[pydantic-url]: https://docs.pydantic.dev/latest/
|
[pydantic-url]: https://docs.pydantic.dev/latest/
|
||||||
[sqlalchemy.org]: https://img.shields.io/badge/SQLAlchemy-2.0.36-bb0000?color=bb0000&style=for-the-badge
|
[sqlalchemy.org]: https://img.shields.io/badge/SQLAlchemy-2.0.38-bb0000?color=bb0000&style=for-the-badge
|
||||||
[sqlalchemy-url]: https://docs.sqlalchemy.org/en/20/
|
[sqlalchemy-url]: https://docs.sqlalchemy.org/en/20/
|
||||||
[uvicorn.org]: https://img.shields.io/badge/Uvicorn-0.34.0-2094f3?style=for-the-badge&logo=uvicorn&logoColor=white
|
[uvicorn.org]: https://img.shields.io/badge/Uvicorn-0.34.0-2094f3?style=for-the-badge&logo=uvicorn&logoColor=white
|
||||||
[uvicorn-url]: https://www.uvicorn.org/
|
[uvicorn-url]: https://www.uvicorn.org/
|
||||||
[asyncpg.github.io]: https://img.shields.io/badge/asyncpg-0.30.0-2e6fce?style=for-the-badge&logo=postgresql&logoColor=white
|
[asyncpg.github.io]: https://img.shields.io/badge/asyncpg-0.30.0-2e6fce?style=for-the-badge&logo=postgresql&logoColor=white
|
||||||
[asyncpg-url]: https://magicstack.github.io/asyncpg/current/
|
[asyncpg-url]: https://magicstack.github.io/asyncpg/current/
|
||||||
[pytest.org]: https://img.shields.io/badge/pytest-8.3.4-fff?style=for-the-badge&logo=pytest&logoColor=white
|
[pytest.org]: https://img.shields.io/badge/pytest-8.3.5-fff?style=for-the-badge&logo=pytest&logoColor=white
|
||||||
[pytest-url]: https://docs.pytest.org/en/6.2.x/
|
[pytest-url]: https://docs.pytest.org/en/6.2.x/
|
||||||
[alembic.sqlalchemy.org]: https://img.shields.io/badge/alembic-1.14.0-6BA81E?style=for-the-badge&logo=alembic&logoColor=white
|
[alembic.sqlalchemy.org]: https://img.shields.io/badge/alembic-1.15.1-6BA81E?style=for-the-badge&logo=alembic&logoColor=white
|
||||||
[alembic-url]: https://alembic.sqlalchemy.org/en/latest/
|
[alembic-url]: https://alembic.sqlalchemy.org/en/latest/
|
||||||
[rich.readthedocs.io]: https://img.shields.io/badge/rich-13.9.4-009485?style=for-the-badge&logo=rich&logoColor=white
|
[rich.readthedocs.io]: https://img.shields.io/badge/rich-13.9.4-009485?style=for-the-badge&logo=rich&logoColor=white
|
||||||
[rich-url]: https://rich.readthedocs.io/en/latest/
|
[rich-url]: https://rich.readthedocs.io/en/latest/
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import APIRouter, status, Request, Depends, Query
|
from fastapi import APIRouter, Depends, Query, Request, status
|
||||||
from pydantic import EmailStr
|
from pydantic import EmailStr
|
||||||
from starlette.concurrency import run_in_threadpool
|
from starlette.concurrency import run_in_threadpool
|
||||||
|
|
||||||
from app.services.smtp import SMTPEmailService
|
from app.services.smtp import SMTPEmailService
|
||||||
|
|
||||||
from app.utils.logging import AppLogger
|
from app.utils.logging import AppLogger
|
||||||
|
|
||||||
logger = AppLogger().get_logger()
|
logger = AppLogger().get_logger()
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import io
|
import io
|
||||||
from fastapi import APIRouter, Depends, status, UploadFile, HTTPException
|
|
||||||
from sqlalchemy.exc import SQLAlchemyError
|
|
||||||
import polars as pl
|
import polars as pl
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, UploadFile, status
|
||||||
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from app.database import get_db
|
from app.database import get_db
|
||||||
|
@ -3,8 +3,6 @@ from typing import Annotated
|
|||||||
from fastapi import APIRouter, Depends, Query
|
from fastapi import APIRouter, Depends, Query
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from fastapi_cache.decorator import cache
|
|
||||||
|
|
||||||
from app.database import get_db
|
from app.database import get_db
|
||||||
from app.models.shakespeare import Paragraph
|
from app.models.shakespeare import Paragraph
|
||||||
|
|
||||||
@ -14,7 +12,6 @@ router = APIRouter(prefix="/v1/shakespeare")
|
|||||||
@router.get(
|
@router.get(
|
||||||
"/",
|
"/",
|
||||||
)
|
)
|
||||||
@cache(namespace="test-2", expire=60)
|
|
||||||
async def find_paragraph(
|
async def find_paragraph(
|
||||||
character: Annotated[str, Query(description="Character name")],
|
character: Annotated[str, Query(description="Character name")],
|
||||||
db_session: AsyncSession = Depends(get_db),
|
db_session: AsyncSession = Depends(get_db),
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from fastapi import APIRouter, Depends, HTTPException, status, Request
|
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
||||||
from sqlalchemy.exc import SQLAlchemyError
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, status, Request, HTTPException, Form
|
from fastapi import APIRouter, Depends, Form, HTTPException, Request, status
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from app.database import get_db
|
from app.database import get_db
|
||||||
from app.models.user import User
|
from app.models.user import User
|
||||||
from app.schemas.user import UserSchema, UserResponse, UserLogin, TokenResponse
|
from app.schemas.user import TokenResponse, UserLogin, UserResponse, UserSchema
|
||||||
from app.services.auth import create_access_token
|
from app.services.auth import create_access_token
|
||||||
from app.utils.logging import AppLogger
|
from app.utils.logging import AppLogger
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
from pydantic import PostgresDsn, RedisDsn, computed_field, BaseModel
|
from pydantic import BaseModel, PostgresDsn, RedisDsn, computed_field
|
||||||
from pydantic_core import MultiHostUrl
|
from pydantic_core import MultiHostUrl
|
||||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
from collections.abc import AsyncGenerator
|
from collections.abc import AsyncGenerator
|
||||||
|
|
||||||
from sqlalchemy.ext.asyncio import create_async_engine
|
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
|
||||||
from sqlalchemy.ext.asyncio import async_sessionmaker
|
|
||||||
|
|
||||||
from app.config import settings as global_settings
|
from app.config import settings as global_settings
|
||||||
from app.utils.logging import AppLogger
|
from app.utils.logging import AppLogger
|
||||||
|
30
app/main.py
30
app/main.py
@ -1,25 +1,22 @@
|
|||||||
import asyncpg
|
from contextlib import asynccontextmanager
|
||||||
from apscheduler.eventbrokers.redis import RedisEventBroker
|
|
||||||
from apscheduler.datastores.sqlalchemy import SQLAlchemyDataStore
|
|
||||||
from fastapi import FastAPI, Depends
|
|
||||||
from fastapi_cache import FastAPICache
|
|
||||||
from fastapi_cache.backends.redis import RedisBackend
|
|
||||||
|
|
||||||
|
import asyncpg
|
||||||
|
from apscheduler import AsyncScheduler
|
||||||
|
from apscheduler.datastores.sqlalchemy import SQLAlchemyDataStore
|
||||||
|
from apscheduler.eventbrokers.redis import RedisEventBroker
|
||||||
|
from fastapi import Depends, FastAPI
|
||||||
|
|
||||||
|
from app.api.health import router as health_router
|
||||||
from app.api.nonsense import router as nonsense_router
|
from app.api.nonsense import router as nonsense_router
|
||||||
from app.api.shakespeare import router as shakespeare_router
|
from app.api.shakespeare import router as shakespeare_router
|
||||||
from app.api.stuff import router as stuff_router
|
from app.api.stuff import router as stuff_router
|
||||||
|
from app.api.user import router as user_router
|
||||||
from app.config import settings as global_settings
|
from app.config import settings as global_settings
|
||||||
from app.database import engine
|
from app.database import engine
|
||||||
from app.utils.logging import AppLogger
|
from app.redis import get_redis
|
||||||
from app.api.user import router as user_router
|
|
||||||
from app.api.health import router as health_router
|
|
||||||
from app.redis import get_redis, get_cache
|
|
||||||
from app.services.auth import AuthBearer
|
from app.services.auth import AuthBearer
|
||||||
from app.services.scheduler import SchedulerMiddleware
|
from app.services.scheduler import SchedulerMiddleware
|
||||||
|
from app.utils.logging import AppLogger
|
||||||
from contextlib import asynccontextmanager
|
|
||||||
|
|
||||||
from apscheduler import AsyncScheduler
|
|
||||||
|
|
||||||
logger = AppLogger().get_logger()
|
logger = AppLogger().get_logger()
|
||||||
|
|
||||||
@ -32,10 +29,7 @@ async def lifespan(_app: FastAPI):
|
|||||||
_postgres_dsn = global_settings.postgres_url.unicode_string()
|
_postgres_dsn = global_settings.postgres_url.unicode_string()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Initialize the cache with the redis connection
|
# TODO: cache with the redis connection
|
||||||
redis_cache = await get_cache()
|
|
||||||
FastAPICache.init(RedisBackend(redis_cache), prefix="fastapi-cache")
|
|
||||||
# logger.info(FastAPICache.get_cache_status_header())
|
|
||||||
# Initialize the postgres connection pool
|
# Initialize the postgres connection pool
|
||||||
_app.postgres_pool = await asyncpg.create_pool(
|
_app.postgres_pool = await asyncpg.create_pool(
|
||||||
dsn=_postgres_dsn,
|
dsn=_postgres_dsn,
|
||||||
|
@ -2,9 +2,10 @@ from typing import Any
|
|||||||
|
|
||||||
from asyncpg import UniqueViolationError
|
from asyncpg import UniqueViolationError
|
||||||
from fastapi import HTTPException, status
|
from fastapi import HTTPException, status
|
||||||
from sqlalchemy.exc import SQLAlchemyError, IntegrityError
|
from sqlalchemy.exc import IntegrityError, SQLAlchemyError
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy.orm import declared_attr, DeclarativeBase
|
from sqlalchemy.orm import DeclarativeBase, declared_attr
|
||||||
|
|
||||||
from app.utils.logging import AppLogger
|
from app.utils.logging import AppLogger
|
||||||
|
|
||||||
logger = AppLogger().get_logger()
|
logger = AppLogger().get_logger()
|
||||||
|
@ -4,7 +4,7 @@ from fastapi import HTTPException, status
|
|||||||
from sqlalchemy import String, select
|
from sqlalchemy import String, select
|
||||||
from sqlalchemy.dialects.postgresql import UUID
|
from sqlalchemy.dialects.postgresql import UUID
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy.orm import mapped_column, Mapped
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
from app.models.base import Base
|
from app.models.base import Base
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@ from sqlalchemy import (
|
|||||||
)
|
)
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
from app.models.base import Base
|
from app.models.base import Base
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from sqlalchemy import String, select, ForeignKey
|
from sqlalchemy import ForeignKey, String, select
|
||||||
from sqlalchemy.dialects.postgresql import UUID
|
from sqlalchemy.dialects.postgresql import UUID
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy.orm import mapped_column, Mapped, relationship, joinedload
|
from sqlalchemy.orm import Mapped, joinedload, mapped_column, relationship
|
||||||
|
|
||||||
from app.models.base import Base
|
from app.models.base import Base
|
||||||
from app.models.nonsense import Nonsense
|
from app.models.nonsense import Nonsense
|
||||||
|
@ -3,10 +3,10 @@ from typing import Any
|
|||||||
|
|
||||||
import bcrypt
|
import bcrypt
|
||||||
from pydantic import SecretStr
|
from pydantic import SecretStr
|
||||||
from sqlalchemy import String, LargeBinary, select, Column
|
from sqlalchemy import Column, LargeBinary, String, select
|
||||||
from sqlalchemy.dialects.postgresql import UUID
|
from sqlalchemy.dialects.postgresql import UUID
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy.orm import mapped_column, Mapped
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
from app.models.base import Base
|
from app.models.base import Base
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from pydantic import BaseModel, Field, ConfigDict
|
from pydantic import BaseModel, ConfigDict, Field
|
||||||
|
|
||||||
config = ConfigDict(from_attributes=True)
|
config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from pydantic import BaseModel, Field, ConfigDict
|
from pydantic import BaseModel, ConfigDict, Field
|
||||||
|
|
||||||
config = ConfigDict(from_attributes=True)
|
config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from pydantic import BaseModel, Field, EmailStr, ConfigDict, SecretStr
|
from pydantic import BaseModel, ConfigDict, EmailStr, Field, SecretStr
|
||||||
|
|
||||||
config = ConfigDict(from_attributes=True)
|
config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import time
|
import time
|
||||||
|
|
||||||
import jwt
|
import jwt
|
||||||
|
from fastapi import HTTPException, Request
|
||||||
|
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
||||||
|
|
||||||
from app.config import settings as global_settings
|
from app.config import settings as global_settings
|
||||||
from app.models.user import User
|
from app.models.user import User
|
||||||
|
|
||||||
from fastapi import Request, HTTPException
|
|
||||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
|
||||||
from app.utils.logging import AppLogger
|
from app.utils.logging import AppLogger
|
||||||
|
|
||||||
logger = AppLogger().get_logger()
|
logger = AppLogger().get_logger()
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from attrs import define
|
|
||||||
|
|
||||||
from sqlalchemy import text
|
|
||||||
from starlette.types import ASGIApp, Receive, Scope, Send
|
|
||||||
from apscheduler import AsyncScheduler
|
from apscheduler import AsyncScheduler
|
||||||
from apscheduler.triggers.interval import IntervalTrigger
|
from apscheduler.triggers.interval import IntervalTrigger
|
||||||
|
from attrs import define
|
||||||
|
from sqlalchemy import text
|
||||||
|
from starlette.types import ASGIApp, Receive, Scope, Send
|
||||||
|
|
||||||
from app.database import AsyncSessionFactory
|
from app.database import AsyncSessionFactory
|
||||||
from app.utils.logging import AppLogger
|
from app.utils.logging import AppLogger
|
||||||
|
@ -1,18 +1,15 @@
|
|||||||
from attrs import define, field
|
|
||||||
import smtplib
|
import smtplib
|
||||||
from email.mime.multipart import MIMEMultipart
|
from email.mime.multipart import MIMEMultipart
|
||||||
from email.mime.text import MIMEText
|
from email.mime.text import MIMEText
|
||||||
|
|
||||||
from app.config import settings as global_settings
|
from attrs import define, field
|
||||||
|
|
||||||
from fastapi.templating import Jinja2Templates
|
from fastapi.templating import Jinja2Templates
|
||||||
|
|
||||||
from pydantic import EmailStr
|
from pydantic import EmailStr
|
||||||
|
|
||||||
|
from app.config import settings as global_settings
|
||||||
from app.utils.logging import AppLogger
|
from app.utils.logging import AppLogger
|
||||||
from app.utils.singleton import SingletonMetaNoArgs
|
from app.utils.singleton import SingletonMetaNoArgs
|
||||||
|
|
||||||
|
|
||||||
logger = AppLogger().get_logger()
|
logger = AppLogger().get_logger()
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
from sqlalchemy.dialects import postgresql
|
|
||||||
|
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
|
||||||
def compile_sql_or_scalar(func):
|
def compile_sql_or_scalar(func):
|
||||||
"""
|
"""
|
||||||
|
@ -3,7 +3,6 @@ import logging
|
|||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from rich.logging import RichHandler
|
from rich.logging import RichHandler
|
||||||
|
|
||||||
|
|
||||||
from app.utils.singleton import SingletonMeta
|
from app.utils.singleton import SingletonMeta
|
||||||
|
|
||||||
|
|
||||||
@ -21,5 +20,5 @@ class RichConsoleHandler(RichHandler):
|
|||||||
def __init__(self, width=200, style=None, **kwargs):
|
def __init__(self, width=200, style=None, **kwargs):
|
||||||
super().__init__(
|
super().__init__(
|
||||||
console=Console(color_system="256", width=width, style=style, stderr=True),
|
console=Console(color_system="256", width=width, style=style, stderr=True),
|
||||||
**kwargs
|
**kwargs,
|
||||||
)
|
)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# pull official base image
|
# pull official base image
|
||||||
FROM postgres:16-alpine
|
FROM postgres:17.4-alpine
|
||||||
|
|
||||||
# run create.sql on init
|
# run create.sql on init
|
||||||
ADD create.sql /docker-entrypoint-initdb.d
|
ADD create.sql /docker-entrypoint-initdb.d
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from locust import HttpUser, task, between
|
from locust import HttpUser, between, task
|
||||||
|
|
||||||
|
|
||||||
class Stuff(HttpUser):
|
class Stuff(HttpUser):
|
||||||
|
4288
poetry.lock
generated
4288
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
90
pyproject.old
Normal file
90
pyproject.old
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
[tool.poetry]
|
||||||
|
name = "fastapi-sqlalchemy-asyncpg"
|
||||||
|
version = "0.0.17"
|
||||||
|
description = ""
|
||||||
|
authors = ["Jakub Miazek <the@grillazz.com>"]
|
||||||
|
packages = []
|
||||||
|
license = "MIT"
|
||||||
|
package-mode = false
|
||||||
|
|
||||||
|
[tool.poetry.dependencies]
|
||||||
|
python = "^3.13"
|
||||||
|
fastapi = {version = "^0.115.6", extras = ["all"]}
|
||||||
|
pydantic = {version = "^2.10.3", extras = ["email"]}
|
||||||
|
pydantic-settings = "^2.7.0"
|
||||||
|
sqlalchemy = "^2.0.36"
|
||||||
|
uvicorn = { version = "^0.34.0", extras = ["standard"]}
|
||||||
|
asyncpg = "^0.30.0"
|
||||||
|
alembic = "^1.14.0"
|
||||||
|
httpx = "^0.28.1"
|
||||||
|
pytest = "^8.3.4"
|
||||||
|
pytest-cov = "^6.0.0"
|
||||||
|
uvloop = "^0.21.0"
|
||||||
|
httptools = "^0.6.4"
|
||||||
|
rich = "^13.9.4"
|
||||||
|
pyjwt = {version = "^2.10.1", extras = ["cryptography"]}
|
||||||
|
redis = "^5.2.1"
|
||||||
|
bcrypt = "^4.2.1"
|
||||||
|
polars = "^1.17.1"
|
||||||
|
python-multipart = "^0.0.20"
|
||||||
|
fastexcel = "^0.12.0"
|
||||||
|
fastapi-cache2 = "^0.2.1"
|
||||||
|
inline-snapshot = "^0.17.0"
|
||||||
|
dirty-equals = "^0.8.0"
|
||||||
|
polyfactory = "^2.18.1"
|
||||||
|
granian = "^1.7.0"
|
||||||
|
apscheduler = {version = "^4.0.0a5", extras = ["redis,sqlalchemy"]}
|
||||||
|
pendulum = {git = "https://github.com/sdispater/pendulum.git", rev="develop"}
|
||||||
|
|
||||||
|
[tool.poetry.group.dev.dependencies]
|
||||||
|
devtools = { extras = ["pygments"], version = "^0.12.2" }
|
||||||
|
safety = "*"
|
||||||
|
pyupgrade = "*"
|
||||||
|
ipython = "^8.26.0"
|
||||||
|
ruff = "^0.6.1"
|
||||||
|
sqlacodegen = "^3.0.0rc5"
|
||||||
|
tryceratops = "^2.3.3"
|
||||||
|
locust = "^2.31.3"
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["poetry-core>=1.0.0"]
|
||||||
|
build-backend = "poetry.core.masonry.api"
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
line-length = 120
|
||||||
|
indent-width = 4
|
||||||
|
|
||||||
|
lint.select = ["E", "F", "UP", "N", "C", "B"]
|
||||||
|
lint.ignore = ["E501"]
|
||||||
|
|
||||||
|
# Exclude a variety of commonly ignored directories.
|
||||||
|
exclude = ["alembic",]
|
||||||
|
# Assume Python 3.13
|
||||||
|
target-version = "py313"
|
||||||
|
|
||||||
|
[tool.ruff.lint.flake8-quotes]
|
||||||
|
docstring-quotes = "double"
|
||||||
|
|
||||||
|
[tool.ruff.lint.flake8-bugbear]
|
||||||
|
extend-immutable-calls = ["fastapi.Depends",]
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
addopts = "-v --doctest-modules --doctest-glob=*.md --ignore=alembic"
|
||||||
|
asyncio_mode = "strict"
|
||||||
|
env_files = [".env"]
|
||||||
|
|
||||||
|
[tool.tryceratops]
|
||||||
|
exclude = ["alembic",]
|
||||||
|
|
||||||
|
[tool.ruff.format]
|
||||||
|
# Like Black, use double quotes for strings.
|
||||||
|
quote-style = "double"
|
||||||
|
|
||||||
|
# Like Black, indent with spaces, rather than tabs.
|
||||||
|
indent-style = "space"
|
||||||
|
|
||||||
|
# Like Black, respect magic trailing commas.
|
||||||
|
skip-magic-trailing-comma = false
|
||||||
|
|
||||||
|
# Like Black, automatically detect the appropriate line ending.
|
||||||
|
line-ending = "auto"
|
147
pyproject.toml
147
pyproject.toml
@ -1,90 +1,75 @@
|
|||||||
[tool.poetry]
|
[project]
|
||||||
name = "fastapi-sqlalchemy-asyncpg"
|
name = "fastapi-sqlalchemy-asyncpg"
|
||||||
version = "0.0.17"
|
version = "0.1.0"
|
||||||
description = ""
|
description = "A modern FastAPI application with SQLAlchemy 2.0 and AsyncPG for high-performance async database operations. Features include JWT authentication with Redis token storage, password hashing, connection pooling, data processing with Polars, Rich logging, task scheduling with APScheduler, and Shakespeare datasets integration."
|
||||||
authors = ["Jakub Miazek <the@grillazz.com>"]
|
readme = "README.md"
|
||||||
packages = []
|
requires-python = ">=3.13"
|
||||||
license = "MIT"
|
dependencies = [
|
||||||
package-mode = false
|
"fastapi[all]>=0.115.11",
|
||||||
|
"pydantic[email]>=2.10.6",
|
||||||
|
"pydantic-settings>=2.8.1",
|
||||||
|
"sqlalchemy>=2.0.38",
|
||||||
|
"uvicorn[standard]>=0.34.0",
|
||||||
|
"asyncpg>=0.30.0",
|
||||||
|
"alembic>=1.15.1",
|
||||||
|
"httpx>=0.28.1",
|
||||||
|
"pytest>=8.3.5",
|
||||||
|
"pytest-cov>=6.0.0",
|
||||||
|
"uvloop>=0.21.0",
|
||||||
|
"httptools>=0.6.4",
|
||||||
|
"rich>=13.9.4",
|
||||||
|
"pyjwt>=2.10.1",
|
||||||
|
"redis>=5.2.1",
|
||||||
|
"bcrypt>=4.3.0",
|
||||||
|
"polars>=1.24.0",
|
||||||
|
"python-multipart>=0.0.20",
|
||||||
|
"fastexcel>=0.13.0",
|
||||||
|
"inline-snapshot>=0.17.0",
|
||||||
|
"dirty-equals>=0.8.0",
|
||||||
|
"polyfactory>=2.18.1",
|
||||||
|
"granian>=1.7.0",
|
||||||
|
"apscheduler[redis,sqlalchemy]>=4.0.0a5",
|
||||||
|
]
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
[tool.uv]
|
||||||
python = "^3.13"
|
dev-dependencies = [
|
||||||
fastapi = {version = "^0.115.6", extras = ["all"]}
|
"ruff>=0.9.10",
|
||||||
pydantic = {version = "^2.10.3", extras = ["email"]}
|
"devtools[pygments]>=0.12.2",
|
||||||
pydantic-settings = "^2.7.0"
|
"pyupgrade>=3.19.1",
|
||||||
sqlalchemy = "^2.0.36"
|
"ipython>=9.0.2",
|
||||||
uvicorn = { version = "^0.34.0", extras = ["standard"]}
|
"sqlacodegen>=3.0.0",
|
||||||
asyncpg = "^0.30.0"
|
"tryceratops>=2.4.1",
|
||||||
alembic = "^1.14.0"
|
"locust>=2.33.0"
|
||||||
httpx = "^0.28.1"
|
|
||||||
pytest = "^8.3.4"
|
|
||||||
pytest-cov = "^6.0.0"
|
|
||||||
uvloop = "^0.21.0"
|
|
||||||
httptools = "^0.6.4"
|
|
||||||
rich = "^13.9.4"
|
|
||||||
pyjwt = {version = "^2.10.1", extras = ["cryptography"]}
|
|
||||||
redis = "^5.2.1"
|
|
||||||
bcrypt = "^4.2.1"
|
|
||||||
polars = "^1.17.1"
|
|
||||||
python-multipart = "^0.0.20"
|
|
||||||
fastexcel = "^0.12.0"
|
|
||||||
fastapi-cache2 = "^0.2.1"
|
|
||||||
inline-snapshot = "^0.17.0"
|
|
||||||
dirty-equals = "^0.8.0"
|
|
||||||
polyfactory = "^2.18.1"
|
|
||||||
granian = "^1.7.0"
|
|
||||||
apscheduler = {version = "^4.0.0a5", extras = ["redis,sqlalchemy"]}
|
|
||||||
pendulum = {git = "https://github.com/sdispater/pendulum.git", rev="develop"}
|
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
]
|
||||||
devtools = { extras = ["pygments"], version = "^0.12.2" }
|
|
||||||
safety = "*"
|
|
||||||
pyupgrade = "*"
|
|
||||||
ipython = "^8.26.0"
|
|
||||||
ruff = "^0.6.1"
|
|
||||||
sqlacodegen = "^3.0.0rc5"
|
|
||||||
tryceratops = "^2.3.3"
|
|
||||||
locust = "^2.31.3"
|
|
||||||
|
|
||||||
[build-system]
|
|
||||||
requires = ["poetry-core>=1.0.0"]
|
[tool.mypy]
|
||||||
build-backend = "poetry.core.masonry.api"
|
strict = true
|
||||||
|
exclude = ["venv", ".venv", "alembic"]
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
line-length = 120
|
|
||||||
indent-width = 4
|
|
||||||
|
|
||||||
lint.select = ["E", "F", "UP", "N", "C", "B"]
|
|
||||||
lint.ignore = ["E501"]
|
|
||||||
|
|
||||||
# Exclude a variety of commonly ignored directories.
|
|
||||||
exclude = ["alembic",]
|
|
||||||
# Assume Python 3.13
|
|
||||||
target-version = "py313"
|
target-version = "py313"
|
||||||
|
exclude = ["alembic"]
|
||||||
|
|
||||||
[tool.ruff.lint.flake8-quotes]
|
[tool.ruff.lint]
|
||||||
docstring-quotes = "double"
|
select = [
|
||||||
|
"E", # pycodestyle errors
|
||||||
|
"W", # pycodestyle warnings
|
||||||
|
"F", # pyflakes
|
||||||
|
"I", # isort
|
||||||
|
"B", # flake8-bugbear
|
||||||
|
"C4", # flake8-comprehensions
|
||||||
|
"UP", # pyupgrade
|
||||||
|
"ARG001", # unused arguments in functions
|
||||||
|
]
|
||||||
|
ignore = [
|
||||||
|
"E501", # line too long, handled by black
|
||||||
|
"B008", # do not perform function calls in argument defaults
|
||||||
|
"W191", # indentation contains tabs
|
||||||
|
"B904", # Allow raising exceptions without from e, for HTTPException
|
||||||
|
]
|
||||||
|
|
||||||
[tool.ruff.lint.flake8-bugbear]
|
[tool.ruff.lint.pyupgrade]
|
||||||
extend-immutable-calls = ["fastapi.Depends",]
|
# Preserve types, even if a file imports `from __future__ import annotations`.
|
||||||
|
keep-runtime-typing = true
|
||||||
[tool.pytest.ini_options]
|
|
||||||
addopts = "-v --doctest-modules --doctest-glob=*.md --ignore=alembic"
|
|
||||||
asyncio_mode = "strict"
|
|
||||||
env_files = [".env"]
|
|
||||||
|
|
||||||
[tool.tryceratops]
|
|
||||||
exclude = ["alembic",]
|
|
||||||
|
|
||||||
[tool.ruff.format]
|
|
||||||
# Like Black, use double quotes for strings.
|
|
||||||
quote-style = "double"
|
|
||||||
|
|
||||||
# Like Black, indent with spaces, rather than tabs.
|
|
||||||
indent-style = "space"
|
|
||||||
|
|
||||||
# Like Black, respect magic trailing commas.
|
|
||||||
skip-magic-trailing-comma = false
|
|
||||||
|
|
||||||
# Like Black, automatically detect the appropriate line ending.
|
|
||||||
line-ending = "auto"
|
|
@ -1,9 +1,9 @@
|
|||||||
import pytest
|
|
||||||
from httpx import AsyncClient
|
|
||||||
from starlette import status
|
|
||||||
import jwt
|
import jwt
|
||||||
|
import pytest
|
||||||
|
from dirty_equals import IsPositiveFloat, IsStr, IsUUID
|
||||||
|
from httpx import AsyncClient
|
||||||
from inline_snapshot import snapshot
|
from inline_snapshot import snapshot
|
||||||
from dirty_equals import IsStr, IsUUID, IsPositiveFloat
|
from starlette import status
|
||||||
|
|
||||||
pytestmark = pytest.mark.anyio
|
pytestmark = pytest.mark.anyio
|
||||||
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
from anyio import Path
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from anyio import Path
|
||||||
from fastapi import status
|
from fastapi import status
|
||||||
from httpx import AsyncClient
|
from httpx import AsyncClient
|
||||||
|
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
from dirty_equals import IsUUID
|
||||||
from fastapi import status
|
from fastapi import status
|
||||||
from httpx import AsyncClient
|
from httpx import AsyncClient
|
||||||
from inline_snapshot import snapshot
|
from inline_snapshot import snapshot
|
||||||
from dirty_equals import IsUUID
|
|
||||||
|
|
||||||
from polyfactory.factories.pydantic_factory import ModelFactory
|
from polyfactory.factories.pydantic_factory import ModelFactory
|
||||||
|
|
||||||
from app.schemas.stuff import StuffSchema
|
from app.schemas.stuff import StuffSchema
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
|
from collections.abc import AsyncGenerator
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from httpx import AsyncClient, ASGITransport
|
from httpx import ASGITransport, AsyncClient
|
||||||
|
|
||||||
from app.database import engine
|
from app.database import engine
|
||||||
from app.main import app
|
from app.main import app
|
||||||
@ -28,13 +31,11 @@ async def start_db():
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
async def client(start_db) -> AsyncClient:
|
async def client(start_db) -> AsyncGenerator[AsyncClient, Any]: # noqa: ARG001
|
||||||
|
|
||||||
transport = ASGITransport(
|
transport = ASGITransport(
|
||||||
app=app,
|
app=app,
|
||||||
)
|
)
|
||||||
async with AsyncClient(
|
async with AsyncClient(
|
||||||
# app=app,
|
|
||||||
base_url="http://testserver/v1",
|
base_url="http://testserver/v1",
|
||||||
headers={"Content-Type": "application/json"},
|
headers={"Content-Type": "application/json"},
|
||||||
transport=transport,
|
transport=transport,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user