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 - main
jobs: jobs:
test: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
python-version: [ "3.13" ] python-version: [ "3.13" ]
poetry-version: [ "1.8.5" ]
env: env:
PYTHONDONTWRITEBYTECODE: 1 PYTHONDONTWRITEBYTECODE: 1
PYTHONUNBUFFERED: 1 PYTHONUNBUFFERED: 1
POSTGRES_DB: testdb POSTGRES_DB: testdb
POSTGRES_HOST: 127.0.0.1 POSTGRES_HOST: 127.0.0.1
POSTGRES_USER: app-user POSTGRES_USER: panettone
POSTGRES_PASSWORD: secret POSTGRES_PASSWORD: secret
PGPASSWORD: secret PGPASSWORD: secret
REDIS_HOST: 127.0.0.1 REDIS_HOST: 127.0.0.1
@ -37,7 +36,7 @@ jobs:
sqldb: sqldb:
image: postgres:16 image: postgres:16
env: env:
POSTGRES_USER: app-user POSTGRES_USER: panettone
POSTGRES_PASSWORD: secret POSTGRES_PASSWORD: secret
POSTGRES_DB: testdb POSTGRES_DB: testdb
ports: ports:
@ -48,18 +47,17 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Create database schema - name: Create database schema
run: PGPASSWORD=secret psql -h 127.0.0.1 -d testdb -U app-user -c "CREATE SCHEMA shakespeare; CREATE SCHEMA happy_hog;" run: PGPASSWORD=secret psql -h 127.0.0.1 -d testdb -U panettone -c "CREATE SCHEMA shakespeare; CREATE SCHEMA happy_hog;"
- uses: actions/setup-python@v5
- name: Install the latest version of uv and set the python version
uses: astral-sh/setup-uv@v5
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: Install Poetry
uses: abatilo/actions-poetry@v3 - name: Lint with ruff
with: run: uv run --frozen ruff check .
poetry-version: ${{ matrix.poetry-version }}
- name: Install dependencies - name: Test with python ${{ matrix.python-version }}
if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' run: uv run --frozen pytest
run: poetry install --no-interaction --no-root
- name: Test Code
run: poetry run pytest tests/
- name: Lint Code
run: poetry run ruff check .

View File

@ -1,43 +1,62 @@
FROM python:3.13-slim-bookworm AS base FROM ubuntu:oracular AS build
RUN apt-get update \
&& apt-get upgrade -y \
&& apt-get install -y --no-install-recommends curl git build-essential \
&& apt-get autoremove -y
ENV POETRY_HOME="/opt/poetry"
RUN curl -sSL https://install.python-poetry.org | python3 -
FROM base AS install RUN apt-get update -qy && apt-get install -qyy \
WORKDIR /home/code -o APT::Install-Recommends=false \
-o APT::Install-Suggests=false \
build-essential \
ca-certificates \
python3-setuptools \
python3.13-dev \
git
# allow controlling the poetry installation of dependencies via external args COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
ARG INSTALL_ARGS="--no-root --no-interaction --no-ansi"
ENV POETRY_HOME="/opt/poetry"
ENV PATH="$POETRY_HOME/bin:$PATH"
COPY pyproject.toml poetry.lock ./
# install without virtualenv, since we are inside a container ENV UV_LINK_MODE=copy \
RUN poetry config virtualenvs.create false \ UV_COMPILE_BYTECODE=1 \
&& poetry install $INSTALL_ARGS UV_PYTHON_DOWNLOADS=never \
UV_PYTHON=python3.13 \
UV_PROJECT_ENVIRONMENT=/panettone
# cleanup COPY pyproject.toml /_lock/
RUN curl -sSL https://install.python-poetry.org | python3 - --uninstall COPY uv.lock /_lock/
RUN apt-get purge -y curl git build-essential \
&& apt-get clean -y \
&& rm -rf /root/.cache \
&& rm -rf /var/apt/lists/* \
&& rm -rf /var/cache/apt/*
FROM install AS app-image RUN --mount=type=cache,target=/root/.cache
RUN cd /_lock && uv sync \
--locked \
--no-dev \
--no-install-project
##########################################################################
FROM ubuntu:oracular
ENV PYTHONPATH=/home/code/ PYTHONHASHSEED=0 PYTHONASYNCIODEBUG=1 ENV PATH=/panettone/bin:$PATH
COPY tests/ tests/ RUN groupadd -r panettone
COPY app/ app/ RUN useradd -r -d /panettone -g panettone -N panettone
COPY alembic/ alembic/
COPY .env alembic.ini ./
# create a non-root user and switch to it, for security. STOPSIGNAL SIGINT
RUN addgroup --system --gid 1001 "app-user"
RUN adduser --system --uid 1001 "app-user"
USER "app-user"
RUN apt-get update -qy && apt-get install -qyy \
-o APT::Install-Recommends=false \
-o APT::Install-Suggests=false \
python3.13 \
libpython3.13 \
libpcre3 \
libxml2
RUN apt-get clean
RUN rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
COPY --from=build --chown=panettone:panettone /panettone /panettone
USER panettone
WORKDIR /panettone
COPY /app/ app/
COPY /tests/ tests/
COPY .env app/
COPY alembic.ini app/
COPY alembic/ app/alembic/
COPY logging-uvicorn.json /panettone/logging-uvicorn.json
RUN python -V
RUN python -Im site
RUN python -Ic 'import uvicorn'

View File

@ -39,7 +39,7 @@ safety: ## Check project and dependencies with safety https://github.com/pyupio/
.PHONY: py-upgrade .PHONY: py-upgrade
py-upgrade: ## Upgrade project py files with pyupgrade library for python version 3.10 py-upgrade: ## Upgrade project py files with pyupgrade library for python version 3.10
pyupgrade --py312-plus `find app -name "*.py"` pyupgrade --py313-plus `find app -name "*.py"`
.PHONY: lint .PHONY: lint
lint: ## Lint project code. lint: ## Lint project code.

View File

@ -26,10 +26,11 @@
<li><a href="#how-to-feed-database">How to feed database</a></li> <li><a href="#how-to-feed-database">How to feed database</a></li>
<li><a href="#rainbow-logs-with-rich">Rainbow logs with rich</a></li> <li><a href="#rainbow-logs-with-rich">Rainbow logs with rich</a></li>
<li><a href="#setup-user-auth">Setup user auth</a></li> <li><a href="#setup-user-auth">Setup user auth</a></li>
<li><a href="#local-development-with-poetry">Local development with poetry</a></li> <li><a href="#setup-local-env-with-uv">Setup local development with uv</a></li>
<li><a href="#import-xlsx-files-with-polars-and-calamine">Import xlsx files with polars and calamine</a></li> <li><a href="#import-xlsx-files-with-polars-and-calamine">Import xlsx files with polars and calamine</a></li>
<li><a href="#worker-aware-async-scheduler">Schedule jobs</a></li> <li><a href="#worker-aware-async-scheduler">Schedule jobs</a></li>
<li><a href="#smtp-setup">Email Configuration</a></li> <li><a href="#smtp-setup">Email Configuration</a></li>
<li><a href="#uv-knowledge-and-inspirations">UV knowledge and inspirations</a></li>
</ul> </ul>
</li> </li>
<li><a href="#acknowledgments">Acknowledgments</a></li> <li><a href="#acknowledgments">Acknowledgments</a></li>
@ -44,7 +45,7 @@
This example demonstrates the seamless integration of [FastAPI](https://fastapi.tiangolo.com/), a modern, high-performance web framework, This example demonstrates the seamless integration of [FastAPI](https://fastapi.tiangolo.com/), a modern, high-performance web framework,
with [Pydantic 2.0](https://github.com/pydantic/pydantic), a robust and powerful data validation library. with [Pydantic 2.0](https://github.com/pydantic/pydantic), a robust and powerful data validation library.
The integration is further enhanced by the use of [SQLAlchemy ORM](https://www.sqlalchemy.org/), a popular and feature-rich Object-Relational Mapping tool, The integration is further enhanced by the use of [SQLAlchemy ORM](https://www.sqlalchemy.org/), a popular and feature-rich Object-Relational Mapping tool,
and [PostgreSQL16](https://www.postgresql.org/about/news/postgresql-16-released-2715/) relational database. and [PostgreSQL17](https://www.postgresql.org/docs/17/release.html) relational database.
The entire stack is connected using the [asyncpg](https://github.com/MagicStack/asyncpg) Database Client Library, The entire stack is connected using the [asyncpg](https://github.com/MagicStack/asyncpg) Database Client Library,
which provides a robust and efficient way to interact with PostgreSQL databases in Python, which provides a robust and efficient way to interact with PostgreSQL databases in Python,
@ -56,7 +57,7 @@ allowing for the rapid development of APIs with Python 3.8+.
FastAPI has received significant recognition in the industry, including a review on thoughtworks Technology Radar in April 2021, FastAPI has received significant recognition in the industry, including a review on thoughtworks Technology Radar in April 2021,
where it was classified as a Trial technology, with comments praising its performance, ease of use, where it was classified as a Trial technology, with comments praising its performance, ease of use,
and features such as API documentation using OpenAPI. Additionally, FastAPI was recognized in the Python Developers Survey 2022 Results, and features such as API documentation using OpenAPI. Additionally, FastAPI was recognized in the Python Developers Survey 2023 Results,
conducted by the Python Software Foundation and JetBrains, where it was reported that 1 in 4 Python developers use FastAPI, conducted by the Python Software Foundation and JetBrains, where it was reported that 1 in 4 Python developers use FastAPI,
with a 4 percentage point increase from the previous year. with a 4 percentage point increase from the previous year.
@ -86,7 +87,8 @@ To build , run and test and more ... use magic of make help to play with this pr
4. make docker-feed-database 4. make docker-feed-database
``` ```
### Adjust make with just
[//]: # (TODO: switch form make to just)
<p align="right">(<a href="#readme-top">back to top</a>)</p> <p align="right">(<a href="#readme-top">back to top</a>)</p>
### How to feed database ### How to feed database
@ -126,15 +128,11 @@ he following steps were taken to integrate [rich](https://github.com/Textualize/
Setup user authentication with JWT and Redis as token storage. Setup user authentication with JWT and Redis as token storage.
### Local development with poetry ### Setup local env with uv
```shell ```shell
pyenv install 3.13 && pyenv local 3.13 uv sync
source .venv/bin/activate
``` ```
```shell
poetry install --with dev
```
Hope you enjoy it.
### Import xlsx files with polars and calamine ### Import xlsx files with polars and calamine
Power of Polars Library in data manipulation and analysis. Power of Polars Library in data manipulation and analysis.
@ -165,6 +163,16 @@ It is implemented as a singleton to ensure that only one SMTP connection is main
throughout the application lifecycle, optimizing resource usage. throughout the application lifecycle, optimizing resource usage.
<p align="right">(<a href="#readme-top">back to top</a>)</p>
### UV knowledge and inspirations
- https://docs.astral.sh/uv/
- https://hynek.me/articles/docker-uv/
- https://thedataquarry.com/posts/towards-a-unified-python-toolchain/
- https://www.youtube.com/watch?v=ifj-izwXKRA&t=760s > UV and Ruff: Next-gen Python Tooling
- https://www.youtube.com/watch?v=8UuW8o4bHbw&t=1s > uv IS the Future of Python Packaging! 🐍📦
<p align="right">(<a href="#readme-top">back to top</a>)</p> <p align="right">(<a href="#readme-top">back to top</a>)</p>
## Acknowledgments ## Acknowledgments
@ -206,6 +214,7 @@ I've included a few of my favorites to kick things off!
- **[OCT 16 2024]** apscheduler added to project :clock1: - **[OCT 16 2024]** apscheduler added to project :clock1:
- **[DEC 16 2024]** bump project to Python 3.13 :fast_forward: - **[DEC 16 2024]** bump project to Python 3.13 :fast_forward:
- **[JAN 28 2025]** add SMTP setup :email: - **[JAN 28 2025]** add SMTP setup :email:
- **[MAR 8 2025]** switch from poetry to uv :fast_forward:
<p align="right">(<a href="#readme-top">back to top</a>)</p> <p align="right">(<a href="#readme-top">back to top</a>)</p>
@ -225,19 +234,19 @@ I've included a few of my favorites to kick things off!
[linkedin-shield]: https://img.shields.io/badge/-LinkedIn-black.svg?style=for-the-badge&logo=linkedin&colorB=555 [linkedin-shield]: https://img.shields.io/badge/-LinkedIn-black.svg?style=for-the-badge&logo=linkedin&colorB=555
[linkedin-url]: https://www.linkedin.com/in/python-has-powers/ [linkedin-url]: https://www.linkedin.com/in/python-has-powers/
[fastapi.tiangolo.com]: https://img.shields.io/badge/FastAPI-0.115.6-009485?style=for-the-badge&logo=fastapi&logoColor=white [fastapi.tiangolo.com]: https://img.shields.io/badge/FastAPI-0.115.11-009485?style=for-the-badge&logo=fastapi&logoColor=white
[fastapi-url]: https://fastapi.tiangolo.com/ [fastapi-url]: https://fastapi.tiangolo.com/
[pydantic.com]: https://img.shields.io/badge/Pydantic-2.10.3-e92063?style=for-the-badge&logo=pydantic&logoColor=white [pydantic.com]: https://img.shields.io/badge/Pydantic-2.10.6-e92063?style=for-the-badge&logo=pydantic&logoColor=white
[pydantic-url]: https://docs.pydantic.dev/latest/ [pydantic-url]: https://docs.pydantic.dev/latest/
[sqlalchemy.org]: https://img.shields.io/badge/SQLAlchemy-2.0.36-bb0000?color=bb0000&style=for-the-badge [sqlalchemy.org]: https://img.shields.io/badge/SQLAlchemy-2.0.38-bb0000?color=bb0000&style=for-the-badge
[sqlalchemy-url]: https://docs.sqlalchemy.org/en/20/ [sqlalchemy-url]: https://docs.sqlalchemy.org/en/20/
[uvicorn.org]: https://img.shields.io/badge/Uvicorn-0.34.0-2094f3?style=for-the-badge&logo=uvicorn&logoColor=white [uvicorn.org]: https://img.shields.io/badge/Uvicorn-0.34.0-2094f3?style=for-the-badge&logo=uvicorn&logoColor=white
[uvicorn-url]: https://www.uvicorn.org/ [uvicorn-url]: https://www.uvicorn.org/
[asyncpg.github.io]: https://img.shields.io/badge/asyncpg-0.30.0-2e6fce?style=for-the-badge&logo=postgresql&logoColor=white [asyncpg.github.io]: https://img.shields.io/badge/asyncpg-0.30.0-2e6fce?style=for-the-badge&logo=postgresql&logoColor=white
[asyncpg-url]: https://magicstack.github.io/asyncpg/current/ [asyncpg-url]: https://magicstack.github.io/asyncpg/current/
[pytest.org]: https://img.shields.io/badge/pytest-8.3.4-fff?style=for-the-badge&logo=pytest&logoColor=white [pytest.org]: https://img.shields.io/badge/pytest-8.3.5-fff?style=for-the-badge&logo=pytest&logoColor=white
[pytest-url]: https://docs.pytest.org/en/6.2.x/ [pytest-url]: https://docs.pytest.org/en/6.2.x/
[alembic.sqlalchemy.org]: https://img.shields.io/badge/alembic-1.14.0-6BA81E?style=for-the-badge&logo=alembic&logoColor=white [alembic.sqlalchemy.org]: https://img.shields.io/badge/alembic-1.15.1-6BA81E?style=for-the-badge&logo=alembic&logoColor=white
[alembic-url]: https://alembic.sqlalchemy.org/en/latest/ [alembic-url]: https://alembic.sqlalchemy.org/en/latest/
[rich.readthedocs.io]: https://img.shields.io/badge/rich-13.9.4-009485?style=for-the-badge&logo=rich&logoColor=white [rich.readthedocs.io]: https://img.shields.io/badge/rich-13.9.4-009485?style=for-the-badge&logo=rich&logoColor=white
[rich-url]: https://rich.readthedocs.io/en/latest/ [rich-url]: https://rich.readthedocs.io/en/latest/

View File

@ -1,12 +1,11 @@
import logging import logging
from typing import Annotated from typing import Annotated
from fastapi import APIRouter, status, Request, Depends, Query from fastapi import APIRouter, Depends, Query, Request, status
from pydantic import EmailStr from pydantic import EmailStr
from starlette.concurrency import run_in_threadpool from starlette.concurrency import run_in_threadpool
from app.services.smtp import SMTPEmailService from app.services.smtp import SMTPEmailService
from app.utils.logging import AppLogger from app.utils.logging import AppLogger
logger = AppLogger().get_logger() logger = AppLogger().get_logger()

View File

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

View File

@ -3,8 +3,6 @@ from typing import Annotated
from fastapi import APIRouter, Depends, Query from fastapi import APIRouter, Depends, Query
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from fastapi_cache.decorator import cache
from app.database import get_db from app.database import get_db
from app.models.shakespeare import Paragraph from app.models.shakespeare import Paragraph
@ -14,7 +12,6 @@ router = APIRouter(prefix="/v1/shakespeare")
@router.get( @router.get(
"/", "/",
) )
@cache(namespace="test-2", expire=60)
async def find_paragraph( async def find_paragraph(
character: Annotated[str, Query(description="Character name")], character: Annotated[str, Query(description="Character name")],
db_session: AsyncSession = Depends(get_db), db_session: AsyncSession = Depends(get_db),

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.exc import SQLAlchemyError
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession

View File

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

View File

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

View File

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

View File

@ -1,25 +1,22 @@
import asyncpg from contextlib import asynccontextmanager
from apscheduler.eventbrokers.redis import RedisEventBroker
from apscheduler.datastores.sqlalchemy import SQLAlchemyDataStore
from fastapi import FastAPI, Depends
from fastapi_cache import FastAPICache
from fastapi_cache.backends.redis import RedisBackend
import asyncpg
from apscheduler import AsyncScheduler
from apscheduler.datastores.sqlalchemy import SQLAlchemyDataStore
from apscheduler.eventbrokers.redis import RedisEventBroker
from fastapi import Depends, FastAPI
from app.api.health import router as health_router
from app.api.nonsense import router as nonsense_router from app.api.nonsense import router as nonsense_router
from app.api.shakespeare import router as shakespeare_router from app.api.shakespeare import router as shakespeare_router
from app.api.stuff import router as stuff_router from app.api.stuff import router as stuff_router
from app.api.user import router as user_router
from app.config import settings as global_settings from app.config import settings as global_settings
from app.database import engine from app.database import engine
from app.utils.logging import AppLogger from app.redis import get_redis
from app.api.user import router as user_router
from app.api.health import router as health_router
from app.redis import get_redis, get_cache
from app.services.auth import AuthBearer from app.services.auth import AuthBearer
from app.services.scheduler import SchedulerMiddleware from app.services.scheduler import SchedulerMiddleware
from app.utils.logging import AppLogger
from contextlib import asynccontextmanager
from apscheduler import AsyncScheduler
logger = AppLogger().get_logger() logger = AppLogger().get_logger()
@ -32,10 +29,7 @@ async def lifespan(_app: FastAPI):
_postgres_dsn = global_settings.postgres_url.unicode_string() _postgres_dsn = global_settings.postgres_url.unicode_string()
try: try:
# Initialize the cache with the redis connection # TODO: cache with the redis connection
redis_cache = await get_cache()
FastAPICache.init(RedisBackend(redis_cache), prefix="fastapi-cache")
# logger.info(FastAPICache.get_cache_status_header())
# Initialize the postgres connection pool # Initialize the postgres connection pool
_app.postgres_pool = await asyncpg.create_pool( _app.postgres_pool = await asyncpg.create_pool(
dsn=_postgres_dsn, dsn=_postgres_dsn,

View File

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

View File

@ -4,7 +4,7 @@ from fastapi import HTTPException, status
from sqlalchemy import String, select from sqlalchemy import String, select
from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import mapped_column, Mapped from sqlalchemy.orm import Mapped, mapped_column
from app.models.base import Base from app.models.base import Base

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,11 +1,11 @@
import time import time
import jwt import jwt
from fastapi import HTTPException, Request
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from app.config import settings as global_settings from app.config import settings as global_settings
from app.models.user import User from app.models.user import User
from fastapi import Request, HTTPException
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from app.utils.logging import AppLogger from app.utils.logging import AppLogger
logger = AppLogger().get_logger() logger = AppLogger().get_logger()

View File

@ -1,11 +1,10 @@
from datetime import datetime from datetime import datetime
from attrs import define
from sqlalchemy import text
from starlette.types import ASGIApp, Receive, Scope, Send
from apscheduler import AsyncScheduler from apscheduler import AsyncScheduler
from apscheduler.triggers.interval import IntervalTrigger from apscheduler.triggers.interval import IntervalTrigger
from attrs import define
from sqlalchemy import text
from starlette.types import ASGIApp, Receive, Scope, Send
from app.database import AsyncSessionFactory from app.database import AsyncSessionFactory
from app.utils.logging import AppLogger from app.utils.logging import AppLogger

View File

@ -1,18 +1,15 @@
from attrs import define, field
import smtplib import smtplib
from email.mime.multipart import MIMEMultipart from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText from email.mime.text import MIMEText
from app.config import settings as global_settings from attrs import define, field
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
from pydantic import EmailStr from pydantic import EmailStr
from app.config import settings as global_settings
from app.utils.logging import AppLogger from app.utils.logging import AppLogger
from app.utils.singleton import SingletonMetaNoArgs from app.utils.singleton import SingletonMetaNoArgs
logger = AppLogger().get_logger() logger = AppLogger().get_logger()

View File

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

View File

@ -3,7 +3,6 @@ import logging
from rich.console import Console from rich.console import Console
from rich.logging import RichHandler from rich.logging import RichHandler
from app.utils.singleton import SingletonMeta from app.utils.singleton import SingletonMeta
@ -21,5 +20,5 @@ class RichConsoleHandler(RichHandler):
def __init__(self, width=200, style=None, **kwargs): def __init__(self, width=200, style=None, **kwargs):
super().__init__( super().__init__(
console=Console(color_system="256", width=width, style=style, stderr=True), console=Console(color_system="256", width=width, style=style, stderr=True),
**kwargs **kwargs,
) )

View File

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

View File

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

4288
poetry.lock generated

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" name = "fastapi-sqlalchemy-asyncpg"
version = "0.0.17" version = "0.1.0"
description = "" description = "A modern FastAPI application with SQLAlchemy 2.0 and AsyncPG for high-performance async database operations. Features include JWT authentication with Redis token storage, password hashing, connection pooling, data processing with Polars, Rich logging, task scheduling with APScheduler, and Shakespeare datasets integration."
authors = ["Jakub Miazek <the@grillazz.com>"] readme = "README.md"
packages = [] requires-python = ">=3.13"
license = "MIT" dependencies = [
package-mode = false "fastapi[all]>=0.115.11",
"pydantic[email]>=2.10.6",
"pydantic-settings>=2.8.1",
"sqlalchemy>=2.0.38",
"uvicorn[standard]>=0.34.0",
"asyncpg>=0.30.0",
"alembic>=1.15.1",
"httpx>=0.28.1",
"pytest>=8.3.5",
"pytest-cov>=6.0.0",
"uvloop>=0.21.0",
"httptools>=0.6.4",
"rich>=13.9.4",
"pyjwt>=2.10.1",
"redis>=5.2.1",
"bcrypt>=4.3.0",
"polars>=1.24.0",
"python-multipart>=0.0.20",
"fastexcel>=0.13.0",
"inline-snapshot>=0.17.0",
"dirty-equals>=0.8.0",
"polyfactory>=2.18.1",
"granian>=1.7.0",
"apscheduler[redis,sqlalchemy]>=4.0.0a5",
]
[tool.poetry.dependencies] [tool.uv]
python = "^3.13" dev-dependencies = [
fastapi = {version = "^0.115.6", extras = ["all"]} "ruff>=0.9.10",
pydantic = {version = "^2.10.3", extras = ["email"]} "devtools[pygments]>=0.12.2",
pydantic-settings = "^2.7.0" "pyupgrade>=3.19.1",
sqlalchemy = "^2.0.36" "ipython>=9.0.2",
uvicorn = { version = "^0.34.0", extras = ["standard"]} "sqlacodegen>=3.0.0",
asyncpg = "^0.30.0" "tryceratops>=2.4.1",
alembic = "^1.14.0" "locust>=2.33.0"
httpx = "^0.28.1"
pytest = "^8.3.4"
pytest-cov = "^6.0.0"
uvloop = "^0.21.0"
httptools = "^0.6.4"
rich = "^13.9.4"
pyjwt = {version = "^2.10.1", extras = ["cryptography"]}
redis = "^5.2.1"
bcrypt = "^4.2.1"
polars = "^1.17.1"
python-multipart = "^0.0.20"
fastexcel = "^0.12.0"
fastapi-cache2 = "^0.2.1"
inline-snapshot = "^0.17.0"
dirty-equals = "^0.8.0"
polyfactory = "^2.18.1"
granian = "^1.7.0"
apscheduler = {version = "^4.0.0a5", extras = ["redis,sqlalchemy"]}
pendulum = {git = "https://github.com/sdispater/pendulum.git", rev="develop"}
[tool.poetry.group.dev.dependencies] ]
devtools = { extras = ["pygments"], version = "^0.12.2" }
safety = "*"
pyupgrade = "*"
ipython = "^8.26.0"
ruff = "^0.6.1"
sqlacodegen = "^3.0.0rc5"
tryceratops = "^2.3.3"
locust = "^2.31.3"
[build-system]
requires = ["poetry-core>=1.0.0"] [tool.mypy]
build-backend = "poetry.core.masonry.api" strict = true
exclude = ["venv", ".venv", "alembic"]
[tool.ruff] [tool.ruff]
line-length = 120
indent-width = 4
lint.select = ["E", "F", "UP", "N", "C", "B"]
lint.ignore = ["E501"]
# Exclude a variety of commonly ignored directories.
exclude = ["alembic",]
# Assume Python 3.13
target-version = "py313" target-version = "py313"
exclude = ["alembic"]
[tool.ruff.lint.flake8-quotes] [tool.ruff.lint]
docstring-quotes = "double" select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes
"I", # isort
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"UP", # pyupgrade
"ARG001", # unused arguments in functions
]
ignore = [
"E501", # line too long, handled by black
"B008", # do not perform function calls in argument defaults
"W191", # indentation contains tabs
"B904", # Allow raising exceptions without from e, for HTTPException
]
[tool.ruff.lint.flake8-bugbear] [tool.ruff.lint.pyupgrade]
extend-immutable-calls = ["fastapi.Depends",] # Preserve types, even if a file imports `from __future__ import annotations`.
keep-runtime-typing = true
[tool.pytest.ini_options]
addopts = "-v --doctest-modules --doctest-glob=*.md --ignore=alembic"
asyncio_mode = "strict"
env_files = [".env"]
[tool.tryceratops]
exclude = ["alembic",]
[tool.ruff.format]
# Like Black, use double quotes for strings.
quote-style = "double"
# Like Black, indent with spaces, rather than tabs.
indent-style = "space"
# Like Black, respect magic trailing commas.
skip-magic-trailing-comma = false
# Like Black, automatically detect the appropriate line ending.
line-ending = "auto"

View File

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

View File

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

View File

@ -1,9 +1,8 @@
import pytest import pytest
from dirty_equals import IsUUID
from fastapi import status from fastapi import status
from httpx import AsyncClient from httpx import AsyncClient
from inline_snapshot import snapshot from inline_snapshot import snapshot
from dirty_equals import IsUUID
from polyfactory.factories.pydantic_factory import ModelFactory from polyfactory.factories.pydantic_factory import ModelFactory
from app.schemas.stuff import StuffSchema from app.schemas.stuff import StuffSchema

View File

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

1742
uv.lock generated Normal file

File diff suppressed because it is too large Load Diff