F1 circuit layouts with year-by-year SVGs — manually traced track variations
您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

175 行
6.9KB

  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": [c.to_dict() for c in 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": [c.to_dict() for c in 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": [c.to_dict() for c in 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.to_dict()
  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.to_dict()
  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)