Logging cleanup, linter.

This commit is contained in:
2026-01-24 01:08:00 +00:00
parent aae33e60ce
commit 2100a7e14f
5 changed files with 38 additions and 66 deletions
+3 -6
View File
@@ -57,7 +57,6 @@ async def flush() -> None:
async def _background_loop(): async def _background_loop():
"""Background task that periodically flushes changes and cleans up.""" """Background task that periodically flushes changes and cleans up."""
_logger.info("Background loop starting")
# Run cleanup immediately on startup to clear old expired items # Run cleanup immediately on startup to clear old expired items
cleanup() cleanup()
await flush() await flush()
@@ -77,18 +76,17 @@ async def _background_loop():
await flush() # Flush cleanup changes await flush() # Flush cleanup changes
last_cleanup = now last_cleanup = now
except asyncio.CancelledError: except asyncio.CancelledError:
_logger.info("Background loop cancelled, final flush")
# Final flush before exit # Final flush before exit
await flush() await flush()
break break
except Exception: except Exception:
_logger.exception("Error in database background loop") _logger.debug("Error in database background loop", exc_info=True)
async def start_background(): async def start_background():
"""Start the background flush/cleanup task.""" """Start the background flush/cleanup task."""
global _background_task global _background_task
# Check if task exists but is no longer running (e.g., after uvicorn reload) # Check if task exists but is no longer running (e.g., after uvicorn reload)
if _background_task is not None: if _background_task is not None:
if _background_task.done(): if _background_task.done():
@@ -106,10 +104,9 @@ async def start_background():
except Exception as e: except Exception as e:
_logger.debug("Error checking background task loop: %s, restarting", e) _logger.debug("Error checking background task loop: %s, restarting", e)
_background_task = None _background_task = None
if _background_task is None: if _background_task is None:
_background_task = asyncio.create_task(_background_loop()) _background_task = asyncio.create_task(_background_loop())
_logger.info("Database background task started")
else: else:
_logger.debug("Background task already running: %s", _background_task) _logger.debug("Background task already running: %s", _background_task)
+3 -4
View File
@@ -84,7 +84,9 @@ def compute_diff(previous: dict, current: dict) -> dict | None:
return diff if diff else None return diff if diff else None
def create_change_record(action: str, diff: dict, user: str | None = None) -> _ChangeRecord: def create_change_record(
action: str, diff: dict, user: str | None = None
) -> _ChangeRecord:
"""Create a change record for persistence.""" """Create a change record for persistence."""
return _ChangeRecord( return _ChangeRecord(
ts=datetime.now(timezone.utc), ts=datetime.now(timezone.utc),
@@ -121,9 +123,6 @@ async def flush_changes(
# Append all lines in a single write (binary mode for Windows compatibility) # Append all lines in a single write (binary mode for Windows compatibility)
async with aiofiles.open(db_path, "ab") as f: async with aiofiles.open(db_path, "ab") as f:
await f.write(b"\n".join(lines) + b"\n") await f.write(b"\n".join(lines) + b"\n")
_logger.debug(
"Flushed %d change(s) to %s", len(changes_to_write), db_path
)
return True return True
except OSError: except OSError:
_logger.exception("Failed to flush database changes") _logger.exception("Failed to flush database changes")
+24 -44
View File
@@ -7,9 +7,11 @@ Write operations: Functions that validate and commit, or raise ValueError.
""" """
import hashlib import hashlib
import json
import logging import logging
import os import os
import secrets import secrets
import sys
from collections import deque from collections import deque
from contextlib import contextmanager from contextlib import contextmanager
from datetime import datetime, timezone from datetime import datetime, timezone
@@ -113,19 +115,15 @@ class DB:
user_display = self._data.users[user_uuid].display_name user_display = self._data.users[user_uuid].display_name
except (ValueError, KeyError): except (ValueError, KeyError):
user_display = self._current_user user_display = self._current_user
import json
diff_json = json.dumps(diff, default=str)
if user_display: if user_display:
_logger.info( print(
"DB %s by %s: %s", f"{self._current_action} by {user_display}: {diff_json}",
self._current_action, file=sys.stderr,
user_display,
json.dumps(diff, default=str),
) )
else: else:
_logger.info( print(f"{self._current_action}: {diff_json}", file=sys.stderr)
"DB %s: %s", self._current_action, json.dumps(diff, default=str)
)
@contextmanager @contextmanager
def transaction( def transaction(
@@ -527,9 +525,7 @@ def get_session_context(
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
def create_permission( def create_permission(perm: Permission, *, ctx: SessionContext | None = None) -> None:
perm: Permission, *, ctx: SessionContext | None = None
) -> None:
"""Create a new permission.""" """Create a new permission."""
if perm.uuid in _db._data.permissions: if perm.uuid in _db._data.permissions:
raise ValueError(f"Permission {perm.uuid} already exists") raise ValueError(f"Permission {perm.uuid} already exists")
@@ -542,9 +538,7 @@ def create_permission(
) )
def update_permission( def update_permission(perm: Permission, *, ctx: SessionContext | None = None) -> None:
perm: Permission, *, ctx: SessionContext | None = None
) -> None:
"""Update a permission's scope, display_name, and domain.""" """Update a permission's scope, display_name, and domain."""
if perm.uuid not in _db._data.permissions: if perm.uuid not in _db._data.permissions:
raise ValueError(f"Permission {perm.uuid} not found") raise ValueError(f"Permission {perm.uuid} not found")
@@ -583,9 +577,7 @@ def rename_permission(
_db._data.permissions[key].domain = domain _db._data.permissions[key].domain = domain
def delete_permission( def delete_permission(uuid: str | UUID, *, ctx: SessionContext | None = None) -> None:
uuid: str | UUID, *, ctx: SessionContext | None = None
) -> None:
"""Delete a permission and remove it from all roles.""" """Delete a permission and remove it from all roles."""
if isinstance(uuid, str): if isinstance(uuid, str):
uuid = UUID(uuid) uuid = UUID(uuid)
@@ -598,9 +590,7 @@ def delete_permission(
del _db._data.permissions[uuid] del _db._data.permissions[uuid]
def create_organization( def create_organization(org: Org, *, ctx: SessionContext | None = None) -> None:
org: Org, *, ctx: SessionContext | None = None
) -> None:
"""Create a new organization with an Administration role. """Create a new organization with an Administration role.
Automatically creates an 'Administration' role with auth:org:admin permission. Automatically creates an 'Administration' role with auth:org:admin permission.
@@ -651,9 +641,7 @@ def update_organization_name(
_db._data.orgs[uuid].display_name = display_name _db._data.orgs[uuid].display_name = display_name
def delete_organization( def delete_organization(uuid: str | UUID, *, ctx: SessionContext | None = None) -> None:
uuid: str | UUID, *, ctx: SessionContext | None = None
) -> None:
"""Delete organization and all its roles/users.""" """Delete organization and all its roles/users."""
if isinstance(uuid, str): if isinstance(uuid, str):
uuid = UUID(uuid) uuid = UUID(uuid)
@@ -742,9 +730,7 @@ def remove_permission_from_organization(
_db._data.permissions[permission_uuid].orgs.pop(org_uuid, None) _db._data.permissions[permission_uuid].orgs.pop(org_uuid, None)
def create_role( def create_role(role: Role, *, ctx: SessionContext | None = None) -> None:
role: Role, *, ctx: SessionContext | None = None
) -> None:
"""Create a new role.""" """Create a new role."""
if role.uuid in _db._data.roles: if role.uuid in _db._data.roles:
raise ValueError(f"Role {role.uuid} already exists") raise ValueError(f"Role {role.uuid} already exists")
@@ -809,9 +795,7 @@ def remove_permission_from_role(
_db._data.roles[role_uuid].permissions.pop(permission_uuid, None) _db._data.roles[role_uuid].permissions.pop(permission_uuid, None)
def delete_role( def delete_role(uuid: str | UUID, *, ctx: SessionContext | None = None) -> None:
uuid: str | UUID, *, ctx: SessionContext | None = None
) -> None:
"""Delete a role.""" """Delete a role."""
if isinstance(uuid, str): if isinstance(uuid, str):
uuid = UUID(uuid) uuid = UUID(uuid)
@@ -824,9 +808,7 @@ def delete_role(
del _db._data.roles[uuid] del _db._data.roles[uuid]
def create_user( def create_user(new_user: User, *, ctx: SessionContext | None = None) -> None:
new_user: User, *, ctx: SessionContext | None = None
) -> None:
"""Create a new user.""" """Create a new user."""
if new_user.uuid in _db._data.users: if new_user.uuid in _db._data.users:
raise ValueError(f"User {new_user.uuid} already exists") raise ValueError(f"User {new_user.uuid} already exists")
@@ -849,7 +831,7 @@ def update_user_display_name(
ctx: SessionContext | None = None, ctx: SessionContext | None = None,
) -> None: ) -> None:
"""Update user display name. """Update user display name.
For self-service (user updating own name), ctx can be None and user is derived from uuid. For self-service (user updating own name), ctx can be None and user is derived from uuid.
For admin operations, ctx should be provided. For admin operations, ctx should be provided.
""" """
@@ -909,9 +891,7 @@ def update_user_role_in_organization(
_db._data.users[user_uuid].role = new_role_uuid _db._data.users[user_uuid].role = new_role_uuid
def delete_user( def delete_user(uuid: str | UUID, *, ctx: SessionContext | None = None) -> None:
uuid: str | UUID, *, ctx: SessionContext | None = None
) -> None:
"""Delete user and their credentials/sessions.""" """Delete user and their credentials/sessions."""
if isinstance(uuid, str): if isinstance(uuid, str):
uuid = UUID(uuid) uuid = UUID(uuid)
@@ -933,9 +913,7 @@ def delete_user(
del _db._data.users[uuid] del _db._data.users[uuid]
def create_credential( def create_credential(cred: Credential, *, ctx: SessionContext | None = None) -> None:
cred: Credential, *, ctx: SessionContext | None = None
) -> None:
"""Create a new credential.""" """Create a new credential."""
if cred.uuid in _db._data.credentials: if cred.uuid in _db._data.credentials:
raise ValueError(f"Credential {cred.uuid} already exists") raise ValueError(f"Credential {cred.uuid} already exists")
@@ -1056,7 +1034,7 @@ def set_session_host(key: str, host: str, *, ctx: SessionContext | None = None)
def delete_session(key: str, *, ctx: SessionContext | None = None) -> None: def delete_session(key: str, *, ctx: SessionContext | None = None) -> None:
"""Delete a session. """Delete a session.
For logout (user deleting own session), ctx can be None and user is derived from session. For logout (user deleting own session), ctx can be None and user is derived from session.
For admin operations, ctx should be provided. For admin operations, ctx should be provided.
""" """
@@ -1068,9 +1046,11 @@ def delete_session(key: str, *, ctx: SessionContext | None = None) -> None:
del _db._data.sessions[key] del _db._data.sessions[key]
def delete_sessions_for_user(user_uuid: str | UUID, *, ctx: SessionContext | None = None) -> None: def delete_sessions_for_user(
user_uuid: str | UUID, *, ctx: SessionContext | None = None
) -> None:
"""Delete all sessions for a user. """Delete all sessions for a user.
For logout-all (user deleting own sessions), ctx can be None and user is derived from user_uuid. For logout-all (user deleting own sessions), ctx can be None and user is derived from user_uuid.
For admin operations, ctx should be provided. For admin operations, ctx should be provided.
""" """
@@ -1093,7 +1073,7 @@ def create_reset_token(
ctx: SessionContext | None = None, ctx: SessionContext | None = None,
) -> None: ) -> None:
"""Create a reset token from a passphrase. """Create a reset token from a passphrase.
For self-service (user creating own recovery link), ctx can be None and user is derived from user_uuid. For self-service (user creating own recovery link), ctx can be None and user is derived from user_uuid.
For admin operations, ctx should be provided. For admin operations, ctx should be provided.
""" """
+5 -11
View File
@@ -214,9 +214,7 @@ async def admin_add_org_permission(
ctx = await authz.verify( ctx = await authz.verify(
auth, ["auth:admin"], host=request.headers.get("host"), match=permutil.has_all auth, ["auth:admin"], host=request.headers.get("host"), match=permutil.has_all
) )
db.add_permission_to_organization( db.add_permission_to_organization(str(org_uuid), permission_id, ctx=ctx)
str(org_uuid), permission_id, ctx=ctx
)
return {"status": "ok"} return {"status": "ok"}
@@ -240,9 +238,7 @@ async def admin_remove_org_permission(
"This would lock you out of admin access." "This would lock you out of admin access."
) )
db.remove_permission_from_organization( db.remove_permission_from_organization(str(org_uuid), permission_id, ctx=ctx)
str(org_uuid), permission_id, ctx=ctx
)
return {"status": "ok"} return {"status": "ok"}
@@ -273,7 +269,7 @@ async def admin_create_role(
perms = payload.get("permissions") or [] perms = payload.get("permissions") or []
org = db.get_organization(str(org_uuid)) org = db.get_organization(str(org_uuid))
grantable = set(org.permissions or []) grantable = set(org.permissions or [])
# Normalize permission IDs to UUIDs # Normalize permission IDs to UUIDs
permission_uuids = [] permission_uuids = []
for pid in perms: for pid in perms:
@@ -284,7 +280,7 @@ async def admin_create_role(
if perm_uuid_str not in grantable: if perm_uuid_str not in grantable:
raise ValueError(f"Permission not grantable by org: {pid}") raise ValueError(f"Permission not grantable by org: {pid}")
permission_uuids.append(perm_uuid_str) permission_uuids.append(perm_uuid_str)
role = RoleDC( role = RoleDC(
uuid=role_uuid, uuid=role_uuid,
org_uuid=org_uuid, org_uuid=org_uuid,
@@ -1060,9 +1056,7 @@ async def admin_rename_permission(
_check_admin_lockout(str(perm.uuid), domain_value, request.headers.get("host")) _check_admin_lockout(str(perm.uuid), domain_value, request.headers.get("host"))
# All current backends support rename_permission # All current backends support rename_permission
db.rename_permission( db.rename_permission(old_scope, new_scope, display_name, domain_value, ctx=ctx)
old_scope, new_scope, display_name, domain_value, ctx=ctx
)
return {"status": "ok"} return {"status": "ok"}
+3 -1
View File
@@ -120,7 +120,9 @@ async def second_org_session_token(
@pytest_asyncio.fixture(scope="function") @pytest_asyncio.fixture(scope="function")
async def org_admin_role(test_db: DB, test_org: Org, org_admin_permission: Permission) -> Role: async def org_admin_role(
test_db: DB, test_org: Org, org_admin_permission: Permission
) -> Role:
"""Create a role with org admin permission only (no global admin).""" """Create a role with org admin permission only (no global admin)."""
role = Role( role = Role(
uuid=uuid7.create(), uuid=uuid7.create(),