Add admin user management
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
"""User CLI commands."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import typer
|
||||
|
||||
from app.cli.core.utils import execute_command, render_output
|
||||
from app.cli.commands.portfolio import _confirm
|
||||
from app.schemas.user import CreateUserRequest, UpdateUserRequest, UserResponse
|
||||
from app.services.user_service import UserService
|
||||
|
||||
|
||||
user_app = typer.Typer(help="User management commands.")
|
||||
|
||||
|
||||
def _with_service(ctx: typer.Context, callback):
|
||||
app_context = ctx.obj
|
||||
repository = app_context.repository_factory(app_context.settings)
|
||||
service = UserService(repository)
|
||||
try:
|
||||
return execute_command(app_context.console, callback=lambda: callback(service, app_context))
|
||||
finally:
|
||||
if hasattr(repository, "close"):
|
||||
repository.close()
|
||||
|
||||
|
||||
def _to_user_response(user) -> UserResponse:
|
||||
return UserResponse(
|
||||
id=user.id,
|
||||
username=user.username,
|
||||
email=user.email,
|
||||
created_at=user.created_at,
|
||||
updated_at=user.updated_at,
|
||||
)
|
||||
|
||||
|
||||
@user_app.command("list")
|
||||
def list_users(ctx: typer.Context) -> None:
|
||||
users = _with_service(ctx, lambda service, app_context: [_to_user_response(user) for user in service.list()])
|
||||
render_output(ctx.obj.console, ctx.obj.output_format, users, empty_message="No users found.")
|
||||
|
||||
|
||||
@user_app.command("get")
|
||||
def get_user(
|
||||
ctx: typer.Context,
|
||||
user_id: str = typer.Argument(..., help="User identifier."),
|
||||
) -> None:
|
||||
user = _with_service(ctx, lambda service, app_context: _to_user_response(service.get(user_id)))
|
||||
render_output(ctx.obj.console, ctx.obj.output_format, user)
|
||||
|
||||
|
||||
@user_app.command("create")
|
||||
def create_user(
|
||||
ctx: typer.Context,
|
||||
username: str = typer.Option(..., help="Username."),
|
||||
email: str | None = typer.Option(None, help="Email address."),
|
||||
force: bool = typer.Option(False, "--force", help="Skip confirmation prompt."),
|
||||
) -> None:
|
||||
_confirm(force, f"Create user {username}?")
|
||||
user = _with_service(
|
||||
ctx,
|
||||
lambda service, app_context: _to_user_response(
|
||||
service.create(CreateUserRequest(username=username, email=email))
|
||||
),
|
||||
)
|
||||
render_output(ctx.obj.console, ctx.obj.output_format, user)
|
||||
|
||||
|
||||
@user_app.command("update")
|
||||
def update_user(
|
||||
ctx: typer.Context,
|
||||
user_id: str = typer.Argument(..., help="User identifier."),
|
||||
username: str | None = typer.Option(None, help="New username."),
|
||||
email: str | None = typer.Option(None, help="New email address."),
|
||||
force: bool = typer.Option(False, "--force", help="Skip confirmation prompt."),
|
||||
) -> None:
|
||||
_confirm(force, f"Update user {user_id}?")
|
||||
user = _with_service(
|
||||
ctx,
|
||||
lambda service, app_context: _to_user_response(
|
||||
service.update(user_id, UpdateUserRequest(username=username, email=email))
|
||||
),
|
||||
)
|
||||
render_output(ctx.obj.console, ctx.obj.output_format, user)
|
||||
|
||||
|
||||
@user_app.command("delete")
|
||||
def delete_user(
|
||||
ctx: typer.Context,
|
||||
user_id: str = typer.Argument(..., help="User identifier."),
|
||||
force: bool = typer.Option(False, "--force", help="Skip confirmation prompt."),
|
||||
) -> None:
|
||||
_confirm(force, f"Delete user {user_id}?")
|
||||
_with_service(ctx, lambda service, app_context: service.delete(user_id))
|
||||
if ctx.obj.output_format == "json":
|
||||
render_output(ctx.obj.console, ctx.obj.output_format, {"deleted": user_id})
|
||||
else:
|
||||
ctx.obj.console.print(f"Deleted user: {user_id}")
|
||||
+46
-1
@@ -14,6 +14,7 @@ from rich.table import Table
|
||||
|
||||
from app.core.errors import FeatureNotReadyError
|
||||
from app.schemas.portfolio import PortfolioDetailResponse, PortfolioListItem
|
||||
from app.schemas.user import UserResponse
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
@@ -126,6 +127,41 @@ def print_portfolio_detail(console: Console, detail: PortfolioDetailResponse) ->
|
||||
console.print(ledger_table)
|
||||
|
||||
|
||||
def print_user_table(console: Console, users: list[UserResponse]) -> None:
|
||||
table = Table(title="Users", expand=True)
|
||||
table.add_column("ID", style="cyan", no_wrap=True)
|
||||
table.add_column("Username", no_wrap=True)
|
||||
table.add_column("Email")
|
||||
table.add_column("Created At", no_wrap=True)
|
||||
table.add_column("Updated At", no_wrap=True)
|
||||
for user in users:
|
||||
table.add_row(
|
||||
user.id,
|
||||
user.username,
|
||||
user.email or "N/A",
|
||||
user.created_at.isoformat(timespec="seconds") if user.created_at else "N/A",
|
||||
user.updated_at.isoformat(timespec="seconds") if user.updated_at else "N/A",
|
||||
)
|
||||
console.print(table)
|
||||
|
||||
|
||||
def print_user_detail(console: Console, user: UserResponse) -> None:
|
||||
console.print(
|
||||
Panel.fit(
|
||||
"\n".join(
|
||||
[
|
||||
f"id: {user.id}",
|
||||
f"username: {user.username}",
|
||||
f"email: {user.email or 'N/A'}",
|
||||
f"created_at: {user.created_at.isoformat(timespec='seconds') if user.created_at else 'N/A'}",
|
||||
f"updated_at: {user.updated_at.isoformat(timespec='seconds') if user.updated_at else 'N/A'}",
|
||||
]
|
||||
),
|
||||
title="User",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def print_json(console: Console, payload: Any) -> None:
|
||||
console.print_json(data=to_plain_data(payload))
|
||||
|
||||
@@ -139,13 +175,22 @@ def render_output(console: Console, output_format: str, payload: Any, *, empty_m
|
||||
if not payload and empty_message:
|
||||
console.print(empty_message)
|
||||
return
|
||||
print_portfolio_table(console, payload)
|
||||
if all(isinstance(item, PortfolioListItem) for item in payload):
|
||||
print_portfolio_table(console, payload)
|
||||
return
|
||||
if all(isinstance(item, UserResponse) for item in payload):
|
||||
print_user_table(console, payload)
|
||||
return
|
||||
return
|
||||
|
||||
if isinstance(payload, PortfolioDetailResponse):
|
||||
print_portfolio_detail(console, payload)
|
||||
return
|
||||
|
||||
if isinstance(payload, UserResponse):
|
||||
print_user_detail(console, payload)
|
||||
return
|
||||
|
||||
console.print(payload)
|
||||
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ from rich.console import Console
|
||||
from app.cli.commands.health import register_health_commands
|
||||
from app.cli.commands.portfolio import portfolio_app
|
||||
from app.cli.commands.strategy import strategy_app
|
||||
from app.cli.commands.user import user_app
|
||||
from app.core.config import Settings, get_settings
|
||||
from app.repositories.factory import get_portfolio_repository_from_settings
|
||||
from app.repositories.portfolio_repository import PortfolioRepository
|
||||
@@ -47,6 +48,7 @@ def create_app(
|
||||
|
||||
app.add_typer(portfolio_app, name="portfolio")
|
||||
app.add_typer(strategy_app, name="strategy")
|
||||
app.add_typer(user_app, name="user")
|
||||
register_health_commands(app)
|
||||
return app
|
||||
|
||||
|
||||
Reference in New Issue
Block a user