Api + frontend

This commit is contained in:
2026-03-25 20:55:52 +01:00
parent 569b8ee4f8
commit 7e88e91077
21 changed files with 2141 additions and 0 deletions

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,47 @@
import logging
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from beaky.api.routes import router
from beaky.config import Config, load_config
from beaky.link_classifier.classifier import LinkClassifier
from beaky.resolvers.resolver import TicketResolver
from beaky.screenshotter.screenshotter import Screenshotter
logger = logging.getLogger(__name__)
def create_app(config_path: str = "config/application.yml") -> FastAPI:
app = FastAPI(title="Beaky API", version="0.1.0")
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:5173"],
allow_methods=["POST"],
allow_headers=["Content-Type"],
)
app.include_router(router)
@app.on_event("startup")
def startup() -> None:
config: Config = load_config(config_path)
log_level_str = config.log_level.upper()
log_level: int = getattr(logging, log_level_str, logging.INFO)
logging.basicConfig(
level=log_level,
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
)
logger.info("Config loaded from %s (log_level=%s)", config_path, log_level_str)
app.state.config = config
app.state.screenshotter = Screenshotter(config)
app.state.link_classifier = LinkClassifier()
app.state.resolver = TicketResolver(config.resolver)
logger.info("Beaky API ready")
return app
app = create_app() # default path; overridden by main() via create_app(config_path)

View File

@@ -0,0 +1,20 @@
import argparse
import uvicorn
from beaky.api.app import create_app
from beaky.config import load_config, Config
def main() -> None:
parser = argparse.ArgumentParser(prog="beaky-api")
parser.add_argument("--config", default="config/application.yml", help="Path to config file.")
args = parser.parse_args()
config: Config = load_config(args.config)
app = create_app(config_path=args.config)
uvicorn.run(app, host=config.api.host, port=config.api.port)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,42 @@
import logging
from fastapi import APIRouter, HTTPException, Request
from beaky.datamodels.api import ResolveRequest, ResolveResponse
from beaky.api.service import run_pipeline
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/v1")
@router.post("/resolve", response_model=ResolveResponse)
def resolve(body: ResolveRequest, request: Request) -> ResolveResponse:
logger.info("POST /api/v1/resolve url=%s debug=%s", body.url, body.debug)
config = request.app.state.config
screenshotter = request.app.state.screenshotter
link_classifier = request.app.state.link_classifier
resolver = request.app.state.resolver
try:
resolved, link_ticket, img_ticket = run_pipeline(
url=body.url,
config=config,
screenshotter=screenshotter,
link_classifier=link_classifier,
resolver=resolver,
)
except Exception as exc:
logger.exception("Pipeline failed for url=%s", body.url)
raise HTTPException(status_code=500, detail=str(exc)) from exc
if not body.debug:
for rb in resolved.bets:
rb.match_info = None
return ResolveResponse(
resolved_ticket=resolved,
verdict=resolved.verdict.value,
link_ticket=link_ticket if body.debug else None,
img_ticket=img_ticket if body.debug else None,
)

View File

@@ -0,0 +1,45 @@
import hashlib
import logging
from pathlib import Path
from beaky.config import Config
from beaky.datamodels.ticket import Ticket
from beaky.image_classifier.classifier import img_classify
from beaky.link_classifier.classifier import LinkClassifier
from beaky.resolvers.resolver import ResolvedTicket, TicketResolver
from beaky.scanner.scanner import Link
from beaky.screenshotter.screenshotter import Screenshotter
logger = logging.getLogger(__name__)
def run_pipeline(
url: str,
config: Config,
screenshotter: Screenshotter,
link_classifier: LinkClassifier,
resolver: TicketResolver,
) -> tuple[ResolvedTicket, Ticket, Ticket]:
ticket_id = int(hashlib.md5(url.encode()).hexdigest(), 16) % (10**9)
link = Link(id=ticket_id, url=url)
logger.info("Pipeline started for ticket_id=%d url=%s", ticket_id, url)
logger.info("Screenshotting ticket_id=%d", ticket_id)
screenshotter.capture_tickets([link])
logger.info("Screenshot done for ticket_id=%d", ticket_id)
logger.info("Link classifying ticket_id=%d", ticket_id)
link_ticket = link_classifier.classify(link)
#link_ticket = img_classify([f"./data/screenshots/{ticket_id}.png"], ticket_id=ticket_id)
logger.info("Link classification done: %d bets for ticket_id=%d", len(link_ticket.bets), ticket_id)
screenshot_path = Path(config.screenshotter.target_path) / f"{ticket_id}.png"
logger.info("Image classifying ticket_id=%d from %s", ticket_id, screenshot_path)
img_ticket = img_classify([str(screenshot_path)], ticket_id=ticket_id)
logger.info("Image classification done: %d bets for ticket_id=%d", len(img_ticket.bets), ticket_id)
logger.info("Resolving ticket_id=%d", ticket_id)
resolved = resolver.resolve(link_ticket)
logger.info("Resolve done for ticket_id=%d verdict=%s", ticket_id, resolved.verdict.value)
return resolved, link_ticket, img_ticket

View File

@@ -0,0 +1,19 @@
from pydantic import BaseModel
from beaky.datamodels.ticket import Ticket
from beaky.resolvers.resolver import ResolvedTicket
class ResolveRequest(BaseModel):
url: str
debug: bool = False
class ResolveResponse(BaseModel):
model_config = {"arbitrary_types_allowed": True}
resolved_ticket: ResolvedTicket
verdict: str
# populated only when debug=True
link_ticket: Ticket | None = None
img_ticket: Ticket | None = None