Skip to content
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
7 changes: 5 additions & 2 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@
"extensions": [
"charliermarsh.ruff",
"ms-azuretools.vscode-docker",
"ms-python.mypy-type-checker",
"ms-python.python",
"ms-python.vscode-pylance",
"tamasfe.even-better-toml"
],
"settings": {
Expand All @@ -48,7 +49,9 @@
"python.testing.pytestArgs": [
"src/tests"
],
"python.testing.pytestEnabled": true
"python.testing.pytestEnabled": true,
"python.analysis.typeCheckingMode": "standard",
"python.analysis.autoImportCompletions": true
}
},
"black-formatter.args": [
Expand Down
2 changes: 1 addition & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Dockerfile
docker-compose.yaml

# python related
.mypy_cache
.pyright_cache
.pytest_cache
*.pyc
*.pyo
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,11 @@ var/
.venv
logs/*
.ptpython-history
.mypy_cache
.pyright_cache
.pytest_cache
.coverage
*.coverage
.python-version

.devcontainer/commandhistory
!.devcontainer/commandhistory/.gitkeep
Expand Down
31 changes: 24 additions & 7 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ default_stages: [pre-commit, pre-push]
files: ^src/|^alembic/versions/
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.2.0
rev: v4.6.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
Expand All @@ -12,21 +12,38 @@ repos:

- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.5.5
rev: v0.11.3
hooks:
# Run the linter.
- id: ruff
args: [ --select=I, --fix ]
# Run the formatter.
- id: ruff-format

- repo: https://github.com/pre-commit/mirrors-mypy
rev: 'v1.8.0'
- repo: https://github.com/RobertCraigie/pyright-python
rev: v1.1.402
hooks:
- id: mypy
- id: pyright
exclude: ^src/alembic/|^scripts/|^\\.devcontainer/|^\\.github/
additional_dependencies: [
fastapi==0.115.12,
pydantic==2.10.6,
SQLAlchemy==2.0.39,
types-mock==5.2.0.20250306
pydantic-settings==2.8.1,
sqlalchemy==2.0.39,
asyncpg==0.30.0,
structlog==25.3.0,
sqladmin==0.20.1,
fastapi-pagination==0.12.34,
opentelemetry-api==1.31.1,
opentelemetry-sdk==1.31.1,
opentelemetry-instrumentation-fastapi==0.52b1,
opentelemetry-instrumentation-sqlalchemy==0.52b1,
opentelemetry-exporter-otlp==1.31.1,
celery==5.4.0,
python-jose==3.4.0,
passlib==1.7.4,
uvicorn==0.34.0,
httpx==0.28.1,
pytest==8.3.5,
ptpython==3.0.29
]
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ We use Alembic as database migration tool. You can run migration commands direct
Linters, formatters, etc.

- **ruff**: Linter and formatter
- **mypy**: Static type checker
- **pyright**: Static type checker

### pre-commit

Expand Down
63 changes: 33 additions & 30 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,7 @@ dev = [
"coverage>=7.7.1,<8",
"flower>=2.0.1,<3",
"mock>=5.2.0,<6",
"mypy>=1.15.0,<2",
"mypy-extensions>=1.0.0,<2",
"pyright>=1.1.402,<2",
"pytest>=8.3.5,<9",
"pytest-asyncio==0.26.0",
"pre-commit>=4.2.0,<5",
Expand Down Expand Up @@ -68,6 +67,7 @@ include = ["src/*"]
[tool.hatch.build.targets.wheel.sources]
"src/*" = "*"


[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
Expand All @@ -93,34 +93,37 @@ known-third-party = ["fastapi", "sqlalchemy", "pydantic"]
force-single-line = false
combine-as-imports = true

[tool.mypy]
plugins = ["pydantic.mypy", "sqlalchemy.ext.mypy.plugin"]
ignore_missing_imports = true
disallow_untyped_defs = true
warn_unused_ignores = false
no_implicit_optional = true
implicit_reexport = true
explicit_package_bases = true
namespace_packages = true
follow_imports = "silent"
warn_redundant_casts = true
check_untyped_defs = true
no_implicit_reexport = true
disable_error_code = ["name-defined", "call-arg", "attr-defined"]

[[tool.mypy.overrides]]
module = "starlette_context.plugins"
implicit_reexport = true

[[tool.mypy.overrides]]
module = "app.middlewares.logging_middleware"
warn_unused_ignores = false

[tool.pydantic-mypy]
init_forbid_extra = true
init_typed = true
warn_required_dynamic_aliases = false
warn_untyped_fields = true
[tool.pyright]
include = ["src"]
exclude = ["src/alembic/versions", "**/__pycache__", "scripts"]
reportMissingImports = true
reportMissingTypeStubs = false
pythonVersion = "3.13"
typeCheckingMode = "standard"
useLibraryCodeForTypes = true
strictListInference = true
strictDictionaryInference = true
strictSetInference = true
analyzeUnannotatedFunctions = true
strictParameterNoneValue = true
enableTypeIgnoreComments = true
reportGeneralTypeIssues = true
reportOptionalSubscript = true
reportOptionalMemberAccess = true
reportOptionalCall = true
reportOptionalIterable = true
reportOptionalContextManager = true
reportOptionalOperand = true
reportTypedDictNotRequiredAccess = false
reportPrivateUsage = false
reportUnknownArgumentType = false
reportUnknownLambdaType = false
reportUnknownMemberType = false
reportUnknownParameterType = false
reportUnknownVariableType = false
reportUnnecessaryIsInstance = false
reportUnnecessaryCast = false
reportUnnecessaryComparison = false

[tool.pytest.ini_options]
asyncio_mode = "auto"
Expand Down
8 changes: 4 additions & 4 deletions scripts/format.sh
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
#!/bin/bash

printf "\nRunning mypy...\n"
uv run python -m mypy src
printf "\nRunning pyright...\n"
uv run pyright src

printf "\nRunning ruff check...\n"
ruff check --fix
uv run ruff check --fix

printf "\nRunning ruff format...\n"
ruff format
uv run ruff format
9 changes: 6 additions & 3 deletions src/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ async def authenticate(self, request: Request) -> RedirectResponse | bool:
token = request.session.get(AdminAuth.cookie_name)
if not token:
return failed_auth_response

session = None
try:
async_session = async_session_generator()
async with async_session() as session:
Expand All @@ -44,13 +46,14 @@ async def authenticate(self, request: Request) -> RedirectResponse | bool:
except Exception:
return failed_auth_response
finally:
await session.close()
if session is not None:
await session.close()
if not user.is_superuser:
return failed_auth_response
return True


class UserAdmin(ModelView, model=User):
class UserAdmin(ModelView, model=User): # type: ignore[misc]
column_list = [
User.email,
User.created_at,
Expand All @@ -59,7 +62,7 @@ class UserAdmin(ModelView, model=User):
column_searchable_list = [User.id, User.email]


class ItemAdmin(ModelView, model=Item):
class ItemAdmin(ModelView, model=Item): # type: ignore[misc]
column_list = [
Item.name,
Item.description,
Expand Down
4 changes: 2 additions & 2 deletions src/alembic/env.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import asyncio
from logging.config import fileConfig

from alembic import context
from sqlalchemy import engine_from_config, pool
from sqlalchemy import pool
from sqlalchemy.engine import Connection
from sqlalchemy.ext.asyncio import async_engine_from_config

from alembic import context
from src import models # noqa F401
from src.core.config import settings
from src.core.database import SQLBase
Expand Down
7 changes: 5 additions & 2 deletions src/api/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,18 @@


async def db_session() -> AsyncIterator[AsyncSession]:
session = None
try:
async_session = async_session_generator()
async with async_session() as session:
yield session
except Exception:
await session.rollback()
if session is not None:
await session.rollback()
raise
finally:
await session.close()
if session is not None:
await session.close()


async def get_user(request: Request, session: AsyncSession = Depends(db_session)) -> User:
Expand Down
4 changes: 2 additions & 2 deletions src/api/v1/routers/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@
# you have to implement the task logic in the src/task_queue/task.py file
@router.post("/add", response_model=Task)
def add_task(payload: TaskCreate = Body(...)) -> Any:
task = add.delay(payload.delay, payload.x, payload.y)
task = add.delay(payload.delay, payload.x, payload.y) # type: ignore[reportFunctionMemberAccess]
return {"task_id": task.id}


@router.get("/task/{task_id}", response_model=TaskResult)
def get_task_result(task_id: str = Path(...)) -> Any:
task: AsyncResult = add.AsyncResult(task_id)
task: AsyncResult = add.AsyncResult(task_id) # type: ignore[reportFunctionMemberAccess]
return {"task_id": task.id, "task_status": task.status, "task_result": task.result}
8 changes: 6 additions & 2 deletions src/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ class LogSettings(BaseSettings):
structured_log: bool | Literal["auto"] = "auto"
cache_loggers: bool = True

def __hash__(self) -> int:
"""Make LogSettings hashable so it can be used as a dict key"""
return hash((self.log_level, self.structured_log, self.cache_loggers))

@property
def enable_structured_log(self) -> bool:
return not sys.stdout.isatty() if self.structured_log == "auto" else self.structured_log
Expand All @@ -42,7 +46,7 @@ def parse_log_level(cls, data: Any) -> Any:

class Settings(BaseSettings):
# Makes the settings immutable and hashable (can be used as dicts key)
model_config = SettingsConfigDict(frozen=True)
model_config = SettingsConfigDict(frozen=True, env_file=".env", env_file_encoding="utf-8")

env: str = "dev"
project_name: str
Expand Down Expand Up @@ -72,4 +76,4 @@ class Settings(BaseSettings):
otel_exporter_otlp_endpoint: str


settings = Settings()
settings = Settings() # type: ignore[reportCallIssue]
21 changes: 18 additions & 3 deletions src/helpers/shell.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import asyncio
import inspect
from typing import Dict, Type

from ptpython.repl import embed

from src import models
from src.core.config import settings
from src.core.database import SessionLocal, SQLBase
from src.core.database import SQLBase, async_session_generator


def _get_models() -> Dict[str, Type[SQLBase]]:
Expand All @@ -16,12 +17,26 @@ def _get_models() -> Dict[str, Type[SQLBase]]:
return models_dict


def _start_shell() -> None:
async def _start_shell_async() -> None:
"""Async shell function that properly handles the async session"""
models = _get_models()
with SessionLocal() as session:
async_session = async_session_generator()
async with async_session() as session:
locals = {"session": session, "settings": settings, **models}
embed(locals=locals, history_filename=".ptpython-history")


def _start_shell() -> None:
"""Start the shell - wrapper to handle async context"""
try:
# Try to use existing event loop if available
asyncio.get_running_loop()
# If we're already in an async context, we can't use asyncio.run
asyncio.create_task(_start_shell_async())
except RuntimeError:
# No event loop running, start new one
asyncio.run(_start_shell_async())


if __name__ == "__main__":
_start_shell()
7 changes: 5 additions & 2 deletions src/tests/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,18 @@ def async_session_generator() -> async_sessionmaker[AsyncSession]:


async def override_get_db() -> AsyncIterator[AsyncSession]:
session = None
try:
async_session = async_session_generator()
async with async_session() as session:
yield session
except Exception:
await session.rollback()
if session is not None:
await session.rollback()
raise
finally:
await session.close()
if session is not None:
await session.close()


app.dependency_overrides[db_session] = override_get_db
1 change: 0 additions & 1 deletion src/tests/test_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

from httpx import AsyncClient, Response
from jose import jwt
from mock import patch

from src.core.config import settings
from src.core.security import AuthManager, PasswordManager
Expand Down
Loading