|
2 | 2 | FastAPI main application for XRD Analysis Tool. |
3 | 3 | Serves both the API endpoints and the static React frontend. |
4 | 4 | """ |
5 | | -from fastapi import FastAPI, UploadFile, File |
| 5 | +from fastapi import FastAPI, UploadFile, File, HTTPException |
6 | 6 | from fastapi.staticfiles import StaticFiles |
7 | | -from fastapi.responses import FileResponse, JSONResponse |
| 7 | +from fastapi.responses import FileResponse, JSONResponse, PlainTextResponse |
8 | 8 | from fastapi.middleware.cors import CORSMiddleware |
9 | 9 | from pathlib import Path |
10 | 10 | from typing import Dict, List |
| 11 | +import re |
11 | 12 | import torch |
12 | 13 | import numpy as np |
13 | 14 |
|
@@ -110,7 +111,82 @@ async def predict(data: dict): |
110 | 111 | ) |
111 | 112 |
|
112 | 113 |
|
| 114 | +# --------------------------------------------------------------------------- |
| 115 | +# Example data endpoints |
| 116 | +# --------------------------------------------------------------------------- |
| 117 | +EXAMPLE_DATA_DIR = Path(__file__).parent.parent / "example_data" |
| 118 | + |
| 119 | +# Map of crystal system number -> human-readable name |
| 120 | +CRYSTAL_SYSTEM_NAMES = { |
| 121 | + "1": "Triclinic", |
| 122 | + "2": "Monoclinic", |
| 123 | + "3": "Orthorhombic", |
| 124 | + "4": "Tetragonal", |
| 125 | + "5": "Trigonal", |
| 126 | + "6": "Hexagonal", |
| 127 | + "7": "Cubic", |
| 128 | +} |
| 129 | + |
| 130 | + |
| 131 | +def _parse_example_metadata(filepath: Path) -> dict: |
| 132 | + """Extract metadata from the header lines of a .dif file.""" |
| 133 | + meta = { |
| 134 | + "filename": filepath.name, |
| 135 | + "material_id": None, |
| 136 | + "crystal_system": None, |
| 137 | + "crystal_system_name": None, |
| 138 | + "space_group": None, |
| 139 | + "wavelength": None, |
| 140 | + } |
| 141 | + with open(filepath, "r") as f: |
| 142 | + for line in f: |
| 143 | + line = line.strip() |
| 144 | + # Stop reading once we hit the data section |
| 145 | + if line and not line.startswith("#") and not line.startswith("CELL") and not line.startswith("SPACE") and not line.lower().startswith("wavelength"): |
| 146 | + break |
| 147 | + |
| 148 | + if m := re.search(r"Material ID:\s*(\S+)", line): |
| 149 | + meta["material_id"] = m.group(1) |
| 150 | + if m := re.search(r"Crystal System:\s*(\d+)", line): |
| 151 | + num = m.group(1) |
| 152 | + meta["crystal_system"] = num |
| 153 | + meta["crystal_system_name"] = CRYSTAL_SYSTEM_NAMES.get(num, f"Unknown ({num})") |
| 154 | + if m := re.search(r"SPACE GROUP:\s*(\d+)", line): |
| 155 | + meta["space_group"] = m.group(1) |
| 156 | + if m := re.search(r"wavelength:\s*([\d.]+)", line, re.IGNORECASE): |
| 157 | + meta["wavelength"] = m.group(1) |
| 158 | + return meta |
| 159 | + |
| 160 | + |
| 161 | +@app.get("/api/examples") |
| 162 | +async def list_examples(): |
| 163 | + """List available example data files with metadata.""" |
| 164 | + if not EXAMPLE_DATA_DIR.exists(): |
| 165 | + return [] |
| 166 | + |
| 167 | + examples = [] |
| 168 | + for fp in sorted(EXAMPLE_DATA_DIR.glob("*.dif")): |
| 169 | + examples.append(_parse_example_metadata(fp)) |
| 170 | + return examples |
| 171 | + |
| 172 | + |
| 173 | +@app.get("/api/examples/{filename}") |
| 174 | +async def get_example(filename: str): |
| 175 | + """Return the raw text content of an example data file.""" |
| 176 | + # Sanitise: only allow filenames, no path traversal |
| 177 | + if "/" in filename or "\\" in filename or ".." in filename: |
| 178 | + raise HTTPException(status_code=400, detail="Invalid filename") |
| 179 | + |
| 180 | + filepath = EXAMPLE_DATA_DIR / filename |
| 181 | + if not filepath.exists() or not filepath.is_file(): |
| 182 | + raise HTTPException(status_code=404, detail="Example file not found") |
| 183 | + |
| 184 | + return PlainTextResponse(filepath.read_text()) |
| 185 | + |
| 186 | + |
| 187 | +# --------------------------------------------------------------------------- |
113 | 188 | # Static files and SPA support |
| 189 | +# --------------------------------------------------------------------------- |
114 | 190 | frontend_dist = Path(__file__).parent.parent / "frontend" / "dist" |
115 | 191 |
|
116 | 192 | if frontend_dist.exists(): |
|
0 commit comments