|
- from fastapi import FastAPI, HTTPException, Response
- from fastapi.responses import FileResponse
- from fastapi.middleware.cors import CORSMiddleware
- from pathlib import Path
- import logging
-
- # Import our CircuitService
- from circuits_service import CircuitService, Format
- from models.track_layout import TrackLayout
-
- # Initialize FastAPI app
- app = FastAPI(
- title="F1 Track Layouts API",
- description="API serving F1 track layout images, vectors and GeoJSON files",
- version="1.0.0"
- )
-
- # Add CORS middleware
- app.add_middleware(
- CORSMiddleware,
- allow_origins=["*"],
- allow_credentials=True,
- allow_methods=["*"],
- allow_headers=["*"],
- )
-
- # Configure logging
- logging.basicConfig(level=logging.INFO)
- logger = logging.getLogger(__name__)
-
- # Initialize CircuitService
- circuit_service = CircuitService()
-
- def get_content_disposition(format: Format) -> str:
- if format == Format.PNG:
- return "inline"
- # SVG and GeoJSON files as attachments for security/convention
- return "attachment"
-
- def get_media_type(format: Format) -> str:
- media_types = {
- Format.PNG: "image/png",
- Format.SVG: "image/svg+xml",
- Format.GEOJSON: "application/geo+json"
- }
- return media_types.get(format, "application/octet-stream") # fallback for unknown formats
-
- @app.get("/")
- async def root():
- """Root endpoint returning API information"""
- return {
- "name": "F1 Track Layouts API",
- "version": "1.0.0",
- "endpoints": [
- "/circuits",
- "/circuits/{country_slug}",
- "/circuits/{country_slug}/{city_slug}",
- "/circuits/{country_slug}/{city_slug}/{circuit_slug}",
- "/circuits/{country_slug}/{city_slug}/{circuit_slug}/layout/{layout_slug}",
- "/tracks/{country_slug}/{city_slug}/{circuit_slug}/year/{year}/{format}",
- "/tracks/{country_slug}/{city_slug}/{circuit_slug}/layout/{layout_slug}/{format}",
- "/grand-prix/{grand_prix_name}/year/{year}"
- ]
- }
-
- @app.get("/circuits")
- async def get_countries():
- """Get list of all available countries with their slugs"""
- return {
- "countries": circuit_service.get_countries_list()
- }
-
- @app.get("/circuits/{country_slug}")
- async def get_cities(country_slug: str):
- """Get list of cities for a specific country"""
- cities = circuit_service.get_localities_list(country_slug)
- if not cities:
- raise HTTPException(status_code=404, detail="Country not found")
- return {"cities": cities}
-
- @app.get("/circuits/{country_slug}/{city_slug}")
- async def get_circuits(country_slug: str, city_slug: str):
- """Get list of circuits for a specific city"""
- circuits = circuit_service.get_circuits_list(country_slug, city_slug)
- if not circuits:
- raise HTTPException(status_code=404, detail="City not found")
- return {"circuits": circuits}
-
- @app.get("/circuits/{country_slug}/{city_slug}/{circuit_slug}")
- async def get_circuit_details(country_slug: str, city_slug: str, circuit_slug: str):
- """Get circuit details and available formats"""
- circuit_details = circuit_service.get_circuit_details(country_slug, city_slug, circuit_slug)
- if not circuit_details:
- raise HTTPException(status_code=404, detail="Circuit not found")
- return circuit_details
-
- @app.get("/circuits/{country_slug}/{city_slug}/{circuit_slug}/layout/{layout_slug}")
- async def get_layout_details(country_slug: str, city_slug: str, circuit_slug: str, layout_slug: str):
- """Get details for a specific layout"""
- layout_details = circuit_service.get_layout_details(country_slug, city_slug, circuit_slug, layout_slug)
- if not layout_details:
- raise HTTPException(status_code=404, detail="Layout not found")
- return layout_details
-
- @app.get("/tracks/{country_slug}/{city_slug}/{circuit_slug}/year/{year}/{image_format}")
- async def get_track_by_year(
- country_slug: str,
- city_slug: str,
- circuit_slug: str,
- year: int,
- image_format: Format,
- response: Response
- ):
- """Get track layout for a specific year"""
- circuit_details = circuit_service.get_circuit_details(country_slug, city_slug, circuit_slug)
- if not circuit_details:
- raise HTTPException(status_code=404, detail="Circuit not found")
-
- try:
- layout_slug = circuit_service.find_layout_slug_for_year(circuit_details, year)
- return get_file_response(circuit_service.get_layout_details(country_slug, city_slug, circuit_slug, layout_slug), image_format, response)
- except KeyError as e:
- logger.error(f"KeyError in get_track_by_year: {str(e)}")
- raise HTTPException(status_code=404, detail="Circuit or layout not found")
-
- @app.get("/circuits/{country_slug}/{city_slug}/{circuit_slug}/layout/{layout_slug}/{image_format}")
- async def get_track_by_layout(
- country_slug: str,
- city_slug: str,
- circuit_slug: str,
- layout_slug: str,
- image_format: Format,
- response: Response
- ):
- """Get specific track layout"""
- return get_file_response(circuit_service.get_layout_details(country_slug, city_slug, circuit_slug, layout_slug), image_format, response)
-
- @app.get("/grand-prix/{grand_prix_name}/{season}/{image_format}")
- async def get_grand_prix_circuit(grand_prix_name: str, season: int, image_format: Format, response: Response):
- layout = circuit_service.get_circuit_layout_by_ergast_data(grand_prix_name, season)
-
- if not layout:
- raise HTTPException(status_code=404, detail="Circuit not found")
-
- try:
- return get_file_response(layout, image_format, response)
- except KeyError as e:
- logger.error(f"KeyError in get_track_by_year: {str(e)}")
- raise HTTPException(status_code=404, detail="Circuit or layout not found")
-
- def get_file_response(layout: TrackLayout, image_format: Format, response: Response) -> FileResponse:
- # layout = circuit_service.get_layout_details(country_slug, city_slug, circuit_slug, layout_slug)
- file_path = Path(f"./circuits").joinpath(layout.relative_svg_filepath)
- if not file_path or not file_path.exists():
- logger.error(f"File not found: {file_path}")
- logger.info(f"Looking for {layout.slug} in format {image_format}")
- raise HTTPException(status_code=404, detail=f"File not found in {image_format} format")
-
- # Get actual filename from the path for the Content-Disposition header
- filename = file_path.name
- logger.info(f"Serving file: {file_path}")
- return FileResponse(
- file_path,
- media_type=get_media_type(image_format),
- content_disposition_type=get_content_disposition(image_format),
- filename=filename,
- headers={
- "Cache-Control": "public, max-age=31536000",
- "ETag": f"{layout.circuit.locality.country.slug}/{layout.circuit.locality.slug}/{layout.circuit.slug}-{layout.slug}-{image_format}"
- }
- )
-
- if __name__ == "__main__":
- import uvicorn
- uvicorn.run(app, host="0.0.0.0", port=8000)
|