Skip to content

195 switch project to uv #197

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Mar 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ jobs:
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


2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 1 addition & 2 deletions app/api/health.py
Original file line number Diff line number Diff line change
@@ -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()
Expand Down
5 changes: 3 additions & 2 deletions app/api/nonsense.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
3 changes: 0 additions & 3 deletions app/api/shakespeare.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
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

Expand All @@ -14,7 +12,6 @@
@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),
Expand Down
2 changes: 1 addition & 1 deletion app/api/stuff.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down
4 changes: 2 additions & 2 deletions app/api/user.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down
2 changes: 1 addition & 1 deletion app/config.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down
3 changes: 1 addition & 2 deletions app/database.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
26 changes: 10 additions & 16 deletions app/main.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,22 @@
from contextlib import asynccontextmanager

import asyncpg
from apscheduler.eventbrokers.redis import RedisEventBroker
from apscheduler import AsyncScheduler
from apscheduler.datastores.sqlalchemy import SQLAlchemyDataStore
from fastapi import FastAPI, Depends
from fastapi_cache import FastAPICache
from fastapi_cache.backends.redis import RedisBackend
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()

Expand All @@ -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,
Expand Down
5 changes: 3 additions & 2 deletions app/models/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@

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()
Expand Down
2 changes: 1 addition & 1 deletion app/models/nonsense.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
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

Expand Down
1 change: 1 addition & 0 deletions app/models/shakespeare.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
)
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Mapped, mapped_column, relationship

from app.models.base import Base


Expand Down
4 changes: 2 additions & 2 deletions app/models/stuff.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
4 changes: 2 additions & 2 deletions app/models/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@

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

Expand Down
2 changes: 1 addition & 1 deletion app/schemas/nnonsense.py
Original file line number Diff line number Diff line change
@@ -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)

Expand Down
2 changes: 1 addition & 1 deletion app/schemas/stuff.py
Original file line number Diff line number Diff line change
@@ -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)

Expand Down
2 changes: 1 addition & 1 deletion app/schemas/user.py
Original file line number Diff line number Diff line change
@@ -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)

Expand Down
6 changes: 3 additions & 3 deletions app/services/auth.py
Original file line number Diff line number Diff line change
@@ -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()
Expand Down
5 changes: 2 additions & 3 deletions app/services/scheduler.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
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
Expand Down
7 changes: 2 additions & 5 deletions app/services/smtp.py
Original file line number Diff line number Diff line change
@@ -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()


Expand Down
4 changes: 2 additions & 2 deletions app/utils/decorators.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from sqlalchemy.dialects import postgresql

from functools import wraps

from sqlalchemy.dialects import postgresql


def compile_sql_or_scalar(func):
"""
Expand Down
3 changes: 1 addition & 2 deletions app/utils/logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
from rich.console import Console
from rich.logging import RichHandler


from app.utils.singleton import SingletonMeta


Expand All @@ -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,
)
2 changes: 1 addition & 1 deletion performance/locustfile.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from locust import HttpUser, task, between
from locust import HttpUser, between, task


class Stuff(HttpUser):
Expand Down
67 changes: 54 additions & 13 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,71 @@ description = "A modern FastAPI application with SQLAlchemy 2.0 and AsyncPG for
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"fastapi[all]>=0.115.6",
"pydantic[email]>=2.10.3",
"pydantic-settings>=2.7.0",
"sqlalchemy>=2.0.36",
"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.14.0",
"alembic>=1.15.1",
"httpx>=0.28.1",
"pytest>=8.3.4",
"pytest>=8.3.5",
"pytest-cov>=6.0.0",
"uvloop>=0.21.0",
"httptools>=0.6.4",
"rich>=13.9.4",
"pyjwt[cryptography]>=2.10.1",
"pyjwt>=2.10.1",
"redis>=5.2.1",
"bcrypt>=4.2.1",
"polars>=1.17.1",
"bcrypt>=4.3.0",
"polars>=1.24.0",
"python-multipart>=0.0.20",
"fastexcel>=0.12.0",
"fastapi-cache2>=0.2.1",
"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",
"pendulum @ git+https://github.com/sdispater/pendulum.git@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.mypy]
strict = true
exclude = ["venv", ".venv", "alembic"]

[tool.ruff]
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.pyupgrade]
# Preserve types, even if a file imports `from __future__ import annotations`.
keep-runtime-typing = true
Loading