from fastapi import APIRouter, HTTPException, status from fastapi.responses import FileResponse from pydantic import BaseModel, Field, HttpUrl from typing import Optional, Dict, Any, Literal from datetime import datetime as dt import os from pif_compiler.functions.common_func import generate_pdf from pif_compiler.services.srv_pubchem import pubchem_dap from pif_compiler.services.srv_cir import search_ingredient from pif_compiler.functions.common_log import get_logger from pif_compiler.functions.db_utils import db_connect logger = get_logger() router = APIRouter() class GeneratePdfRequest(BaseModel): link: str = Field(..., description="URL of the page to convert to PDF") name: str = Field(..., description="Name for the generated PDF file (without extension)") class Config: json_schema_extra = { "example": { "link": "https://example.com/page", "name": "my_document" } } class GeneratePdfResponse(BaseModel): success: bool name: str message: str file_path: Optional[str] = None @router.post("/common/generate-pdf", response_model=GeneratePdfResponse, tags=["Common"]) async def generate_pdf_endpoint(request: GeneratePdfRequest): """ Generate a PDF from a web page URL. This endpoint uses Playwright to: 1. Navigate to the provided URL 2. Render the page 3. Generate a PDF file 4. Save it in the 'pdfs/' directory If a PDF with the same name already exists, it will skip generation and return success immediately. Args: request: GeneratePdfRequest with the URL and desired PDF name Returns: GeneratePdfResponse with success status and file information """ logger.info(f"API request received to generate PDF: name='{request.name}', link='{request.link}'") try: result = await generate_pdf(request.link, request.name) if result: file_path = f"pdfs/{request.name}.pdf" # Check if file was already existing or newly created if os.path.exists(file_path): logger.info(f"PDF available for '{request.name}'") return GeneratePdfResponse( success=True, name=request.name, message=f"PDF generated successfully or already exists", file_path=file_path ) else: logger.error(f"PDF file not found after generation for '{request.name}'") return GeneratePdfResponse( success=False, name=request.name, message="PDF generation completed but file not found", file_path=None ) else: logger.error(f"PDF generation failed for '{request.name}'") return GeneratePdfResponse( success=False, name=request.name, message="PDF generation failed", file_path=None ) except Exception as e: logger.error(f"Error generating PDF for '{request.name}': {str(e)}", exc_info=True) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Internal error while generating PDF: {str(e)}" ) @router.get("/common/download-pdf/{name}", response_class=FileResponse, tags=["Common"]) async def download_pdf(name: str): """ Download a previously generated PDF file. Args: name: Name of the PDF file (without extension) Returns: FileResponse with the PDF file for download """ logger.info(f"API request received to download PDF: name='{name}'") file_path = f"pdfs/{name}.pdf" if not os.path.exists(file_path): logger.warning(f"PDF file not found: {file_path}") raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"PDF file '{name}' not found. Please generate it first using /common/generate-pdf" ) logger.info(f"Serving PDF file: {file_path}") return FileResponse( path=file_path, media_type="application/pdf", filename=f"{name}.pdf" ) class PubchemRequest(BaseModel): cas: str = Field(..., description="CAS number of the substance to search for in PubChem") class Config: json_schema_extra = { "example": { "cas": "64-17-5" } } class PubchemResponse(BaseModel): success: bool cas: str data: Optional[Dict[str, Any]] = None error: Optional[str] = None @router.post("/common/pubchem", response_model=PubchemResponse, tags=["Common"]) async def search_pubchem(request: PubchemRequest): """ Search for substance information in PubChem database. This endpoint retrieves comprehensive substance data from PubChem including: - **Basic info**: CID, CAS, first PubChem name, PubChem link - **First level properties**: XLogP, molecular weight, TPSA, exact mass - **Second level properties**: Melting Point, Dissociation Constants, pH The data is automatically cleaned and formatted for easier consumption. Args: request: PubchemRequest containing the CAS number Returns: PubchemResponse with the substance data or error information """ logger.info(f"API request received for PubChem search: CAS={request.cas}") try: result = pubchem_dap(request.cas) # Check if result is None (error occurred) if result is None: logger.error(f"PubChem search returned None for CAS: {request.cas}") return PubchemResponse( success=False, cas=request.cas, data=None, error="An error occurred while searching PubChem. Please check the logs for details." ) # Check if result is a string (no results found) if isinstance(result, str): logger.warning(f"No results found in PubChem for CAS: {request.cas}") return PubchemResponse( success=False, cas=request.cas, data=None, error=result ) # Successful result logger.info(f"Successfully retrieved PubChem data for CAS: {request.cas}") return PubchemResponse( success=True, cas=request.cas, data=result, error=None ) except Exception as e: logger.error(f"Error processing PubChem request for CAS {request.cas}: {str(e)}", exc_info=True) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Internal error while processing PubChem request: {str(e)}" ) class CirSearchRequest(BaseModel): text: str = Field(..., description="Text to search for in the CIR database") class Config: json_schema_extra = { "example": { "text": "olio di argan" } } class CirSearchResponse(BaseModel): success: bool text: str results: Optional[list] = None count: Optional[int] = None error: Optional[str] = None @router.post("/common/cir-search", response_model=CirSearchResponse, tags=["Common"]) async def cir_search_endpoint(request: CirSearchRequest): """ Search for ingredients in the CIR (Cosmetic Ingredient Review) database. This endpoint searches the CIR NOAEL database for ingredients matching the provided text query. Args: request: CirSearchRequest containing the search text Returns: CirSearchResponse with the search results or error information """ logger.info(f"API request received for CIR search: text='{request.text}'") try: results = search_ingredient(request.text) if results is None: logger.error(f"CIR search returned None for text: {request.text}") return CirSearchResponse( success=False, text=request.text, results=None, count=0, error="An error occurred while searching the CIR database. Please check the logs for details." ) logger.info(f"Successfully retrieved {len(results)} results from CIR database for text: {request.text}") return CirSearchResponse( success=True, text=request.text, results=results, count=len(results), error=None ) except Exception as e: logger.error(f"Error processing CIR search request for text '{request.text}': {str(e)}", exc_info=True) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Internal error while processing CIR search request: {str(e)}" ) class SegnalazioneRequest(BaseModel): cas: Optional[str] = None page: str description: str error: Optional[str] = None priority: Literal["bassa", "media", "alta"] = "media" @router.post("/common/segnalazione", tags=["Common"]) async def create_segnalazione(request: SegnalazioneRequest): """Salva una segnalazione/ticket nella collection MongoDB 'segnalazioni'.""" collection = db_connect(collection_name="segnalazioni") if collection is None: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Connessione MongoDB fallita" ) doc = { **request.model_dump(), "created_at": dt.now().isoformat(), "stato": "aperta", } try: result = collection.insert_one(doc) logger.info(f"Segnalazione creata: id={result.inserted_id}, page={request.page}, priority={request.priority}") return {"success": True, "id": str(result.inserted_id)} except Exception as e: logger.error(f"create_segnalazione: errore MongoDB — {e}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Errore nel salvataggio della segnalazione" ) @router.get("/common/health", tags=["Common"]) async def common_health_check(): """ Health check endpoint for common functions service. Returns the status of the common functions components. """ return { "status": "healthy", "service": "common-functions", "components": { "api": "operational", "logging": "operational", "utilities": "operational", "pubchem": "operational", "cir": "operational" } }