Compare commits

..

No commits in common. "41a8688aaec940f563fd900a5a02405d4d5f8469" and "e9750107e7bac1198e3bf7b9e75444c1cd16ee72" have entirely different histories.

35 changed files with 4504 additions and 2045 deletions

View File

@ -7,19 +7,20 @@ on:
- main
jobs:
build:
test:
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: panettone
POSTGRES_USER: app-user
POSTGRES_PASSWORD: secret
PGPASSWORD: secret
REDIS_HOST: 127.0.0.1
@ -36,7 +37,7 @@ jobs:
sqldb:
image: postgres:16
env:
POSTGRES_USER: panettone
POSTGRES_USER: app-user
POSTGRES_PASSWORD: secret
POSTGRES_DB: testdb
ports:
@ -47,17 +48,18 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Create database schema
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
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
with:
python-version: ${{ matrix.python-version }}
- name: Lint with ruff
run: uv run --frozen ruff check .
- name: Test with python ${{ matrix.python-version }}
run: uv run --frozen pytest
- 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 .

View File

@ -1,62 +1,43 @@
FROM ubuntu:oracular AS build
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 -
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
FROM base AS install
WORKDIR /home/code
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
# 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 ./
ENV UV_LINK_MODE=copy \
UV_COMPILE_BYTECODE=1 \
UV_PYTHON_DOWNLOADS=never \
UV_PYTHON=python3.13 \
UV_PROJECT_ENVIRONMENT=/panettone
# install without virtualenv, since we are inside a container
RUN poetry config virtualenvs.create false \
&& poetry install $INSTALL_ARGS
COPY pyproject.toml /_lock/
COPY uv.lock /_lock/
# 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/*
RUN --mount=type=cache,target=/root/.cache
RUN cd /_lock && uv sync \
--locked \
--no-dev \
--no-install-project
##########################################################################
FROM ubuntu:oracular
FROM install AS app-image
ENV PATH=/panettone/bin:$PATH
ENV PYTHONPATH=/home/code/ PYTHONHASHSEED=0 PYTHONASYNCIODEBUG=1
RUN groupadd -r panettone
RUN useradd -r -d /panettone -g panettone -N panettone
COPY tests/ tests/
COPY app/ app/
COPY alembic/ alembic/
COPY .env alembic.ini ./
STOPSIGNAL SIGINT
# 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"
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'

View File

@ -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 --py313-plus `find app -name "*.py"`
pyupgrade --py312-plus `find app -name "*.py"`
.PHONY: lint
lint: ## Lint project code.

View File

@ -26,11 +26,10 @@
<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="#setup-local-env-with-uv">Setup local development with uv</a></li>
<li><a href="#local-development-with-poetry">Local development with poetry</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>
@ -45,7 +44,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 [PostgreSQL17](https://www.postgresql.org/docs/17/release.html) relational database.
and [PostgreSQL16](https://www.postgresql.org/about/news/postgresql-16-released-2715/) 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,
@ -57,7 +56,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 2023 Results,
and features such as API documentation using OpenAPI. Additionally, FastAPI was recognized in the Python Developers Survey 2022 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.
@ -87,8 +86,7 @@ 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
@ -128,11 +126,15 @@ he following steps were taken to integrate [rich](https://github.com/Textualize/
Setup user authentication with JWT and Redis as token storage.
### Setup local env with uv
### Local development with poetry
```shell
uv sync
source .venv/bin/activate
pyenv install 3.13 && pyenv local 3.13
```
```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.
@ -163,16 +165,6 @@ 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
@ -214,7 +206,6 @@ 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>
@ -234,19 +225,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.11-009485?style=for-the-badge&logo=fastapi&logoColor=white
[fastapi.tiangolo.com]: https://img.shields.io/badge/FastAPI-0.115.6-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.6-e92063?style=for-the-badge&logo=pydantic&logoColor=white
[pydantic.com]: https://img.shields.io/badge/Pydantic-2.10.3-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.38-bb0000?color=bb0000&style=for-the-badge
[sqlalchemy.org]: https://img.shields.io/badge/SQLAlchemy-2.0.36-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.5-fff?style=for-the-badge&logo=pytest&logoColor=white
[pytest.org]: https://img.shields.io/badge/pytest-8.3.4-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.15.1-6BA81E?style=for-the-badge&logo=alembic&logoColor=white
[alembic.sqlalchemy.org]: https://img.shields.io/badge/alembic-1.14.0-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/

View File

@ -1,11 +1,12 @@
import logging
from typing import Annotated
from fastapi import APIRouter, Depends, Query, Request, status
from fastapi import APIRouter, status, Request, Depends, Query
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()

View File

@ -1,8 +1,7 @@
import io
import polars as pl
from fastapi import APIRouter, Depends, HTTPException, UploadFile, status
from fastapi import APIRouter, Depends, status, UploadFile, HTTPException
from sqlalchemy.exc import SQLAlchemyError
import polars as pl
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db

View File

@ -3,6 +3,8 @@ 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
@ -12,6 +14,7 @@ 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),

View File

@ -1,4 +1,4 @@
from fastapi import APIRouter, Depends, HTTPException, Request, status
from fastapi import APIRouter, Depends, HTTPException, status, Request
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.ext.asyncio import AsyncSession

View File

@ -1,11 +1,11 @@
from typing import Annotated
from fastapi import APIRouter, Depends, Form, HTTPException, Request, status
from fastapi import APIRouter, Depends, status, Request, HTTPException, Form
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db
from app.models.user import User
from app.schemas.user import TokenResponse, UserLogin, UserResponse, UserSchema
from app.schemas.user import UserSchema, UserResponse, UserLogin, TokenResponse
from app.services.auth import create_access_token
from app.utils.logging import AppLogger

View File

@ -1,6 +1,6 @@
import os
from pydantic import BaseModel, PostgresDsn, RedisDsn, computed_field
from pydantic import PostgresDsn, RedisDsn, computed_field, BaseModel
from pydantic_core import MultiHostUrl
from pydantic_settings import BaseSettings, SettingsConfigDict

View File

@ -1,6 +1,7 @@
from collections.abc import AsyncGenerator
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy.ext.asyncio import async_sessionmaker
from app.config import settings as global_settings
from app.utils.logging import AppLogger

View File

@ -1,22 +1,25 @@
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 apscheduler.datastores.sqlalchemy import SQLAlchemyDataStore
from fastapi import FastAPI, Depends
from fastapi_cache import FastAPICache
from fastapi_cache.backends.redis import RedisBackend
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.redis import get_redis
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.services.auth import AuthBearer
from app.services.scheduler import SchedulerMiddleware
from app.utils.logging import AppLogger
from contextlib import asynccontextmanager
from apscheduler import AsyncScheduler
logger = AppLogger().get_logger()
@ -29,7 +32,10 @@ async def lifespan(_app: FastAPI):
_postgres_dsn = global_settings.postgres_url.unicode_string()
try:
# TODO: cache with the redis connection
# 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())
# Initialize the postgres connection pool
_app.postgres_pool = await asyncpg.create_pool(
dsn=_postgres_dsn,

View File

@ -2,10 +2,9 @@ from typing import Any
from asyncpg import UniqueViolationError
from fastapi import HTTPException, status
from sqlalchemy.exc import IntegrityError, SQLAlchemyError
from sqlalchemy.exc import SQLAlchemyError, IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import DeclarativeBase, declared_attr
from sqlalchemy.orm import declared_attr, DeclarativeBase
from app.utils.logging import AppLogger
logger = AppLogger().get_logger()

View File

@ -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, mapped_column
from sqlalchemy.orm import mapped_column, Mapped
from app.models.base import Base

View File

@ -11,7 +11,6 @@ from sqlalchemy import (
)
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.models.base import Base

View File

@ -1,9 +1,9 @@
import uuid
from sqlalchemy import ForeignKey, String, select
from sqlalchemy import String, select, ForeignKey
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Mapped, joinedload, mapped_column, relationship
from sqlalchemy.orm import mapped_column, Mapped, relationship, joinedload
from app.models.base import Base
from app.models.nonsense import Nonsense

View File

@ -3,10 +3,10 @@ from typing import Any
import bcrypt
from pydantic import SecretStr
from sqlalchemy import Column, LargeBinary, String, select
from sqlalchemy import String, LargeBinary, select, Column
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.orm import mapped_column, Mapped
from app.models.base import Base

View File

@ -1,6 +1,6 @@
from uuid import UUID
from pydantic import BaseModel, ConfigDict, Field
from pydantic import BaseModel, Field, ConfigDict
config = ConfigDict(from_attributes=True)

View File

@ -1,6 +1,6 @@
from uuid import UUID
from pydantic import BaseModel, ConfigDict, Field
from pydantic import BaseModel, Field, ConfigDict
config = ConfigDict(from_attributes=True)

View File

@ -1,6 +1,6 @@
from uuid import UUID
from pydantic import BaseModel, ConfigDict, EmailStr, Field, SecretStr
from pydantic import BaseModel, Field, EmailStr, ConfigDict, SecretStr
config = ConfigDict(from_attributes=True)

View File

@ -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()

View File

@ -1,10 +1,11 @@
from datetime import datetime
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 apscheduler import AsyncScheduler
from apscheduler.triggers.interval import IntervalTrigger
from app.database import AsyncSessionFactory
from app.utils.logging import AppLogger

View File

@ -1,15 +1,18 @@
from attrs import define, field
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from attrs import define, field
from app.config import settings as global_settings
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()

View File

@ -1,7 +1,7 @@
from functools import wraps
from sqlalchemy.dialects import postgresql
from functools import wraps
def compile_sql_or_scalar(func):
"""

View File

@ -3,6 +3,7 @@ import logging
from rich.console import Console
from rich.logging import RichHandler
from app.utils.singleton import SingletonMeta
@ -20,5 +21,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
)

View File

@ -1,5 +1,5 @@
# pull official base image
FROM postgres:17.4-alpine
FROM postgres:16-alpine
# run create.sql on init
ADD create.sql /docker-entrypoint-initdb.d

View File

@ -1,4 +1,4 @@
from locust import HttpUser, between, task
from locust import HttpUser, task, between
class Stuff(HttpUser):

4288
poetry.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,90 +0,0 @@
[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"

View File

@ -1,75 +1,90 @@
[project]
[tool.poetry]
name = "fastapi-sqlalchemy-asyncpg"
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",
]
version = "0.0.17"
description = ""
authors = ["Jakub Miazek <the@grillazz.com>"]
packages = []
license = "MIT"
package-mode = false
[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.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"
[tool.mypy]
strict = true
exclude = ["venv", ".venv", "alembic"]
[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"
exclude = ["alembic"]
[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-quotes]
docstring-quotes = "double"
[tool.ruff.lint.pyupgrade]
# Preserve types, even if a file imports `from __future__ import annotations`.
keep-runtime-typing = true
[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"

View File

@ -1,9 +1,9 @@
import jwt
import pytest
from dirty_equals import IsPositiveFloat, IsStr, IsUUID
from httpx import AsyncClient
from inline_snapshot import snapshot
from starlette import status
import jwt
from inline_snapshot import snapshot
from dirty_equals import IsStr, IsUUID, IsPositiveFloat
pytestmark = pytest.mark.anyio

View File

@ -1,5 +1,6 @@
import pytest
from anyio import Path
import pytest
from fastapi import status
from httpx import AsyncClient

View File

@ -1,8 +1,9 @@
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

View File

@ -1,8 +1,5 @@
from collections.abc import AsyncGenerator
from typing import Any
import pytest
from httpx import ASGITransport, AsyncClient
from httpx import AsyncClient, ASGITransport
from app.database import engine
from app.main import app
@ -31,11 +28,13 @@ async def start_db():
@pytest.fixture(scope="session")
async def client(start_db) -> AsyncGenerator[AsyncClient, Any]: # noqa: ARG001
async def client(start_db) -> AsyncClient:
transport = ASGITransport(
app=app,
)
async with AsyncClient(
# app=app,
base_url="http://testserver/v1",
headers={"Content-Type": "application/json"},
transport=transport,

1742
uv.lock generated

File diff suppressed because it is too large Load Diff