Api + frontend
This commit is contained in:
1
beaky-backend/src/beaky/api/__init__.py
Normal file
1
beaky-backend/src/beaky/api/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
47
beaky-backend/src/beaky/api/app.py
Normal file
47
beaky-backend/src/beaky/api/app.py
Normal 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)
|
||||
20
beaky-backend/src/beaky/api/main.py
Normal file
20
beaky-backend/src/beaky/api/main.py
Normal 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()
|
||||
42
beaky-backend/src/beaky/api/routes.py
Normal file
42
beaky-backend/src/beaky/api/routes.py
Normal 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,
|
||||
)
|
||||
45
beaky-backend/src/beaky/api/service.py
Normal file
45
beaky-backend/src/beaky/api/service.py
Normal 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
|
||||
19
beaky-backend/src/beaky/datamodels/api.py
Normal file
19
beaky-backend/src/beaky/datamodels/api.py
Normal 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
|
||||
Reference in New Issue
Block a user