feat: asset catalog API, 태그 DB 기초, 마이그레이션 3건 및 도메인/테스트 갱신 (2026-05-19)

This commit is contained in:
2026-05-19 22:15:55 +00:00
parent e2226e28b6
commit 158929b2d9
33 changed files with 2575 additions and 102 deletions
+130
View File
@@ -0,0 +1,130 @@
"""Asset catalog CLI commands."""
from __future__ import annotations
import typer
from app.cli.core.utils import execute_command
asset_app = typer.Typer(help="Asset catalog management commands.")
def _with_repo(ctx: typer.Context, callback):
app_context = ctx.obj
repository = app_context.repository_factory(app_context.settings)
try:
return execute_command(app_context.console, callback=lambda: callback(repository, app_context))
finally:
if callable(getattr(type(repository), "close", None)):
repository.close()
def _confirm(force: bool, message: str) -> None:
if force:
return
if not typer.confirm(message):
raise typer.Exit(code=0)
def _asset_to_dict(asset) -> dict:
return {
"asset_id": asset.asset_id,
"symbol": asset.symbol,
"markets": asset.markets,
"display_name": asset.display_name,
"status": asset.status,
"created_at": str(asset.created_at) if asset.created_at else None,
"updated_at": str(asset.updated_at) if asset.updated_at else None,
}
@asset_app.command("create")
def asset_create(
ctx: typer.Context,
symbol: str = typer.Argument(..., help="Asset symbol (e.g. 005930, BTC)."),
display_name: str | None = typer.Option(None, "--display-name", help="Human-readable name."),
market: list[str] | None = typer.Option(None, "--market", help="Market code. Repeatable."),
status: str = typer.Option("active", help="Asset status."),
force: bool = typer.Option(False, "--force", help="Skip confirmation prompt."),
) -> None:
_confirm(force, f"Create asset {symbol!r}?")
markets = list(market or [])
def _callback(repository, app_context):
return repository.insert_asset(symbol=symbol, markets=markets, display_name=display_name, status=status)
result = _with_repo(ctx, _callback)
d = _asset_to_dict(result)
if ctx.obj.output_format == "json":
ctx.obj.console.print_json(data=d)
else:
ctx.obj.console.print(f"Created asset: {d['symbol']} {d['asset_id']}")
@asset_app.command("list")
def asset_list(
ctx: typer.Context,
active_only: bool = typer.Option(False, "--active-only", help="Show active assets only."),
) -> None:
def _callback(repository, app_context):
assets = repository.list_assets()
if active_only:
assets = [a for a in assets if a.status == "active"]
return assets
results = _with_repo(ctx, _callback)
items = [_asset_to_dict(a) for a in results] if results else []
if not items:
ctx.obj.console.print("No assets found.")
return
if ctx.obj.output_format == "json":
ctx.obj.console.print_json(data=items)
else:
for a in items:
display_name = a["display_name"] or ""
markets = ",".join(a["markets"]) if a["markets"] else "-"
ctx.obj.console.print(
f"{a['symbol']} {a['status']} [{markets}] {display_name} {a['asset_id']}"
)
@asset_app.command("update")
def asset_update(
ctx: typer.Context,
asset_id: str = typer.Argument(..., help="Asset UUID."),
display_name: str | None = typer.Option(None, "--display-name", help="Updated display name."),
market: list[str] | None = typer.Option(None, "--market", help="Updated market codes. Repeatable."),
status: str | None = typer.Option(None, help="Updated status."),
force: bool = typer.Option(False, "--force", help="Skip confirmation prompt."),
) -> None:
_confirm(force, f"Update asset {asset_id!r}?")
markets = list(market) if market is not None else None
def _callback(repository, app_context):
return repository.update_asset(asset_id, markets=markets, display_name=display_name, status=status)
result = _with_repo(ctx, _callback)
d = _asset_to_dict(result)
if ctx.obj.output_format == "json":
ctx.obj.console.print_json(data=d)
else:
ctx.obj.console.print(f"Updated asset: {d['symbol']} {d['status']} {d['asset_id']}")
@asset_app.command("deactivate")
def asset_deactivate(
ctx: typer.Context,
asset_id: str = typer.Argument(..., help="Asset UUID."),
force: bool = typer.Option(False, "--force", help="Skip confirmation prompt."),
) -> None:
_confirm(force, f"Deactivate asset {asset_id!r}?")
def _callback(repository, app_context):
return repository.update_asset(asset_id, status="inactive")
result = _with_repo(ctx, _callback)
d = _asset_to_dict(result)
if ctx.obj.output_format == "json":
ctx.obj.console.print_json(data=d)
else:
ctx.obj.console.print(f"Deactivated asset: {d['symbol']} {d['status']} {d['asset_id']}")