diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..b405bfc --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,9 @@ +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.16.2 + hooks: + - id: ruff-check + args: [--fix] + files: ^backend/ + - id: ruff-format + files: ^backend/ diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..c1ea73e --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,7 @@ +{ + "recommendations": [ + "charliermarsh.ruff", + "ms-python.python", + "ms-python.vscode-pylance" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..5789121 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,20 @@ +{ + "python.defaultInterpreterPath": "${workspaceFolder}/backend/.venv/bin/python", + "python.analysis.typeCheckingMode": "standard", + "python.analysis.diagnosticSeverityOverrides": { + "reportDeprecated": "error" + }, + + "[python]": { + "editor.defaultFormatter": "charliermarsh.ruff", + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + // Applies ruff's autofixes, which includes deleting unused imports (F401). + "source.fixAll.ruff": "explicit", + "source.organizeImports.ruff": "explicit" + } + }, + + "files.insertFinalNewline": true, + "files.trimTrailingWhitespace": true +} diff --git a/README.md b/README.md index 63ef1d0..5dafa4d 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ ```bash cp .env.template .env # defaults already match docker-compose just install +just hooks # pre-commit hook, one time just up # postgres + redis + localstack just migrate just seed # roles, permissions, first owner invitation @@ -38,13 +39,14 @@ Docker compose uses offset host ports so it does not collide with anything alrea | `just dev` | run the API with reload | | `just migrate` | apply migrations | | `just rollback` | undo the last migration | -| `just revision m` | create a migration | +| `just revision ` | create a migration | | `just seed` | sync roles and permissions | | `just test` | pytest | | `just lint` | ruff check + format check | | `just fmt` | ruff autofix + format | | `just typecheck` | mypy over `src` | | `just check` | lint + typecheck + test | +| `just hooks` | install the pre-commit hook | ## Tests diff --git a/backend/alembic/env.py b/backend/alembic/env.py index d8d2dcb..131755b 100644 --- a/backend/alembic/env.py +++ b/backend/alembic/env.py @@ -3,7 +3,7 @@ from alembic import context from sqlalchemy import create_engine -from generate_admin.core.config import get_settings +from admin.core.config import get_settings config = context.config diff --git a/backend/alembic/versions/0001_base.py b/backend/alembic/versions/3ec5081bde0b_base.py similarity index 87% rename from backend/alembic/versions/0001_base.py rename to backend/alembic/versions/3ec5081bde0b_base.py index 48ff523..63b778a 100644 --- a/backend/alembic/versions/0001_base.py +++ b/backend/alembic/versions/3ec5081bde0b_base.py @@ -1,14 +1,15 @@ -"""shared helpers every table builds on +"""base -Revision ID: 0001 +Revision ID: 3ec5081bde0b Revises: +Create Date: 2026-08-08 22:51:50.576882 """ from collections.abc import Sequence from alembic import op -revision: str = "0001" +revision: str = "3ec5081bde0b" down_revision: str | None = None branch_labels: str | Sequence[str] | None = None depends_on: str | Sequence[str] | None = None diff --git a/backend/alembic/versions/0003_identity_and_rbac.py b/backend/alembic/versions/ce8c0c720681_rbac.py similarity index 96% rename from backend/alembic/versions/0003_identity_and_rbac.py rename to backend/alembic/versions/ce8c0c720681_rbac.py index 83f14bc..bc12854 100644 --- a/backend/alembic/versions/0003_identity_and_rbac.py +++ b/backend/alembic/versions/ce8c0c720681_rbac.py @@ -1,18 +1,20 @@ -"""users, roles, invitations and access requests +"""rbac -Revision ID: 0003 -Revises: 0002 +Revision ID: ce8c0c720681 +Revises: f453db2e2da7 +Create Date: 2026-08-08 22:53:34.662744 """ from collections.abc import Sequence from alembic import op -revision: str = "0003" -down_revision: str | None = "0002" +revision: str = "ce8c0c720681" +down_revision: str | None = "f453db2e2da7" branch_labels: str | Sequence[str] | None = None depends_on: str | Sequence[str] | None = None + UPGRADE = """ CREATE TABLE permissions ( key VARCHAR(128) PRIMARY KEY, diff --git a/backend/alembic/versions/0002_audit_logs.py b/backend/alembic/versions/f453db2e2da7_audit_log.py similarity index 84% rename from backend/alembic/versions/0002_audit_logs.py rename to backend/alembic/versions/f453db2e2da7_audit_log.py index 7e03bc4..351d21a 100644 --- a/backend/alembic/versions/0002_audit_logs.py +++ b/backend/alembic/versions/f453db2e2da7_audit_log.py @@ -1,18 +1,20 @@ -"""audit_logs, the append-only record of who changed what +"""audit log -Revision ID: 0002 -Revises: 0001 +Revision ID: f453db2e2da7 +Revises: 3ec5081bde0b +Create Date: 2026-08-08 22:53:05.480514 """ from collections.abc import Sequence from alembic import op -revision: str = "0002" -down_revision: str | None = "0001" +revision: str = "f453db2e2da7" +down_revision: str | None = "3ec5081bde0b" branch_labels: str | Sequence[str] | None = None depends_on: str | Sequence[str] | None = None + UPGRADE = """ CREATE TABLE audit_logs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), diff --git a/backend/pyproject.toml b/backend/pyproject.toml index c8dd89e..f714275 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -18,6 +18,7 @@ dependencies = [ "email-validator>=2.2.0", "boto3>=1.35.0", "redis>=5.2.0", + "typer[all]>=0.27.1", ] [dependency-groups] @@ -26,6 +27,7 @@ dev = [ "pytest-asyncio>=0.24.0", "ruff>=0.8.0", "mypy>=1.13.0", + "pre-commit>=4.0.0", ] [build-system] @@ -33,7 +35,7 @@ requires = ["hatchling"] build-backend = "hatchling.build" [tool.hatch.build.targets.wheel] -packages = ["src/generate_admin"] +packages = ["src/admin"] [tool.ruff] line-length = 100 @@ -43,7 +45,7 @@ src = ["src", "tests"] select = ["E", "F", "I", "N", "UP", "B", "SIM", "TID", "RUF"] [tool.ruff.lint.isort] -known-first-party = ["generate_admin"] +known-first-party = ["admin"] [tool.mypy] python_version = "3.12" @@ -56,6 +58,14 @@ warn_return_any = false module = ["asyncpg.*", "boto3.*", "botocore.*"] ignore_missing_imports = true +[tool.pyright] +include = ["src", "tests"] +venvPath = "." +venv = ".venv" +pythonVersion = "3.12" +typeCheckingMode = "standard" +reportDeprecated = "error" + [tool.pytest.ini_options] asyncio_mode = "auto" testpaths = ["tests"] diff --git a/backend/src/generate_admin/__init__.py b/backend/src/admin/__init__.py similarity index 100% rename from backend/src/generate_admin/__init__.py rename to backend/src/admin/__init__.py diff --git a/backend/src/generate_admin/api/__init__.py b/backend/src/admin/api/__init__.py similarity index 100% rename from backend/src/generate_admin/api/__init__.py rename to backend/src/admin/api/__init__.py diff --git a/backend/src/generate_admin/api/dependencies.py b/backend/src/admin/api/dependencies.py similarity index 85% rename from backend/src/generate_admin/api/dependencies.py rename to backend/src/admin/api/dependencies.py index a54d57e..1665166 100644 --- a/backend/src/generate_admin/api/dependencies.py +++ b/backend/src/admin/api/dependencies.py @@ -6,31 +6,31 @@ from fastapi import Depends, Request from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer -from generate_admin.core.cache import Cache as CacheProtocol -from generate_admin.core.config import Settings, get_settings -from generate_admin.core.errors import ( +from admin.core.cache import Cache as CacheProtocol +from admin.core.config import Settings, get_settings +from admin.core.errors import ( AccountNotProvisionedError, AccountSuspendedError, PermissionDeniedError, ) -from generate_admin.core.security import TokenVerifier -from generate_admin.core.storage import S3Storage -from generate_admin.domain.access import PermissionSet, ResolvedAccess -from generate_admin.domain.enums import AccessState -from generate_admin.domain.permissions import Permission -from generate_admin.repositories import ( +from admin.core.security import TokenVerifier +from admin.core.storage import S3Storage +from admin.domain.access import PermissionSet, ResolvedAccess +from admin.domain.enums import AccessState +from admin.domain.permissions import Permission +from admin.repositories import ( AccessRequestRepository, AuditRepository, InvitationRepository, RoleRepository, UserRepository, ) -from generate_admin.schemas.session import Identity, Session -from generate_admin.schemas.user import UserRead -from generate_admin.services.access import AccessService -from generate_admin.services.access_request import AccessRequestService -from generate_admin.services.invitation import InvitationService -from generate_admin.services.member import MemberService +from admin.schemas.session import Identity, Session +from admin.schemas.user import UserRead +from admin.services.access import AccessService +from admin.services.access_request import AccessRequestService +from admin.services.invitation import InvitationService +from admin.services.member import MemberService bearer_scheme = HTTPBearer(auto_error=True) @@ -44,8 +44,11 @@ class AuthContext: permissions: PermissionSet -async def get_connection(request: Request) -> AsyncIterator[asyncpg.Connection]: +async def get_connection( + request: Request, +) -> AsyncIterator[asyncpg.pool.PoolConnectionProxy]: pool: asyncpg.Pool = request.app.state.pool + async with pool.acquire() as connection, connection.transaction(): yield connection diff --git a/backend/src/generate_admin/api/router.py b/backend/src/admin/api/router.py similarity index 92% rename from backend/src/generate_admin/api/router.py rename to backend/src/admin/api/router.py index 1e5bf1e..74ae0ad 100644 --- a/backend/src/generate_admin/api/router.py +++ b/backend/src/admin/api/router.py @@ -1,6 +1,6 @@ from fastapi import APIRouter -from generate_admin.api.v1 import ( +from admin.api.v1 import ( access_requests, health, invitations, diff --git a/backend/src/generate_admin/api/v1/__init__.py b/backend/src/admin/api/v1/__init__.py similarity index 100% rename from backend/src/generate_admin/api/v1/__init__.py rename to backend/src/admin/api/v1/__init__.py diff --git a/backend/src/generate_admin/api/v1/access_requests.py b/backend/src/admin/api/v1/access_requests.py similarity index 87% rename from backend/src/generate_admin/api/v1/access_requests.py rename to backend/src/admin/api/v1/access_requests.py index 596d76b..9ee8e7a 100644 --- a/backend/src/generate_admin/api/v1/access_requests.py +++ b/backend/src/admin/api/v1/access_requests.py @@ -3,9 +3,9 @@ from fastapi import APIRouter, Depends -from generate_admin.api.dependencies import AccessRequestServiceDep, AuthContext, require -from generate_admin.domain.permissions import Permission -from generate_admin.schemas.access_request import ( +from admin.api.dependencies import AccessRequestServiceDep, AuthContext, require +from admin.domain.permissions import Permission +from admin.schemas.access_request import ( AccessRequestApproval, AccessRequestDenial, AccessRequestRead, diff --git a/backend/src/generate_admin/api/v1/health.py b/backend/src/admin/api/v1/health.py similarity index 100% rename from backend/src/generate_admin/api/v1/health.py rename to backend/src/admin/api/v1/health.py diff --git a/backend/src/generate_admin/api/v1/invitations.py b/backend/src/admin/api/v1/invitations.py similarity index 82% rename from backend/src/generate_admin/api/v1/invitations.py rename to backend/src/admin/api/v1/invitations.py index b48b7eb..f88661b 100644 --- a/backend/src/generate_admin/api/v1/invitations.py +++ b/backend/src/admin/api/v1/invitations.py @@ -3,9 +3,9 @@ from fastapi import APIRouter, Depends, status -from generate_admin.api.dependencies import AuthContext, InvitationServiceDep, require -from generate_admin.domain.permissions import Permission -from generate_admin.schemas.invitation import InvitationCreate, InvitationCreated, InvitationRead +from admin.api.dependencies import AuthContext, InvitationServiceDep, require +from admin.domain.permissions import Permission +from admin.schemas.invitation import InvitationCreate, InvitationCreated, InvitationRead router = APIRouter(prefix="/invitations", tags=["invitations"]) diff --git a/backend/src/generate_admin/api/v1/members.py b/backend/src/admin/api/v1/members.py similarity index 82% rename from backend/src/generate_admin/api/v1/members.py rename to backend/src/admin/api/v1/members.py index dc2c6bb..f170578 100644 --- a/backend/src/generate_admin/api/v1/members.py +++ b/backend/src/admin/api/v1/members.py @@ -3,10 +3,10 @@ from fastapi import APIRouter, Depends -from generate_admin.api.dependencies import AuthContext, MemberServiceDep, require -from generate_admin.domain.permissions import Permission -from generate_admin.schemas.role import RoleGrantRequest -from generate_admin.schemas.user import UserRead +from admin.api.dependencies import AuthContext, MemberServiceDep, require +from admin.domain.permissions import Permission +from admin.schemas.role import RoleGrantRequest +from admin.schemas.user import UserRead router = APIRouter(prefix="/members", tags=["members"]) diff --git a/backend/src/generate_admin/api/v1/roles.py b/backend/src/admin/api/v1/roles.py similarity index 75% rename from backend/src/generate_admin/api/v1/roles.py rename to backend/src/admin/api/v1/roles.py index e2c32be..03ee726 100644 --- a/backend/src/generate_admin/api/v1/roles.py +++ b/backend/src/admin/api/v1/roles.py @@ -2,9 +2,9 @@ from fastapi import APIRouter, Depends -from generate_admin.api.dependencies import AuthContext, Roles, require -from generate_admin.domain.permissions import Permission -from generate_admin.schemas.role import PermissionRead, RoleRead +from admin.api.dependencies import AuthContext, Roles, require +from admin.domain.permissions import Permission +from admin.schemas.role import PermissionRead, RoleRead router = APIRouter(tags=["roles"]) diff --git a/backend/src/generate_admin/api/v1/session.py b/backend/src/admin/api/v1/session.py similarity index 77% rename from backend/src/generate_admin/api/v1/session.py rename to backend/src/admin/api/v1/session.py index 69638df..e0121a0 100644 --- a/backend/src/generate_admin/api/v1/session.py +++ b/backend/src/admin/api/v1/session.py @@ -1,12 +1,12 @@ from fastapi import APIRouter, status -from generate_admin.api.dependencies import ( +from admin.api.dependencies import ( AccessRequestServiceDep, CurrentIdentity, CurrentSession, ) -from generate_admin.schemas.access_request import AccessRequestCreate, AccessRequestRead -from generate_admin.schemas.session import Session +from admin.schemas.access_request import AccessRequestCreate, AccessRequestRead +from admin.schemas.session import Session router = APIRouter(tags=["session"]) diff --git a/backend/src/generate_admin/cli.py b/backend/src/admin/cli.py similarity index 76% rename from backend/src/generate_admin/cli.py rename to backend/src/admin/cli.py index d96b2fc..1d9862d 100644 --- a/backend/src/generate_admin/cli.py +++ b/backend/src/admin/cli.py @@ -1,21 +1,27 @@ import asyncio -import sys from datetime import UTC, datetime, timedelta -import asyncpg +import typer -from generate_admin.core.config import Settings, get_settings -from generate_admin.core.database import create_pool -from generate_admin.domain.permissions import ( +from admin.core.config import Settings, get_settings +from admin.core.database import DBConnection, create_pool +from admin.domain.permissions import ( PERMISSION_DESCRIPTIONS, ROLE_DEFINITIONS, Permission, SystemRole, ) -from generate_admin.services.invitation import generate_token +from admin.services.invitation import generate_token +app = typer.Typer(help="generate-admin management commands") -async def sync_permissions(connection: asyncpg.Connection) -> None: + +@app.callback() +def main() -> None: + """Cli for admin app""" + + +async def _sync_permissions(connection: DBConnection) -> None: await connection.executemany( """ INSERT INTO permissions (key, description) @@ -30,7 +36,7 @@ async def sync_permissions(connection: asyncpg.Connection) -> None: ) -async def sync_roles(connection: asyncpg.Connection) -> None: +async def _sync_roles(connection: DBConnection) -> None: for definition in ROLE_DEFINITIONS: role_id = await connection.fetchval( """ @@ -62,7 +68,7 @@ async def sync_roles(connection: asyncpg.Connection) -> None: ) -async def ensure_owner_invitation(connection: asyncpg.Connection, settings: Settings) -> None: +async def _ensure_owner_invitation(connection: DBConnection, settings: Settings) -> None: email = settings.initial_owner_email.strip().lower() if not email: return @@ -107,30 +113,23 @@ async def ensure_owner_invitation(connection: asyncpg.Connection, settings: Sett print(f"invitation token: {token}") -async def seed() -> None: +async def _seed() -> None: settings = get_settings() pool = await create_pool(settings.database) try: async with pool.acquire() as connection, connection.transaction(): - await sync_permissions(connection) - await sync_roles(connection) - await ensure_owner_invitation(connection, settings) + await _sync_permissions(connection) + await _sync_roles(connection) + await _ensure_owner_invitation(connection, settings) print("seed complete") finally: await pool.close() -COMMANDS = {"seed": seed} - - -def main() -> None: - command = sys.argv[1] if len(sys.argv) > 1 else "" - handler = COMMANDS.get(command) - if handler is None: - print(f"usage: python -m generate_admin.cli [{'|'.join(COMMANDS)}]") - raise SystemExit(1) - asyncio.run(handler()) +@app.command() +def seed() -> None: + asyncio.run(_seed()) if __name__ == "__main__": - main() + app() diff --git a/backend/src/generate_admin/core/__init__.py b/backend/src/admin/core/__init__.py similarity index 100% rename from backend/src/generate_admin/core/__init__.py rename to backend/src/admin/core/__init__.py diff --git a/backend/src/generate_admin/core/cache.py b/backend/src/admin/core/cache.py similarity index 98% rename from backend/src/generate_admin/core/cache.py rename to backend/src/admin/core/cache.py index 82cfc23..7f667f0 100644 --- a/backend/src/generate_admin/core/cache.py +++ b/backend/src/admin/core/cache.py @@ -6,7 +6,7 @@ from pydantic import TypeAdapter from redis.asyncio import Redis -from generate_admin.core.logging import get_logger +from admin.core.logging import get_logger logger = get_logger(__name__) diff --git a/backend/src/generate_admin/core/config.py b/backend/src/admin/core/config.py similarity index 100% rename from backend/src/generate_admin/core/config.py rename to backend/src/admin/core/config.py diff --git a/backend/src/generate_admin/core/database.py b/backend/src/admin/core/database.py similarity index 68% rename from backend/src/generate_admin/core/database.py rename to backend/src/admin/core/database.py index 95eb6b0..2bff5f2 100644 --- a/backend/src/generate_admin/core/database.py +++ b/backend/src/admin/core/database.py @@ -1,10 +1,15 @@ import json -from collections.abc import AsyncIterator +from collections.abc import AsyncGenerator from contextlib import asynccontextmanager import asyncpg -from generate_admin.core.config import DatabaseConfig +from admin.core.config import DatabaseConfig + +# Pool.acquire() yields a PoolConnectionProxy, which forwards to Connection at runtime via +# metaclass delegation but is not a subclass of it. Anything that just runs queries should +# accept either. +type DBConnection = asyncpg.Connection | asyncpg.pool.PoolConnectionProxy ASYNCPG_SCHEME = "postgresql://" SQLALCHEMY_SCHEME = "postgresql+asyncpg://" @@ -36,6 +41,6 @@ async def create_pool(config: DatabaseConfig) -> asyncpg.Pool: @asynccontextmanager -async def transaction(pool: asyncpg.Pool) -> AsyncIterator[asyncpg.Connection]: +async def transaction(pool: asyncpg.Pool) -> AsyncGenerator[DBConnection]: async with pool.acquire() as connection, connection.transaction(): yield connection diff --git a/backend/src/generate_admin/core/errors.py b/backend/src/admin/core/errors.py similarity index 100% rename from backend/src/generate_admin/core/errors.py rename to backend/src/admin/core/errors.py diff --git a/backend/src/generate_admin/core/logging.py b/backend/src/admin/core/logging.py similarity index 95% rename from backend/src/generate_admin/core/logging.py rename to backend/src/admin/core/logging.py index 2303341..12a2b4a 100644 --- a/backend/src/generate_admin/core/logging.py +++ b/backend/src/admin/core/logging.py @@ -3,7 +3,7 @@ import structlog -from generate_admin.core.config import AppConfig +from admin.core.config import AppConfig def configure_logging(config: AppConfig) -> None: diff --git a/backend/src/generate_admin/core/openapi.py b/backend/src/admin/core/openapi.py similarity index 100% rename from backend/src/generate_admin/core/openapi.py rename to backend/src/admin/core/openapi.py diff --git a/backend/src/generate_admin/core/security.py b/backend/src/admin/core/security.py similarity index 95% rename from backend/src/generate_admin/core/security.py rename to backend/src/admin/core/security.py index 8b2dd61..2d57d3f 100644 --- a/backend/src/generate_admin/core/security.py +++ b/backend/src/admin/core/security.py @@ -10,10 +10,10 @@ from pydantic import EmailStr, TypeAdapter from pydantic import ValidationError as PydanticError -from generate_admin.core.config import EntraConfig -from generate_admin.core.errors import AuthenticationError -from generate_admin.core.logging import get_logger -from generate_admin.schemas.session import Identity +from admin.core.config import EntraConfig +from admin.core.errors import AuthenticationError +from admin.core.logging import get_logger +from admin.schemas.session import Identity logger = get_logger(__name__) diff --git a/backend/src/generate_admin/core/storage.py b/backend/src/admin/core/storage.py similarity index 97% rename from backend/src/generate_admin/core/storage.py rename to backend/src/admin/core/storage.py index 8142f51..46233d0 100644 --- a/backend/src/generate_admin/core/storage.py +++ b/backend/src/admin/core/storage.py @@ -10,8 +10,8 @@ from botocore.exceptions import ClientError from pydantic import TypeAdapter -from generate_admin.core.cache import Cache, CacheNamespace -from generate_admin.core.config import StorageConfig +from admin.core.cache import Cache, CacheNamespace +from admin.core.config import StorageConfig class MediaVisibility(StrEnum): diff --git a/backend/src/generate_admin/domain/__init__.py b/backend/src/admin/domain/__init__.py similarity index 100% rename from backend/src/generate_admin/domain/__init__.py rename to backend/src/admin/domain/__init__.py diff --git a/backend/src/generate_admin/domain/access.py b/backend/src/admin/domain/access.py similarity index 87% rename from backend/src/generate_admin/domain/access.py rename to backend/src/admin/domain/access.py index b2496c0..b32ad4e 100644 --- a/backend/src/generate_admin/domain/access.py +++ b/backend/src/admin/domain/access.py @@ -1,7 +1,7 @@ from dataclasses import dataclass -from generate_admin.domain.permissions import Permission -from generate_admin.schemas.session import Session +from admin.domain.permissions import Permission +from admin.schemas.session import Session @dataclass(frozen=True, slots=True) diff --git a/backend/src/generate_admin/domain/enums.py b/backend/src/admin/domain/enums.py similarity index 100% rename from backend/src/generate_admin/domain/enums.py rename to backend/src/admin/domain/enums.py diff --git a/backend/src/generate_admin/domain/permissions.py b/backend/src/admin/domain/permissions.py similarity index 100% rename from backend/src/generate_admin/domain/permissions.py rename to backend/src/admin/domain/permissions.py diff --git a/backend/src/generate_admin/main.py b/backend/src/admin/main.py similarity index 77% rename from backend/src/generate_admin/main.py rename to backend/src/admin/main.py index f13892d..5e5a66f 100644 --- a/backend/src/generate_admin/main.py +++ b/backend/src/admin/main.py @@ -1,4 +1,4 @@ -from collections.abc import AsyncIterator +from collections.abc import AsyncGenerator from contextlib import asynccontextmanager import httpx @@ -6,15 +6,15 @@ from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse -from generate_admin.api.router import api_router, root_router -from generate_admin.core.cache import build_cache -from generate_admin.core.config import Settings, get_settings -from generate_admin.core.database import create_pool -from generate_admin.core.errors import DomainError -from generate_admin.core.logging import configure_logging, get_logger -from generate_admin.core.openapi import operation_id_for -from generate_admin.core.security import build_token_verifier -from generate_admin.core.storage import S3Storage +from admin.api.router import api_router, root_router +from admin.core.cache import build_cache +from admin.core.config import Settings, get_settings +from admin.core.database import create_pool +from admin.core.errors import DomainError +from admin.core.logging import configure_logging, get_logger +from admin.core.openapi import operation_id_for +from admin.core.security import build_token_verifier +from admin.core.storage import S3Storage logger = get_logger(__name__) @@ -27,7 +27,7 @@ def verify_production_readiness(settings: Settings) -> None: @asynccontextmanager -async def lifespan(app: FastAPI) -> AsyncIterator[None]: +async def lifespan(app: FastAPI) -> AsyncGenerator[None]: settings = get_settings() verify_production_readiness(settings) diff --git a/backend/src/admin/repositories/__init__.py b/backend/src/admin/repositories/__init__.py new file mode 100644 index 0000000..7fdc534 --- /dev/null +++ b/backend/src/admin/repositories/__init__.py @@ -0,0 +1,13 @@ +from admin.repositories.access_request import AccessRequestRepository +from admin.repositories.audit import AuditRepository +from admin.repositories.invitation import InvitationRepository +from admin.repositories.role import RoleRepository +from admin.repositories.user import UserRepository + +__all__ = [ + "AccessRequestRepository", + "AuditRepository", + "InvitationRepository", + "RoleRepository", + "UserRepository", +] diff --git a/backend/src/generate_admin/repositories/access_request.py b/backend/src/admin/repositories/access_request.py similarity index 92% rename from backend/src/generate_admin/repositories/access_request.py rename to backend/src/admin/repositories/access_request.py index f893e40..93d8757 100644 --- a/backend/src/generate_admin/repositories/access_request.py +++ b/backend/src/admin/repositories/access_request.py @@ -1,8 +1,8 @@ import uuid -from generate_admin.domain.enums import AccessRequestStatus -from generate_admin.repositories.base import Repository, required_row -from generate_admin.schemas.access_request import AccessRequestRead +from admin.domain.enums import AccessRequestStatus +from admin.repositories.base import Repository, required_row +from admin.schemas.access_request import AccessRequestRead ACCESS_REQUEST_COLUMNS = """ id, email, entra_object_id, name, message, status, diff --git a/backend/src/generate_admin/repositories/audit.py b/backend/src/admin/repositories/audit.py similarity index 90% rename from backend/src/generate_admin/repositories/audit.py rename to backend/src/admin/repositories/audit.py index fa3ac2c..f6aface 100644 --- a/backend/src/generate_admin/repositories/audit.py +++ b/backend/src/admin/repositories/audit.py @@ -1,9 +1,9 @@ import uuid from datetime import datetime -from generate_admin.repositories.base import ConditionSet, Repository -from generate_admin.schemas.audit import AuditEntry, AuditLogRead -from generate_admin.schemas.base import CursorParams +from admin.repositories.base import ConditionSet, Repository +from admin.schemas.audit import AuditEntry, AuditLogRead +from admin.schemas.base import CursorParams AUDIT_COLUMNS = """ id, actor_id, actor_email, action, resource_type, resource_id, diff --git a/backend/src/generate_admin/repositories/base.py b/backend/src/admin/repositories/base.py similarity index 100% rename from backend/src/generate_admin/repositories/base.py rename to backend/src/admin/repositories/base.py diff --git a/backend/src/generate_admin/repositories/invitation.py b/backend/src/admin/repositories/invitation.py similarity index 95% rename from backend/src/generate_admin/repositories/invitation.py rename to backend/src/admin/repositories/invitation.py index 63f17e2..1c1c39c 100644 --- a/backend/src/generate_admin/repositories/invitation.py +++ b/backend/src/admin/repositories/invitation.py @@ -1,8 +1,8 @@ import uuid from datetime import datetime -from generate_admin.repositories.base import Repository, required_row -from generate_admin.schemas.invitation import InvitationRead +from admin.repositories.base import Repository, required_row +from admin.schemas.invitation import InvitationRead INVITATION_SELECT = """ SELECT i.id, diff --git a/backend/src/generate_admin/repositories/role.py b/backend/src/admin/repositories/role.py similarity index 95% rename from backend/src/generate_admin/repositories/role.py rename to backend/src/admin/repositories/role.py index ebf18d3..d86e428 100644 --- a/backend/src/generate_admin/repositories/role.py +++ b/backend/src/admin/repositories/role.py @@ -1,8 +1,8 @@ import uuid from datetime import datetime -from generate_admin.repositories.base import Repository -from generate_admin.schemas.role import PermissionRead, RoleRead +from admin.repositories.base import Repository +from admin.schemas.role import PermissionRead, RoleRead ROLE_SELECT = """ SELECT r.id, diff --git a/backend/src/generate_admin/repositories/user.py b/backend/src/admin/repositories/user.py similarity index 94% rename from backend/src/generate_admin/repositories/user.py rename to backend/src/admin/repositories/user.py index d723bcc..9dae2d0 100644 --- a/backend/src/generate_admin/repositories/user.py +++ b/backend/src/admin/repositories/user.py @@ -1,9 +1,9 @@ import uuid -from generate_admin.domain.enums import UserStatus -from generate_admin.repositories.base import Repository, required_row -from generate_admin.schemas.role import RoleAssignmentRead -from generate_admin.schemas.user import UserRead +from admin.domain.enums import UserStatus +from admin.repositories.base import Repository, required_row +from admin.schemas.role import RoleAssignmentRead +from admin.schemas.user import UserRead USER_SELECT = """ SELECT u.id, diff --git a/backend/src/generate_admin/schemas/__init__.py b/backend/src/admin/schemas/__init__.py similarity index 100% rename from backend/src/generate_admin/schemas/__init__.py rename to backend/src/admin/schemas/__init__.py diff --git a/backend/src/generate_admin/schemas/access_request.py b/backend/src/admin/schemas/access_request.py similarity index 86% rename from backend/src/generate_admin/schemas/access_request.py rename to backend/src/admin/schemas/access_request.py index 6c95d2a..bd3290e 100644 --- a/backend/src/generate_admin/schemas/access_request.py +++ b/backend/src/admin/schemas/access_request.py @@ -3,8 +3,8 @@ from pydantic import EmailStr, Field -from generate_admin.domain.enums import AccessRequestStatus -from generate_admin.schemas.base import ReadDTO, RequestDTO +from admin.domain.enums import AccessRequestStatus +from admin.schemas.base import ReadDTO, RequestDTO class AccessRequestCreate(RequestDTO): diff --git a/backend/src/generate_admin/schemas/audit.py b/backend/src/admin/schemas/audit.py similarity index 79% rename from backend/src/generate_admin/schemas/audit.py rename to backend/src/admin/schemas/audit.py index 47fe1f1..e6d24ac 100644 --- a/backend/src/generate_admin/schemas/audit.py +++ b/backend/src/admin/schemas/audit.py @@ -2,8 +2,8 @@ from datetime import datetime from typing import Any -from generate_admin.domain.enums import AuditAction -from generate_admin.schemas.base import ReadDTO +from admin.domain.enums import AuditAction +from admin.schemas.base import ReadDTO class AuditEntry(ReadDTO): diff --git a/backend/src/generate_admin/schemas/base.py b/backend/src/admin/schemas/base.py similarity index 97% rename from backend/src/generate_admin/schemas/base.py rename to backend/src/admin/schemas/base.py index 725eebf..e5d13c5 100644 --- a/backend/src/generate_admin/schemas/base.py +++ b/backend/src/admin/schemas/base.py @@ -5,7 +5,7 @@ from pydantic import BaseModel, ConfigDict, Field, TypeAdapter from pydantic import ValidationError as PydanticError -from generate_admin.core.errors import ValidationError +from admin.core.errors import ValidationError class RequestDTO(BaseModel): diff --git a/backend/src/generate_admin/schemas/invitation.py b/backend/src/admin/schemas/invitation.py similarity index 77% rename from backend/src/generate_admin/schemas/invitation.py rename to backend/src/admin/schemas/invitation.py index b17b6dd..a667b0b 100644 --- a/backend/src/generate_admin/schemas/invitation.py +++ b/backend/src/admin/schemas/invitation.py @@ -3,9 +3,9 @@ from pydantic import EmailStr, Field -from generate_admin.domain.enums import InvitationStatus -from generate_admin.schemas.base import ReadDTO, RequestDTO -from generate_admin.schemas.role import RoleSummary +from admin.domain.enums import InvitationStatus +from admin.schemas.base import ReadDTO, RequestDTO +from admin.schemas.role import RoleSummary class InvitationCreate(RequestDTO): diff --git a/backend/src/generate_admin/schemas/role.py b/backend/src/admin/schemas/role.py similarity index 90% rename from backend/src/generate_admin/schemas/role.py rename to backend/src/admin/schemas/role.py index dbb1534..084d354 100644 --- a/backend/src/generate_admin/schemas/role.py +++ b/backend/src/admin/schemas/role.py @@ -3,7 +3,7 @@ from pydantic import Field -from generate_admin.schemas.base import ReadDTO, RequestDTO +from admin.schemas.base import ReadDTO, RequestDTO class PermissionRead(ReadDTO): diff --git a/backend/src/generate_admin/schemas/session.py b/backend/src/admin/schemas/session.py similarity index 73% rename from backend/src/generate_admin/schemas/session.py rename to backend/src/admin/schemas/session.py index 9efad38..3cde573 100644 --- a/backend/src/generate_admin/schemas/session.py +++ b/backend/src/admin/schemas/session.py @@ -2,9 +2,9 @@ from pydantic import EmailStr, Field -from generate_admin.domain.enums import AccessState -from generate_admin.schemas.base import ReadDTO -from generate_admin.schemas.user import UserRead +from admin.domain.enums import AccessState +from admin.schemas.base import ReadDTO +from admin.schemas.user import UserRead class Identity(ReadDTO): diff --git a/backend/src/generate_admin/schemas/user.py b/backend/src/admin/schemas/user.py similarity index 70% rename from backend/src/generate_admin/schemas/user.py rename to backend/src/admin/schemas/user.py index 390163a..aae4a64 100644 --- a/backend/src/generate_admin/schemas/user.py +++ b/backend/src/admin/schemas/user.py @@ -3,9 +3,9 @@ from pydantic import EmailStr, Field -from generate_admin.domain.enums import UserStatus -from generate_admin.schemas.base import ReadDTO -from generate_admin.schemas.role import RoleAssignmentRead +from admin.domain.enums import UserStatus +from admin.schemas.base import ReadDTO +from admin.schemas.role import RoleAssignmentRead class UserSummary(ReadDTO): diff --git a/backend/src/generate_admin/services/__init__.py b/backend/src/admin/services/__init__.py similarity index 100% rename from backend/src/generate_admin/services/__init__.py rename to backend/src/admin/services/__init__.py diff --git a/backend/src/generate_admin/services/access.py b/backend/src/admin/services/access.py similarity index 85% rename from backend/src/generate_admin/services/access.py rename to backend/src/admin/services/access.py index 6a63323..f2accd3 100644 --- a/backend/src/generate_admin/services/access.py +++ b/backend/src/admin/services/access.py @@ -1,19 +1,19 @@ -from generate_admin.domain.access import PermissionSet, ResolvedAccess -from generate_admin.domain.enums import ( +from admin.domain.access import PermissionSet, ResolvedAccess +from admin.domain.enums import ( AccessRequestStatus, AccessState, AuditAction, UserStatus, ) -from generate_admin.repositories.access_request import AccessRequestRepository -from generate_admin.repositories.audit import AuditRepository -from generate_admin.repositories.invitation import InvitationRepository -from generate_admin.repositories.role import RoleRepository -from generate_admin.repositories.user import UserRepository -from generate_admin.schemas.audit import AuditEntry -from generate_admin.schemas.invitation import InvitationRead -from generate_admin.schemas.session import Identity, Session -from generate_admin.schemas.user import UserRead +from admin.repositories.access_request import AccessRequestRepository +from admin.repositories.audit import AuditRepository +from admin.repositories.invitation import InvitationRepository +from admin.repositories.role import RoleRepository +from admin.repositories.user import UserRepository +from admin.schemas.audit import AuditEntry +from admin.schemas.invitation import InvitationRead +from admin.schemas.session import Identity, Session +from admin.schemas.user import UserRead class AccessService: diff --git a/backend/src/generate_admin/services/access_request.py b/backend/src/admin/services/access_request.py similarity index 85% rename from backend/src/generate_admin/services/access_request.py rename to backend/src/admin/services/access_request.py index a2aeb75..b06d047 100644 --- a/backend/src/generate_admin/services/access_request.py +++ b/backend/src/admin/services/access_request.py @@ -1,22 +1,22 @@ import uuid -from generate_admin.core.errors import ConflictError, NotFoundError, ValidationError -from generate_admin.domain.access import PermissionSet -from generate_admin.domain.enums import AccessRequestStatus, AuditAction -from generate_admin.repositories.access_request import AccessRequestRepository -from generate_admin.repositories.audit import AuditRepository -from generate_admin.repositories.role import RoleRepository -from generate_admin.repositories.user import UserRepository -from generate_admin.schemas.access_request import ( +from admin.core.errors import ConflictError, NotFoundError, ValidationError +from admin.domain.access import PermissionSet +from admin.domain.enums import AccessRequestStatus, AuditAction +from admin.repositories.access_request import AccessRequestRepository +from admin.repositories.audit import AuditRepository +from admin.repositories.role import RoleRepository +from admin.repositories.user import UserRepository +from admin.schemas.access_request import ( AccessRequestApproval, AccessRequestCreate, AccessRequestDenial, AccessRequestRead, ) -from generate_admin.schemas.audit import AuditEntry -from generate_admin.schemas.session import Identity -from generate_admin.schemas.user import UserRead -from generate_admin.services.guards import ensure_can_delegate +from admin.schemas.audit import AuditEntry +from admin.schemas.session import Identity +from admin.schemas.user import UserRead +from admin.services.guards import ensure_can_delegate class AccessRequestService: diff --git a/backend/src/generate_admin/services/guards.py b/backend/src/admin/services/guards.py similarity index 69% rename from backend/src/generate_admin/services/guards.py rename to backend/src/admin/services/guards.py index cc9127c..5fb7e53 100644 --- a/backend/src/generate_admin/services/guards.py +++ b/backend/src/admin/services/guards.py @@ -1,8 +1,8 @@ -from generate_admin.core.errors import LastOwnerError, PrivilegeEscalationError -from generate_admin.domain.access import PermissionSet -from generate_admin.domain.permissions import Permission, SystemRole -from generate_admin.repositories.user import UserRepository -from generate_admin.schemas.role import RoleRead +from admin.core.errors import LastOwnerError, PrivilegeEscalationError +from admin.domain.access import PermissionSet +from admin.domain.permissions import Permission, SystemRole +from admin.repositories.user import UserRepository +from admin.schemas.role import RoleRead def ensure_can_delegate(actor: PermissionSet, role: RoleRead) -> None: diff --git a/backend/src/generate_admin/services/invitation.py b/backend/src/admin/services/invitation.py similarity index 81% rename from backend/src/generate_admin/services/invitation.py rename to backend/src/admin/services/invitation.py index 7fe38b8..97fe49b 100644 --- a/backend/src/generate_admin/services/invitation.py +++ b/backend/src/admin/services/invitation.py @@ -3,17 +3,17 @@ import uuid from datetime import UTC, datetime, timedelta -from generate_admin.core.errors import ConflictError, NotFoundError, ValidationError -from generate_admin.domain.access import PermissionSet -from generate_admin.domain.enums import AuditAction -from generate_admin.repositories.audit import AuditRepository -from generate_admin.repositories.invitation import InvitationRepository -from generate_admin.repositories.role import RoleRepository -from generate_admin.repositories.user import UserRepository -from generate_admin.schemas.audit import AuditEntry -from generate_admin.schemas.invitation import InvitationCreate, InvitationCreated, InvitationRead -from generate_admin.schemas.user import UserRead -from generate_admin.services.guards import ensure_can_delegate +from admin.core.errors import ConflictError, NotFoundError, ValidationError +from admin.domain.access import PermissionSet +from admin.domain.enums import AuditAction +from admin.repositories.audit import AuditRepository +from admin.repositories.invitation import InvitationRepository +from admin.repositories.role import RoleRepository +from admin.repositories.user import UserRepository +from admin.schemas.audit import AuditEntry +from admin.schemas.invitation import InvitationCreate, InvitationCreated, InvitationRead +from admin.schemas.user import UserRead +from admin.services.guards import ensure_can_delegate TOKEN_BYTES = 32 diff --git a/backend/src/generate_admin/services/member.py b/backend/src/admin/services/member.py similarity index 82% rename from backend/src/generate_admin/services/member.py rename to backend/src/admin/services/member.py index 981efb6..fc7c9bd 100644 --- a/backend/src/generate_admin/services/member.py +++ b/backend/src/admin/services/member.py @@ -1,15 +1,15 @@ import uuid -from generate_admin.core.errors import NotFoundError -from generate_admin.domain.access import PermissionSet -from generate_admin.domain.enums import AuditAction -from generate_admin.repositories.audit import AuditRepository -from generate_admin.repositories.role import RoleRepository -from generate_admin.repositories.user import UserRepository -from generate_admin.schemas.audit import AuditEntry -from generate_admin.schemas.role import RoleGrantRequest -from generate_admin.schemas.user import UserRead -from generate_admin.services.guards import ensure_can_delegate, ensure_not_last_owner +from admin.core.errors import NotFoundError +from admin.domain.access import PermissionSet +from admin.domain.enums import AuditAction +from admin.repositories.audit import AuditRepository +from admin.repositories.role import RoleRepository +from admin.repositories.user import UserRepository +from admin.schemas.audit import AuditEntry +from admin.schemas.role import RoleGrantRequest +from admin.schemas.user import UserRead +from admin.services.guards import ensure_can_delegate, ensure_not_last_owner class MemberService: diff --git a/backend/src/generate_admin/repositories/__init__.py b/backend/src/generate_admin/repositories/__init__.py deleted file mode 100644 index a386835..0000000 --- a/backend/src/generate_admin/repositories/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -from generate_admin.repositories.access_request import AccessRequestRepository -from generate_admin.repositories.audit import AuditRepository -from generate_admin.repositories.invitation import InvitationRepository -from generate_admin.repositories.role import RoleRepository -from generate_admin.repositories.user import UserRepository - -__all__ = [ - "AccessRequestRepository", - "AuditRepository", - "InvitationRepository", - "RoleRepository", - "UserRepository", -] diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index 272ad43..703a4d3 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -3,8 +3,8 @@ import pytest from fastapi.testclient import TestClient -from generate_admin.core.config import Settings, get_settings -from generate_admin.main import create_app +from admin.core.config import Settings, get_settings +from admin.main import create_app @pytest.fixture(scope="session") diff --git a/backend/tests/test_cache.py b/backend/tests/test_cache.py index 75f5aaf..8366f90 100644 --- a/backend/tests/test_cache.py +++ b/backend/tests/test_cache.py @@ -4,8 +4,8 @@ import pytest from pydantic import TypeAdapter -from generate_admin.core.cache import Cache, CacheNamespace, build_cache -from generate_admin.core.config import Settings +from admin.core.cache import Cache, CacheNamespace, build_cache +from admin.core.config import Settings STRING_ADAPTER = TypeAdapter(str) diff --git a/backend/tests/test_permissions.py b/backend/tests/test_permissions.py index e95cc64..41a2a6a 100644 --- a/backend/tests/test_permissions.py +++ b/backend/tests/test_permissions.py @@ -2,15 +2,15 @@ import pytest -from generate_admin.core.errors import PrivilegeEscalationError -from generate_admin.domain.access import PermissionSet -from generate_admin.domain.permissions import ( +from admin.core.errors import PrivilegeEscalationError +from admin.domain.access import PermissionSet +from admin.domain.permissions import ( ROLE_DEFINITIONS_BY_KEY, Permission, SystemRole, ) -from generate_admin.schemas.role import RoleRead -from generate_admin.services.guards import ensure_can_delegate +from admin.schemas.role import RoleRead +from admin.services.guards import ensure_can_delegate def role_for(key: SystemRole) -> RoleRead: diff --git a/backend/uv.lock b/backend/uv.lock index 4e02841..18b7f1e 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -277,6 +277,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/aa/29/35e016098c814cd93de9cd320c66b5bfba14dc6ecedd3cb518fa7c408c69/cffi-2.1.1-cp315-cp315t-win_arm64.whl", hash = "sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692", size = 186360, upload-time = "2026-08-03T21:21:13.636Z" }, ] +[[package]] +name = "cfgv" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, +] + [[package]] name = "click" version = "8.4.2" @@ -348,6 +357,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/57/30/4a22984d4f1bdfb8c054f07a92bc176b97a3134cc1d6c4b3bffb1f3688b4/cryptography-50.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:d24fead1d4d076e1bfb006dcec392074a3cd8d7b4fc8a595aa64073b2b7a96ba", size = 3874135, upload-time = "2026-07-31T14:24:50.085Z" }, ] +[[package]] +name = "distlib" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/02/bd72be9134d25ed783ecbbc38a539ffaefbf90c78418c7fb7229600dbac7/distlib-0.4.3.tar.gz", hash = "sha256:f152097224a0ae24be5a0f6bae1b9359af82133bce63f98a95f86cae1aede9ed", size = 615141, upload-time = "2026-06-12T08:04:52.847Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl", hash = "sha256:4b0ce306c966eb73bc3a7b6abad017c556dadd92c44701562cd528ac7fde4d5b", size = 470628, upload-time = "2026-06-12T08:04:50.506Z" }, +] + [[package]] name = "dnspython" version = "2.8.0" @@ -386,6 +404,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/03/10388a42375ee7e4ac9b94eb2c5c569c8b5795e377e701c9ac3ad63de890/fastapi-0.141.1-py3-none-any.whl", hash = "sha256:bfb91aa2d334c61cb35ba9a116fc123b3d3df31640b801cf57a7a78ec3f603b3", size = 131954, upload-time = "2026-07-29T17:18:04.364Z" }, ] +[[package]] +name = "filelock" +version = "3.32.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/57/3ba6e6cb097f85b855b00163d169f35365f44277df044dcf96d55b8f62a3/filelock-3.32.2.tar.gz", hash = "sha256:c33351e1f49cae33414acbc6d56784e6ecee82514ec90795da1161fc4836b5b8", size = 217172, upload-time = "2026-07-29T22:46:04.895Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/e8/72f8cef9fdfeffe06213fe8508039396ee48daa0e3259457ed766173bfd6/filelock-3.32.2-py3-none-any.whl", hash = "sha256:87dd94cf281e586d135fa51132b8e3d9a598b316e90377a288663c9321036c82", size = 98830, upload-time = "2026-07-29T22:46:03.52Z" }, +] + [[package]] name = "generate-admin-backend" version = "0.1.0" @@ -404,12 +431,14 @@ dependencies = [ { name = "redis" }, { name = "sqlalchemy" }, { name = "structlog" }, + { name = "typer" }, { name = "uvicorn", extra = ["standard"] }, ] [package.dev-dependencies] dev = [ { name = "mypy" }, + { name = "pre-commit" }, { name = "pytest" }, { name = "pytest-asyncio" }, { name = "ruff" }, @@ -430,12 +459,14 @@ requires-dist = [ { name = "redis", specifier = ">=5.2.0" }, { name = "sqlalchemy", specifier = ">=2.0.36" }, { name = "structlog", specifier = ">=24.4.0" }, + { name = "typer", extras = ["all"], specifier = ">=0.27.1" }, { name = "uvicorn", extras = ["standard"], specifier = ">=0.32.0" }, ] [package.metadata.requires-dev] dev = [ { name = "mypy", specifier = ">=1.13.0" }, + { name = "pre-commit", specifier = ">=4.0.0" }, { name = "pytest", specifier = ">=8.3.0" }, { name = "pytest-asyncio", specifier = ">=0.24.0" }, { name = "ruff", specifier = ">=0.8.0" }, @@ -569,6 +600,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, ] +[[package]] +name = "identify" +version = "2.6.19" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/63/51723b5f116cc04b061cb6f5a561790abf249d25931d515cd375e063e0f4/identify-2.6.19.tar.gz", hash = "sha256:6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842", size = 99567, upload-time = "2026-04-17T18:39:50.265Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl", hash = "sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a", size = 99397, upload-time = "2026-04-17T18:39:49.221Z" }, +] + [[package]] name = "idna" version = "3.18" @@ -709,6 +749,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a5/54/12ed58d458474aaab5c3d180173e745a4fe131bb330370596876d19ff60f/mako-1.4.1-py3-none-any.whl", hash = "sha256:a359d9a94a541213958742b2698d0a7757bb83551767bc468a74b9905aba9617", size = 80010, upload-time = "2026-08-05T06:10:58.248Z" }, ] +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + [[package]] name = "markupsafe" version = "3.0.3" @@ -772,6 +824,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, ] +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + [[package]] name = "mypy" version = "2.3.0" @@ -826,6 +887,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, ] +[[package]] +name = "nodeenv" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, +] + [[package]] name = "packaging" version = "26.3" @@ -844,6 +914,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, ] +[[package]] +name = "platformdirs" +version = "4.11.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/36/0a/062135c9a98dac804265073cc3afdbec5ae1aa37980bb354f461bafe81b4/platformdirs-4.11.1.tar.gz", hash = "sha256:bb1af68078f25e2f3e111e2d43b8d536df41b73c8a684b40bb018223b66fae27", size = 32396, upload-time = "2026-08-07T23:06:48.516Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/85/9b31b44296cfa3bb56cddb35e6a0f6578bab0b490c0806c0245e32c6110c/platformdirs-4.11.1-py3-none-any.whl", hash = "sha256:2efd27d363e8dd2e661639ffb398865a5e0a46442a11d266bf375a0e0c10e386", size = 23261, upload-time = "2026-08-07T23:06:47.219Z" }, +] + [[package]] name = "pluggy" version = "1.6.0" @@ -853,6 +932,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] +[[package]] +name = "pre-commit" +version = "4.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/25/3a/ddb78f32a0814e66b18a099377a106a2dcdce92d86a034d69d65df9b256e/pre_commit-4.6.1.tar.gz", hash = "sha256:03e809865c7d178b9979d06c761fcbfe6808fdaded8581a745bb110e52050421", size = 198646, upload-time = "2026-07-21T20:56:58.225Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/49/bc925106abcdac498074f2cbe6137e94e09f418dd2b7775df5b577dc0313/pre_commit-4.6.1-py2.py3-none-any.whl", hash = "sha256:0e3b2942510d1fb34eec167a3ec57331bf8442122f1153a9fb8b58f5c49b2717", size = 226186, upload-time = "2026-07-21T20:56:57.064Z" }, +] + [[package]] name = "psycopg" version = "3.3.4" @@ -1088,6 +1183,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, ] +[[package]] +name = "python-discovery" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/b7/1581a8103855c43567776aa34135e5ec3c597346c23bfd10c7eb5e0b10a4/python_discovery-1.5.1.tar.gz", hash = "sha256:e2ea8b884cd1701f386eda8cf327b87743f1dc21b7f784470799537d95635384", size = 77200, upload-time = "2026-07-31T22:06:02.48Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/07/a89b539750a159d5101c4eb9fc84e2961f65cefbd5e0b7440b284471c0b0/python_discovery-1.5.1-py3-none-any.whl", hash = "sha256:ac07f44cade589d954e9d6a1e1468539fdddd2cf676beb51da73e0f156b7c932", size = 35752, upload-time = "2026-07-31T22:06:01.116Z" }, +] + [[package]] name = "python-dotenv" version = "1.2.2" @@ -1152,6 +1259,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/66/9d/c5731f6e3608663d4d3656fd8d3aecee8b509c3082818f5a13eae925baea/redis-8.1.0-py3-none-any.whl", hash = "sha256:a4fe1aac3d3b3cc791d4b3d5931c5a956045dc951ee74d1c913ee3ac4d2ee9fb", size = 560618, upload-time = "2026-07-30T08:50:58.497Z" }, ] +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + [[package]] name = "ruff" version = "0.16.2" @@ -1189,6 +1309,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bc/e7/5c595c75e9f41a44f30e526eda465ea0b4eec93470e074e4a111b253f13a/s3transfer-0.19.2-py3-none-any.whl", hash = "sha256:d8168eccca828cbb2cd573675333f3bddd254313a9c42494b84c76b539e8ba25", size = 90216, upload-time = "2026-07-22T19:30:43.251Z" }, ] +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + [[package]] name = "six" version = "1.17.0" @@ -1261,6 +1390,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a9/18/489c97b834dfff9cf2fc2507cede4bcd4b11e67f84bc462acd1992496f86/structlog-26.1.0-py3-none-any.whl", hash = "sha256:e081a26d6c373e6d201eca24eede26d8ffab07f88f477822e679183428d3d91e", size = 73764, upload-time = "2026-06-06T07:33:38.046Z" }, ] +[[package]] +name = "typer" +version = "0.27.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "rich" }, + { name = "shellingham" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/40/4a3db7990d1f62a53182aa96eaef57aeb2886a27f90a195bc66713565d31/typer-0.27.1.tar.gz", hash = "sha256:a79bef8469a79c45498e7b814ecf8d603cc7644e9acbd9e19cac0334240b18df", size = 203994, upload-time = "2026-08-03T14:41:03.438Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/89/9518bc0c3929bee36b3a4a8e3daddd6e03f92f9961c66d4983b837160543/typer-0.27.1-py3-none-any.whl", hash = "sha256:53150287edd11baeb4e4722c8e394fcdf8181c0ae89485cba8d25c778d5edd56", size = 122874, upload-time = "2026-08-03T14:41:04.391Z" }, +] + [[package]] name = "typing-extensions" version = "4.16.0" @@ -1355,6 +1499,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" }, ] +[[package]] +name = "virtualenv" +version = "21.7.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, + { name = "python-discovery" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9c/10/8b7a5454efc032be50c1c5641467dbb5d31314500205212b2aa66d3c8af4/virtualenv-21.7.3.tar.gz", hash = "sha256:5e9e287f5c808070eea3b40403d3368248e64b24f1b60bd9a36e85ac841d2c3e", size = 5525482, upload-time = "2026-08-08T14:43:20.286Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/2a/83d779d2dfb61f101d7b1c10073d18984e37262d8b4f171c99911a952430/virtualenv-21.7.3-py3-none-any.whl", hash = "sha256:26dfda3c34f29bf1a3ca167426a67658d59979b9954e705aef60a5f724ce1773", size = 5504590, upload-time = "2026-08-08T14:43:18.57Z" }, +] + [[package]] name = "watchfiles" version = "1.2.0" diff --git a/justfile b/justfile index 386809c..207a022 100644 --- a/justfile +++ b/justfile @@ -9,6 +9,9 @@ default: install: cd {{backend}} && uv sync --all-groups +hooks: + cd {{backend}} && uv run pre-commit install + up: docker compose up -d --wait @@ -19,8 +22,11 @@ reset: down docker volume rm generate-admin_postgres-data || true @just up +connect-db: + docker compose exec postgres psql -U postgres -d generate_admin + dev: - cd {{backend}} && uv run uvicorn generate_admin.main:app --reload --reload-dir src \ + cd {{backend}} && uv run uvicorn admin.main:app --reload --reload-dir src \ --host ${APP_HOST:-0.0.0.0} --port ${APP_PORT:-8000} migrate: @@ -33,7 +39,7 @@ revision message: cd {{backend}} && uv run alembic revision -m "{{message}}" seed: - cd {{backend}} && uv run python -m generate_admin.cli seed + cd {{backend}} && uv run python -m admin.cli seed test: cd {{backend}} && uv run pytest