IDKHowToCodeFr commited on
Commit
677e796
·
1 Parent(s): 846dbfc

feat: pin semantic-backend v1.2, set /semantic route, and harden container permissions

Browse files
Files changed (6) hide show
  1. Dockerfile +4 -1
  2. main.py +3 -3
  3. registry.py +0 -1
  4. requirements.in +1 -1
  5. requirements.txt +7 -10
  6. 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="/docs",
58
- redoc_url="/redoc",
59
- openapi_url="/openapi.json",
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.1#subdirectory=backend
 
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@a123207a1239cb9ff59a09e597fe80dfc635aeec#subdirectory=backend
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
- p1 = types.ModuleType("mock_p1")
18
- p1_router_mod = types.ModuleType("mock_p1.router")
19
- r1 = APIRouter()
20
 
21
- @r1.get("/ping")
22
- def ping_p1():
23
- return {"project": "p1", "status": "ok"}
24
 
25
- @r1.post("/echo")
26
- def echo_p1(data: dict):
27
- return {"project": "p1", "echo": data}
28
 
29
- @r1.get("/slow")
30
- async def slow_p1():
31
  await asyncio.sleep(0.5)
32
  return {"status": "finished"}
33
 
34
- p1_router_mod.router = r1
35
- p1.router = p1_router_mod
36
- sys.modules["mock_p1"] = p1
37
- sys.modules["mock_p1.router"] = p1_router_mod
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
- "/project-1": "mock_p1.router:router",
64
- "/project-2": "mock_p2.router:router",
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 "[/project-1] Successfully mounted at /project-1" in caplog.text
76
- assert "[/project-2] Successfully mounted at /project-2" in caplog.text
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 "/project-1" in health_data["loaded"]
84
- assert "/project-2" in health_data["loaded"]
85
- assert "/project-3" in health_data["failed"]
86
 
87
- res_p1 = client.get("/project-1/ping")
88
- assert res_p1.status_code == 200
89
- assert res_p1.json() == {"project": "p1", "status": "ok"}
 
90
 
91
- res_p2 = client.get("/project-2/ping")
92
- assert res_p2.status_code == 200
93
- assert res_p2.json() == {"project": "p2", "status": "ok"}
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
- "/project-1/ping",
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
- "/project-1/ping",
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("/project-1/echo", json=oversized_data)
113
  assert res_oversized.status_code == 413
114
 
115
- res_slow = client.get("/project-1/slow")
116
  assert res_slow.status_code == 504
117
 
118
 
119
  def test_kill_switch(monkeypatch):
120
  create_mock_modules()
121
- monkeypatch.setenv("DISABLE_PROJECTS", "project-2")
122
 
123
  import registry
124
  registry.PROJECT_REGISTRY = {
125
- "/project-1": "mock_p1.router:router",
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 "/project-1" in health_data["loaded"]
138
- assert "/project-2" not in health_data["loaded"]
139
- assert "/project-2" not in health_data["failed"]
140
 
141
- assert client.get("/project-1/ping").status_code == 200
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
- "/project-1": "mock_p1.router:router",
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