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
30 changes: 28 additions & 2 deletions FARM/FARM/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,26 @@
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY", "dev-insecure-key-change-me")


def env_bool(name: str, default: bool = False) -> bool:
"""Parse boolean-like environment variables safely."""
value = os.environ.get(name)
if value is None:
return default
return value.strip().lower() in {"1", "true", "yes", "on"}

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
DEBUG = env_bool("DJANGO_DEBUG", default=False)

ALLOWED_HOSTS = [
host.strip()
for host in os.environ.get("DJANGO_ALLOWED_HOSTS", "localhost,127.0.0.1").split(",")
if host.strip()
]

ALLOWED_HOSTS = []
if not DEBUG and SECRET_KEY == "dev-insecure-key-change-me":

Check warning on line 45 in FARM/FARM/settings.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

FARM/FARM/settings.py#L45

Possible hardcoded password: 'dev-insecure-key-change-me'

Check notice

Code scanning / Bandit

Possible hardcoded password: 'dev-insecure-key-change-me' Note

Possible hardcoded password: 'dev-insecure-key-change-me'
msg = "DJANGO_SECRET_KEY must be set in non-debug environments."
raise RuntimeError(msg)


# Application definition
Expand Down Expand Up @@ -116,3 +132,13 @@
# https://docs.djangoproject.com/en/6.0/howto/static-files/

STATIC_URL = "static/"


# Basic production security hardening. These can be tuned via environment.
SECURE_BROWSER_XSS_FILTER = True
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The SECURE_BROWSER_XSS_FILTER setting was deprecated in Django 3.0 and removed in Django 4.0. Since this project is using Django 6.0.1 (as indicated in the file header), this setting is obsolete and has no effect. It should be removed to avoid configuration clutter.

SECURE_CONTENT_TYPE_NOSNIFF = True
X_FRAME_OPTIONS = "DENY"
SECURE_REFERRER_POLICY = "same-origin"
CSRF_COOKIE_SECURE = env_bool("DJANGO_CSRF_COOKIE_SECURE", default=not DEBUG)
SESSION_COOKIE_SECURE = env_bool("DJANGO_SESSION_COOKIE_SECURE", default=not DEBUG)
SECURE_SSL_REDIRECT = env_bool("DJANGO_SECURE_SSL_REDIRECT", default=not DEBUG)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Configure proxy SSL header before forcing HTTPS redirects

Turning SECURE_SSL_REDIRECT on by default in non-debug mode can break deployments behind a TLS-terminating reverse proxy, because Django will see backend traffic as HTTP unless SECURE_PROXY_SSL_HEADER is configured. In that setup, every request is treated as insecure and redirected again, causing redirect loops and an effectively unavailable app; this change introduces that risk for any non-debug environment that doesn’t explicitly override the new default.

Useful? React with 👍 / 👎.

4 changes: 4 additions & 0 deletions MAGE/api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
from fastapi import FastAPI
from fastmcp import FastMCP

from MAGE.api.services.encoders import list_encoders
from MAGE.api.services.encoders import service_app as encoders_service
from MAGE.api.services.libraries import libraries
from MAGE.api.services.libraries import service_app as libraries_service
from MAGE.api.services.models import (
Expand Down Expand Up @@ -44,6 +46,7 @@ async def read_root() -> dict[str, str]:

# Backward-compatible routes served directly by the gateway.
api.add_api_route("/libraries", libraries, methods=["GET"], tags=["libraries"])
api.add_api_route("/encoders", list_encoders, methods=["GET"], tags=["encoders"])
api.add_api_route("/model", model_list, methods=["GET"], tags=["models"])
api.add_api_route(
"/model/{model_id}/is_pretrained",
Expand Down Expand Up @@ -73,6 +76,7 @@ async def read_root() -> dict[str, str]:

# Microservice mounts (service-style boundaries under dedicated prefixes).
api.mount("/services/libraries", libraries_service)
api.mount("/services/encoders", encoders_service)
api.mount("/services/models", models_service)
api.mount("/services/modules", modules_service)

Expand Down
40 changes: 40 additions & 0 deletions MAGE/api/services/encoders.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Copyright (C) 2026 AIMER contributors.

"""Encoders service: inspect SMP encoders and TIMM-backed support."""

from __future__ import annotations

from importlib import import_module

from fastapi import APIRouter, FastAPI, HTTPException

router = APIRouter(tags=["encoders"])


@router.get("/health")
async def healthcheck() -> dict[str, str]:
"""Service-local health-check endpoint."""
return {"encoders_service": "UP"}


@router.get("/encoders")
async def list_encoders() -> dict[str, list[str] | int]:
"""List SMP encoders and a subset that are TIMM-backed."""
try:
smp = import_module("segmentation_models_pytorch")
smp_encoders = smp.encoders
except Exception as exc:
raise HTTPException(status_code=503, detail="segmentation_models_pytorch is unavailable") from exc

names = sorted(smp_encoders.get_encoder_names())
timm_backed = [name for name in names if name.startswith("tu-")]
return {
"encoders": names,
"timm_backed_encoders": timm_backed,
"total": len(names),
"timm_backed_total": len(timm_backed),
}
Comment on lines +7 to +36
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Importing a heavy library like segmentation_models_pytorch inside an async request handler is inefficient and can block the FastAPI event loop, especially during the first call when the module is actually loaded. Since the list of encoders is static for a given installation, it should be imported and computed once at the module level or cached. Additionally, catching a generic Exception is too broad; it's better to specifically catch ImportError or AttributeError to handle missing optional dependencies.

from fastapi import APIRouter, FastAPI, HTTPException

# Pre-calculate encoder metadata to avoid expensive/blocking imports in the request path.
try:
    import segmentation_models_pytorch as smp
    _NAMES = sorted(smp.encoders.get_encoder_names())
    _TIMM_BACKED = [n for n in _NAMES if n.startswith("tu-")]
    _ENCODER_DATA = {
        "encoders": _NAMES,
        "timm_backed_encoders": _TIMM_BACKED,
        "total": len(_NAMES),
        "timm_backed_total": len(_TIMM_BACKED),
    }
except (ImportError, AttributeError):
    _ENCODER_DATA = None

router = APIRouter(tags=["encoders"])


@router.get("/health")
async def healthcheck() -> dict[str, str]:
    """Service-local health-check endpoint."""
    return {"encoders_service": "UP"}


@router.get("/encoders")
async def list_encoders() -> dict[str, list[str] | int]:
    """List SMP encoders and a subset that are TIMM-backed."""
    if _ENCODER_DATA is None:
        raise HTTPException(status_code=503, detail="segmentation_models_pytorch is unavailable")
    return _ENCODER_DATA



service_app = FastAPI(title="Encoders Service")
service_app.include_router(router)
30 changes: 30 additions & 0 deletions MAGE/tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,25 @@
return bool(self._pretrained.get(model_id, False))




class FakeSMPEncoders:
"""Minimal fake of SMP encoders registry."""

@staticmethod
def get_encoder_names() -> list[str]:

Check notice on line 77 in MAGE/tests/test_main.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

MAGE/tests/test_main.py#L77

Missing function or method docstring
return ["resnet34", "tu-efficientnet_b0", "tu-convnext_tiny"]


class FakeSMP(ModuleType):
"""In-memory stand-in for ``segmentation_models_pytorch``."""

def __init__(self) -> None:
super().__init__("segmentation_models_pytorch")
self.__version__ = "0.0.0"
self.encoders = FakeSMPEncoders()


class FakeMCPApp:
"""Minimal fake app exposing the MCP ping route."""

Expand Down Expand Up @@ -148,6 +167,7 @@

"""
monkeypatch.setitem(sys.modules, "timm", FakeTimm())
monkeypatch.setitem(sys.modules, "segmentation_models_pytorch", FakeSMP())

fastmcp_mod = ModuleType("fastmcp")
fastmcp_mod.FastMCP = FakeFastMCP
Expand Down Expand Up @@ -329,3 +349,13 @@
check(response.status_code == HTTP_OK, "Expected 200 on /libraries with error")
ai = response.json()["AI"]
check(ai["keras"] is None, "keras should be None on unexpected import error")


def test_encoders_list(client: TestClient) -> None:
"""`/encoders` should expose SMP encoders including TIMM-backed ones."""
response = client.get("/encoders")
check(response.status_code == HTTP_OK, "Expected 200 on /encoders")
data = response.json()
check("encoders" in data, "Missing encoders key")
check("timm_backed_encoders" in data, "Missing timm_backed_encoders key")
check("tu-efficientnet_b0" in data["timm_backed_encoders"], "Expected tu- encoder")
Loading