homelab/.planning/codebase/CONVENTIONS.md
Mikkel Georgsen a639a53b0b docs: add codebase map and domain research
Codebase: 7 documents (stack, architecture, structure, conventions, testing, integrations, concerns)
Research: 5 documents (stack, features, architecture, pitfalls, summary)
2026-02-04 13:50:03 +00:00

9.7 KiB

Coding Conventions

Analysis Date: 2026-02-04

Naming Patterns

Files:

  • Python files: lowercase with underscores (e.g., bot.py, credentials)
  • Bash scripts: lowercase with hyphens (e.g., npm-api, uptime-kuma)
  • Helper scripts in ~/bin/: all lowercase, no extension (e.g., pve, pbs, dns)

Functions:

  • Python: snake_case (e.g., cmd_status(), get_authorized_users(), run_command())
  • Bash: snake_case with cmd_ prefix for command handlers (e.g., cmd_status(), cmd_tasks())
  • Bash: auxiliary functions also use snake_case (e.g., ssh_pbs(), get_token())

Variables:

  • Python: snake_case for local/module vars (e.g., authorized_users, output_lines)
  • Python: UPPERCASE for constants (e.g., TOKEN, INBOX_FILE, AUTHORIZED_FILE, NODE, PBS_HOST)
  • Bash: UPPERCASE for environment variables and constants (e.g., PBS_HOST, TOKEN, BASE, DEFAULT_ZONE)
  • Bash: lowercase for local variables (e.g., hours, cutoff, status_icon)

Types/Classes:

  • Python: PascalCase for imported classes (e.g., ProxmoxAPI, Update, Application)
  • Dictionary/config keys: lowercase with hyphens or underscores (e.g., token_name, max-mem)

Code Style

Formatting:

  • No automated formatter detected in codebase
  • Python: PEP 8 conventions followed informally
    • 4-space indentation
    • Max line length ~90-100 characters (observed in practice)
    • Blank lines: 2 lines before module-level functions, 1 line before methods
  • Bash: 4-space indentation (observed)

Linting:

  • No linting configuration detected (no .pylintrc, .flake8, .eslintrc)
  • Code style is manually maintained

Docstrings:

  • Python: Triple-quoted strings at module level describing purpose
    • Example from telegram/bot.py:
    """
    Homelab Telegram Bot
    Two-way interactive bot for homelab management and notifications.
    """
    
  • Python: Function docstrings used for major functions
    • Single-line format for simple functions
    • Example: """Handle /start command - first contact with bot."""
    • Example: """Load authorized user IDs."""

Import Organization

Order:

  1. Standard library imports (e.g., sys, os, json, subprocess)
  2. Third-party imports (e.g., ProxmoxAPI, telegram, pocketbase)
  3. Local imports (rarely used in this codebase)

Path Aliases:

  • No aliases detected
  • Absolute imports used throughout

Credential Loading Pattern: All scripts that need credentials follow the same pattern:

# Load credentials
creds_path = Path.home() / ".config" / <service> / "credentials"
creds = {}
with open(creds_path) as f:
    for line in f:
        if '=' in line:
            key, value = line.strip().split('=', 1)
            creds[key] = value

Or in Bash:

source ~/.config/dns/credentials

Error Handling

Patterns:

  • Python: Try-except with broad exception catching (bare except: used in pve script lines 70, 82, 95, 101)

    • Not ideal but pragmatic for CLI tools that need to try multiple approaches
    • Example from pve:
    try:
        status = pve.nodes(NODE).lxc(vmid).status.current.get()
        # ...
        return
    except:
        pass
    
  • Python: Explicit exception handling in telegram bot

    • Catches subprocess.TimeoutExpired specifically in run_command() function
    • Example from telegram/bot.py:
    try:
        result = subprocess.run(...)
        output = result.stdout or result.stderr or "No output"
        if len(output) > 4000:
            output = output[:4000] + "\n... (truncated)"
        return output
    except subprocess.TimeoutExpired:
        return "Command timed out"
    except Exception as e:
        return f"Error: {e}"
    
  • Bash: Set strict mode with set -e in some scripts (dns script line 12)

    • Causes script to exit on first error
  • Bash: No error handling in most scripts (pbs, beszel, kuma)

    • Relies on exit codes implicitly

Return Value Handling:

  • Python: Functions return data directly or None on failure

    • Example from pbs helper: Returns JSON-parsed data or string output
    • Example from pve: Returns nothing (prints output), but uses exceptions for flow control
  • Python: Command runner returns error strings: "Command timed out", "Error: {e}"

Logging

Framework:

  • Python: Standard logging module
    • Configured in telegram/bot.py lines 18-22:
    logging.basicConfig(
        format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
        level=logging.INFO
    )
    logger = logging.getLogger(__name__)
    
    • Log level: INFO
    • Format includes timestamp, logger name, level, message

Patterns:

  • logger.info() for general informational messages

    • Example: logger.info("Starting Homelab Bot...")
    • Example: logger.info(f"Inbox message from {user.first_name}: {message[:50]}...")
    • Example: logger.info(f"Photo saved from {user.first_name}: {filepath}")
  • Bash: Uses echo for output, no structured logging

    • Informational messages for user feedback
    • Error messages sent to stdout (not stderr)

Comments

When to Comment:

  • Module-level docstrings at top of file (required for all scripts)
  • Usage examples in module docstrings (e.g., pve, pbs, kuma)
  • Inline comments for complex logic (e.g., in pbs script parsing hex timestamps)
  • Comments on tricky regex patterns (e.g., pbs tasks parsing)

Bash Comments:

  • Header comment with script name, purpose, and usage (lines 1-10)
  • Inline comments before major sections (e.g., # Datastore info, # Storage stats)
  • No comments in simple expressions

Python Comments:

  • Header comment with purpose (module docstring)
  • Sparse inline comments except for complex sections
  • Example from telegram/bot.py line 71: # Telegram has 4096 char limit per message
  • Example from pve line 70: # Try as container first

Function Design

Size:

  • Python: Functions are generally 10-50 lines

    • Smaller functions for simple operations (e.g., is_authorized() is 2 lines)
    • Larger functions for command handlers that do setup + API calls (e.g., status() is 40 lines)
  • Bash: Functions are typically 20-80 lines

    • Longer functions acceptable for self-contained operations like cmd_status() in pbs

Parameters:

  • Python: Explicit parameters, typically 1-5 parameters per function

    • Optional parameters with defaults (e.g., timeout: int = 30, port=45876)
    • Type hints not used consistently (some functions have them, many don't)
  • Bash: Parameters passed as positional arguments

    • Some functions take zero parameters and rely on global variables
    • Example: ssh_pbs() in pbs uses global $PBS_HOST

Return Values:

  • Python: Functions return data (strings, dicts, lists) or None

    • Command handlers often return nothing (implicitly None)
    • Helper functions return computed values (e.g., is_authorized() returns bool)
  • Bash: Functions print output directly, return exit codes

    • No explicit return values beyond exit codes
    • Output captured by caller with $()

Module Design

Exports:

  • Python: All functions are module-level, no explicit exports

    • if __name__ == "__main__": pattern used in all scripts to guard main execution
    • Example from beszel lines 101-152
  • Bash: All functions are script-level, called via case statement

    • Main dispatch logic at bottom of script
    • Example from dns lines 29-106: case "$1" in ... esac

Async/Await (Telegram Bot Only):

  • Python telegram bot uses asyncio and async def for all handlers
  • All command handlers are async (e.g., async def start())
  • Use await for async operations (e.g., await update.message.reply_text())
  • Example from telegram/bot.py lines 81-94:
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Handle /start command - first contact with bot."""
    user = update.effective_user
    chat_id = update.effective_chat.id
    # ... async operations with await

File Structure:

  • Single-file modules: Most helpers are single files
  • telegram/bot.py: Main bot implementation with all handlers
  • /bin/ scripts: Each script is self-contained with helper functions + main dispatch

Data Structures

JSON/Config Files:

  • Credentials files: Simple KEY=value format (no JSON)
  • PBS task logging: Uses hex-encoded UPID format, parsed with regex
  • Telegram bot: Saves messages to text files with timestamp prefix
  • JSON output: Parsed with python3 -c "import sys, json; ..." in Bash scripts

Error Response Patterns:

  • API calls: Check for .get('status') == 'ok' or similar
  • Command execution: Check returncode == 0, capture stdout/stderr
  • API clients: Let exceptions bubble up, caught at command handler level

Conditionals and Flow Control

Python:

  • if/elif/else chains for command dispatch
  • Simple truthiness checks: if not user_id:, if not alerts:
  • Example from telegram/bot.py line 86-100: Authorization check pattern

Bash:

  • case/esac for command dispatch (preferred)
  • if with regex matching for parsing
  • Example from pbs lines 122-143: Complex regex with BASH_REMATCH array

Security Patterns

Credential Management:

  • Credentials stored in ~/.config/<service>/credentials with restricted permissions (not enforced in code)
  • Telegram token loaded from file, not environment
  • Credentials never logged or printed

Input Validation:

  • Bash: Basic validation with isalnum() check in ping_host() function
    • Example: if not host.replace('.', '').replace('-', '').isalnum():
  • Bash: Whitelist command names from case statements
  • No SQL injection risk (no databases used directly)

Shell Injection:

  • Bash scripts use quoted variables appropriately
  • Some inline Python in Bash uses string interpolation (potential risk)
    • Example from dns lines 31-37: curl ... | python3 -c "..." with variable interpolation

Convention analysis: 2026-02-04