F1 circuit layouts with year-by-year SVGs — manually traced track variations
Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

175 řádky
6.8KB

  1. from fastapi import FastAPI, HTTPException, Response
  2. from fastapi.responses import FileResponse
  3. from fastapi.middleware.cors import CORSMiddleware
  4. from pathlib import Path
  5. import logging
  6. # Import our CircuitService
  7. from circuits_service import CircuitService, Format
  8. from models.track_layout import TrackLayout
  9. # Initialize FastAPI app
  10. app = FastAPI(
  11. title="F1 Track Layouts API",
  12. description="API serving F1 track layout images, vectors and GeoJSON files",
  13. version="1.0.0"
  14. )
  15. # Add CORS middleware
  16. app.add_middleware(
  17. CORSMiddleware,
  18. allow_origins=["*"],
  19. allow_credentials=True,
  20. allow_methods=["*"],
  21. allow_headers=["*"],
  22. )
  23. # Configure logging
  24. logging.basicConfig(level=logging.INFO)
  25. logger = logging.getLogger(__name__)
  26. # Initialize CircuitService
  27. circuit_service = CircuitService()
  28. def get_content_disposition(format: Format) -> str:
  29. if format == Format.PNG:
  30. return "inline"
  31. # SVG and GeoJSON files as attachments for security/convention
  32. return "attachment"
  33. def get_media_type(format: Format) -> str:
  34. media_types = {
  35. Format.PNG: "image/png",
  36. Format.SVG: "image/svg+xml",
  37. Format.GEOJSON: "application/geo+json"
  38. }
  39. return media_types.get(format, "application/octet-stream") # fallback for unknown formats
  40. @app.get("/")
  41. async def root():
  42. """Root endpoint returning API information"""
  43. return {
  44. "name": "F1 Track Layouts API",
  45. "version": "1.0.0",
  46. "endpoints": [
  47. "/circuits",
  48. "/circuits/{country_slug}",
  49. "/circuits/{country_slug}/{city_slug}",
  50. "/circuits/{country_slug}/{city_slug}/{circuit_slug}",
  51. "/circuits/{country_slug}/{city_slug}/{circuit_slug}/layout/{layout_slug}",
  52. "/tracks/{country_slug}/{city_slug}/{circuit_slug}/year/{year}/{format}",
  53. "/tracks/{country_slug}/{city_slug}/{circuit_slug}/layout/{layout_slug}/{format}",
  54. "/grand-prix/{grand_prix_name}/year/{year}"
  55. ]
  56. }
  57. @app.get("/circuits")
  58. async def get_countries():
  59. """Get list of all available countries with their slugs"""
  60. return {
  61. "countries": circuit_service.get_countries_list()
  62. }
  63. @app.get("/circuits/{country_slug}")
  64. async def get_cities(country_slug: str):
  65. """Get list of cities for a specific country"""
  66. cities = circuit_service.get_localities_list(country_slug)
  67. if not cities:
  68. raise HTTPException(status_code=404, detail="Country not found")
  69. return {"cities": cities}
  70. @app.get("/circuits/{country_slug}/{city_slug}")
  71. async def get_circuits(country_slug: str, city_slug: str):
  72. """Get list of circuits for a specific city"""
  73. circuits = circuit_service.get_circuits_list(country_slug, city_slug)
  74. if not circuits:
  75. raise HTTPException(status_code=404, detail="City not found")
  76. return {"circuits": circuits}
  77. @app.get("/circuits/{country_slug}/{city_slug}/{circuit_slug}")
  78. async def get_circuit_details(country_slug: str, city_slug: str, circuit_slug: str):
  79. """Get circuit details and available formats"""
  80. circuit_details = circuit_service.get_circuit_details(country_slug, city_slug, circuit_slug)
  81. if not circuit_details:
  82. raise HTTPException(status_code=404, detail="Circuit not found")
  83. return circuit_details
  84. @app.get("/circuits/{country_slug}/{city_slug}/{circuit_slug}/layout/{layout_slug}")
  85. async def get_layout_details(country_slug: str, city_slug: str, circuit_slug: str, layout_slug: str):
  86. """Get details for a specific layout"""
  87. layout_details = circuit_service.get_layout_details(country_slug, city_slug, circuit_slug, layout_slug)
  88. if not layout_details:
  89. raise HTTPException(status_code=404, detail="Layout not found")
  90. return layout_details
  91. @app.get("/tracks/{country_slug}/{city_slug}/{circuit_slug}/year/{year}/{image_format}")
  92. async def get_track_by_year(
  93. country_slug: str,
  94. city_slug: str,
  95. circuit_slug: str,
  96. year: int,
  97. image_format: Format,
  98. response: Response
  99. ):
  100. """Get track layout for a specific year"""
  101. circuit_details = circuit_service.get_circuit_details(country_slug, city_slug, circuit_slug)
  102. if not circuit_details:
  103. raise HTTPException(status_code=404, detail="Circuit not found")
  104. try:
  105. layout_slug = circuit_service.find_layout_slug_for_year(circuit_details, year)
  106. return get_file_response(circuit_service.get_layout_details(country_slug, city_slug, circuit_slug, layout_slug), image_format, response)
  107. except KeyError as e:
  108. logger.error(f"KeyError in get_track_by_year: {str(e)}")
  109. raise HTTPException(status_code=404, detail="Circuit or layout not found")
  110. @app.get("/circuits/{country_slug}/{city_slug}/{circuit_slug}/layout/{layout_slug}/{image_format}")
  111. async def get_track_by_layout(
  112. country_slug: str,
  113. city_slug: str,
  114. circuit_slug: str,
  115. layout_slug: str,
  116. image_format: Format,
  117. response: Response
  118. ):
  119. """Get specific track layout"""
  120. return get_file_response(circuit_service.get_layout_details(country_slug, city_slug, circuit_slug, layout_slug), image_format, response)
  121. @app.get("/grand-prix/{grand_prix_name}/{season}/{image_format}")
  122. async def get_grand_prix_circuit(grand_prix_name: str, season: int, image_format: Format, response: Response):
  123. layout = circuit_service.get_circuit_layout_by_ergast_data(grand_prix_name, season)
  124. if not layout:
  125. raise HTTPException(status_code=404, detail="Circuit not found")
  126. try:
  127. return get_file_response(layout, image_format, response)
  128. except KeyError as e:
  129. logger.error(f"KeyError in get_track_by_year: {str(e)}")
  130. raise HTTPException(status_code=404, detail="Circuit or layout not found")
  131. def get_file_response(layout: TrackLayout, image_format: Format, response: Response) -> FileResponse:
  132. # layout = circuit_service.get_layout_details(country_slug, city_slug, circuit_slug, layout_slug)
  133. file_path = Path(f"./circuits").joinpath(layout.relative_svg_filepath)
  134. if not file_path or not file_path.exists():
  135. logger.error(f"File not found: {file_path}")
  136. logger.info(f"Looking for {layout.slug} in format {image_format}")
  137. raise HTTPException(status_code=404, detail=f"File not found in {image_format} format")
  138. # Get actual filename from the path for the Content-Disposition header
  139. filename = file_path.name
  140. logger.info(f"Serving file: {file_path}")
  141. return FileResponse(
  142. file_path,
  143. media_type=get_media_type(image_format),
  144. content_disposition_type=get_content_disposition(image_format),
  145. filename=filename,
  146. headers={
  147. "Cache-Control": "public, max-age=31536000",
  148. "ETag": f"{layout.circuit.locality.country.slug}/{layout.circuit.locality.slug}/{layout.circuit.slug}-{layout.slug}-{image_format}"
  149. }
  150. )
  151. if __name__ == "__main__":
  152. import uvicorn
  153. uvicorn.run(app, host="0.0.0.0", port=8000)