Debug CLI Design
Date: 2026-02-22
Status: Approved
Goal
Add a debug CLI that mirrors all web UI interactions, with superuser access to impersonate any user. Purpose: debug flows without authorization overhead.
Decisions
- Scope: All API endpoints (listings, decisions, POIs, tasks, districts)
- Execution mode: Switchable — direct service calls (default) or HTTP to running API (
--http)
- User identity:
--user-email flag to impersonate any user
- Location: New
cli/ package with submodules, registered under a debug group in main.py
- Output: Pretty-printed by default,
--json flag for machine-readable output
Structure
cli/
__init__.py # Exports all groups for registration in main.py
_context.py # Shared context: user identity, HTTP vs direct, output formatting
listings.py # list, detail, stream, refresh
decisions.py # set, list, remove
pois.py # create, list, update, delete, calculate, distances
tasks.py # status, list, cancel, clear
districts.py # list
Usage
python main.py debug --user-email user@example.com listings list --type RENT --min-bedrooms 2
python main.py debug --user-email user@example.com --http decisions set 12345 liked --type RENT
python main.py debug --user-email user@example.com --json pois list
Shared Context (_context.py)
CliContext dataclass: Holds user_email, use_http, json_output, api_base_url
get_user(): Creates User(sub=email, email=email, name=email) from provided email — no token validation
- Direct mode: Instantiates repositories and calls service functions, passing the fake
User
- HTTP mode: Mints a self-signed JWT using
JWT_SECRET from env (passkey-style token), makes requests via httpx
- Output helpers:
output(data, json_mode) — prints JSON or pretty-formatted text
Command Mapping
listings group
| Command |
Direct mode |
HTTP mode |
list |
listing_service.get_listings() |
GET /api/listing |
detail <id> |
Repository + poi_service |
GET /api/listing/{id}/detail |
stream |
export_service.export_to_geojson() |
GET /api/listing_geojson/stream |
refresh |
listing_service.refresh_listings() |
POST /api/refresh_listings |
decisions group
| Command |
Direct mode |
HTTP mode |
set <id> <liked|disliked> |
decision_service.set_decision() |
PUT /api/decisions/{id} |
list |
decision_service.get_user_decisions() |
GET /api/decisions |
remove <id> |
decision_service.remove_decision() |
DELETE /api/decisions/{id} |
pois group
| Command |
Direct mode |
HTTP mode |
list |
poi_service.get_user_pois() |
GET /api/poi |
create |
poi_service.create_poi() |
POST /api/poi |
update <id> |
poi_service.update_poi() |
PUT /api/poi/{id} |
delete <id> |
poi_service.delete_poi() |
DELETE /api/poi/{id} |
calculate <id> |
poi_service.trigger_calculation() |
POST /api/poi/{id}/calculate |
distances |
poi_service.get_distances_for_listing() |
GET /api/poi/distances |
tasks group
| Command |
Direct mode |
HTTP mode |
status <id> |
task_service.get_task_status() |
GET /api/task_status |
list |
task_service.get_user_tasks() |
GET /api/tasks_for_user |
cancel <id> |
task_service.cancel_task() |
POST /api/cancel_task |
clear |
task_service.clear_all_tasks() |
POST /api/clear_all_tasks |
districts group
| Command |
Direct mode |
HTTP mode |
list |
district_service.get_all_districts() |
GET /api/get_districts |
Error Handling
- Direct mode: Catch service exceptions, print readable messages (or
{"error": "..."} in JSON mode)
- HTTP mode: Print status code + response body on non-2xx
- Missing services: Fail fast with clear message about which service (DB, Redis) is unavailable
Testing
- Unit tests for
_context.py (JWT minting, user creation, output formatting)
- Integration tests for representative commands in direct mode (mocked repositories)
- No HTTP-mode tests (that's API testing, already covered)
Dependencies
No new dependencies. Uses existing: click, httpx, pyjwt.