Download return_api.py from adoomy/StocKing: direct link, hf CLI and curl.
- Browser
- Download file 9.64 kB
-
https://huggingface.co/spaces/adoomy/StocKing/resolve/main/return_api.py
- Command line
-
hf download hf://spaces/adoomy/StocKing/return_api.py
-
curl -L -o return_api.py https://huggingface.co/spaces/adoomy/StocKing/resolve/main/return_api.py
9.64 kB
| """Admin review endpoints. Collection runs only from collect.py.""" | |
| from datetime import date, datetime | |
| from typing import Literal | |
| from urllib.parse import urlparse | |
| import re | |
| from fastapi import APIRouter, Depends, HTTPException | |
| from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator | |
| from auth import require_admin | |
| from models import Company, ReturnSource, ReturnDocument, ReturnEvent, ReturnPolicy, ReturnAudit | |
| from shareholder_returns import audit, document_data, load_documents, serialize | |
| from return_lifecycle import automatic_status | |
| class StrictModel(BaseModel): | |
| model_config = ConfigDict(extra="forbid", allow_inf_nan=False) | |
| class EventInput(StrictModel): | |
| year: int = Field(ge=2000, le=2100) | |
| amount: float | None = Field(default=None, ge=0) | |
| scope: Literal["common", "company", "preferred", "unknown"] = "unknown" | |
| cancellation_date: date | None = None | |
| outside_policy: bool = False | |
| evidence: str = Field(default="", max_length=10000) | |
| def date_matches_year(self): | |
| if self.cancellation_date and self.cancellation_date.year != self.year: | |
| raise ValueError("μκ°μμ μΌκ³Ό μ μ© μ°λκ° λ€λ¦ λλ€.") | |
| return self | |
| class PolicyInput(StrictModel): | |
| year: int = Field(ge=2000, le=2100) | |
| kind: Literal["cancellation", "total_amount", "total_ratio", "none"] | |
| scope: Literal["common", "company", "preferred", "unknown"] = "unknown" | |
| amount: float | None = Field(default=None, ge=0) | |
| ratio: float | None = Field(default=None, ge=0, le=1) | |
| cancellation_commitment: bool = False | |
| income_basis: str = Field(default="unknown", max_length=200) | |
| income_amount: float | None = None | |
| dividend_amount: float | None = Field(default=None, ge=0) | |
| evidence: str = Field(default="", max_length=10000) | |
| def validate_url(value): | |
| url = urlparse(value) | |
| if url.scheme not in ("http", "https") or not url.hostname or url.username or url.password: | |
| raise ValueError("곡μ μλ£μ http(s) URLμ μ λ ₯νμΈμ.") | |
| return value | |
| class ReviewInput(StrictModel): | |
| ticker: str = Field(pattern=r"^[0-9A-Z]{6}$") | |
| source_key: str = Field(min_length=1, max_length=500) | |
| source_url: str = Field(max_length=2000) | |
| title: str = Field(min_length=1, max_length=500) | |
| published_at: date | None = None | |
| evidence: str = Field(default="", max_length=30000) | |
| status: Literal["active", "reference", "pending", "approved", "excluded"] = "active" | |
| supersedes_id: int | None = None | |
| is_withdrawal: bool = False | |
| events: list[EventInput] = Field(default_factory=list, max_length=100) | |
| policies: list[PolicyInput] = Field(default_factory=list, max_length=100) | |
| _url = field_validator("source_url")(validate_url) | |
| def approval_requires_evidence(self): | |
| if self.status in {"active", "approved"}: | |
| if not self.evidence.strip(): | |
| raise ValueError("μΉμΈνλ €λ©΄ κ·Όκ±° λ¬Έκ΅¬κ° νμν©λλ€.") | |
| if self.is_withdrawal and not self.supersedes_id: | |
| raise ValueError("μ² ν λμ μλ£λ₯Ό μ§μ νμΈμ.") | |
| if not self.is_withdrawal and not (self.events or self.policies): | |
| raise ValueError("μΉμΈν μκ° μ¬κ±΄ λλ μ μ± μ΄ νμν©λλ€.") | |
| for e in self.events: | |
| if e.amount is None or not e.evidence.strip(): | |
| raise ValueError("μκ° μ¬κ±΄μλ κΈμ‘κ³Ό κ·Όκ±°κ° νμν©λλ€.") | |
| for p in self.policies: | |
| if not p.evidence.strip(): | |
| raise ValueError("μ μ± μ κ·Όκ±°κ° νμν©λλ€.") | |
| if p.kind in ("cancellation", "total_amount") and p.amount is None: | |
| raise ValueError("μ μ± μ κΈμ‘μ΄ νμν©λλ€.") | |
| if p.kind == "total_ratio" and p.ratio is None: | |
| raise ValueError("νμμ¨μ΄ νμν©λλ€. 50%λ 0.5λ‘ μ λ ₯νμΈμ.") | |
| if p.kind in ("total_amount", "total_ratio") and not p.cancellation_commitment: | |
| raise ValueError("μμ¬μ‘μ μκ°νλ €λ νμ¬ λ°©μΉ¨μ΄ νμν©λλ€.") | |
| return self | |
| class SourceInput(StrictModel): | |
| ticker: str = Field(pattern=r"^[0-9A-Z]{6}$") | |
| url: str = Field(max_length=2000) | |
| is_index: bool = False | |
| enabled: bool = True | |
| _url = field_validator("url")(validate_url) | |
| class StatusInput(StrictModel): | |
| action: Literal["cancel", "restore"] | |
| reason: str = Field(default="", max_length=2000) | |
| def write_review(db, payload, doc=None, actor_id=None): | |
| if not db.get(Company, payload.ticker): | |
| raise HTTPException(404, "μ’ λͺ©μ μ°Ύμ μ μμ΅λλ€.") | |
| duplicate = db.query(ReturnDocument).filter_by(source_key=payload.source_key).first() | |
| if duplicate and (doc is None or duplicate.id != doc.id): | |
| raise HTTPException(409, "μ΄λ―Έ λ±λ‘λ μΆμ² μλ³μμ λλ€.") | |
| if doc and (doc.ticker != payload.ticker or doc.source_key != payload.source_key): | |
| raise HTTPException(422, "μ’ λͺ©κ³Ό μΆμ² μλ³μλ λ³κ²½ν μ μμ΅λλ€.") | |
| if payload.status == "approved" and re.search(r"μ μ |μ² ν|μ·¨μ", payload.title) and not payload.supersedes_id: | |
| raise HTTPException(422, "μ μ Β·μ² ν μλ£λ λ체νλ μ΄μ μλ£λ₯Ό μ§μ νμΈμ.") | |
| if payload.supersedes_id: | |
| parent = db.get(ReturnDocument, payload.supersedes_id) | |
| visited = {doc.id} if doc else set() | |
| if not parent or parent.ticker != payload.ticker: | |
| raise HTTPException(422, "κ°μ μ’ λͺ©μ κΈ°μ‘΄ μλ£λ₯Ό μ§μ νμΈμ.") | |
| while parent: | |
| if parent.id in visited: | |
| raise HTTPException(422, "μ μ κ΄κ³κ° μνν©λλ€.") | |
| visited.add(parent.id) | |
| parent = db.get(ReturnDocument, parent.supersedes_id) if parent.supersedes_id else None | |
| if payload.status == "approved": | |
| sibling = db.query(ReturnDocument).filter_by(supersedes_id=payload.supersedes_id, status="approved").first() | |
| if sibling and (not doc or sibling.id != doc.id): | |
| raise HTTPException(409, "μ΄λ―Έ μΉμΈλ μ μ μλ£λ₯Ό λμμΌλ‘ μ§μ νμΈμ.") | |
| before = document_data(db, doc) if doc else None | |
| if doc is None: | |
| doc = ReturnDocument() | |
| db.add(doc) | |
| values = payload.model_dump(mode="json", exclude={"events", "policies"}) | |
| for key, value in values.items(): | |
| setattr(doc, key, value) | |
| doc.updated_at = datetime.utcnow() | |
| doc.error = None | |
| db.flush() | |
| for model in (ReturnEvent, ReturnPolicy): | |
| db.query(model).filter_by(document_id=doc.id).delete(synchronize_session=False) | |
| for key, model in (("events", ReturnEvent), ("policies", ReturnPolicy)): | |
| for item in getattr(payload, key): | |
| db.add(model(document_id=doc.id, **item.model_dump(mode="json"))) | |
| audit(db, doc, "review", before, actor_id) | |
| db.commit() | |
| return document_data(db, doc) | |
| def create_return_router(get_db, invalidate): | |
| router = APIRouter(prefix="/api/admin/returns", dependencies=[Depends(require_admin)]) | |
| def list_documents(db=Depends(get_db)): | |
| return load_documents(db) | |
| def add_document(payload: ReviewInput, db=Depends(get_db), user=Depends(require_admin)): | |
| result = write_review(db, payload, actor_id=user.id) | |
| invalidate() | |
| return result | |
| def edit_document(document_id: int, payload: ReviewInput, db=Depends(get_db), user=Depends(require_admin)): | |
| doc = db.get(ReturnDocument, document_id) | |
| if not doc: | |
| raise HTTPException(404, "μλ£λ₯Ό μ°Ύμ μ μμ΅λλ€.") | |
| result = write_review(db, payload, doc, user.id) | |
| invalidate() | |
| return result | |
| def history(document_id: int, db=Depends(get_db)): | |
| return [serialize(a) for a in db.query(ReturnAudit).filter_by(document_id=document_id).order_by(ReturnAudit.id)] | |
| def change_status(document_id: int, payload: StatusInput, db=Depends(get_db), user=Depends(require_admin)): | |
| doc = db.get(ReturnDocument, document_id) | |
| if not doc: | |
| raise HTTPException(404, "μλ£λ₯Ό μ°Ύμ μ μμ΅λλ€.") | |
| before = document_data(db, doc) | |
| desired = "excluded" if payload.action == "cancel" else automatic_status(before) | |
| if doc.status != desired: | |
| doc.status, doc.updated_at = desired, datetime.utcnow() | |
| audit(db, doc, payload.action, before, user.id, payload.reason) | |
| db.commit() | |
| invalidate() | |
| return document_data(db, doc) | |
| def sources(db=Depends(get_db)): | |
| return [serialize(s) for s in db.query(ReturnSource).order_by(ReturnSource.id)] | |
| def add_source(payload: SourceInput, db=Depends(get_db)): | |
| if not db.get(Company, payload.ticker): | |
| raise HTTPException(404, "μ’ λͺ©μ μ°Ύμ μ μμ΅λλ€.") | |
| source = db.query(ReturnSource).filter_by(ticker=payload.ticker, url=payload.url).first() | |
| if source is None: | |
| source = ReturnSource(ticker=payload.ticker, url=payload.url) | |
| db.add(source) | |
| source.is_index, source.enabled = payload.is_index, payload.enabled | |
| db.commit() | |
| return serialize(source) | |
| return router | |