Skip to content
Open
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
24 changes: 20 additions & 4 deletions backend/Models/MethodsTemplate.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,40 @@
from pydantic import BaseModel, Field, RootModel
from typing import Dict, Any, Optional, Union


class ParameterInfo(BaseModel):
"""Schema for parameter information"""

type: str = Field(..., description="Type annotation of the parameter")
value: Any = Field(..., description="Default or required value of the parameter")
desc: str = Field(..., description="Description of the parameter")


class MethodTemplate(BaseModel):
"""Schema for individual method template"""

method_name: str = Field(..., description="Name of the method")
module_path: str = Field(..., description="Full path to the module containing the method")
module_path: str = Field(
..., description="Full path to the module containing the method"
)
method_desc: str = Field(..., description="Description of what the method does")
method_doc: str = Field(..., description="Link to Docs for the method")
parameters: Dict[str, ParameterInfo] = Field(..., description="Dictionary of parameter information")
parameters: Dict[str, ParameterInfo] = Field(
..., description="Dictionary of parameter information"
)


class ModuleTemplates(RootModel):
"""Schema for all methods in a module"""
root: Dict[str, MethodTemplate] = Field(..., description="Dictionary of method templates in a module")

root: Dict[str, MethodTemplate] = Field(
..., description="Dictionary of method templates in a module"
)


class AllTemplates(RootModel):
"""Schema for all modules and their methods"""
root: Dict[str, Dict[str, MethodTemplate]] = Field(..., description="Dictionary of all modules and their method templates")

root: Dict[str, Dict[str, MethodTemplate]] = Field(
..., description="Dictionary of all modules and their method templates"
)
4 changes: 3 additions & 1 deletion backend/Models/YamlModels.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
from pydantic import BaseModel
from typing import Dict, Optional,List,Any
from typing import Dict, Optional, List, Any


class SweepConfig(BaseModel):
methodId: str
paramName: str
sweepType: str


class YamlGenerateRequest(BaseModel):
data: List[Dict[str, Any]]
fileName: str
Expand Down
2 changes: 0 additions & 2 deletions backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@
)



app.include_router(methods_router)
app.include_router(yaml_router)
app.include_router(proxy_router)

9 changes: 7 additions & 2 deletions backend/routers/methods.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from utils.methods import METHOD_CATEGORIES
from fastapi import HTTPException,APIRouter
from fastapi import HTTPException, APIRouter
from utils.generator import generate_method_template
from typing import Dict, List
from Models.MethodsTemplate import AllTemplates
Expand All @@ -11,6 +11,7 @@
tags=["methods"],
)


def get_methods_templates(module_methods: Dict[str, List[str]]) -> Dict:
"""
Helper function to generate templates for a given module and its methods
Expand Down Expand Up @@ -39,13 +40,16 @@ async def endpoint():
return AllTemplates(root=result)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))

return endpoint


# Register all category endpoints
for category in METHOD_CATEGORIES.keys():
endpoint = create_category_endpoint(category)
methods_router.get(f"/{category}", response_model=AllTemplates)(endpoint)


def filter_pipelines_with_tomopy(pipelines: dict) -> dict:
"""
Filters out pipelines where any method has a module_path starting with 'tomopy.'.
Expand All @@ -68,13 +72,15 @@ def filter_pipelines_with_tomopy(pipelines: dict) -> dict:

return filtered_pipelines


@methods_router.get("/fullpipelines")
async def get_full_pipelines():
try:
return filter_pipelines_with_tomopy(process_all_yaml_files())
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))


# Main endpoint for all methods
@methods_router.get("/", response_model=AllTemplates)
async def get_all_methods():
Expand All @@ -86,4 +92,3 @@ async def get_all_methods():
return AllTemplates(root=all_templates)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))

127 changes: 74 additions & 53 deletions backend/routers/proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,34 +11,39 @@

proxy_router = APIRouter(prefix="/proxy", tags=["proxy"])


@proxy_router.get("/tiff")
async def proxy_tiff(url: str = Query(..., description="The S3 URL to proxy")):
"""
Proxy endpoint to fetch TIFF files from S3 and return them to avoid CORS issues.
"""
try:
logger.info(f"Proxying TIFF file from URL: {url}")

# Validate that it's a reasonable URL (basic security)
if not url.startswith(("https://", "http://")):
raise HTTPException(status_code=400, detail="Invalid URL scheme")

# Use httpx to fetch the file
async with httpx.AsyncClient(timeout=60.0) as client:
response = await client.get(url)

if response.status_code != 200:
logger.error(f"Failed to fetch file: {response.status_code} - {response.text}")
logger.error(
f"Failed to fetch file: {response.status_code} - {response.text}"
)
raise HTTPException(
status_code=response.status_code,
detail=f"Failed to fetch file: {response.status_code}"
status_code=response.status_code,
detail=f"Failed to fetch file: {response.status_code}",
)

# Get the content type from the original response
content_type = response.headers.get("content-type", "image/tiff")

logger.info(f"Successfully fetched file, size: {len(response.content)} bytes")


logger.info(
f"Successfully fetched file, size: {len(response.content)} bytes"
)

# Return the file content with appropriate headers
return Response(
content=response.content,
Expand All @@ -48,27 +53,31 @@ async def proxy_tiff(url: str = Query(..., description="The S3 URL to proxy")):
"Access-Control-Allow-Methods": "GET",
"Access-Control-Allow-Headers": "*",
"Content-Length": str(len(response.content)),
"Cache-Control": "public, max-age=3600" # Cache for 1 hour
}
"Cache-Control": "public, max-age=3600", # Cache for 1 hour
},
)

except httpx.TimeoutException:
logger.error(f"Timeout while fetching URL: {url}")
raise HTTPException(status_code=504, detail="Timeout while fetching file")

except httpx.HTTPError as e:
logger.error(f"HTTP error while fetching URL {url}: {str(e)}")
raise HTTPException(status_code=502, detail=f"Error fetching file: {str(e)}")

except Exception as e:
logger.error(f"Unexpected error while proxying URL {url}: {str(e)}")
raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")


@proxy_router.get("/tiff-pages")
async def proxy_tiff_pages(
url: str = Query(..., description="The S3 URL to proxy"),
page: int = Query(None, description="Page number to extract (0-based). If not provided, returns metadata."),
downsample_rate: int = 1
page: int = Query(
None,
description="Page number to extract (0-based). If not provided, returns metadata.",
),
downsample_rate: int = 1,
):
"""
Process multi-page TIFF files server-side using Pillow.
Expand All @@ -77,27 +86,27 @@ async def proxy_tiff_pages(
"""
try:
logger.info(f"Processing TIFF pages from URL: {url}, page: {page}")

# Validate URL
if not url.startswith(("https://", "http://")):
raise HTTPException(status_code=400, detail="Invalid URL scheme")

# Fetch the TIFF file
async with httpx.AsyncClient(timeout=60.0) as client:
response = await client.get(url)

if response.status_code != 200:
logger.error(f"Failed to fetch TIFF file: {response.status_code}")
raise HTTPException(
status_code=response.status_code,
detail=f"Failed to fetch TIFF file: {response.status_code}"
status_code=response.status_code,
detail=f"Failed to fetch TIFF file: {response.status_code}",
)

logger.info(f"Fetched TIFF file, size: {len(response.content)} bytes")

# Process TIFF with Pillow
tiff_data = BytesIO(response.content)

with Image.open(tiff_data) as img:
# If no page specified, return metadata
if page is None:
Expand All @@ -109,55 +118,63 @@ async def proxy_tiff_pages(
page_count += 1
except EOFError:
pass

# Get dimensions from first page
img.seek(0)
width, height = img.size

metadata = {
"page_count": page_count,
"width": width,
"height": height,
"format": img.format,
"mode": img.mode
"mode": img.mode,
}

logger.info(f"TIFF metadata: {metadata}")

return JSONResponse(
content=metadata,
headers={
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET",
"Access-Control-Allow-Headers": "*",
"Cache-Control": "public, max-age=3600"
}
"Cache-Control": "public, max-age=3600",
},
)

# Extract specific page
else:
try:
img.seek(page)
if downsample_rate != 1:
width, height = img.size
img = img.resize((width // downsample_rate, height // downsample_rate))
img = img.resize(
(width // downsample_rate, height // downsample_rate)
)
logger.info(f"Extracting page {page}, size: {img.size}")

# Convert to RGB if necessary (TIFF might be in different modes)
if img.mode not in ('RGB', 'RGBA'):
if img.mode == 'I;16':
if img.mode not in ("RGB", "RGBA"):
if img.mode == "I;16":
array = np.array(img)
normalized = (array.astype(np.uint16) - array.min()) * 255.0 / (array.max() - array.min())
normalized = (
(array.astype(np.uint16) - array.min())
* 255.0
/ (array.max() - array.min())
)
img = Image.fromarray(normalized.astype(np.uint8))
img = img.convert('RGB')
img = img.convert("RGB")

# Save as PNG to BytesIO
png_buffer = BytesIO()
img.save(png_buffer, format='PNG', optimize=True)
img.save(png_buffer, format="PNG", optimize=True)
png_data = png_buffer.getvalue()

logger.info(f"Converted page {page} to PNG, size: {len(png_data)} bytes")


logger.info(
f"Converted page {page} to PNG, size: {len(png_data)} bytes"
)

return Response(
content=png_data,
media_type="image/png",
Expand All @@ -166,25 +183,29 @@ async def proxy_tiff_pages(
"Access-Control-Allow-Methods": "GET",
"Access-Control-Allow-Headers": "*",
"Content-Length": str(len(png_data)),
"Cache-Control": "public, max-age=3600"
}
"Cache-Control": "public, max-age=3600",
},
)

except EOFError:
logger.error(f"Page {page} does not exist in TIFF")
raise HTTPException(status_code=404, detail=f"Page {page} not found")
raise HTTPException(
status_code=404, detail=f"Page {page} not found"
)
except Exception as e:
logger.error(f"Error processing page {page}: {str(e)}")
raise HTTPException(status_code=500, detail=f"Error processing page: {str(e)}")

raise HTTPException(
status_code=500, detail=f"Error processing page: {str(e)}"
)

except httpx.TimeoutException:
logger.error(f"Timeout while fetching URL: {url}")
raise HTTPException(status_code=504, detail="Timeout while fetching file")

except httpx.HTTPError as e:
logger.error(f"HTTP error while fetching URL {url}: {str(e)}")
raise HTTPException(status_code=502, detail=f"Error fetching file: {str(e)}")

except Exception as e:
logger.error(f"Unexpected error processing TIFF: {str(e)}")
raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
Loading
Loading