Spaces:
Running
Running
Download backend/main.py from Tataaicoltd/api: direct link, hf CLI and curl.
- Browser
- Download file 27 kB
-
https://huggingface.co/spaces/Tataaicoltd/api/resolve/main/backend/main.py
- Command line
-
hf download hf://spaces/Tataaicoltd/api/backend/main.py
-
curl -L -o main.py https://huggingface.co/spaces/Tataaicoltd/api/resolve/main/backend/main.py
27 kB
| import sys | |
| import os | |
| # Add backend directory to path to import modules | |
| sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) | |
| from fastapi import FastAPI, HTTPException, Query, Body | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from fastapi.staticfiles import StaticFiles | |
| from starlette.middleware.base import BaseHTTPMiddleware | |
| from starlette.responses import JSONResponse, Response | |
| from typing import Optional, List | |
| import logging | |
| from config import supabase, settings | |
| from models import ( | |
| Category, CategoryCreate, CategoryUpdate, | |
| Intent, IntentCreate, IntentUpdate, IntentWithCategory, | |
| City, CityCreate, CityUpdate, | |
| District, DistrictCreate, DistrictUpdate, | |
| Street, StreetCreate, StreetUpdate, | |
| Position, PositionCreate, PositionUpdate, | |
| Group, GroupCreate, GroupUpdate, | |
| ) | |
| from services.category_service import CategoryService | |
| from services.intent_service import IntentService | |
| from services.maps_service import MapsService | |
| # Setup logging | |
| logging.basicConfig(level=logging.INFO) | |
| logger = logging.getLogger(__name__) | |
| # Create FastAPI app | |
| app = FastAPI( | |
| title="Admin Dashboard API", | |
| description="CRUD API for managing categories and intents", | |
| version="1.0.0" | |
| ) | |
| # API Key authentication middleware | |
| class APIKeyMiddleware(BaseHTTPMiddleware): | |
| async def dispatch(self, request, call_next): | |
| path = request.url.path | |
| if path.startswith('/api/') and path != '/api/auth/verify': | |
| api_key = request.headers.get('X-API-Key', '') | |
| if api_key != settings.admin_api_key: | |
| return JSONResponse({"detail": "Unauthorized"}, status_code=401) | |
| return await call_next(request) | |
| app.add_middleware(APIKeyMiddleware) | |
| # Add CORS middleware | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], | |
| allow_credentials=True, | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| # Auth verify endpoint (no key required) | |
| async def verify_api_key(body: dict = Body(...)): | |
| key = body.get("key", "") | |
| return {"valid": key == settings.admin_api_key} | |
| # Map images listing | |
| async def get_map_images(): | |
| images_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'admin', 'images') | |
| try: | |
| files = [f for f in os.listdir(images_dir) if f.lower().endswith(('.jpg', '.jpeg', '.png', '.webp'))] | |
| files.sort(key=lambda x: int(x.rsplit('.', 1)[0].split('_')[-1]) if x.rsplit('.', 1)[0].split('_')[-1].isdigit() else 0) | |
| return {"images": [f"images/{f}" for f in files]} | |
| except FileNotFoundError: | |
| raise HTTPException(status_code=404, detail="Images directory not found") | |
| # Health check | |
| async def health_check(): | |
| """Health check endpoint""" | |
| try: | |
| # Test Supabase connection by fetching one record | |
| result = supabase.table("Categories").select("id").limit(1).execute() | |
| return {"status": "ok", "database": "connected"} | |
| except Exception as e: | |
| logger.error(f"Health check failed: {str(e)}") | |
| raise HTTPException(status_code=500, detail="Database connection failed") | |
| # Flow tickets (fake data) | |
| FAKE_TICKETS = [ | |
| {"id": 1, "title": "ticket 1", "detail": "", "status": "open", "message": ""}, | |
| {"id": 2, "title": "ticket 2", "detail": "", "status": "open", "message": ""}, | |
| ] | |
| async def list_flow_tickets(): | |
| return [{"id": t["id"], "title": t["title"], "status": t["status"]} for t in FAKE_TICKETS] | |
| async def get_flow_ticket(ticket_id: int): | |
| for t in FAKE_TICKETS: | |
| if t["id"] == ticket_id: | |
| return t | |
| raise HTTPException(status_code=404, detail="Ticket not found") | |
| async def update_flow_ticket(body: dict = Body(...)): | |
| ticket_id = body.get("id") | |
| status = body.get("status") | |
| message = body.get("message") | |
| detail = body.get("detail") | |
| for t in FAKE_TICKETS: | |
| if t["id"] == ticket_id: | |
| t["status"] = status | |
| t["message"] = message | |
| t["detail"] = detail | |
| return t | |
| raise HTTPException(status_code=404, detail="Ticket not found") | |
| async def list_pois(city_id: Optional[int] = Query(None), district_id: Optional[int] = Query(None)): | |
| try: | |
| query = supabase.table("Pois").select("id, name, address, type, lat, long, city_id, district_id") | |
| if district_id is not None: | |
| query = query.eq("district_id", district_id) | |
| elif city_id is not None: | |
| query = query.eq("city_id", city_id) | |
| result = query.order("name").execute() | |
| return result.data or [] | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def update_poi(poi_id: int, body: dict = Body(...)): | |
| try: | |
| return await MapsService.update_poi(poi_id, body) | |
| except Exception as e: | |
| logger.error(f"Error updating POI {poi_id}: {str(e)}") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def update_pois_bulk(payload: List[dict] = Body(...)): | |
| """Bulk-update POIs. | |
| Payload: [{poi_id, lat?, long?, name?, address?, type?}, ...] | |
| """ | |
| try: | |
| updated = await MapsService.update_pois_bulk(payload) | |
| return {"updated": updated} | |
| except Exception as e: | |
| logger.error(f"Error updating POIs bulk: {str(e)}") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| # ==================== CATEGORIES ENDPOINTS ==================== | |
| async def list_categories(search: Optional[str] = Query(None)): | |
| """Get all categories with optional search""" | |
| try: | |
| categories = await CategoryService.get_all_categories(search) | |
| return categories | |
| except Exception as e: | |
| logger.error(f"Error fetching categories: {str(e)}") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def get_category(category_id: int): | |
| """Get a single category by id""" | |
| try: | |
| category = await CategoryService.get_category_by_id(category_id) | |
| if not category: | |
| raise HTTPException(status_code=404, detail="Category not found") | |
| return category | |
| except HTTPException: | |
| raise | |
| except Exception as e: | |
| logger.error(f"Error fetching category: {str(e)}") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def create_category(category: CategoryCreate): | |
| """Create a new category""" | |
| try: | |
| new_category = await CategoryService.create_category(category) | |
| return new_category | |
| except Exception as e: | |
| logger.error(f"Error creating category: {str(e)}") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def update_category(category_id: int, category: CategoryUpdate): | |
| """Update an existing category""" | |
| try: | |
| updated = await CategoryService.update_category(category_id, category) | |
| return updated | |
| except Exception as e: | |
| logger.error(f"Error updating category: {str(e)}") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def delete_category(category_id: int): | |
| """Delete a category""" | |
| try: | |
| await CategoryService.delete_category(category_id) | |
| return {"message": "Category deleted successfully"} | |
| except Exception as e: | |
| logger.error(f"Error deleting category: {str(e)}") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| # ==================== INTENTS ENDPOINTS ==================== | |
| async def list_intents( | |
| search: Optional[str] = Query(None), | |
| category_id: Optional[int] = Query(None), | |
| priority: Optional[str] = Query(None) | |
| ): | |
| """Get all intents with optional filters""" | |
| try: | |
| intents = await IntentService.get_all_intents(search, category_id, priority) | |
| return intents | |
| except Exception as e: | |
| logger.error(f"Error fetching intents: {str(e)}") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def get_intent(intent_id: int): | |
| """Get a single intent by ID""" | |
| try: | |
| intent = await IntentService.get_intent_by_id(intent_id) | |
| if not intent: | |
| raise HTTPException(status_code=404, detail="Intent not found") | |
| return intent | |
| except HTTPException: | |
| raise | |
| except Exception as e: | |
| logger.error(f"Error fetching intent: {str(e)}") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def get_intents_by_category(category_id: int): | |
| """Get all intents for a specific category""" | |
| try: | |
| intents = await IntentService.get_intents_by_category(category_id) | |
| return intents | |
| except Exception as e: | |
| logger.error(f"Error fetching intents for category: {str(e)}") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def create_intent(intent: IntentCreate): | |
| """Create a new intent""" | |
| try: | |
| new_intent = await IntentService.create_intent(intent) | |
| return new_intent | |
| except Exception as e: | |
| logger.error(f"Error creating intent: {str(e)}") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def update_intent(intent_id: int, intent: IntentUpdate): | |
| """Update an existing intent""" | |
| try: | |
| updated = await IntentService.update_intent(intent_id, intent) | |
| return updated | |
| except Exception as e: | |
| logger.error(f"Error updating intent: {str(e)}") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def delete_intent(intent_id: int): | |
| """Delete an intent""" | |
| try: | |
| await IntentService.delete_intent(intent_id) | |
| return {"message": "Intent deleted successfully"} | |
| except Exception as e: | |
| logger.error(f"Error deleting intent: {str(e)}") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| # ==================== MAPS ENDPOINTS ==================== | |
| async def list_cities(): | |
| """Get all cities""" | |
| try: | |
| return await MapsService.get_all_cities() | |
| except Exception as e: | |
| logger.error(f"Error fetching cities: {str(e)}") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def create_city(city: CityCreate): | |
| """Create a new city""" | |
| try: | |
| return await MapsService.create_city(city) | |
| except Exception as e: | |
| logger.error(f"Error creating city: {str(e)}") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def get_city(city_id: int): | |
| try: | |
| city = await MapsService.get_city_by_id(city_id) | |
| if not city: | |
| raise HTTPException(status_code=404, detail="City not found") | |
| return city | |
| except HTTPException: | |
| raise | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def update_city(city_id: int, city: CityUpdate): | |
| try: | |
| return await MapsService.update_city(city_id, city) | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def delete_city(city_id: int): | |
| try: | |
| await MapsService.delete_city(city_id) | |
| return {"message": "City deleted"} | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def list_districts(city_id: Optional[int] = Query(None)): | |
| """Get districts, optionally filtered by city_id""" | |
| try: | |
| return await MapsService.get_districts(city_id) | |
| except Exception as e: | |
| logger.error(f"Error fetching districts: {str(e)}") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def create_district(district: DistrictCreate): | |
| """Create a new district""" | |
| try: | |
| return await MapsService.create_district(district) | |
| except Exception as e: | |
| logger.error(f"Error creating district: {str(e)}") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def get_district(district_id: int): | |
| try: | |
| district = await MapsService.get_district_by_id(district_id) | |
| if not district: | |
| raise HTTPException(status_code=404, detail="District not found") | |
| return district | |
| except HTTPException: | |
| raise | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def update_district(district_id: int, district: DistrictUpdate): | |
| try: | |
| return await MapsService.update_district(district_id, district) | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def delete_district(district_id: int): | |
| try: | |
| await MapsService.delete_district(district_id) | |
| return {"message": "District deleted"} | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def list_streets( | |
| district_id: Optional[int] = Query(None), | |
| city_id: Optional[int] = Query(None) | |
| ): | |
| """Get streets, optionally filtered by district_id or city_id""" | |
| try: | |
| return await MapsService.get_streets(district_id, city_id) | |
| except Exception as e: | |
| logger.error(f"Error fetching streets: {str(e)}") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def search_streets(payload: dict = Body(...)): | |
| """Search streets by text. | |
| Payload: { text: str, district_id?: int, city_id?: int, limit?: int } | |
| Returns list of matches with score. | |
| """ | |
| try: | |
| text = payload.get('text') | |
| if not text: | |
| raise HTTPException(status_code=422, detail='text is required') | |
| district_id = payload.get('district_id') | |
| city_id = payload.get('city_id') | |
| limit = int(payload.get('limit') or 10) | |
| results = await MapsService.search_streets_by_text(text, district_id=district_id, city_id=city_id, limit=limit) | |
| return { 'results': results } | |
| except HTTPException: | |
| raise | |
| except Exception as e: | |
| logger.error(f"Error searching streets: {str(e)}") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def search_districts(payload: dict = Body(...)): | |
| """Search districts by text. | |
| Payload: { text: str, city_id?: int, limit?: int } | |
| Returns list of matches with score. | |
| """ | |
| try: | |
| text = payload.get('text') | |
| if not text: | |
| raise HTTPException(status_code=422, detail='text is required') | |
| city_id = payload.get('city_id') | |
| limit = int(payload.get('limit') or 10) | |
| results = await MapsService.search_districts_by_text(text, city_id=city_id, limit=limit) | |
| return { 'results': results } | |
| except HTTPException: | |
| raise | |
| except Exception as e: | |
| logger.error(f"Error searching districts: {str(e)}") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def create_street(street: StreetCreate): | |
| """Create a new street. Returns 409 with {error:'duplicate', street:{id,name}} if similar exists.""" | |
| try: | |
| return await MapsService.create_street(street) | |
| except Exception as e: | |
| msg = str(e) | |
| # Structured duplicate signal: "Failed to create street: DUPLICATE_STREET:<id>:<name>" | |
| inner = msg.replace("Failed to create street: ", "") | |
| if inner.startswith("DUPLICATE_STREET:"): | |
| parts = inner.split(":", 2) # ["DUPLICATE_STREET", "<id>", "<name>"] | |
| dup_id = int(parts[1]) if len(parts) > 1 and parts[1].isdigit() else None | |
| dup_name = parts[2] if len(parts) > 2 else "" | |
| from fastapi.responses import JSONResponse | |
| return JSONResponse( | |
| status_code=409, | |
| content={"error": "duplicate", "street": {"id": dup_id, "name": dup_name}} | |
| ) | |
| logger.error(f"Error creating street: {msg}") | |
| raise HTTPException(status_code=500, detail=msg) | |
| async def get_street(street_id: int): | |
| try: | |
| street = await MapsService.get_street_by_id(street_id) | |
| if not street: | |
| raise HTTPException(status_code=404, detail="Street not found") | |
| return street | |
| except HTTPException: | |
| raise | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def update_street(street_id: int, street: StreetUpdate): | |
| try: | |
| return await MapsService.update_street(street_id, street) | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def delete_street(street_id: int): | |
| try: | |
| await MapsService.delete_street(street_id) | |
| return {"message": "Street deleted"} | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| # ==================== POSITIONS ENDPOINTS ==================== | |
| async def list_positions(street_id: Optional[int] = Query(None)): | |
| try: | |
| return await MapsService.get_positions(street_id) | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def get_position(position_id: int): | |
| try: | |
| pos = await MapsService.get_position_by_id(position_id) | |
| if not pos: | |
| raise HTTPException(status_code=404, detail="Position not found") | |
| return pos | |
| except HTTPException: | |
| raise | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def create_position(position: PositionCreate): | |
| try: | |
| return await MapsService.create_position(position) | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def create_positions_bulk(payload: List[dict] = Body(...)): | |
| """Accepts a flat array of unique positions and upserts them. | |
| Expected payload: [{lat, lon, streets: [street_id, ...], pos_id?}, ...] | |
| pos_id present → UPDATE; absent → INSERT. | |
| """ | |
| try: | |
| created = await MapsService.create_positions_bulk(payload) | |
| return {"created": created} | |
| except Exception as e: | |
| logger.error(f"Error creating positions bulk: {str(e)}") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def update_position(position_id: int, position: PositionUpdate): | |
| try: | |
| return await MapsService.update_position(position_id, position) | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def delete_position(position_id: int): | |
| try: | |
| await MapsService.delete_position(position_id) | |
| return {"message": "Position deleted"} | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def delete_position_cascade(position_id: int): | |
| try: | |
| await MapsService.delete_position_cascade(position_id) | |
| return {"message": "Position deleted and Streets updated"} | |
| except Exception as e: | |
| logger.error(f"Error deleting position cascade: {str(e)}") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| # ==================== GROUPS ENDPOINTS ==================== | |
| async def list_groups(): | |
| """Get all groups (used by the neighbor dropdown in Create Group).""" | |
| try: | |
| return await MapsService.get_groups() | |
| except Exception as e: | |
| logger.error(f"Error fetching groups: {str(e)}") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def get_group(group_id: int): | |
| try: | |
| group = await MapsService.get_group_by_id(group_id) | |
| if not group: | |
| raise HTTPException(status_code=404, detail="Group not found") | |
| return group | |
| except HTTPException: | |
| raise | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def create_group(group: GroupCreate): | |
| """Create a new group. | |
| Body: { lat_center, long_center, streets: [street_id, ...], neighbor?: [group_id, ...] } | |
| """ | |
| try: | |
| return await MapsService.create_group(group) | |
| except Exception as e: | |
| logger.error(f"Error creating group: {str(e)}") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def update_group(group_id: int, group: GroupUpdate): | |
| try: | |
| return await MapsService.update_group(group_id, group) | |
| except Exception as e: | |
| logger.error(f"Error updating group {group_id}: {str(e)}") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def delete_group(group_id: int): | |
| try: | |
| await MapsService.delete_group(group_id) | |
| return {"message": "Group deleted"} | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def pathfind(payload: dict = Body(...)): | |
| """A* pathfinding between two position IDs. | |
| Body: { start_id: int, end_id: int } | |
| Returns: { found, node_path, street_path, street_details, total_m } | |
| """ | |
| try: | |
| start_id = payload.get("start_id") | |
| end_id = payload.get("end_id") | |
| if start_id is None or end_id is None: | |
| raise HTTPException(status_code=422, detail="start_id and end_id are required") | |
| return await MapsService.pathfind(int(start_id), int(end_id)) | |
| except HTTPException: | |
| raise | |
| except Exception as e: | |
| logger.error(f"Pathfind error: {str(e)}") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def search_nearest(payload: dict = Body(...)): | |
| """Find the nearest DB position for each query point. | |
| Expected: {points: [{lat, lon}, ...]} (1 or 2 points) | |
| Returns: {data, label, warning, message} | |
| """ | |
| try: | |
| points = payload.get("points") or [] | |
| if not points: | |
| raise HTTPException(status_code=422, detail="points array is required") | |
| return await MapsService.search_nearest(points) | |
| except HTTPException: | |
| raise | |
| except Exception as e: | |
| logger.error(f"Error searching nearest: {str(e)}") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def search_point(payload: dict = Body(...)): | |
| """Nearest single DB position to a lat/long (2-step Group strategy). | |
| Body: { lat, lon } (also accepts long/lng for the longitude) | |
| Returns: { found, position: {id,lat,lon,streets,street_names}|None, distance_m, group_id } | |
| """ | |
| try: | |
| lat = payload.get("lat") | |
| lon = payload.get("lon") | |
| if lon is None: | |
| lon = payload.get("long", payload.get("lng")) | |
| if lat is None or lon is None: | |
| raise HTTPException(status_code=422, detail="lat and lon are required") | |
| return await MapsService.nearest_position(float(lat), float(lon)) | |
| except HTTPException: | |
| raise | |
| except Exception as e: | |
| logger.error(f"Error searching nearest point: {str(e)}") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| # ==================== TEST / UTILITY ENDPOINTS ==================== | |
| async def search_uuid(id: str = Query(..., description="ID string to search for in utest table")): | |
| """Search the utest table for the most similar id string. | |
| Returns the best-matching row and its normalised Levenshtein score (0.0 = exact). | |
| """ | |
| try: | |
| if not id or not id.strip(): | |
| raise HTTPException(status_code=422, detail="id query param is required") | |
| return await MapsService.search_uuid(id.strip()) | |
| except HTTPException: | |
| raise | |
| except Exception as e: | |
| logger.error(f"search_uuid error: {str(e)}") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| # ==================== EXPORT ENDPOINTS ==================== | |
| async def export_position_bin(): | |
| """Export the full position graph as a compact binary file for A* pathfinding. | |
| Format: POSBIN v1 — header (16B) + node ids (4B each) + edges (12B each). | |
| Nodes contain id only; client maps back to DB to retrieve lat/lon/name. | |
| Edges are undirected (from_id < to_id); client mirrors both directions for A*. | |
| """ | |
| try: | |
| data = await MapsService.export_graph_bin() | |
| logger.info(f"Exported position.bin: {len(data)} bytes") | |
| return Response( | |
| content=data, | |
| media_type="application/octet-stream", | |
| headers={ | |
| "Content-Disposition": 'attachment; filename="position.bin"', | |
| "Content-Length": str(len(data)), | |
| }, | |
| ) | |
| except Exception as e: | |
| logger.error(f"Error exporting position.bin: {str(e)}") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| # Serve admin static files (must be mounted last, after all API routes) | |
| _admin_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'admin') | |
| if os.path.isdir(_admin_dir): | |
| app.mount("/", StaticFiles(directory=_admin_dir, html=True), name="admin") | |
| if __name__ == '__main__': | |
| import uvicorn | |
| uvicorn.run(app, host="0.0.0.0", port=4000, log_level="info") | |