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
|
||||
|
||||
jobs:
|
||||
test:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: [ "3.13" ]
|
||||
poetry-version: [ "1.8.5" ]
|
||||
|
||||
env:
|
||||
PYTHONDONTWRITEBYTECODE: 1
|
||||
PYTHONUNBUFFERED: 1
|
||||
POSTGRES_DB: testdb
|
||||
POSTGRES_HOST: 127.0.0.1
|
||||
POSTGRES_USER: app-user
|
||||
POSTGRES_USER: panettone
|
||||
POSTGRES_PASSWORD: secret
|
||||
PGPASSWORD: secret
|
||||
REDIS_HOST: 127.0.0.1
|
||||
@ -37,7 +36,7 @@ jobs:
|
||||
sqldb:
|
||||
image: postgres:16
|
||||
env:
|
||||
POSTGRES_USER: app-user
|
||||
POSTGRES_USER: panettone
|
||||
POSTGRES_PASSWORD: secret
|
||||
POSTGRES_DB: testdb
|
||||
ports:
|
||||
@ -48,18 +47,17 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- 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;"
|
||||
- uses: actions/setup-python@v5
|
||||
run: PGPASSWORD=secret psql -h 127.0.0.1 -d testdb -U panettone -c "CREATE SCHEMA shakespeare; CREATE SCHEMA happy_hog;"
|
||||
|
||||
- name: Install the latest version of uv and set the python version
|
||||
uses: astral-sh/setup-uv@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install Poetry
|
||||
uses: abatilo/actions-poetry@v3
|
||||
with:
|
||||
poetry-version: ${{ matrix.poetry-version }}
|
||||
- name: Install dependencies
|
||||
if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
|
||||
run: poetry install --no-interaction --no-root
|
||||
- name: Test Code
|
||||
run: poetry run pytest tests/
|
||||
- name: Lint Code
|
||||
run: poetry run ruff check .
|
||||
|
||||
- name: Lint with ruff
|
||||
run: uv run --frozen ruff check .
|
||||
|
||||
- name: Test with python ${{ matrix.python-version }}
|
||||
run: uv run --frozen pytest
|
||||
|
||||
|
||||
|
87
Dockerfile
87
Dockerfile
@ -1,43 +1,62 @@
|
||||
FROM python:3.13-slim-bookworm AS base
|
||||
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 ubuntu:oracular AS build
|
||||
|
||||
FROM base AS install
|
||||
WORKDIR /home/code
|
||||
RUN apt-get update -qy && apt-get install -qyy \
|
||||
-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
|
||||
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 ./
|
||||
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
|
||||
|
||||
# install without virtualenv, since we are inside a container
|
||||
RUN poetry config virtualenvs.create false \
|
||||
&& poetry install $INSTALL_ARGS
|
||||
ENV UV_LINK_MODE=copy \
|
||||
UV_COMPILE_BYTECODE=1 \
|
||||
UV_PYTHON_DOWNLOADS=never \
|
||||
UV_PYTHON=python3.13 \
|
||||
UV_PROJECT_ENVIRONMENT=/panettone
|
||||
|
||||
# cleanup
|
||||
RUN curl -sSL https://install.python-poetry.org | python3 - --uninstall
|
||||
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/*
|
||||
COPY pyproject.toml /_lock/
|
||||
COPY uv.lock /_lock/
|
||||
|
||||
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/
|
||||
COPY app/ app/
|
||||
COPY alembic/ alembic/
|
||||
COPY .env alembic.ini ./
|
||||
RUN groupadd -r panettone
|
||||
RUN useradd -r -d /panettone -g panettone -N panettone
|
||||
|
||||
# create a non-root user and switch to it, for security.
|
||||
RUN addgroup --system --gid 1001 "app-user"
|
||||
RUN adduser --system --uid 1001 "app-user"
|
||||
USER "app-user"
|
||||
STOPSIGNAL SIGINT
|
||||
|
||||
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
|
||||
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
|
||||
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="#rainbow-logs-with-rich">Rainbow logs with rich</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="#worker-aware-async-scheduler">Schedule jobs</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>
|
||||
</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,
|
||||
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,
|
||||
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,
|
||||
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,
|
||||
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,
|
||||
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
|
||||
```
|
||||
|
||||
|
||||
### Adjust make with just
|
||||
[//]: # (TODO: switch form make to just)
|
||||
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
||||
|
||||
### 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.
|
||||
|
||||
### Local development with poetry
|
||||
|
||||
### Setup local env with uv
|
||||
```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
|
||||
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.
|
||||
|
||||
|
||||
<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>
|
||||
|
||||
## 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:
|
||||
- **[DEC 16 2024]** bump project to Python 3.13 :fast_forward:
|
||||
- **[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>
|
||||
|
||||
@ -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-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/
|
||||
[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/
|
||||
[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/
|
||||
[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/
|
||||
[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/
|
||||
[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/
|
||||
[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/
|
||||
[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/
|
||||
|
@ -1,12 +1,11 @@
|
||||
import logging
|
||||
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 starlette.concurrency import run_in_threadpool
|
||||
|
||||
from app.services.smtp import SMTPEmailService
|
||||
|
||||
from app.utils.logging import AppLogger
|
||||
|
||||
logger = AppLogger().get_logger()
|
||||
|
@ -1,7 +1,8 @@
|
||||
import io
|
||||
from fastapi import APIRouter, Depends, status, UploadFile, HTTPException
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
|
||||
import polars as pl
|
||||
from fastapi import APIRouter, Depends, HTTPException, UploadFile, status
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database import get_db
|
||||
|
@ -3,8 +3,6 @@ from typing import Annotated
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from fastapi_cache.decorator import cache
|
||||
|
||||
from app.database import get_db
|
||||
from app.models.shakespeare import Paragraph
|
||||
|
||||
@ -14,7 +12,6 @@ router = APIRouter(prefix="/v1/shakespeare")
|
||||
@router.get(
|
||||
"/",
|
||||
)
|
||||
@cache(namespace="test-2", expire=60)
|
||||
async def find_paragraph(
|
||||
character: Annotated[str, Query(description="Character name")],
|
||||
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.ext.asyncio import AsyncSession
|
||||
|
||||
|
@ -1,11 +1,11 @@
|
||||
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 app.database import get_db
|
||||
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.utils.logging import AppLogger
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
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_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
from collections.abc import AsyncGenerator
|
||||
|
||||
from sqlalchemy.ext.asyncio import create_async_engine
|
||||
from sqlalchemy.ext.asyncio import async_sessionmaker
|
||||
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
|
||||
|
||||
from app.config import settings as global_settings
|
||||
from app.utils.logging import AppLogger
|
||||
|
30
app/main.py
30
app/main.py
@ -1,25 +1,22 @@
|
||||
import asyncpg
|
||||
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
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
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.shakespeare import router as shakespeare_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.database import engine
|
||||
from app.utils.logging import AppLogger
|
||||
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.redis import get_redis
|
||||
from app.services.auth import AuthBearer
|
||||
from app.services.scheduler import SchedulerMiddleware
|
||||
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from apscheduler import AsyncScheduler
|
||||
from app.utils.logging import AppLogger
|
||||
|
||||
logger = AppLogger().get_logger()
|
||||
|
||||
@ -32,10 +29,7 @@ async def lifespan(_app: FastAPI):
|
||||
_postgres_dsn = global_settings.postgres_url.unicode_string()
|
||||
|
||||
try:
|
||||
# Initialize the 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())
|
||||
# TODO: cache with the redis connection
|
||||
# Initialize the postgres connection pool
|
||||
_app.postgres_pool = await asyncpg.create_pool(
|
||||
dsn=_postgres_dsn,
|
||||
|
@ -2,9 +2,10 @@ from typing import Any
|
||||
|
||||
from asyncpg import UniqueViolationError
|
||||
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.orm import declared_attr, DeclarativeBase
|
||||
from sqlalchemy.orm import DeclarativeBase, declared_attr
|
||||
|
||||
from app.utils.logging import AppLogger
|
||||
|
||||
logger = AppLogger().get_logger()
|
||||
|
@ -4,7 +4,7 @@ from fastapi import HTTPException, status
|
||||
from sqlalchemy import String, select
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
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
|
||||
|
||||
|
@ -11,6 +11,7 @@ from sqlalchemy import (
|
||||
)
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.models.base import Base
|
||||
|
||||
|
||||
|
@ -1,9 +1,9 @@
|
||||
import uuid
|
||||
|
||||
from sqlalchemy import String, select, ForeignKey
|
||||
from sqlalchemy import ForeignKey, String, select
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
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.nonsense import Nonsense
|
||||
|
@ -3,10 +3,10 @@ from typing import Any
|
||||
|
||||
import bcrypt
|
||||
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.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import mapped_column, Mapped
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.models.base import Base
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel, Field, ConfigDict
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel, Field, ConfigDict
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
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)
|
||||
|
||||
|
@ -1,11 +1,11 @@
|
||||
import time
|
||||
|
||||
import jwt
|
||||
from fastapi import HTTPException, Request
|
||||
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
||||
|
||||
from app.config import settings as global_settings
|
||||
from app.models.user import User
|
||||
|
||||
from fastapi import Request, HTTPException
|
||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
from app.utils.logging import AppLogger
|
||||
|
||||
logger = AppLogger().get_logger()
|
||||
|
@ -1,11 +1,10 @@
|
||||
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.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.utils.logging import AppLogger
|
||||
|
@ -1,18 +1,15 @@
|
||||
from attrs import define, field
|
||||
import smtplib
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
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 pydantic import EmailStr
|
||||
|
||||
from app.config import settings as global_settings
|
||||
from app.utils.logging import AppLogger
|
||||
from app.utils.singleton import SingletonMetaNoArgs
|
||||
|
||||
|
||||
logger = AppLogger().get_logger()
|
||||
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
from functools import wraps
|
||||
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
|
||||
def compile_sql_or_scalar(func):
|
||||
"""
|
||||
|
@ -3,7 +3,6 @@ import logging
|
||||
from rich.console import Console
|
||||
from rich.logging import RichHandler
|
||||
|
||||
|
||||
from app.utils.singleton import SingletonMeta
|
||||
|
||||
|
||||
@ -21,5 +20,5 @@ class RichConsoleHandler(RichHandler):
|
||||
def __init__(self, width=200, style=None, **kwargs):
|
||||
super().__init__(
|
||||
console=Console(color_system="256", width=width, style=style, stderr=True),
|
||||
**kwargs
|
||||
**kwargs,
|
||||
)
|
||||
|
@ -1,5 +1,5 @@
|
||||
# pull official base image
|
||||
FROM postgres:16-alpine
|
||||
FROM postgres:17.4-alpine
|
||||
|
||||
# run create.sql on init
|
||||
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):
|
||||
|
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"
|
||||
version = "0.0.17"
|
||||
description = ""
|
||||
authors = ["Jakub Miazek <the@grillazz.com>"]
|
||||
packages = []
|
||||
license = "MIT"
|
||||
package-mode = false
|
||||
version = "0.1.0"
|
||||
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."
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = [
|
||||
"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]
|
||||
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.uv]
|
||||
dev-dependencies = [
|
||||
"ruff>=0.9.10",
|
||||
"devtools[pygments]>=0.12.2",
|
||||
"pyupgrade>=3.19.1",
|
||||
"ipython>=9.0.2",
|
||||
"sqlacodegen>=3.0.0",
|
||||
"tryceratops>=2.4.1",
|
||||
"locust>=2.33.0"
|
||||
|
||||
[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.mypy]
|
||||
strict = true
|
||||
exclude = ["venv", ".venv", "alembic"]
|
||||
|
||||
[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"
|
||||
exclude = ["alembic"]
|
||||
|
||||
[tool.ruff.lint.flake8-quotes]
|
||||
docstring-quotes = "double"
|
||||
[tool.ruff.lint]
|
||||
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]
|
||||
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"
|
||||
[tool.ruff.lint.pyupgrade]
|
||||
# Preserve types, even if a file imports `from __future__ import annotations`.
|
||||
keep-runtime-typing = true
|
@ -1,9 +1,9 @@
|
||||
import pytest
|
||||
from httpx import AsyncClient
|
||||
from starlette import status
|
||||
import jwt
|
||||
import pytest
|
||||
from dirty_equals import IsPositiveFloat, IsStr, IsUUID
|
||||
from httpx import AsyncClient
|
||||
from inline_snapshot import snapshot
|
||||
from dirty_equals import IsStr, IsUUID, IsPositiveFloat
|
||||
from starlette import status
|
||||
|
||||
pytestmark = pytest.mark.anyio
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
from anyio import Path
|
||||
|
||||
import pytest
|
||||
from anyio import Path
|
||||
from fastapi import status
|
||||
from httpx import AsyncClient
|
||||
|
||||
|
@ -1,9 +1,8 @@
|
||||
import pytest
|
||||
from dirty_equals import IsUUID
|
||||
from fastapi import status
|
||||
from httpx import AsyncClient
|
||||
from inline_snapshot import snapshot
|
||||
from dirty_equals import IsUUID
|
||||
|
||||
from polyfactory.factories.pydantic_factory import ModelFactory
|
||||
|
||||
from app.schemas.stuff import StuffSchema
|
||||
|
@ -1,5 +1,8 @@
|
||||
from collections.abc import AsyncGenerator
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from httpx import AsyncClient, ASGITransport
|
||||
from httpx import ASGITransport, AsyncClient
|
||||
|
||||
from app.database import engine
|
||||
from app.main import app
|
||||
@ -28,13 +31,11 @@ async def start_db():
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
async def client(start_db) -> AsyncClient:
|
||||
|
||||
async def client(start_db) -> AsyncGenerator[AsyncClient, Any]: # noqa: ARG001
|
||||
transport = ASGITransport(
|
||||
app=app,
|
||||
)
|
||||
async with AsyncClient(
|
||||
# app=app,
|
||||
base_url="http://testserver/v1",
|
||||
headers={"Content-Type": "application/json"},
|
||||
transport=transport,
|
||||
|
Loading…
x
Reference in New Issue
Block a user