chore: hygiene sweep — dead code, role race, user lookup, stale files

One pass over the LOW-tier review items plus two folded issues:

- Delete validate_journal_entry (dead since the Fava migration; it
  validated the pre-string-amount model) with its exports, unused
  crud imports, and tests. Beancount validates entries now.
- Migration m006: UNIQUE index on user_roles(user_id, role_id) after
  deduping; assign_user_role inserts with ON CONFLICT DO NOTHING and
  returns the existing assignment — closes the auto-assign
  check-then-act race on concurrent logins.
- Extract _get_username_from_user_id (110 lines in views_api, fresh
  LNbits Database per call inside per-row hot paths) into
  user_lookup.py with one shared core-DB handle, a 60s TTL cache and
  a batch get_usernames API (review #18).
- Receivable-entry responses report CLEARED, matching the flag the
  formatter actually writes; PENDING misled the UI (libra-#35).
- Replace the remaining print() calls in tasks.py with logger.
- get_all_accounts derives valid roots from
  account_utils.ACCOUNT_TYPE_ROOTS instead of a hardcoded tuple, and
  the no-op per-test rate-limit reset is gone (libra-#54).
- Delete migrations_old.py.bak, MIGRATION_SQUASH_SUMMARY.md,
  docs/PHASE*_COMPLETE.md and the rendered .html; .gitignore data/
  (it holds the runtime .lnbits_auth_key secret).
- Track docs/CODE-REVIEW-2026-06.md with finding statuses updated for
  the PR #55-#59 + chore/hygiene series.
- CLAUDE.md notes LNbits pins Pydantic v1: keep .dict(), don't
  "modernize" to .model_dump().

Note: format_payment_entry's is_payable docstring (flagged in review
follow-up) turned out to be consistent with the body — no change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Padreug 2026-07-12 16:01:04 +02:00
commit ec6cac51f0
19 changed files with 454 additions and 2942 deletions

126
user_lookup.py Normal file
View file

@ -0,0 +1,126 @@
"""Username resolution for UI display.
Extracted from views_api (CODE-REVIEW-2026-06 #18): the old helper
constructed a fresh LNbits `Database` per call inside per-row hot paths
(entry listings, all-user balances). This module keeps one shared core-DB
handle and a short TTL cache, so listing N rows for the same few users
costs one lookup per unique user per TTL window instead of one per row.
Accepted id shapes (they all occur in ledger data):
- Full UUID with dashes (36 chars): "375ec158-686c-4a21-b44d-a51cc90ef07d"
- Dashless UUID (32 chars): "375ec158686c4a21b44da51cc90ef07d"
- Partial id from account names (8 chars): "375ec158"
"""
from typing import Dict, Iterable, Optional
from lnbits.core.crud.users import get_user
from lnbits.db import Database
from lnbits.utils.cache import Cache
from loguru import logger
# One shared handle to the LNbits core DB (username lives on core
# `accounts`, not in libra's extension DB).
_core_db = Database("database")
_username_cache = Cache()
_USERNAME_CACHE_TTL = 60 # seconds — usernames change rarely
def _dashed(user_id: str) -> str:
return (
f"{user_id[0:8]}-{user_id[8:12]}-{user_id[12:16]}"
f"-{user_id[16:20]}-{user_id[20:32]}"
)
async def _resolve(user_id: str) -> str:
from .crud import get_all_user_wallet_settings
# Case 1: full UUID with dashes
if len(user_id) == 36 and user_id.count('-') == 4:
user = await get_user(user_id)
return user.username if user and user.username else f"User-{user_id[:8]}"
# Case 2: dashless 32-char UUID — libra user settings first, then
# the LNbits core DB directly
if len(user_id) == 32 and '-' not in user_id:
try:
user_id_with_dashes = _dashed(user_id)
user_settings = await get_all_user_wallet_settings()
for setting in user_settings:
if setting.id == user_id_with_dashes:
user = await get_user(setting.id)
return (
user.username
if user and user.username
else f"User-{user_id[:8]}"
)
async with _core_db.connect() as conn:
row = await conn.fetchone(
"SELECT id, username FROM accounts WHERE id = :user_id LIMIT 1",
{"user_id": user_id_with_dashes},
)
if row and row["username"]:
return row["username"]
return f"User-{user_id[:8]}"
except Exception as e:
logger.error(f"Error looking up user by dashless UUID {user_id}: {e}")
return f"User-{user_id[:8]}"
# Case 3: 8-char partial id from an account name — resolve to a full
# id via libra user settings
if len(user_id) == 8:
try:
user_settings = await get_all_user_wallet_settings()
for setting in user_settings:
if setting.id.startswith(user_id):
user = await get_user(setting.id)
return (
user.username
if user and user.username
else f"User-{user_id}"
)
return f"User-{user_id}"
except Exception as e:
logger.error(f"Error looking up user by partial ID {user_id}: {e}")
return f"User-{user_id}"
# Case 4: unknown shape — try as-is, fall back
try:
user = await get_user(user_id)
return user.username if user and user.username else f"User-{user_id[:8]}"
except Exception:
return f"User-{user_id[:8]}"
async def get_username(user_id: str) -> Optional[str]:
"""Resolve a user id (any accepted shape) to a display username.
Returns a "User-{short}" fallback when no username exists, or None
for falsy input.
"""
if not user_id:
return None
cache_key = f"username:{user_id}"
cached = _username_cache.get(cache_key)
if cached is not None:
return cached
result = await _resolve(user_id)
_username_cache.set(cache_key, result, _USERNAME_CACHE_TTL)
return result
async def get_usernames(user_ids: Iterable[str]) -> Dict[str, str]:
"""Resolve many user ids at once, deduplicated and cache-backed."""
result: Dict[str, str] = {}
for user_id in {u for u in user_ids if u}:
username = await get_username(user_id)
if username is not None:
result[user_id] = username
return result