Skip to main content
Glama
routes.py6.96 kB
"""REST API routes.""" from collections.abc import AsyncGenerator from datetime import UTC, datetime from typing import Annotated, Any from fastapi import APIRouter, Depends, File, Form, UploadFile from fastapi.responses import Response from sse_starlette.sse import EventSourceResponse from src.api.schemas import ( ConvertStreamRequest, ConvertTextRequest, ConvertTextResponse, FormatListResponse, HealthCheck, ServiceInfo, ) from src.auth.dependencies import OptionalAuth, RequireAuth from src.config import get_settings from src.core.converter import ConversionOptions as CoreConversionOptions from src.core.converter import ConversionService from src.core.exceptions import FileSizeError router = APIRouter() def get_conversion_service() -> ConversionService: """Dependency to get conversion service.""" return ConversionService() @router.get("/", response_model=ServiceInfo, tags=["Info"]) async def get_service_info() -> ServiceInfo: """Get service information.""" settings = get_settings() return ServiceInfo( name=settings.service_name, version=settings.service_version, endpoints={ "health": "/health", "formats": "/api/v1/formats", "convert_text": "/api/v1/convert/text", "convert_file": "/api/v1/convert/file", "convert_stream": "/api/v1/convert/stream", "mcp": "/mcp", "docs": "/docs", }, ) @router.get("/health", response_model=HealthCheck, tags=["Info"]) async def health_check( service: Annotated[ConversionService, Depends(get_conversion_service)], ) -> HealthCheck: """Health check endpoint.""" pandoc_version = await service.get_pandoc_version() return HealthCheck( status="healthy", pandoc_version=pandoc_version, timestamp=datetime.now(UTC).isoformat(), ) @router.get("/api/v1/formats", response_model=FormatListResponse, tags=["Formats"]) async def list_formats( service: Annotated[ConversionService, Depends(get_conversion_service)], auth: OptionalAuth = None, ) -> FormatListResponse: """Get list of supported input and output formats. This endpoint is public but can optionally include authentication. """ formats = await service.get_formats() return FormatListResponse( input=formats.input, output=formats.output, ) @router.post( "/api/v1/convert/text", response_model=ConvertTextResponse, tags=["Convert"] ) async def convert_text( request: ConvertTextRequest, auth: RequireAuth, service: Annotated[ConversionService, Depends(get_conversion_service)], ) -> ConvertTextResponse: """Convert text content from one format to another. Requires authentication with 'convert:text' or 'admin' scope. """ auth.require_scope("convert:text") options = None if request.options: options = CoreConversionOptions( standalone=request.options.standalone, table_of_contents=request.options.table_of_contents, number_sections=request.options.number_sections, wrap=request.options.wrap, columns=request.options.columns, ) result = await service.convert_text( content=request.content, from_format=request.from_format, to_format=request.to_format, options=options, ) # Return text content directly or base64 for binary result_dict = result.to_dict() return ConvertTextResponse( content=result_dict["content"], content_type=result.content_type, is_binary=result.is_binary, ) @router.post("/api/v1/convert/file", tags=["Convert"]) async def convert_file( file: Annotated[UploadFile, File(description="File to convert")], to_format: Annotated[str, Form(description="Target format")], auth: RequireAuth, service: Annotated[ConversionService, Depends(get_conversion_service)], from_format: Annotated[ str | None, Form(description="Source format (optional)") ] = None, ) -> Response: """Convert an uploaded file to another format. Requires authentication with 'convert:file' or 'admin' scope. Returns the converted file as a download. """ auth.require_scope("convert:file") settings = get_settings() # Read file content file_content = await file.read() # Check file size if len(file_content) > settings.max_file_size_bytes: raise FileSizeError(len(file_content), settings.max_file_size_bytes) filename = file.filename or "document" result = await service.convert_file( file_bytes=file_content, filename=filename, to_format=to_format, from_format=from_format, ) # Determine output filename output_filename = f"converted.{to_format}" if "." in filename: base_name = filename.rsplit(".", 1)[0] output_filename = f"{base_name}.{to_format}" return Response( content=result.content, media_type=result.content_type, headers={ "Content-Disposition": f'attachment; filename="{output_filename}"', }, ) @router.post("/api/v1/convert/stream", tags=["Convert"]) async def convert_stream( request: ConvertStreamRequest, auth: RequireAuth, service: Annotated[ConversionService, Depends(get_conversion_service)], ) -> EventSourceResponse: """Stream conversion with progress events. Requires authentication with 'convert:text' or 'admin' scope. Returns Server-Sent Events with progress updates and final result. """ auth.require_scope("convert:text") async def event_generator() -> AsyncGenerator[dict[str, Any], None]: # Starting event yield { "event": "progress", "data": {"stage": "starting", "progress": 0}, } try: # Simulated progress for converting yield { "event": "progress", "data": {"stage": "converting", "progress": 50}, } result = await service.convert_text( content=request.content, from_format=request.from_format, to_format=request.to_format, ) yield { "event": "progress", "data": {"stage": "finalizing", "progress": 90}, } # Complete event with result result_dict = result.to_dict() yield { "event": "complete", "data": { "content": result_dict["content"], "content_type": result.content_type, "is_binary": result.is_binary, }, } except Exception as e: yield { "event": "error", "data": {"message": str(e)}, } return EventSourceResponse(event_generator())

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Fu-Jie/MCP-OPENAPI-Pandoc'

If you have feedback or need assistance with the MCP directory API, please join our Discord server