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:
parent
c0d371036b
commit
ec6cac51f0
19 changed files with 454 additions and 2942 deletions
126
user_lookup.py
Normal file
126
user_lookup.py
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue