Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion src/Components/API/app/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,13 @@
Detections.create_index(
[("microphoneLLA.0", pymongo.ASCENDING), ("microphoneLLA.1", pymongo.ASCENDING)],
name="idx_microphone_lat_lon"
)
)

AdminBudgets = db.admin_budgets
ServiceStates = db.service_states

Projects = db["projects"]
Projects.create_index("name")
Projects.create_index("status")
Projects.create_index("location")
Projects.create_index("ecologists")
118 changes: 68 additions & 50 deletions src/Components/API/app/main.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@

import os
import time
import logging
import threading
import json
from fastapi import FastAPI, Body, HTTPException, status, APIRouter, Request
# from Components.API.app.routers import add_csv_output_option, audio_upload_router
from .routers import add_csv_output_option, audio_upload_router

from fastapi import FastAPI, Body, HTTPException, status, APIRouter
from fastapi.middleware.cors import CORSMiddleware
from app.routers import species_predictor
from app.routers import auth_router
from app.routers import admin_budget, admin_services
from fastapi.responses import Response, JSONResponse
from fastapi.encoders import jsonable_encoder
from pydantic import BaseModel, Field, EmailStr
Expand All @@ -14,12 +15,27 @@
from app.routers import insights
import datetime
import pymongo
import json

from app.routers import hmi, engine, sim, two_factor
from app.routers import public
app = FastAPI()

# Add the CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:8080"], # 可根据实际需求配置
)
# Routers
from .routers import add_csv_output_option, audio_upload_router
from app.routers import species_predictor, auth_router, hmi, engine, sim, two_factor, public, iot, live, sensors #Websocket

# --- FastAPI App Setup ---
from app.routers import projects
app.include_router(projects.router)

from app.routers import hmi, engine, sim, iot

# ✅ Add metadata here
app = FastAPI(
title="Project Echo API",
description="""
Expand All @@ -33,33 +49,58 @@
version="1.0.0"
)

# ✅ CORS Middleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_origins=["*"], # Allow all origins

allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)

# --- Routers ---

# app.include_router(hmi.router, tags=['hmi'], prefix='/hmi')
# app.include_router(engine.router, tags=['engine'], prefix='/engine')
# app.include_router(sim.router, tags=['sim'], prefix='/sim')
# app.include_router(add_csv_output_option.router, tags=['csv'], prefix='/api')
app.include_router(audio_upload_router.router, tags=['audio'], prefix='/api')


# ✅ Include routers
app.include_router(hmi.router, tags=['hmi'], prefix='/hmi')
app.include_router(engine.router, tags=['engine'], prefix='/engine')
app.include_router(sim.router, tags=['sim'], prefix='/sim')
app.include_router(two_factor.router)
app.include_router(admin_budget.router, tags=["admin"], prefix="/api")
app.include_router(admin_services.router, tags=["admin"], prefix="/api")

app.include_router(public.router, tags=['public'], prefix='/public')

'''try:
file_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'echo_config.json')
with open(file_path, 'r') as f:
echo_config = json.load(f)
print(f"Echo API echo_config successfully loaded", flush=True)
except:
print(f"Could not API echo_config : {file_path}")
print(f" database names: {client.list_database_names()}")
'''

app.include_router(iot.router, tags=['iot'], prefix='/iot')
app.include_router(sensors.router, tags=['sensors'], prefix='/sensors')
app.include_router(species_predictor.router, tags=["predict"])
app.include_router(auth_router.router, tags=["auth"], prefix="/api")
app.include_router(live.router) #Websocket



# ✅ Root endpoint
app.include_router(insights.router, tags=["insights"])

# --- Root Endpoint ---
@app.get("/", response_description="API Root")
def show_home():
return 'Welcome to echo api, move to /docs for more'
return 'Welcome to Project Echo API. Visit /docs for interactive documentation.'

app.include_router(auth_router.router, tags=["auth"], prefix="/api")
from app.routers import detections
Expand All @@ -68,10 +109,18 @@ def show_home():
# ✅ /openapi-export - fetch live OpenAPI spec
@app.get("/openapi-export", include_in_schema=False)
async def get_openapi_spec():
"""
Returns the current OpenAPI spec generated by FastAPI.
Used for downloading and converting to YAML.
"""
return app.openapi()

# ✅ /spec/summary - for OpenAPI spec verification/debug
@app.get("/spec/summary", tags=["debug"], include_in_schema=False)
async def get_spec_summary():
"""
Returns a summary of the OpenAPI spec for deployment verification.
"""
spec = app.openapi()
return {
"title": spec.get("info", {}).get("title"),
Expand All @@ -80,50 +129,19 @@ async def get_spec_summary():
"tags": [tag.get("name") for tag in spec.get("tags", []) if "name" in tag]
}

# ✅ Save OpenAPI spec to file when app starts
def export_openapi_to_file():
"""
Saves the OpenAPI spec to a file on startup.
Creates the 'backend' folder if it doesn't exist.
"""
output_dir = "backend"
os.makedirs(output_dir, exist_ok=True)
os.makedirs(output_dir, exist_ok=True) # creates the folder if it doesn't exist
output_path = os.path.join(output_dir, "project-echo-openapi.json")

with open(output_path, "w") as f:
json.dump(app.openapi(), f, indent=2)

print(f"✅ OpenAPI spec exported to {output_path}")

export_openapi_to_file()

# --- 24/7 Engine Background Task ---

# Load engine interval from config
try:
with open(os.path.join(os.path.dirname(__file__), 'echo_config.json')) as f:
config = json.load(f)
ENGINE_INTERVAL = config.get('engine_interval_seconds', 5)
except Exception as e:
ENGINE_INTERVAL = 5
logging.warning(f"Could not load engine interval from config: {e}")

def continuous_engine_task():
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s %(levelname)s %(message)s',
handlers=[logging.StreamHandler()]
)
while True:
try:
# Replace with your actual processing logic
logging.info("\n [ Place holder for future engine tasks~ ]")
# Example: engine.process_new_data()
time.sleep(ENGINE_INTERVAL)
except Exception as e:
logging.error(f"Engine error: {e}")
time.sleep(ENGINE_INTERVAL)

def start_background_engine():
thread = threading.Thread(target=continuous_engine_task, daemon=True)
thread.start()
logging.info("Continuous engine background task started.")


start_background_engine()



17 changes: 17 additions & 0 deletions src/Components/API/app/middleware/pause_guard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from fastapi import HTTPException, status
from app.services.service_state import get_service_state


def pause_guard(service_name: str):
"""
FastAPI dependency. If service is paused -> 503.
"""
def _guard():
if get_service_state(service_name):
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail=f"Service '{service_name}' is temporarily paused by admin.",
)
return True

return _guard
56 changes: 56 additions & 0 deletions src/Components/API/app/routers/admin_budget.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
from typing import List, Optional, Dict, Any
from fastapi import APIRouter, Body, Depends, HTTPException

from app.middleware.pause_guard import pause_guard
from app.services.budget import (
set_budget_limits,
get_budget_limits,
get_usage,
reset_usage,
)

router = APIRouter(
prefix="/admin/budget",
tags=["admin-budget"],
)


@router.get(
"/limits",
summary="Get current budget limits config",
dependencies=[Depends(pause_guard("admin_budget"))],
)
def get_limits_endpoint():
return get_budget_limits()


@router.post(
"/limits",
summary="Set budget limits config (rules)",
dependencies=[Depends(pause_guard("admin_budget"))],
)
def set_limits_endpoint(
rules: List[Dict[str, Any]] = Body(..., description="List of budget rules"),
):
if not isinstance(rules, list) or len(rules) == 0:
raise HTTPException(status_code=400, detail="rules must be a non-empty list")
return set_budget_limits(rules)


@router.get(
"/usage",
summary="Get current usage for a service (optionally month)",
dependencies=[Depends(pause_guard("admin_budget"))],
)
def get_usage_endpoint(service: str, month_key: Optional[str] = None):
return get_usage(service=service, month_key=month_key)


@router.post(
"/usage/reset",
summary="Reset usage counter for a service (optionally month)",
dependencies=[Depends(pause_guard("admin_budget"))],
)
def reset_usage_endpoint(service: str, month_key: Optional[str] = None):
return reset_usage(service=service, month_key=month_key)

30 changes: 30 additions & 0 deletions src/Components/API/app/routers/admin_services.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from fastapi import APIRouter
from app.schemas import ServicePauseIn, ServicePauseOut
from app.services.service_state import get_service_state, set_service_state
from datetime import datetime

router = APIRouter()

@router.get("/admin/services/{service}/status", response_model=ServicePauseOut)
def get_pause_status(service: str):
paused = get_service_state(service)
# if no doc exists, we still return a valid status
return ServicePauseOut(service=service, paused=paused, updated_at=datetime.utcnow())

@router.post("/admin/services/pause", response_model=ServicePauseOut)
def pause_or_resume_service(payload: ServicePauseIn):
result = set_service_state(payload.service, payload.paused)
return ServicePauseOut(**result)

@router.get("/admin/services", response_model=list[ServicePauseOut])
def list_services_status():
# list everything known in DB
docs = list(__import__("app.database").database.ServiceStates.find({}, {"_id": 0}))
out = []
for d in docs:
out.append(ServicePauseOut(
service=d.get("service"),
paused=bool(d.get("paused", False)),
updated_at=d.get("updated_at") or datetime.utcnow(),
))
return out
Loading
Loading