Formatting everything with black

This commit is contained in:
Ian Roddis committed 2026-06-22 14:56:51 -03:00
1 parent 42a11c0dc6
commit a3539beda9
20 files changed
+173 -18

No files matched your search

+3
View File
@@ -14,3 +14,6 @@ test:
migrate: migrate:
# apply the bundled initial migration # apply the bundled initial migration
uv run alembic upgrade head uv run alembic upgrade head
fmt:
uv run black .
+1
View File
@@ -3,6 +3,7 @@
The DB URL and target metadata come from the application so migrations always The DB URL and target metadata come from the application so migrations always
match the models and the active configuration. match the models and the active configuration.
""" """
import asyncio import asyncio
from logging.config import fileConfig from logging.config import fileConfig
+7 -2
View File
@@ -4,6 +4,7 @@ Revision ID: 0001_initial
Revises: Revises:
Create Date: 2026-01-01 00:00:00.000000 Create Date: 2026-01-01 00:00:00.000000
""" """
from collections.abc import Sequence from collections.abc import Sequence
import sqlalchemy as sa import sqlalchemy as sa
@@ -24,7 +25,9 @@ def upgrade() -> None:
sa.Column("hashed_password", sa.String(length=255), nullable=False), sa.Column("hashed_password", sa.String(length=255), nullable=False),
sa.Column("full_name", sa.String(length=255), nullable=True), sa.Column("full_name", sa.String(length=255), nullable=True),
sa.Column("is_active", sa.Boolean(), nullable=False, server_default=sa.true()), sa.Column("is_active", sa.Boolean(), nullable=False, server_default=sa.true()),
sa.Column("is_superuser", sa.Boolean(), nullable=False, server_default=sa.false()), sa.Column(
"is_superuser", sa.Boolean(), nullable=False, server_default=sa.false()
),
sa.Column( sa.Column(
"created_at", "created_at",
sa.DateTime(timezone=True), sa.DateTime(timezone=True),
@@ -64,7 +67,9 @@ def upgrade() -> None:
sa.Column("role_id", sa.Integer(), nullable=False), sa.Column("role_id", sa.Integer(), nullable=False),
sa.Column("permission_id", sa.Integer(), nullable=False), sa.Column("permission_id", sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(["role_id"], ["roles.id"], ondelete="CASCADE"), sa.ForeignKeyConstraint(["role_id"], ["roles.id"], ondelete="CASCADE"),
sa.ForeignKeyConstraint(["permission_id"], ["permissions.id"], ondelete="CASCADE"), sa.ForeignKeyConstraint(
["permission_id"], ["permissions.id"], ondelete="CASCADE"
),
sa.PrimaryKeyConstraint("role_id", "permission_id"), sa.PrimaryKeyConstraint("role_id", "permission_id"),
) )
+9 -3
View File
@@ -52,7 +52,9 @@ async def get_role(
): ):
role = await role_crud.get(db, role_id) role = await role_crud.get(db, role_id)
if role is None: if role is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Role not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Role not found"
)
return role return role
@@ -65,7 +67,9 @@ async def update_role(
): ):
role = await role_crud.get(db, role_id) role = await role_crud.get(db, role_id)
if role is None: if role is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Role not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Role not found"
)
return await role_crud.update(db, role, payload.model_dump(exclude_unset=True)) return await role_crud.update(db, role, payload.model_dump(exclude_unset=True))
@@ -77,5 +81,7 @@ async def delete_role(
): ):
role = await role_crud.get(db, role_id) role = await role_crud.get(db, role_id)
if role is None: if role is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Role not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Role not found"
)
await role_crud.delete(db, role) await role_crud.delete(db, role)
+9 -3
View File
@@ -53,7 +53,9 @@ async def get_user(
): ):
user = await user_crud.get(db, user_id) user = await user_crud.get(db, user_id)
if user is None: if user is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="User not found"
)
return user return user
@@ -66,7 +68,9 @@ async def update_user(
): ):
user = await user_crud.get(db, user_id) user = await user_crud.get(db, user_id)
if user is None: if user is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="User not found"
)
return await user_crud.update(db, user, payload.model_dump(exclude_unset=True)) return await user_crud.update(db, user, payload.model_dump(exclude_unset=True))
@@ -78,5 +82,7 @@ async def delete_user(
): ):
user = await user_crud.get(db, user_id) user = await user_crud.get(db, user_id)
if user is None: if user is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="User not found"
)
await user_crud.delete(db, user) await user_crud.delete(db, user)
+1
View File
@@ -3,6 +3,7 @@
Settings are read from environment variables first, then from a `.env` file. Settings are read from environment variables first, then from a `.env` file.
Environment variables always take precedence over the file. Environment variables always take precedence over the file.
""" """
from functools import lru_cache from functools import lru_cache
from pydantic_settings import BaseSettings, SettingsConfigDict from pydantic_settings import BaseSettings, SettingsConfigDict
+1
View File
@@ -2,6 +2,7 @@
Works with SQLite (aiosqlite) or Postgres (asyncpg) depending on DATABASE_URL. Works with SQLite (aiosqlite) or Postgres (asyncpg) depending on DATABASE_URL.
""" """
from collections.abc import AsyncGenerator from collections.abc import AsyncGenerator
from sqlalchemy.ext.asyncio import ( from sqlalchemy.ext.asyncio import (
+3
View File
@@ -1,4 +1,5 @@
"""Password hashing and JWT token helpers.""" """Password hashing and JWT token helpers."""
import hashlib import hashlib
import uuid import uuid
from datetime import UTC, datetime, timedelta from datetime import UTC, datetime, timedelta
@@ -14,6 +15,7 @@ REFRESH_TOKEN_TYPE = "refresh"
# --- Passwords --------------------------------------------------------------- # --- Passwords ---------------------------------------------------------------
def hash_password(password: str) -> str: def hash_password(password: str) -> str:
return bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode() return bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode()
@@ -27,6 +29,7 @@ def verify_password(password: str, hashed: str) -> bool:
# --- JWT --------------------------------------------------------------------- # --- JWT ---------------------------------------------------------------------
def _create_token( def _create_token(
subject: str | int, subject: str | int,
expires_delta: timedelta, expires_delta: timedelta,
+1
View File
@@ -66,6 +66,7 @@ async def delete(db: AsyncSession, role: Role) -> None:
# --- Permissions ------------------------------------------------------------- # --- Permissions -------------------------------------------------------------
async def list_permissions(db: AsyncSession) -> list[Permission]: async def list_permissions(db: AsyncSession) -> list[Permission]:
result = await db.execute(select(Permission).order_by(Permission.name)) result = await db.execute(select(Permission).order_by(Permission.name))
return list(result.scalars().all()) return list(result.scalars().all())
+3
View File
@@ -1,4 +1,5 @@
"""Shared FastAPI dependencies: DB session, current user, RBAC enforcement.""" """Shared FastAPI dependencies: DB session, current user, RBAC enforcement."""
import jwt import jwt
from fastapi import Depends, HTTPException, Request, status from fastapi import Depends, HTTPException, Request, status
from fastapi.security import OAuth2PasswordBearer from fastapi.security import OAuth2PasswordBearer
@@ -44,6 +45,7 @@ async def _user_from_token(token: str | None, db: AsyncSession) -> User | None:
# --- JSON API auth ----------------------------------------------------------- # --- JSON API auth -----------------------------------------------------------
async def get_current_user( async def get_current_user(
request: Request, request: Request,
bearer: str | None = Depends(oauth2_scheme), bearer: str | None = Depends(oauth2_scheme),
@@ -86,6 +88,7 @@ def require_superuser(user: User = Depends(get_current_user)) -> User:
# --- Web (HTMX) auth --------------------------------------------------------- # --- Web (HTMX) auth ---------------------------------------------------------
async def get_current_web_user( async def get_current_web_user(
request: Request, request: Request,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
+1
View File
@@ -1,4 +1,5 @@
"""Import all models so they register with ``Base.metadata``.""" """Import all models so they register with ``Base.metadata``."""
from app.models.associations import role_permissions, user_roles from app.models.associations import role_permissions, user_roles
from app.models.permission import Permission from app.models.permission import Permission
from app.models.refresh_token import RefreshToken from app.models.refresh_token import RefreshToken
+6 -1
View File
@@ -1,4 +1,5 @@
"""Many-to-many association tables for RBAC.""" """Many-to-many association tables for RBAC."""
from sqlalchemy import Column, ForeignKey, Table from sqlalchemy import Column, ForeignKey, Table
from app.core.database import Base from app.core.database import Base
@@ -14,5 +15,9 @@ role_permissions = Table(
"role_permissions", "role_permissions",
Base.metadata, Base.metadata,
Column("role_id", ForeignKey("roles.id", ondelete="CASCADE"), primary_key=True), Column("role_id", ForeignKey("roles.id", ondelete="CASCADE"), primary_key=True),
Column("permission_id", ForeignKey("permissions.id", ondelete="CASCADE"), primary_key=True), Column(
"permission_id",
ForeignKey("permissions.id", ondelete="CASCADE"),
primary_key=True,
),
) )
+4 -1
View File
@@ -1,4 +1,5 @@
"""Token issuance / rotation shared by the JSON API and the HTMX UI.""" """Token issuance / rotation shared by the JSON API and the HTMX UI."""
import jwt import jwt
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@@ -30,7 +31,9 @@ class RefreshError(Exception):
pass pass
async def rotate_refresh_token(db: AsyncSession, raw_refresh: str) -> tuple[str, str, User]: async def rotate_refresh_token(
db: AsyncSession, raw_refresh: str
) -> tuple[str, str, User]:
"""Validate a refresh token, revoke it, and issue a fresh pair. """Validate a refresh token, revoke it, and issue a fresh pair.
Raises ``RefreshError`` if the token is invalid, expired, or already revoked. Raises ``RefreshError`` if the token is invalid, expired, or already revoked.
+7 -1
View File
@@ -1,4 +1,5 @@
"""HTMX-driven admin dashboard for users and roles.""" """HTMX-driven admin dashboard for users and roles."""
from fastapi import APIRouter, Depends, Form, HTTPException, Request, status from fastapi import APIRouter, Depends, Form, HTTPException, Request, status
from fastapi.responses import HTMLResponse, RedirectResponse, Response from fastapi.responses import HTMLResponse, RedirectResponse, Response
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@@ -34,6 +35,7 @@ async def admin_home():
# --- Users ------------------------------------------------------------------- # --- Users -------------------------------------------------------------------
@router.get("/users", response_class=HTMLResponse) @router.get("/users", response_class=HTMLResponse)
async def users_page( async def users_page(
request: Request, request: Request,
@@ -74,7 +76,10 @@ async def create_user(
return templates.TemplateResponse( return templates.TemplateResponse(
request, request,
"admin/_users_table.html", "admin/_users_table.html",
{"users": await user_crud.list_users(db), "roles": await role_crud.list_roles(db)}, {
"users": await user_crud.list_users(db),
"roles": await role_crud.list_roles(db),
},
) )
@@ -152,6 +157,7 @@ async def delete_user(
# --- Roles ------------------------------------------------------------------- # --- Roles -------------------------------------------------------------------
@router.get("/roles", response_class=HTMLResponse) @router.get("/roles", response_class=HTMLResponse)
async def roles_page( async def roles_page(
request: Request, request: Request,
+1 -3
View File
@@ -30,9 +30,7 @@ async def dashboard(
request: Request, request: Request,
user: User = Depends(get_current_web_user), user: User = Depends(get_current_web_user),
): ):
return templates.TemplateResponse( return templates.TemplateResponse(request, "dashboard.html", {"user": user})
request, "dashboard.html", {"user": user}
)
@router.get("/login", response_class=HTMLResponse) @router.get("/login", response_class=HTMLResponse)
+1
View File
@@ -25,6 +25,7 @@ dev = [
"pytest-asyncio>=0.23", "pytest-asyncio>=0.23",
"httpx>=0.27", "httpx>=0.27",
"ruff>=0.5", "ruff>=0.5",
"black>=24",
] ]
[tool.uv] [tool.uv]
+4 -1
View File
@@ -6,6 +6,7 @@ settings (env vars or .env).
Usage: Usage:
uv run python -m scripts.seed uv run python -m scripts.seed
""" """
import asyncio import asyncio
from app.core.config import settings from app.core.config import settings
@@ -38,7 +39,9 @@ async def seed() -> None:
for name, description in PERMISSIONS: for name, description in PERMISSIONS:
perm = await role_crud.get_permission_by_name(db, name) perm = await role_crud.get_permission_by_name(db, name)
if perm is None: if perm is None:
perm = await role_crud.create_permission(db, name=name, description=description) perm = await role_crud.create_permission(
db, name=name, description=description
)
perms[name] = perm perms[name] = perm
# Roles # Roles
+3 -1
View File
@@ -32,7 +32,9 @@ async def seeded(db_sessionmaker):
async with db_sessionmaker() as db: async with db_sessionmaker() as db:
perms = {} perms = {}
for name, desc in PERMISSIONS: for name, desc in PERMISSIONS:
perms[name] = await role_crud.create_permission(db, name=name, description=desc) perms[name] = await role_crud.create_permission(
db, name=name, description=desc
)
roles = {} roles = {}
for role_name, perm_names in ROLES.items(): for role_name, perm_names in ROLES.items():
roles[role_name] = await role_crud.create( roles[role_name] = await role_crud.create(
+8 -2
View File
@@ -2,7 +2,11 @@ async def test_register_login_me_refresh_logout(client, seeded):
# Register # Register
resp = await client.post( resp = await client.post(
"/api/auth/register", "/api/auth/register",
json={"email": "alice@example.com", "password": "secret123", "full_name": "Alice"}, json={
"email": "alice@example.com",
"password": "secret123",
"full_name": "Alice",
},
) )
assert resp.status_code == 201, resp.text assert resp.status_code == 201, resp.text
assert resp.json()["email"] == "alice@example.com" assert resp.json()["email"] == "alice@example.com"
@@ -17,7 +21,9 @@ async def test_register_login_me_refresh_logout(client, seeded):
access, refresh = tokens["access_token"], tokens["refresh_token"] access, refresh = tokens["access_token"], tokens["refresh_token"]
# /me with bearer token # /me with bearer token
resp = await client.get("/api/users/me", headers={"Authorization": f"Bearer {access}"}) resp = await client.get(
"/api/users/me", headers={"Authorization": f"Bearer {access}"}
)
assert resp.status_code == 200 assert resp.status_code == 200
assert resp.json()["email"] == "alice@example.com" assert resp.json()["email"] == "alice@example.com"
Generated
+100
View File
@@ -174,6 +174,43 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e4/f8/972c96f5a2b6c4b3deca57009d93e946bbdbe2241dca9806d502f29dd3ee/bcrypt-5.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:6b8f520b61e8781efee73cba14e3e8c9556ccfb375623f4f97429544734545b4", size = 273375, upload-time = "2025-09-25T19:50:45.43Z" }, { url = "https://files.pythonhosted.org/packages/e4/f8/972c96f5a2b6c4b3deca57009d93e946bbdbe2241dca9806d502f29dd3ee/bcrypt-5.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:6b8f520b61e8781efee73cba14e3e8c9556ccfb375623f4f97429544734545b4", size = 273375, upload-time = "2025-09-25T19:50:45.43Z" },
] ]
[[package]]
name = "black"
version = "26.5.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "mypy-extensions" },
{ name = "packaging" },
{ name = "pathspec" },
{ name = "platformdirs" },
{ name = "pytokens" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c0/37/5628dd55bf2b34257fc7603f0fe97c40e3aaf24265f416a9c85c95ca1436/black-26.5.1.tar.gz", hash = "sha256:dd321f668053961824bcc1be1cc1df748b2d7e4fa28086b08331e577b0100a73", size = 679439, upload-time = "2026-05-18T16:53:36.107Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4b/96/3c3e09f09f44a37aac36b178a279cd19aa7001bd796187a7b162a294c81f/black-26.5.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:96ae2c733b2aabdd9986e2c5df628ff3473676cd1c5faded1ff496cf6d74083c", size = 1970639, upload-time = "2026-05-18T17:05:11.461Z" },
{ url = "https://files.pythonhosted.org/packages/83/ea/5ad117b9ee3ecd933c712bcbae610006e5b7cc9f41c526cd7ed3b6c4124c/black-26.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0e48b87e03bf109288e55cfceadcfa15ff5470aca2851a851950ed2926f450d7", size = 1792130, upload-time = "2026-05-18T17:05:12.983Z" },
{ url = "https://files.pythonhosted.org/packages/06/3a/7c448bc623fcdfa96672531beb5a616ea5e64f6975955254d7731ffb0ad9/black-26.5.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5119fa92ae61f786e8c3662fd60aece1d0a2dd5cca5d0c79417a95e7a4272a59", size = 1846134, upload-time = "2026-05-18T17:05:14.506Z" },
{ url = "https://files.pythonhosted.org/packages/a1/5b/0b39b3a5917f0657ac014ad2edb58c139553a478adfe7f817abf1622ff6e/black-26.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:30d3c14661f2792e9142cce3eeeb1cbc175b3eb5f733be0c8eeb99651e52b0c3", size = 1478883, upload-time = "2026-05-18T17:05:16.542Z" },
{ url = "https://files.pythonhosted.org/packages/4c/48/dc222692e0f95030db1bbfb6c857e76858bad09058221ea7aae815255327/black-26.5.1-cp311-cp311-win_arm64.whl", hash = "sha256:1ef92b76f7733f282fd096ea406200b5a286c42947412b0eaff3a74e3616cefe", size = 1277776, upload-time = "2026-05-18T17:05:18.029Z" },
{ url = "https://files.pythonhosted.org/packages/24/99/7744b906703228264ef73bdd534df88ec1ef3de45c4e78f6d31b9e32d0c9/black-26.5.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4ad6fa01f941920f54f2bbb35f3df7673428a0ef98a0b0840c2eaef3b110efa8", size = 2012518, upload-time = "2026-05-18T17:05:20.108Z" },
{ url = "https://files.pythonhosted.org/packages/b7/c0/c5a3b1636dfd09c42534f2b3cf33506814f6d3e066fb0879ffa16c1ae860/black-26.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3915f256e75a2d7cf88d8953d37f780455dc586cc72dee059c528fe77f581217", size = 1816016, upload-time = "2026-05-18T17:05:21.84Z" },
{ url = "https://files.pythonhosted.org/packages/1f/0e/36044316b65ca471d3bb6d3703fd06fb50c6b727c3562f6a5a3153634f88/black-26.5.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d98d4137277c75dfb898ec8d846c4fd68ba1e9cf77f95e2865c203dc18f4c3d", size = 1884150, upload-time = "2026-05-18T17:05:23.546Z" },
{ url = "https://files.pythonhosted.org/packages/b3/33/dafc5808c2af43672912111d7c3354af1615f7e2be3bed7a878461abbe4d/black-26.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:a1dca32d9f1784af512a13410ec204c6f7f0aa9797a111c42e1c03449821c264", size = 1486825, upload-time = "2026-05-18T17:05:25.004Z" },
{ url = "https://files.pythonhosted.org/packages/82/14/b965ee6ad2a311f28bdbf692def3ee9848d2ae289dab28b27657fcee3e78/black-26.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:1037d5ac7b7b310b2632ad867ec8d0e4c4819dcdb0b820f63135da746a24e418", size = 1288646, upload-time = "2026-05-18T17:05:26.477Z" },
{ url = "https://files.pythonhosted.org/packages/3f/5c/c384363980e11e25ca6b93205949bb331fbf35f4e0dbec376dfa6326cec8/black-26.5.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2b36cf2ddf5566e205f6535f782a62194a184d33e175b64ae8c40b1737522be3", size = 2009020, upload-time = "2026-05-18T17:05:28.132Z" },
{ url = "https://files.pythonhosted.org/packages/0b/df/9f31c5e0babbfed77d505fc5d120beb98b21b33feaeded3924ea941fe360/black-26.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f7ea64ebfa01b50f693508fc39f875e264446d3b097088f84f203b9d09618a0", size = 1813335, upload-time = "2026-05-18T17:05:31.266Z" },
{ url = "https://files.pythonhosted.org/packages/fb/24/8e7b9a2fa61b0afd82209efe937557d180a1fa055bd7f6161eb9defc3719/black-26.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecb3e624844c798144e9bd986954e0adc81d8911a1f30f375e1252fe26e8c294", size = 1881614, upload-time = "2026-05-18T17:05:32.718Z" },
{ url = "https://files.pythonhosted.org/packages/49/ad/b4e0d9365ba8ac34f6bbab62a4b1b2dd5d618fac3fa1b8db968c844201b5/black-26.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:e1a26503279b6b310669fb0b219c39e4820b77e8189fe80f522bb511f247db0a", size = 1488925, upload-time = "2026-05-18T17:05:34.259Z" },
{ url = "https://files.pythonhosted.org/packages/a1/4b/652b859bf5df88a751c30451b09338f7fd26a77d1271c666992f836b7711/black-26.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c34b25da232ead53a6f335b76dbea124f4d152ad568b9080d6f944bc2b34b52", size = 1289883, upload-time = "2026-05-18T17:05:36.019Z" },
{ url = "https://files.pythonhosted.org/packages/a6/16/a8da8eb208c51c7f4ce74609a45d0dcc6d8a2141e45e81ee5289d1bb0d59/black-26.5.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e88976690a64b0af98312ca958415849cb42423423c5f2ee74af4b49a97a2168", size = 2004800, upload-time = "2026-05-18T17:05:38.182Z" },
{ url = "https://files.pythonhosted.org/packages/11/8a/a479296a19e383b70a725882a6cf3d786540601ff03cabbaaf1cce864c5a/black-26.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32d5ea7f6c8bdfa6e648326ebca1f02b0764e2a029edc6f8dce2627e19d468c3", size = 1815576, upload-time = "2026-05-18T17:05:40.309Z" },
{ url = "https://files.pythonhosted.org/packages/81/6b/cfaf3d39f25132c156a068f6b805576c9103a84086019507c70e1911ee7d/black-26.5.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ea8d16dc41655aa113cd64665e7219446cd7e4ff2248d7178eaa905190c86b18", size = 1877927, upload-time = "2026-05-18T17:05:42.463Z" },
{ url = "https://files.pythonhosted.org/packages/66/76/302e313964bcff7e28df329d39f84f5270095730d85ff0acc260610a0d82/black-26.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:577f21094ea469ef92ec1adaf2c9441a226d2144d01a5be2fa823cecf6543e50", size = 1511860, upload-time = "2026-05-18T17:05:43.943Z" },
{ url = "https://files.pythonhosted.org/packages/27/4e/a3827e35e0e567f9f9ee59e2a0ab979267dca98718f25547ca8c6733afd4/black-26.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:ed1a20af114c301a0269bf01163d51dbef72737fd65f850001e7cbe7f3c7abae", size = 1316632, upload-time = "2026-05-18T17:05:45.521Z" },
{ url = "https://files.pythonhosted.org/packages/94/51/f975cae76d44274cc2868dc9040ac5d58d464784610234455b4e7b19c6ef/black-26.5.1-py3-none-any.whl", hash = "sha256:4ed7f7da04046d2e488437170797d3b4a4ad83906683bcb7dfc68b673bbce5e2", size = 213693, upload-time = "2026-05-18T16:53:33.964Z" },
]
[[package]] [[package]]
name = "certifi" name = "certifi"
version = "2026.6.17" version = "2026.6.17"
@@ -263,6 +300,7 @@ dependencies = [
[package.optional-dependencies] [package.optional-dependencies]
dev = [ dev = [
{ name = "black" },
{ name = "httpx" }, { name = "httpx" },
{ name = "pytest" }, { name = "pytest" },
{ name = "pytest-asyncio" }, { name = "pytest-asyncio" },
@@ -275,6 +313,7 @@ requires-dist = [
{ name = "alembic", specifier = ">=1.13" }, { name = "alembic", specifier = ">=1.13" },
{ name = "asyncpg", specifier = ">=0.29" }, { name = "asyncpg", specifier = ">=0.29" },
{ name = "bcrypt", specifier = ">=4.1" }, { name = "bcrypt", specifier = ">=4.1" },
{ name = "black", marker = "extra == 'dev'", specifier = ">=24" },
{ name = "fastapi", specifier = ">=0.111" }, { name = "fastapi", specifier = ">=0.111" },
{ name = "httpx", marker = "extra == 'dev'", specifier = ">=0.27" }, { name = "httpx", marker = "extra == 'dev'", specifier = ">=0.27" },
{ name = "jinja2", specifier = ">=3.1" }, { name = "jinja2", specifier = ">=3.1" },
@@ -563,6 +602,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" }, { 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 = "mypy-extensions"
version = "1.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" }
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]] [[package]]
name = "packaging" name = "packaging"
version = "26.2" version = "26.2"
@@ -572,6 +620,24 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
] ]
[[package]]
name = "pathspec"
version = "1.1.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" }
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.10.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d7/47/e4501f49c178ae1d9f4a75073fda4204f52647993f075a9db4d14930e0c5/platformdirs-4.10.0.tar.gz", hash = "sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7", size = 31224, upload-time = "2026-05-28T03:32:53.587Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl", hash = "sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a", size = 22743, upload-time = "2026-05-28T03:32:52.175Z" },
]
[[package]] [[package]]
name = "pluggy" name = "pluggy"
version = "1.6.0" version = "1.6.0"
@@ -782,6 +848,40 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e1/04/e8135ebd1ad02c56ec633277529b2602ff99ff634be76cdba5744cf554fd/python_multipart-0.0.32-py3-none-any.whl", hash = "sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23", size = 30042, upload-time = "2026-06-04T16:18:57.319Z" }, { url = "https://files.pythonhosted.org/packages/e1/04/e8135ebd1ad02c56ec633277529b2602ff99ff634be76cdba5744cf554fd/python_multipart-0.0.32-py3-none-any.whl", hash = "sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23", size = 30042, upload-time = "2026-06-04T16:18:57.319Z" },
] ]
[[package]]
name = "pytokens"
version = "0.4.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b6/34/b4e015b99031667a7b960f888889c5bd34ef585c85e1cb56a594b92836ac/pytokens-0.4.1.tar.gz", hash = "sha256:292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a", size = 23015, upload-time = "2026-01-30T01:03:45.924Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3d/92/790ebe03f07b57e53b10884c329b9a1a308648fc083a6d4a39a10a28c8fc/pytokens-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d70e77c55ae8380c91c0c18dea05951482e263982911fc7410b1ffd1dadd3440", size = 160864, upload-time = "2026-01-30T01:02:57.882Z" },
{ url = "https://files.pythonhosted.org/packages/13/25/a4f555281d975bfdd1eba731450e2fe3a95870274da73fb12c40aeae7625/pytokens-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a58d057208cb9075c144950d789511220b07636dd2e4708d5645d24de666bdc", size = 248565, upload-time = "2026-01-30T01:02:59.912Z" },
{ url = "https://files.pythonhosted.org/packages/17/50/bc0394b4ad5b1601be22fa43652173d47e4c9efbf0044c62e9a59b747c56/pytokens-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b49750419d300e2b5a3813cf229d4e5a4c728dae470bcc89867a9ad6f25a722d", size = 260824, upload-time = "2026-01-30T01:03:01.471Z" },
{ url = "https://files.pythonhosted.org/packages/4e/54/3e04f9d92a4be4fc6c80016bc396b923d2a6933ae94b5f557c939c460ee0/pytokens-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d9907d61f15bf7261d7e775bd5d7ee4d2930e04424bab1972591918497623a16", size = 264075, upload-time = "2026-01-30T01:03:04.143Z" },
{ url = "https://files.pythonhosted.org/packages/d1/1b/44b0326cb5470a4375f37988aea5d61b5cc52407143303015ebee94abfd6/pytokens-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:ee44d0f85b803321710f9239f335aafe16553b39106384cef8e6de40cb4ef2f6", size = 103323, upload-time = "2026-01-30T01:03:05.412Z" },
{ url = "https://files.pythonhosted.org/packages/41/5d/e44573011401fb82e9d51e97f1290ceb377800fb4eed650b96f4753b499c/pytokens-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:140709331e846b728475786df8aeb27d24f48cbcf7bcd449f8de75cae7a45083", size = 160663, upload-time = "2026-01-30T01:03:06.473Z" },
{ url = "https://files.pythonhosted.org/packages/f0/e6/5bbc3019f8e6f21d09c41f8b8654536117e5e211a85d89212d59cbdab381/pytokens-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d6c4268598f762bc8e91f5dbf2ab2f61f7b95bdc07953b602db879b3c8c18e1", size = 255626, upload-time = "2026-01-30T01:03:08.177Z" },
{ url = "https://files.pythonhosted.org/packages/bf/3c/2d5297d82286f6f3d92770289fd439956b201c0a4fc7e72efb9b2293758e/pytokens-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24afde1f53d95348b5a0eb19488661147285ca4dd7ed752bbc3e1c6242a304d1", size = 269779, upload-time = "2026-01-30T01:03:09.756Z" },
{ url = "https://files.pythonhosted.org/packages/20/01/7436e9ad693cebda0551203e0bf28f7669976c60ad07d6402098208476de/pytokens-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5ad948d085ed6c16413eb5fec6b3e02fa00dc29a2534f088d3302c47eb59adf9", size = 268076, upload-time = "2026-01-30T01:03:10.957Z" },
{ url = "https://files.pythonhosted.org/packages/2e/df/533c82a3c752ba13ae7ef238b7f8cdd272cf1475f03c63ac6cf3fcfb00b6/pytokens-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:3f901fe783e06e48e8cbdc82d631fca8f118333798193e026a50ce1b3757ea68", size = 103552, upload-time = "2026-01-30T01:03:12.066Z" },
{ url = "https://files.pythonhosted.org/packages/cb/dc/08b1a080372afda3cceb4f3c0a7ba2bde9d6a5241f1edb02a22a019ee147/pytokens-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8bdb9d0ce90cbf99c525e75a2fa415144fd570a1ba987380190e8b786bc6ef9b", size = 160720, upload-time = "2026-01-30T01:03:13.843Z" },
{ url = "https://files.pythonhosted.org/packages/64/0c/41ea22205da480837a700e395507e6a24425151dfb7ead73343d6e2d7ffe/pytokens-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5502408cab1cb18e128570f8d598981c68a50d0cbd7c61312a90507cd3a1276f", size = 254204, upload-time = "2026-01-30T01:03:14.886Z" },
{ url = "https://files.pythonhosted.org/packages/e0/d2/afe5c7f8607018beb99971489dbb846508f1b8f351fcefc225fcf4b2adc0/pytokens-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29d1d8fb1030af4d231789959f21821ab6325e463f0503a61d204343c9b355d1", size = 268423, upload-time = "2026-01-30T01:03:15.936Z" },
{ url = "https://files.pythonhosted.org/packages/68/d4/00ffdbd370410c04e9591da9220a68dc1693ef7499173eb3e30d06e05ed1/pytokens-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b08dd6b86058b6dc07efe9e98414f5102974716232d10f32ff39701e841c4", size = 266859, upload-time = "2026-01-30T01:03:17.458Z" },
{ url = "https://files.pythonhosted.org/packages/a7/c9/c3161313b4ca0c601eeefabd3d3b576edaa9afdefd32da97210700e47652/pytokens-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:9bd7d7f544d362576be74f9d5901a22f317efc20046efe2034dced238cbbfe78", size = 103520, upload-time = "2026-01-30T01:03:18.652Z" },
{ url = "https://files.pythonhosted.org/packages/8f/a7/b470f672e6fc5fee0a01d9e75005a0e617e162381974213a945fcd274843/pytokens-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4a14d5f5fc78ce85e426aa159489e2d5961acf0e47575e08f35584009178e321", size = 160821, upload-time = "2026-01-30T01:03:19.684Z" },
{ url = "https://files.pythonhosted.org/packages/80/98/e83a36fe8d170c911f864bfded690d2542bfcfacb9c649d11a9e6eb9dc41/pytokens-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f50fd18543be72da51dd505e2ed20d2228c74e0464e4262e4899797803d7fa", size = 254263, upload-time = "2026-01-30T01:03:20.834Z" },
{ url = "https://files.pythonhosted.org/packages/0f/95/70d7041273890f9f97a24234c00b746e8da86df462620194cef1d411ddeb/pytokens-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc74c035f9bfca0255c1af77ddd2d6ae8419012805453e4b0e7513e17904545d", size = 268071, upload-time = "2026-01-30T01:03:21.888Z" },
{ url = "https://files.pythonhosted.org/packages/da/79/76e6d09ae19c99404656d7db9c35dfd20f2086f3eb6ecb496b5b31163bad/pytokens-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f66a6bbe741bd431f6d741e617e0f39ec7257ca1f89089593479347cc4d13324", size = 271716, upload-time = "2026-01-30T01:03:23.633Z" },
{ url = "https://files.pythonhosted.org/packages/79/37/482e55fa1602e0a7ff012661d8c946bafdc05e480ea5a32f4f7e336d4aa9/pytokens-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:b35d7e5ad269804f6697727702da3c517bb8a5228afa450ab0fa787732055fc9", size = 104539, upload-time = "2026-01-30T01:03:24.788Z" },
{ url = "https://files.pythonhosted.org/packages/30/e8/20e7db907c23f3d63b0be3b8a4fd1927f6da2395f5bcc7f72242bb963dfe/pytokens-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8fcb9ba3709ff77e77f1c7022ff11d13553f3c30299a9fe246a166903e9091eb", size = 168474, upload-time = "2026-01-30T01:03:26.428Z" },
{ url = "https://files.pythonhosted.org/packages/d6/81/88a95ee9fafdd8f5f3452107748fd04c24930d500b9aba9738f3ade642cc/pytokens-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79fc6b8699564e1f9b521582c35435f1bd32dd06822322ec44afdeba666d8cb3", size = 290473, upload-time = "2026-01-30T01:03:27.415Z" },
{ url = "https://files.pythonhosted.org/packages/cf/35/3aa899645e29b6375b4aed9f8d21df219e7c958c4c186b465e42ee0a06bf/pytokens-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d31b97b3de0f61571a124a00ffe9a81fb9939146c122c11060725bd5aea79975", size = 303485, upload-time = "2026-01-30T01:03:28.558Z" },
{ url = "https://files.pythonhosted.org/packages/52/a0/07907b6ff512674d9b201859f7d212298c44933633c946703a20c25e9d81/pytokens-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:967cf6e3fd4adf7de8fc73cd3043754ae79c36475c1c11d514fc72cf5490094a", size = 306698, upload-time = "2026-01-30T01:03:29.653Z" },
{ url = "https://files.pythonhosted.org/packages/39/2a/cbbf9250020a4a8dd53ba83a46c097b69e5eb49dd14e708f496f548c6612/pytokens-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:584c80c24b078eec1e227079d56dc22ff755e0ba8654d8383b2c549107528918", size = 116287, upload-time = "2026-01-30T01:03:30.912Z" },
{ url = "https://files.pythonhosted.org/packages/c6/78/397db326746f0a342855b81216ae1f0a32965deccfd7c830a2dbc66d2483/pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de", size = 13729, upload-time = "2026-01-30T01:03:45.029Z" },
]
[[package]] [[package]]
name = "pyyaml" name = "pyyaml"
version = "6.0.3" version = "6.0.3"