Compare commits

..

13 Commits

Author SHA1 Message Date
Ordinary Hobbit
41a8688aae
Merge pull request #199 from grillazz/195-switch-project-to-uv
195 switch project to uv
2025-03-08 20:53:35 +01:00
grillazz
ce40b7a6d9 update README.md 2025-03-08 20:52:45 +01:00
grillazz
0bb3576e90 bump postgresql version 2025-03-08 20:26:13 +01:00
Ordinary Hobbit
3f4f1efb35
Merge pull request #197 from grillazz/195-switch-project-to-uv 2025-03-08 20:10:04 +01:00
grillazz
b594dee278 fix lint error 2025-03-08 18:34:41 +01:00
grillazz
5ede530a4d uv lock 2025-03-08 17:57:54 +01:00
grillazz
2cc654e6c7 format code 2025-03-08 17:48:04 +01:00
grillazz
0260df4d09 format code 2025-03-08 17:27:53 +01:00
grillazz
8f96c8a472 lint code 2025-03-08 17:26:09 +01:00
Ordinary Hobbit
e88f68e2bf
Merge pull request #196 from grillazz/195-switch-project-to-uv
195 switch project to uv
2025-03-08 11:03:21 +01:00
grillazz
21de7e2dfc wip: refactor ci 2025-03-08 11:01:52 +01:00
grillazz
2404c12542 wip: refactor dockerfile for uv and drop poetry.lock 2025-03-08 10:50:15 +01:00
grillazz
87f8dab32f wip: init with uv 2025-03-08 10:30:24 +01:00
35 changed files with 2049 additions and 4508 deletions

View File

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

View File

@ -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'

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

View File

@ -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/

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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,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

View File

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

View File

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

View File

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

View File

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

View File

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

4288
poetry.lock generated

File diff suppressed because it is too large Load Diff

90
pyproject.old Normal file
View 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"

View File

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

View File

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

View File

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

View File

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

View File

@ -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,

1742
uv.lock generated Normal file

File diff suppressed because it is too large Load Diff