feat(REQ-001): add portfolio comment history API, CLI, repository, migration, and tests
Agent-Logs-Url: https://github.com/genichin/portfolio-hub/sessions/64fc2cec-78f5-41d1-99a4-45f868781edd Co-authored-by: genichin <4282227+genichin@users.noreply.github.com>
This commit is contained in:
co-authored by
genichin
parent
4684597353
commit
e8f782bec6
@@ -10,6 +10,8 @@ import typer
|
||||
from app.cli.core.utils import execute_command, render_output
|
||||
from app.schemas.portfolio import (
|
||||
BuyRequest,
|
||||
CommentCreateRequest,
|
||||
CommentUpdateRequest,
|
||||
CreatePortfolioRequest,
|
||||
InvestRequest,
|
||||
ResetPortfolioRequest,
|
||||
@@ -21,6 +23,8 @@ from app.services.portfolio_service import PortfolioService
|
||||
|
||||
|
||||
portfolio_app = typer.Typer(help="Portfolio lifecycle and trade commands.")
|
||||
comment_app = typer.Typer(help="Portfolio comment history commands.")
|
||||
portfolio_app.add_typer(comment_app, name="comment")
|
||||
|
||||
|
||||
def _parse_strategy_params(value: str) -> dict[str, Any]:
|
||||
@@ -263,4 +267,70 @@ def delete_portfolio(
|
||||
if ctx.obj.output_format == "json":
|
||||
render_output(ctx.obj.console, ctx.obj.output_format, {"deleted": portfolio_id, "keep_ledger": keep_ledger})
|
||||
else:
|
||||
ctx.obj.console.print(f"Deleted portfolio: {portfolio_id}")
|
||||
ctx.obj.console.print(f"Deleted portfolio: {portfolio_id}")
|
||||
|
||||
|
||||
@comment_app.command("add")
|
||||
def comment_add(
|
||||
ctx: typer.Context,
|
||||
portfolio_id: str = typer.Argument(..., help="Portfolio identifier."),
|
||||
content: str = typer.Option(..., "--content", help="Comment text (supports multiline)."),
|
||||
) -> None:
|
||||
detail = _with_service(
|
||||
ctx,
|
||||
lambda service, app_context: service.create_comment(
|
||||
portfolio_id,
|
||||
CommentCreateRequest(content=content),
|
||||
),
|
||||
)
|
||||
render_output(ctx.obj.console, ctx.obj.output_format, detail)
|
||||
|
||||
|
||||
@comment_app.command("list")
|
||||
def comment_list(
|
||||
ctx: typer.Context,
|
||||
portfolio_id: str = typer.Argument(..., help="Portfolio identifier."),
|
||||
) -> None:
|
||||
detail = _with_service(
|
||||
ctx,
|
||||
lambda service, app_context: service.list_comments(portfolio_id),
|
||||
)
|
||||
render_output(ctx.obj.console, ctx.obj.output_format, detail, empty_message="No comments found.")
|
||||
|
||||
|
||||
@comment_app.command("update")
|
||||
def comment_update(
|
||||
ctx: typer.Context,
|
||||
portfolio_id: str = typer.Argument(..., help="Portfolio identifier."),
|
||||
comment_id: str = typer.Argument(..., help="Comment identifier."),
|
||||
content: str = typer.Option(..., "--content", help="Updated comment text."),
|
||||
force: bool = typer.Option(False, "--force", help="Skip confirmation prompt."),
|
||||
) -> None:
|
||||
_confirm(force, f"Update comment {comment_id} for portfolio {portfolio_id}?")
|
||||
detail = _with_service(
|
||||
ctx,
|
||||
lambda service, app_context: service.update_comment(
|
||||
portfolio_id,
|
||||
comment_id,
|
||||
CommentUpdateRequest(content=content),
|
||||
),
|
||||
)
|
||||
render_output(ctx.obj.console, ctx.obj.output_format, detail)
|
||||
|
||||
|
||||
@comment_app.command("delete")
|
||||
def comment_delete(
|
||||
ctx: typer.Context,
|
||||
portfolio_id: str = typer.Argument(..., help="Portfolio identifier."),
|
||||
comment_id: str = typer.Argument(..., help="Comment identifier."),
|
||||
force: bool = typer.Option(False, "--force", help="Skip confirmation prompt."),
|
||||
) -> None:
|
||||
_confirm(force, f"Delete comment {comment_id} for portfolio {portfolio_id}?")
|
||||
_with_service(
|
||||
ctx,
|
||||
lambda service, app_context: service.delete_comment(portfolio_id, comment_id),
|
||||
)
|
||||
if ctx.obj.output_format == "json":
|
||||
render_output(ctx.obj.console, ctx.obj.output_format, {"deleted": comment_id, "portfolio_id": portfolio_id})
|
||||
else:
|
||||
ctx.obj.console.print(f"Deleted comment: {comment_id}")
|
||||
+43
-1
@@ -13,7 +13,7 @@ from rich.panel import Panel
|
||||
from rich.table import Table
|
||||
|
||||
from app.core.errors import FeatureNotReadyError
|
||||
from app.schemas.portfolio import PortfolioDetailResponse, PortfolioListItem
|
||||
from app.schemas.portfolio import CommentResponse, PortfolioDetailResponse, PortfolioListItem
|
||||
from app.schemas.user import UserResponse
|
||||
|
||||
T = TypeVar("T")
|
||||
@@ -162,6 +162,41 @@ def print_user_detail(console: Console, user: UserResponse) -> None:
|
||||
)
|
||||
|
||||
|
||||
def print_comment_table(console: Console, comments: list[CommentResponse]) -> None:
|
||||
table = Table(title="Portfolio Comments", expand=True)
|
||||
table.add_column("ID", style="cyan", no_wrap=True)
|
||||
table.add_column("Portfolio ID", no_wrap=True)
|
||||
table.add_column("Content")
|
||||
table.add_column("Created At", no_wrap=True)
|
||||
table.add_column("Updated At", no_wrap=True)
|
||||
for comment in comments:
|
||||
table.add_row(
|
||||
comment.id,
|
||||
comment.portfolio_id,
|
||||
comment.content,
|
||||
comment.created_at.isoformat(timespec="seconds") if comment.created_at else "N/A",
|
||||
comment.updated_at.isoformat(timespec="seconds") if comment.updated_at else "N/A",
|
||||
)
|
||||
console.print(table)
|
||||
|
||||
|
||||
def print_comment_detail(console: Console, comment: CommentResponse) -> None:
|
||||
console.print(
|
||||
Panel.fit(
|
||||
"\n".join(
|
||||
[
|
||||
f"id: {comment.id}",
|
||||
f"portfolio_id: {comment.portfolio_id}",
|
||||
f"content: {comment.content}",
|
||||
f"created_at: {comment.created_at.isoformat(timespec='seconds') if comment.created_at else 'N/A'}",
|
||||
f"updated_at: {comment.updated_at.isoformat(timespec='seconds') if comment.updated_at else 'N/A'}",
|
||||
]
|
||||
),
|
||||
title="Comment",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def print_json(console: Console, payload: Any) -> None:
|
||||
console.print_json(data=to_plain_data(payload))
|
||||
|
||||
@@ -181,6 +216,9 @@ def render_output(console: Console, output_format: str, payload: Any, *, empty_m
|
||||
if all(isinstance(item, UserResponse) for item in payload):
|
||||
print_user_table(console, payload)
|
||||
return
|
||||
if all(isinstance(item, CommentResponse) for item in payload):
|
||||
print_comment_table(console, payload)
|
||||
return
|
||||
return
|
||||
|
||||
if isinstance(payload, PortfolioDetailResponse):
|
||||
@@ -191,6 +229,10 @@ def render_output(console: Console, output_format: str, payload: Any, *, empty_m
|
||||
print_user_detail(console, payload)
|
||||
return
|
||||
|
||||
if isinstance(payload, CommentResponse):
|
||||
print_comment_detail(console, payload)
|
||||
return
|
||||
|
||||
console.print(payload)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user