Spaces:
Running
Running
Commit ·
677e796
1
Parent(s): 846dbfc
feat: pin semantic-backend v1.2, set /semantic route, and harden container permissions
Browse files- Dockerfile +4 -1
- main.py +3 -3
- registry.py +0 -1
- requirements.in +1 -1
- requirements.txt +7 -10
- tests/test_host.py +73 -63
Dockerfile
CHANGED
|
@@ -11,7 +11,10 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
| 11 |
curl \
|
| 12 |
&& rm -rf /var/lib/apt/lists/*
|
| 13 |
|
| 14 |
-
RUN useradd -m -u 1000 user
|
|
|
|
|
|
|
|
|
|
| 15 |
USER user
|
| 16 |
ENV HOME=/home/user \
|
| 17 |
PATH=/home/user/.local/bin:$PATH
|
|
|
|
| 11 |
curl \
|
| 12 |
&& rm -rf /var/lib/apt/lists/*
|
| 13 |
|
| 14 |
+
RUN useradd -m -u 1000 user && \
|
| 15 |
+
mkdir -p /tmp/hf /tmp/p2 && \
|
| 16 |
+
chown -R user:user /tmp/hf /tmp/p2 && \
|
| 17 |
+
chmod 777 /tmp/hf /tmp/p2
|
| 18 |
USER user
|
| 19 |
ENV HOME=/home/user \
|
| 20 |
PATH=/home/user/.local/bin:$PATH
|
main.py
CHANGED
|
@@ -54,9 +54,9 @@ class RequestLimitMiddleware(BaseHTTPMiddleware):
|
|
| 54 |
|
| 55 |
app = FastAPI(
|
| 56 |
title="HF Backend",
|
| 57 |
-
docs_url=
|
| 58 |
-
redoc_url=
|
| 59 |
-
openapi_url=
|
| 60 |
)
|
| 61 |
|
| 62 |
app.add_middleware(RequestLimitMiddleware, max_size=max_body_size, timeout=request_timeout)
|
|
|
|
| 54 |
|
| 55 |
app = FastAPI(
|
| 56 |
title="HF Backend",
|
| 57 |
+
docs_url=None,
|
| 58 |
+
redoc_url=None,
|
| 59 |
+
openapi_url=None,
|
| 60 |
)
|
| 61 |
|
| 62 |
app.add_middleware(RequestLimitMiddleware, max_size=max_body_size, timeout=request_timeout)
|
registry.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
| 1 |
PROJECT_REGISTRY = {
|
| 2 |
"/semantic": "semantic_backend.router:router",
|
| 3 |
-
"/sementic": "semantic_backend.router:router",
|
| 4 |
}
|
|
|
|
| 1 |
PROJECT_REGISTRY = {
|
| 2 |
"/semantic": "semantic_backend.router:router",
|
|
|
|
| 3 |
}
|
requirements.in
CHANGED
|
@@ -2,4 +2,4 @@ fastapi
|
|
| 2 |
uvicorn[standard]
|
| 3 |
pydantic
|
| 4 |
starlette
|
| 5 |
-
git+https://github.com/rachit-mangawa/Semantic-Comment-Analyze.git@v1.
|
|
|
|
| 2 |
uvicorn[standard]
|
| 3 |
pydantic
|
| 4 |
starlette
|
| 5 |
+
git+https://github.com/rachit-mangawa/Semantic-Comment-Analyze.git@v1.2#subdirectory=backend
|
requirements.txt
CHANGED
|
@@ -1,6 +1,8 @@
|
|
| 1 |
-
--extra-index-url https://download.pytorch.org/whl/cpu
|
| 2 |
# This file was autogenerated by uv via the following command:
|
| 3 |
-
# uv pip compile requirements.in -o requirements.txt --python-version 3.11 --python-platform linux
|
|
|
|
|
|
|
|
|
|
| 4 |
annotated-doc==0.0.5
|
| 5 |
# via
|
| 6 |
# fastapi
|
|
@@ -118,7 +120,7 @@ scipy==1.17.1
|
|
| 118 |
# via
|
| 119 |
# scikit-learn
|
| 120 |
# sentence-transformers
|
| 121 |
-
semantic-backend @ git+https://github.com/rachit-mangawa/Semantic-Comment-Analyze.git@
|
| 122 |
# via -r requirements.in
|
| 123 |
sentence-transformers==6.2.0
|
| 124 |
# via semantic-backend
|
|
@@ -141,9 +143,7 @@ tokenizers==0.23.3
|
|
| 141 |
# sentence-transformers
|
| 142 |
# transformers
|
| 143 |
torch==2.14.1+cpu
|
| 144 |
-
# via
|
| 145 |
-
# semantic-backend
|
| 146 |
-
# sentence-transformers
|
| 147 |
tqdm==4.66.5
|
| 148 |
# via
|
| 149 |
# huggingface-hub
|
|
@@ -178,13 +178,10 @@ typing-inspection==0.4.4
|
|
| 178 |
tzdata==2026.5
|
| 179 |
# via python-dateutil
|
| 180 |
uvicorn==0.54.0
|
| 181 |
-
# via
|
| 182 |
-
# -r requirements.in
|
| 183 |
-
# semantic-backend
|
| 184 |
uvloop==0.23.0
|
| 185 |
# via uvicorn
|
| 186 |
watchfiles==1.3.0
|
| 187 |
# via uvicorn
|
| 188 |
websockets==17.3
|
| 189 |
# via uvicorn
|
| 190 |
-
|
|
|
|
|
|
|
| 1 |
# This file was autogenerated by uv via the following command:
|
| 2 |
+
# uv pip compile requirements.in -o requirements.txt --python-version 3.11 --python-platform linux --extra-index-url https://download.pytorch.org/whl/cpu --emit-index-url
|
| 3 |
+
--index-url https://pypi.org/simple
|
| 4 |
+
--extra-index-url https://download.pytorch.org/whl/cpu
|
| 5 |
+
|
| 6 |
annotated-doc==0.0.5
|
| 7 |
# via
|
| 8 |
# fastapi
|
|
|
|
| 120 |
# via
|
| 121 |
# scikit-learn
|
| 122 |
# sentence-transformers
|
| 123 |
+
semantic-backend @ git+https://github.com/rachit-mangawa/Semantic-Comment-Analyze.git@480d2c3203f4773e805f8f58e63c1f1be315d6d1#subdirectory=backend
|
| 124 |
# via -r requirements.in
|
| 125 |
sentence-transformers==6.2.0
|
| 126 |
# via semantic-backend
|
|
|
|
| 143 |
# sentence-transformers
|
| 144 |
# transformers
|
| 145 |
torch==2.14.1+cpu
|
| 146 |
+
# via sentence-transformers
|
|
|
|
|
|
|
| 147 |
tqdm==4.66.5
|
| 148 |
# via
|
| 149 |
# huggingface-hub
|
|
|
|
| 178 |
tzdata==2026.5
|
| 179 |
# via python-dateutil
|
| 180 |
uvicorn==0.54.0
|
| 181 |
+
# via -r requirements.in
|
|
|
|
|
|
|
| 182 |
uvloop==0.23.0
|
| 183 |
# via uvicorn
|
| 184 |
watchfiles==1.3.0
|
| 185 |
# via uvicorn
|
| 186 |
websockets==17.3
|
| 187 |
# via uvicorn
|
|
|
tests/test_host.py
CHANGED
|
@@ -1,53 +1,39 @@
|
|
| 1 |
import asyncio
|
|
|
|
| 2 |
import os
|
| 3 |
import sys
|
| 4 |
-
|
| 5 |
-
root_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
| 6 |
-
if root_dir not in sys.path:
|
| 7 |
-
sys.path.insert(0, root_dir)
|
| 8 |
-
|
| 9 |
-
import logging
|
| 10 |
import types
|
| 11 |
import pytest
|
| 12 |
from fastapi import APIRouter
|
| 13 |
from fastapi.testclient import TestClient
|
| 14 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
def create_mock_modules():
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
|
| 21 |
-
@
|
| 22 |
-
def
|
| 23 |
-
return {"project": "
|
| 24 |
|
| 25 |
-
@
|
| 26 |
-
def
|
| 27 |
-
return {"project": "
|
| 28 |
|
| 29 |
-
@
|
| 30 |
-
async def
|
| 31 |
await asyncio.sleep(0.5)
|
| 32 |
return {"status": "finished"}
|
| 33 |
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
sys.modules["
|
| 37 |
-
sys.modules["
|
| 38 |
-
|
| 39 |
-
p2 = types.ModuleType("mock_p2")
|
| 40 |
-
p2_router_mod = types.ModuleType("mock_p2.router")
|
| 41 |
-
r2 = APIRouter()
|
| 42 |
-
|
| 43 |
-
@r2.get("/ping")
|
| 44 |
-
def ping_p2():
|
| 45 |
-
return {"project": "p2", "status": "ok"}
|
| 46 |
-
|
| 47 |
-
p2_router_mod.router = r2
|
| 48 |
-
p2.router = p2_router_mod
|
| 49 |
-
sys.modules["mock_p2"] = p2
|
| 50 |
-
sys.modules["mock_p2.router"] = p2_router_mod
|
| 51 |
|
| 52 |
|
| 53 |
def test_host_with_mocks(monkeypatch, caplog):
|
|
@@ -60,9 +46,8 @@ def test_host_with_mocks(monkeypatch, caplog):
|
|
| 60 |
|
| 61 |
import registry
|
| 62 |
registry.PROJECT_REGISTRY = {
|
| 63 |
-
"/
|
| 64 |
-
"/
|
| 65 |
-
"/project-3": "nonexistent_module.router:router",
|
| 66 |
}
|
| 67 |
|
| 68 |
if "main" in sys.modules:
|
|
@@ -72,58 +57,56 @@ def test_host_with_mocks(monkeypatch, caplog):
|
|
| 72 |
import main
|
| 73 |
client = TestClient(main.app)
|
| 74 |
|
| 75 |
-
assert "[/
|
| 76 |
-
assert "[/
|
| 77 |
-
assert "[/project-3] Failed to mount router" in caplog.text
|
| 78 |
|
| 79 |
res_health = client.get("/health")
|
| 80 |
assert res_health.status_code == 200
|
| 81 |
health_data = res_health.json()
|
| 82 |
assert health_data["status"] == "degraded"
|
| 83 |
-
assert "/
|
| 84 |
-
assert "/
|
| 85 |
-
assert "/project-3" in health_data["failed"]
|
| 86 |
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
assert
|
|
|
|
| 90 |
|
| 91 |
-
|
| 92 |
-
assert
|
| 93 |
-
assert
|
| 94 |
|
| 95 |
for doc_url in ["/docs", "/redoc", "/openapi.json"]:
|
| 96 |
assert client.get(doc_url).status_code == 404
|
| 97 |
|
| 98 |
cors_allowed = client.get(
|
| 99 |
-
"/
|
| 100 |
headers={"Origin": "https://allowed.com"}
|
| 101 |
)
|
| 102 |
assert cors_allowed.status_code == 200
|
| 103 |
assert cors_allowed.headers.get("access-control-allow-origin") == "https://allowed.com"
|
| 104 |
|
| 105 |
cors_rejected = client.get(
|
| 106 |
-
"/
|
| 107 |
headers={"Origin": "https://disallowed.com"}
|
| 108 |
)
|
| 109 |
assert cors_rejected.headers.get("access-control-allow-origin") is None
|
| 110 |
|
| 111 |
oversized_data = {"text": "a" * 2000}
|
| 112 |
-
res_oversized = client.post("/
|
| 113 |
assert res_oversized.status_code == 413
|
| 114 |
|
| 115 |
-
res_slow = client.get("/
|
| 116 |
assert res_slow.status_code == 504
|
| 117 |
|
| 118 |
|
| 119 |
def test_kill_switch(monkeypatch):
|
| 120 |
create_mock_modules()
|
| 121 |
-
monkeypatch.setenv("DISABLE_PROJECTS", "
|
| 122 |
|
| 123 |
import registry
|
| 124 |
registry.PROJECT_REGISTRY = {
|
| 125 |
-
"/
|
| 126 |
-
"/project-2": "mock_p2.router:router",
|
| 127 |
}
|
| 128 |
|
| 129 |
if "main" in sys.modules:
|
|
@@ -134,12 +117,10 @@ def test_kill_switch(monkeypatch):
|
|
| 134 |
|
| 135 |
res_health = client.get("/health")
|
| 136 |
health_data = res_health.json()
|
| 137 |
-
assert "/
|
| 138 |
-
assert "/
|
| 139 |
-
assert "/project-2" not in health_data["failed"]
|
| 140 |
|
| 141 |
-
assert client.get("/
|
| 142 |
-
assert client.get("/project-2/ping").status_code == 404
|
| 143 |
|
| 144 |
|
| 145 |
def test_healthy_status_when_all_load(monkeypatch):
|
|
@@ -148,8 +129,7 @@ def test_healthy_status_when_all_load(monkeypatch):
|
|
| 148 |
|
| 149 |
import registry
|
| 150 |
registry.PROJECT_REGISTRY = {
|
| 151 |
-
"/
|
| 152 |
-
"/project-2": "mock_p2.router:router",
|
| 153 |
}
|
| 154 |
|
| 155 |
if "main" in sys.modules:
|
|
@@ -161,3 +141,33 @@ def test_healthy_status_when_all_load(monkeypatch):
|
|
| 161 |
res_health = client.get("/health")
|
| 162 |
assert res_health.status_code == 200
|
| 163 |
assert res_health.json()["status"] == "healthy"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import asyncio
|
| 2 |
+
import logging
|
| 3 |
import os
|
| 4 |
import sys
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
import types
|
| 6 |
import pytest
|
| 7 |
from fastapi import APIRouter
|
| 8 |
from fastapi.testclient import TestClient
|
| 9 |
|
| 10 |
+
root_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
| 11 |
+
if root_dir not in sys.path:
|
| 12 |
+
sys.path.insert(0, root_dir)
|
| 13 |
+
|
| 14 |
|
| 15 |
def create_mock_modules():
|
| 16 |
+
sem = types.ModuleType("mock_semantic")
|
| 17 |
+
sem_router_mod = types.ModuleType("mock_semantic.router")
|
| 18 |
+
r = APIRouter()
|
| 19 |
|
| 20 |
+
@r.get("/ping")
|
| 21 |
+
def ping():
|
| 22 |
+
return {"project": "semantic", "status": "ok"}
|
| 23 |
|
| 24 |
+
@r.post("/echo")
|
| 25 |
+
def echo(data: dict):
|
| 26 |
+
return {"project": "semantic", "echo": data}
|
| 27 |
|
| 28 |
+
@r.get("/slow")
|
| 29 |
+
async def slow():
|
| 30 |
await asyncio.sleep(0.5)
|
| 31 |
return {"status": "finished"}
|
| 32 |
|
| 33 |
+
sem_router_mod.router = r
|
| 34 |
+
sem.router = sem_router_mod
|
| 35 |
+
sys.modules["mock_semantic"] = sem
|
| 36 |
+
sys.modules["mock_semantic.router"] = sem_router_mod
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
|
| 38 |
|
| 39 |
def test_host_with_mocks(monkeypatch, caplog):
|
|
|
|
| 46 |
|
| 47 |
import registry
|
| 48 |
registry.PROJECT_REGISTRY = {
|
| 49 |
+
"/semantic": "mock_semantic.router:router",
|
| 50 |
+
"/broken": "nonexistent_module.router:router",
|
|
|
|
| 51 |
}
|
| 52 |
|
| 53 |
if "main" in sys.modules:
|
|
|
|
| 57 |
import main
|
| 58 |
client = TestClient(main.app)
|
| 59 |
|
| 60 |
+
assert "[/semantic] Successfully mounted at /semantic" in caplog.text
|
| 61 |
+
assert "[/broken] Failed to mount router" in caplog.text
|
|
|
|
| 62 |
|
| 63 |
res_health = client.get("/health")
|
| 64 |
assert res_health.status_code == 200
|
| 65 |
health_data = res_health.json()
|
| 66 |
assert health_data["status"] == "degraded"
|
| 67 |
+
assert "/semantic" in health_data["loaded"]
|
| 68 |
+
assert "/broken" in health_data["failed"]
|
|
|
|
| 69 |
|
| 70 |
+
# Verify GET / returns same info as /health
|
| 71 |
+
res_root = client.get("/")
|
| 72 |
+
assert res_root.status_code == 200
|
| 73 |
+
assert res_root.json() == health_data
|
| 74 |
|
| 75 |
+
res_sem = client.get("/semantic/ping")
|
| 76 |
+
assert res_sem.status_code == 200
|
| 77 |
+
assert res_sem.json() == {"project": "semantic", "status": "ok"}
|
| 78 |
|
| 79 |
for doc_url in ["/docs", "/redoc", "/openapi.json"]:
|
| 80 |
assert client.get(doc_url).status_code == 404
|
| 81 |
|
| 82 |
cors_allowed = client.get(
|
| 83 |
+
"/semantic/ping",
|
| 84 |
headers={"Origin": "https://allowed.com"}
|
| 85 |
)
|
| 86 |
assert cors_allowed.status_code == 200
|
| 87 |
assert cors_allowed.headers.get("access-control-allow-origin") == "https://allowed.com"
|
| 88 |
|
| 89 |
cors_rejected = client.get(
|
| 90 |
+
"/semantic/ping",
|
| 91 |
headers={"Origin": "https://disallowed.com"}
|
| 92 |
)
|
| 93 |
assert cors_rejected.headers.get("access-control-allow-origin") is None
|
| 94 |
|
| 95 |
oversized_data = {"text": "a" * 2000}
|
| 96 |
+
res_oversized = client.post("/semantic/echo", json=oversized_data)
|
| 97 |
assert res_oversized.status_code == 413
|
| 98 |
|
| 99 |
+
res_slow = client.get("/semantic/slow")
|
| 100 |
assert res_slow.status_code == 504
|
| 101 |
|
| 102 |
|
| 103 |
def test_kill_switch(monkeypatch):
|
| 104 |
create_mock_modules()
|
| 105 |
+
monkeypatch.setenv("DISABLE_PROJECTS", "semantic")
|
| 106 |
|
| 107 |
import registry
|
| 108 |
registry.PROJECT_REGISTRY = {
|
| 109 |
+
"/semantic": "mock_semantic.router:router",
|
|
|
|
| 110 |
}
|
| 111 |
|
| 112 |
if "main" in sys.modules:
|
|
|
|
| 117 |
|
| 118 |
res_health = client.get("/health")
|
| 119 |
health_data = res_health.json()
|
| 120 |
+
assert "/semantic" not in health_data["loaded"]
|
| 121 |
+
assert "/semantic" not in health_data["failed"]
|
|
|
|
| 122 |
|
| 123 |
+
assert client.get("/semantic/ping").status_code == 404
|
|
|
|
| 124 |
|
| 125 |
|
| 126 |
def test_healthy_status_when_all_load(monkeypatch):
|
|
|
|
| 129 |
|
| 130 |
import registry
|
| 131 |
registry.PROJECT_REGISTRY = {
|
| 132 |
+
"/semantic": "mock_semantic.router:router",
|
|
|
|
| 133 |
}
|
| 134 |
|
| 135 |
if "main" in sys.modules:
|
|
|
|
| 141 |
res_health = client.get("/health")
|
| 142 |
assert res_health.status_code == 200
|
| 143 |
assert res_health.json()["status"] == "healthy"
|
| 144 |
+
assert "/semantic" in res_health.json()["loaded"]
|
| 145 |
+
|
| 146 |
+
res_root = client.get("/")
|
| 147 |
+
assert res_root.status_code == 200
|
| 148 |
+
assert res_root.json() == res_health.json()
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
def test_real_registry():
|
| 152 |
+
import importlib
|
| 153 |
+
import registry
|
| 154 |
+
importlib.reload(registry)
|
| 155 |
+
assert list(registry.PROJECT_REGISTRY.keys()) == ["/semantic"]
|
| 156 |
+
assert registry.PROJECT_REGISTRY["/semantic"] == "semantic_backend.router:router"
|
| 157 |
+
|
| 158 |
+
if "main" in sys.modules:
|
| 159 |
+
del sys.modules["main"]
|
| 160 |
+
|
| 161 |
+
import main
|
| 162 |
+
client = TestClient(main.app)
|
| 163 |
+
|
| 164 |
+
res = client.get("/health")
|
| 165 |
+
assert res.status_code == 200
|
| 166 |
+
data = res.json()
|
| 167 |
+
assert data["status"] == "healthy"
|
| 168 |
+
assert "/semantic" in data["loaded"]
|
| 169 |
+
assert not data["failed"]
|
| 170 |
+
|
| 171 |
+
res_root = client.get("/")
|
| 172 |
+
assert res_root.status_code == 200
|
| 173 |
+
assert res_root.json() == data
|