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
9 changes: 9 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -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/
7 changes: 7 additions & 0 deletions .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"recommendations": [
"charliermarsh.ruff",
"ms-python.python",
"ms-python.vscode-pylance"
]
}
20 changes: 20 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -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
}
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 <name>` | 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

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

Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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(),
Expand Down
14 changes: 12 additions & 2 deletions backend/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -26,14 +27,15 @@ dev = [
"pytest-asyncio>=0.24.0",
"ruff>=0.8.0",
"mypy>=1.13.0",
"pre-commit>=4.0.0",
]

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.hatch.build.targets.wheel]
packages = ["src/generate_admin"]
packages = ["src/admin"]

[tool.ruff]
line-length = 100
Expand All @@ -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"
Expand All @@ -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"]
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -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)

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

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from fastapi import APIRouter

from generate_admin.api.v1 import (
from admin.api.v1 import (
access_requests,
health,
invitations,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"])

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"])

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"])

Expand Down
Original file line number Diff line number Diff line change
@@ -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"])

Expand Down
Loading
Loading