feat: 태그 HTTP API(BAT-004) 및 pfhub tag CLI + baseline 문서 갱신(BAT-005)
BAT-004 (REQ-004, REQ-005):
- app/api/v1_tags.py: POST /v1/tags, GET /v1/tags, PATCH /v1/tags/{id}/deactivate
- app/api/v1_portfolios.py: POST/DELETE /v1/portfolios/{id}/tags
- app/api/v1_assets.py: POST/DELETE /v1/assets/{id}/tags 엔드포인트 추가
- app/schemas/v1.py: TagItem, TagCreateRequest, TagListResponse, TagApplyRequest/Response 추가
- app/repositories/portfolio_repository.py, postgres.py: append/remove_portfolio_tag_id 추가
- app/main.py: v1_portfolios_router, v1_tags_router 등록
- tests/test_tags_http_api.py: 22개 HTTP API 테스트
BAT-005 (REQ-007, REQ-008):
- app/cli/commands/tag.py: pfhub tag create/list/deactivate/apply/remove 7개 커맨드 신규
- app/cli/main.py: tag_app 등록
- app/cli/commands/portfolio.py: pfhub portfolio tag add/list/remove 완전 제거(breaking change)
- app/cli/core/utils.py: print_tag_table + list[Tag] render 분기 추가
- tests/test_tags_cli.py: REQ-007 AC-1~9 커버 24개 CLI 테스트 신규
- tests/test_cli.py: 제거된 portfolio tag 테스트 2개 삭제
- docs/data-model.md, interface-contract.md, project-structure.md, runtime-flows.md: 태그 모델 baseline 갱신(REQ-008)
- docs/batches/: BAT-004, BAT-005 전체 산출물(release-candidate)
247 passed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+76
-3
@@ -4,8 +4,8 @@
|
||||
- Baseline Seed: .stagepilot/bootstrap/baseline.yaml
|
||||
- Status: confirmed
|
||||
- Owner: repository-maintainers
|
||||
- Last Updated (KST): 2026-05-13 00:00
|
||||
- Source Discovery / Batch: bootstrap-baseline, bat-001, bat-006, bat-007, bat-008, bat-010
|
||||
- Last Updated (KST): 2026-05-20 00:00
|
||||
- Source Discovery / Batch: bootstrap-baseline, bat-001, bat-006, bat-007, bat-008, bat-010, bat-003, bat-004, bat-005
|
||||
- Primary Runtime: mixed
|
||||
|
||||
## Project Summary
|
||||
@@ -22,7 +22,7 @@
|
||||
## Covered Entry Points
|
||||
|
||||
- uvicorn app.main:app :: serve the authenticated HTTP API, OpenAPI docs, and startup bootstrap hooks
|
||||
- pfhub :: run operational CLI commands for health, users, portfolios, strategies, service tokens, and daemon registry
|
||||
- pfhub :: run operational CLI commands for health, users, portfolios, strategies, tags, service tokens, and daemon registry
|
||||
- alembic upgrade head :: apply ordered database schema migrations for persistent environments
|
||||
|
||||
## Shared Components
|
||||
@@ -50,6 +50,33 @@
|
||||
4. Read commands print table or JSON output, while write commands require explicit confirmation unless --force is supplied.
|
||||
5. Command failures surface as validation errors, feature-not-ready errors, or persistence and auth exceptions.
|
||||
|
||||
## Flow: `pfhub tag CLI Command Execution`
|
||||
|
||||
1. `pfhub tag create <name> [--user <username>] [--admin]`
|
||||
- Resolves `--user` to `user_id` via `repo.get_user_by_username()`; raises ValueError if not found.
|
||||
- Calls `repo.create_tag(name=name.strip(), is_admin_tag=--admin, created_by_user_id=resolved_id)`.
|
||||
- Duplicate name → repo ValueError → console error, exit 1.
|
||||
2. `pfhub tag list [--inactive]`
|
||||
- Calls `repo.list_tags_master(include_inactive=--inactive)`.
|
||||
- Renders Tag table (table format) or JSON array (--format json).
|
||||
3. `pfhub tag deactivate <tag_id> --force`
|
||||
- Without `--force`: prompts for confirmation; abort on refusal (exit 0).
|
||||
- Calls `repo.get_tag_by_id(tag_id)`; if not found → ValueError → exit 1.
|
||||
- If already inactive → prints "already inactive", returns (idempotent, exit 0).
|
||||
- Calls `repo.deactivate_tag(tag_id)`.
|
||||
4. `pfhub tag apply asset <asset_id> <tag_id>`
|
||||
- Validates tag: get_tag_by_id → not found → ValueError; not is_admin_tag → ValueError; not is_active → ValueError.
|
||||
- Calls `repo.append_portfolio_tag_id(asset_id, tag_id)`.
|
||||
5. `pfhub tag apply portfolio <portfolio_id> <tag_id>`
|
||||
- Validates tag: get_tag_by_id → not found → ValueError; not is_active → ValueError.
|
||||
- Resolves portfolio: repo.get(portfolio_id) → not found → ValueError.
|
||||
- Policy check: tag.is_admin_tag OR tag.created_by_user_id == portfolio.owner_user_id; else → ValueError.
|
||||
- Calls `repo.append_portfolio_tag_id(portfolio_id, tag_id)`.
|
||||
6. `pfhub tag remove asset <asset_id> <tag_id> --force` / `pfhub tag remove portfolio <portfolio_id> <tag_id> --force`
|
||||
- Without `--force`: prompts for confirmation; abort on refusal (exit 0).
|
||||
- Calls `repo.remove_portfolio_tag_id(...)`.
|
||||
7. `pfhub portfolio tag add/list/remove` — **removed in BAT-005**; Typer returns exit code 2 (no such command).
|
||||
|
||||
## Flow: `CLI Portfolio Command — Name+User Resolution`
|
||||
|
||||
1. All portfolio mutation commands (`status`, `create`, `update`, `buy`, `sell`, `invest`, `transfer`, `reset`, `delete`) accept a `<name>` positional argument and `--user <username>` option.
|
||||
@@ -68,6 +95,51 @@
|
||||
- Request validation and auth resolution should happen before persistence mutation or response rendering.
|
||||
- The health endpoint remains unauthenticated so infrastructure can probe readiness without a token.
|
||||
|
||||
## Flow: `v1 API — Tag Management (POST/GET/PATCH /v1/tags)`
|
||||
|
||||
1. **POST /v1/tags** (create tag):
|
||||
- DB token required; trade-write or admin permission.
|
||||
- Env token → HTTP 403.
|
||||
- `name` is stripped and lowercased; `is_admin_tag` set if caller's permission is admin.
|
||||
- `repo.create_tag(name, is_admin_tag, created_by_user_id=auth.user_id)` → UniqueViolation → HTTP 409.
|
||||
- Returns HTTP 201 `TagItem`.
|
||||
2. **GET /v1/tags** (list tags):
|
||||
- DB token required; read permission.
|
||||
- `?include_inactive=true` → `repo.list_tags_master(include_inactive=True)`; default returns active only.
|
||||
- Returns HTTP 200 `TagListResponse {"tags": [...], "total": N}`.
|
||||
3. **PATCH /v1/tags/{tag_id}/deactivate**:
|
||||
- DB token required; read permission.
|
||||
- `repo.get_tag_by_id(tag_id)` → None → HTTP 404.
|
||||
- Caller must be admin or tag creator (`auth.user_id == tag.created_by_user_id`); else → HTTP 403.
|
||||
- If already inactive → return HTTP 200 immediately (idempotent).
|
||||
- `repo.deactivate_tag(tag_id)` → HTTP 200 `TagItem`.
|
||||
|
||||
## Flow: `v1 API — Tag Apply/Remove (POST/DELETE /v1/assets/{id}/tags and /v1/portfolios/{id}/tags)`
|
||||
|
||||
1. **POST /v1/assets/{asset_id}/tags** (apply tag to asset — admin only):
|
||||
- DB token required; admin permission (`require_admin`).
|
||||
- `repo.get(asset_id)` → ValueError → HTTP 404.
|
||||
- `repo.get_tag_by_id(tag_id)` → None → HTTP 404.
|
||||
- `not tag.is_admin_tag` → HTTP 403; `not tag.is_active` → HTTP 403.
|
||||
- `repo.append_portfolio_tag_id(asset_id, tag_id)` → HTTP 200 `{"tag_id": ..., "status": "applied"}`.
|
||||
2. **DELETE /v1/assets/{asset_id}/tags/{tag_id}** (remove tag from asset — admin only):
|
||||
- DB token required; admin permission.
|
||||
- `repo.get(asset_id)` → ValueError → HTTP 404.
|
||||
- `repo.remove_portfolio_tag_id(asset_id, tag_id)` → HTTP 200 `{"tag_id": ..., "status": "removed"}`.
|
||||
3. **POST /v1/portfolios/{portfolio_id}/tags** (apply tag to portfolio — owner or admin):
|
||||
- DB token required; trade-write or admin permission.
|
||||
- `repo.get(portfolio_id)` → ValueError → HTTP 404.
|
||||
- Caller must be portfolio owner or admin; else → HTTP 403.
|
||||
- `repo.get_tag_by_id(tag_id)` → None → HTTP 404.
|
||||
- `not tag.is_active` → HTTP 403.
|
||||
- Policy: `tag.is_admin_tag OR tag.created_by_user_id == portfolio.owner_user_id`; else → HTTP 403.
|
||||
- `repo.append_portfolio_tag_id(portfolio_id, tag_id)` → HTTP 200 `{"tag_id": ..., "status": "applied"}`.
|
||||
4. **DELETE /v1/portfolios/{portfolio_id}/tags/{tag_id}** (remove tag from portfolio — owner or admin):
|
||||
- DB token required; trade-write or admin permission.
|
||||
- `repo.get(portfolio_id)` → ValueError → HTTP 404.
|
||||
- Caller must be portfolio owner or admin; else → HTTP 403.
|
||||
- `repo.remove_portfolio_tag_id(portfolio_id, tag_id)` → HTTP 200 `{"tag_id": ..., "status": "removed"}`.
|
||||
|
||||
## Flow: `v1 API — DB Token Authentication`
|
||||
|
||||
1. v1 endpoint receives Bearer token from Authorization header.
|
||||
@@ -209,6 +281,7 @@
|
||||
|
||||
## Change Log
|
||||
|
||||
- 2026-05-20 KST: bat-005 — REQ-008 AC-4: added pfhub tag CLI flow; added v1 tag management flow (POST/GET/PATCH /v1/tags); added v1 tag apply/remove flow (/v1/assets/{id}/tags, /v1/portfolios/{id}/tags); updated Covered Entry Points (tags).
|
||||
- 2026-05-13 KST: bat-010 — REQ-006/007/008/009/023/005/012: updated GET /v1/assets flow (daemon-id guard, daemon-scoped filter); updated GET /v1/user_info flow (daemon_id diagnostic, NULL → 200+null); updated POST ledger/trades flow (3-step daemon assignment auth, fail-closed); added "Daemon Registry — Create and Bind Service Token" flow; added "Service-Token Rotate — daemon_id Invariant" flow; updated Alembic/ensure_schema boundary (BAT-010 migration 20260513_0016); updated Covered Entry Points (daemon subcommand).
|
||||
- 2026-05-11 KST: bat-008 — REQ-011 AC-3 confirmed: v1 API flows and runtime_id issuance flow already present from bat-001; Source Discovery/Batch header updated.
|
||||
- 2026-05-11 KST: bat-007 — Added "Local DB Initialization and Validation" flow (Scenario A/B, repeatable run criteria); added "Alembic vs ensure_schema() Schema Ownership Boundary" flow.
|
||||
|
||||
Reference in New Issue
Block a user