Code-API commited on
Commit
62aa98f
·
1 Parent(s): 39f50c2

feat: cleanup junks

Browse files
.gitignore CHANGED
@@ -6,6 +6,7 @@ postman_collection.json
6
  # Runtime data (vector stores, SQLite DBs, model caches, etc.)
7
  data/
8
  *.so
 
9
 
10
  .Python
11
  build/
@@ -127,5 +128,5 @@ persistence/
127
  local_deploy.py
128
  test_vector_store_async.py
129
  deploy_sdk.py
130
-
131
  deploy_hf.py
 
6
  # Runtime data (vector stores, SQLite DBs, model caches, etc.)
7
  data/
8
  *.so
9
+ test_deploy_flow.py
10
 
11
  .Python
12
  build/
 
128
  local_deploy.py
129
  test_vector_store_async.py
130
  deploy_sdk.py
131
+ tests
132
  deploy_hf.py
app/api/server.py CHANGED
@@ -13,7 +13,6 @@ from app.core.database import pool_manager
13
  from app.core.logger import get_logger
14
  from app.core.redis_client import create_redis_client, close_redis
15
  from app.core.scripts import load_scripts
16
- from app.core.vector_store.deps import init_vector_store_db
17
  from app.services.embeddings_service import EmbeddingService
18
  from app.services.vector_store_service import VectorStoreService
19
  from app.api.v1.router import api_v1_router
@@ -49,14 +48,12 @@ async def lifespan(app: FastAPI):
49
  _logger.info("Authentication database initialized")
50
 
51
  _logger.info("Initializing vector store database...")
52
- await init_vector_store_db()
53
  await _vector_store_service.init_db()
54
  _logger.info("Vector store database initialized with %d stores", len(_vector_store_service.list_stores()))
55
 
56
  _logger.info("Initializing embedding service (loading 384-dim model)...")
57
  loop = asyncio.get_running_loop()
58
  await loop.run_in_executor(None, _embedding_service.load_model, 384)
59
- # await loop.run_in_executor(None, _embedding_service.load_vision_model) # DISABLED (OOM mitigation)
60
  _logger.info("Embedding service initialized with dims: %s", _embedding_service.loaded_dimensions)
61
  _logger.info("Vector store service initialized with %d existing stores", len(_vector_store_service.list_stores()))
62
 
 
13
  from app.core.logger import get_logger
14
  from app.core.redis_client import create_redis_client, close_redis
15
  from app.core.scripts import load_scripts
 
16
  from app.services.embeddings_service import EmbeddingService
17
  from app.services.vector_store_service import VectorStoreService
18
  from app.api.v1.router import api_v1_router
 
48
  _logger.info("Authentication database initialized")
49
 
50
  _logger.info("Initializing vector store database...")
 
51
  await _vector_store_service.init_db()
52
  _logger.info("Vector store database initialized with %d stores", len(_vector_store_service.list_stores()))
53
 
54
  _logger.info("Initializing embedding service (loading 384-dim model)...")
55
  loop = asyncio.get_running_loop()
56
  await loop.run_in_executor(None, _embedding_service.load_model, 384)
 
57
  _logger.info("Embedding service initialized with dims: %s", _embedding_service.loaded_dimensions)
58
  _logger.info("Vector store service initialized with %d existing stores", len(_vector_store_service.list_stores()))
59
 
app/api/v1/auth.py CHANGED
@@ -7,7 +7,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
7
 
8
  from app.config import get_settings
9
  from app.core.auth.deps import get_current_user, get_db, get_temp_db_warning, require_application_id
10
- from app.core.auth.models import RefreshSession, User
11
  from app.core.auth.schemas import (
12
  ChangePasswordSchema,
13
  ForgotPasswordSchema,
@@ -24,7 +24,6 @@ from app.core.auth.schemas import (
24
  UpdateProfileSchema,
25
  UserSchemaField,
26
  UserSchemaResponse,
27
- DataResponse,
28
  )
29
  from app.services.auth_service import AuthService
30
 
 
7
 
8
  from app.config import get_settings
9
  from app.core.auth.deps import get_current_user, get_db, get_temp_db_warning, require_application_id
10
+ from app.core.auth.models import User
11
  from app.core.auth.schemas import (
12
  ChangePasswordSchema,
13
  ForgotPasswordSchema,
 
24
  UpdateProfileSchema,
25
  UserSchemaField,
26
  UserSchemaResponse,
 
27
  )
28
  from app.services.auth_service import AuthService
29
 
app/api/v1/batch.py CHANGED
@@ -1,16 +1,16 @@
1
  from __future__ import annotations
2
 
3
  import asyncio
4
- import concurrent.futures
5
  import json as json_mod
6
- import os
7
  import time
8
  from typing import Annotated, List, Optional
 
9
 
10
  import httpx
11
  from fastapi import APIRouter, Depends, File, Form, HTTPException, Query, UploadFile
12
 
13
  from app.api.deps import get_converter_service, get_extraction_service, get_text_cleaner_service, require_auth
 
14
  from app.config import get_settings
15
  from app.core.logger import get_logger
16
  from app.models.domain import ConversionError
@@ -18,7 +18,6 @@ from app.models.schemas import (
18
  BatchFileResult,
19
  BatchResponse,
20
  BatchUrlRequest,
21
- ConversionMetadata,
22
  )
23
  from app.services.converter_service import ConverterService
24
  from app.services.extraction_service import ExtractionService
@@ -29,21 +28,6 @@ _logger = get_logger(__name__)
29
  _settings = get_settings()
30
  _MAX_UPLOAD_BYTES = _settings.max_upload_bytes
31
  _MAX_BATCH_FILES = _settings.max_batch_files
32
- _MAX_WORKERS = min(32, (os.cpu_count() or 1) + 4)
33
- _thread_pool = concurrent.futures.ThreadPoolExecutor(max_workers=_MAX_WORKERS)
34
-
35
-
36
- def _build_metadata(result) -> ConversionMetadata:
37
- return ConversionMetadata(
38
- source=result.source,
39
- char_count=result.char_count,
40
- word_count=result.word_count,
41
- line_count=result.line_count,
42
- file_size_bytes=result.file_size_bytes,
43
- mime_type=result.mime_type,
44
- content_hash=result.content_hash,
45
- token_estimate=result.token_estimate,
46
- )
47
 
48
 
49
  def _batch_result_from_error(name: str, err: ConversionError) -> BatchFileResult:
@@ -165,7 +149,6 @@ async def batch_urls(
165
 
166
  async def process_single_url(url: str) -> BatchFileResult:
167
  _logger.info("Batch processing URL: %s", url)
168
- from urllib.parse import urlparse
169
  parsed = urlparse(url)
170
  filename = parsed.path.split("/")[-1] or "url_content"
171
 
 
1
  from __future__ import annotations
2
 
3
  import asyncio
 
4
  import json as json_mod
 
5
  import time
6
  from typing import Annotated, List, Optional
7
+ from urllib.parse import urlparse
8
 
9
  import httpx
10
  from fastapi import APIRouter, Depends, File, Form, HTTPException, Query, UploadFile
11
 
12
  from app.api.deps import get_converter_service, get_extraction_service, get_text_cleaner_service, require_auth
13
+ from app.api.v1.convert import _build_metadata, _thread_pool
14
  from app.config import get_settings
15
  from app.core.logger import get_logger
16
  from app.models.domain import ConversionError
 
18
  BatchFileResult,
19
  BatchResponse,
20
  BatchUrlRequest,
 
21
  )
22
  from app.services.converter_service import ConverterService
23
  from app.services.extraction_service import ExtractionService
 
28
  _settings = get_settings()
29
  _MAX_UPLOAD_BYTES = _settings.max_upload_bytes
30
  _MAX_BATCH_FILES = _settings.max_batch_files
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
 
32
 
33
  def _batch_result_from_error(name: str, err: ConversionError) -> BatchFileResult:
 
149
 
150
  async def process_single_url(url: str) -> BatchFileResult:
151
  _logger.info("Batch processing URL: %s", url)
 
152
  parsed = urlparse(url)
153
  filename = parsed.path.split("/")[-1] or "url_content"
154
 
app/api/v1/convert.py CHANGED
@@ -2,14 +2,15 @@ from __future__ import annotations
2
 
3
  import asyncio
4
  import concurrent.futures
 
5
  import json as json_mod
6
  import os
7
  from typing import Annotated, Any, Dict, Optional
8
  from urllib.parse import urlparse
9
 
10
  import httpx
 
11
  from fastapi import APIRouter, Depends, File, Form, HTTPException, Query, UploadFile, status
12
- from pydantic import BaseModel
13
 
14
  from app.config import get_settings
15
  from app.api.deps import (
@@ -83,7 +84,6 @@ async def _build_response(
83
  if clean_content and content != result.markdown:
84
  lines = content.splitlines()
85
  words = content.split()
86
- import hashlib
87
  metadata = ConversionMetadata(
88
  source=result.source,
89
  char_count=len(content),
@@ -171,7 +171,6 @@ async def convert_file(
171
  content = await loop.run_in_executor(_thread_pool, text_cleaner_service.clean, content)
172
 
173
  if plain_text:
174
- from fastapi.responses import PlainTextResponse
175
  return PlainTextResponse(content)
176
 
177
  response = await _build_response(
 
2
 
3
  import asyncio
4
  import concurrent.futures
5
+ import hashlib
6
  import json as json_mod
7
  import os
8
  from typing import Annotated, Any, Dict, Optional
9
  from urllib.parse import urlparse
10
 
11
  import httpx
12
+ from fastapi.responses import PlainTextResponse
13
  from fastapi import APIRouter, Depends, File, Form, HTTPException, Query, UploadFile, status
 
14
 
15
  from app.config import get_settings
16
  from app.api.deps import (
 
84
  if clean_content and content != result.markdown:
85
  lines = content.splitlines()
86
  words = content.split()
 
87
  metadata = ConversionMetadata(
88
  source=result.source,
89
  char_count=len(content),
 
171
  content = await loop.run_in_executor(_thread_pool, text_cleaner_service.clean, content)
172
 
173
  if plain_text:
 
174
  return PlainTextResponse(content)
175
 
176
  response = await _build_response(
app/api/v1/embeddings.py CHANGED
@@ -2,19 +2,14 @@ from __future__ import annotations
2
 
3
  import asyncio
4
  import concurrent.futures
5
- # import io # DISABLED (OOM mitigation) — only used by vision
6
  import os
7
  import time
8
- from typing import Annotated, List, Optional
9
-
10
- # import httpx # DISABLED (OOM mitigation) — only used by vision
11
- from fastapi import APIRouter, Depends, File, Form, HTTPException, UploadFile, status
12
- # from PIL import Image # DISABLED (OOM mitigation)
13
 
14
  from app.api.deps import require_auth, get_embeddings_service
15
  from app.config import get_settings
16
  from app.core.logger import get_logger
17
- from app.models.schemas import EmbeddingItem, EmbeddingRequest, EmbeddingResponse # , VisionUrlRequest # DISABLED (OOM mitigation)
18
  from app.services.embeddings_service import EmbeddingService
19
 
20
  router = APIRouter()
@@ -22,41 +17,6 @@ _logger = get_logger(__name__)
22
  _settings = get_settings()
23
  _MAX_WORKERS = min(32, (os.cpu_count() or 1) + 4)
24
  _thread_pool = concurrent.futures.ThreadPoolExecutor(max_workers=_MAX_WORKERS)
25
- # _MAX_VISION_ITEMS = 5 # DISABLED (OOM mitigation)
26
- # _MAX_IMAGE_BYTES = 15 * 1024 * 1024 # DISABLED (OOM mitigation)
27
-
28
-
29
- # def _validate_image(raw: bytes, source: str) -> Image.Image: # DISABLED (OOM mitigation)
30
- # # FIX: Check if the file is completely empty (0 bytes)
31
- # if not raw:
32
- # raise ValueError(f"File {source} is empty (0 bytes).")
33
- #
34
- # if len(raw) > _MAX_IMAGE_BYTES:
35
- # raise ValueError(f"Image {source} exceeds 15 MB limit")
36
- #
37
- # try:
38
- # img = Image.open(io.BytesIO(raw))
39
- # img.load()
40
- # if img.mode != "RGB":
41
- # img = img.convert("RGB")
42
- # return img
43
- # except Exception as exc:
44
- # raise ValueError(f"Invalid image {source}: {exc}")
45
-
46
-
47
- # async def _download_image(url: str) -> bytes: # DISABLED (OOM mitigation)
48
- # try:
49
- # async with httpx.AsyncClient(timeout=30.0, follow_redirects=True) as client:
50
- # resp = await client.get(url)
51
- # resp.raise_for_status()
52
- # ctype = resp.headers.get("content-type", "")
53
- # if not ctype.startswith("image/"):
54
- # raise ValueError(f"URL {url} returned non-image Content-Type: {ctype}")
55
- # return resp.content
56
- # except httpx.HTTPError as exc:
57
- # raise ValueError(f"Failed to download {url}: {exc}")
58
-
59
-
60
  @router.post(
61
  "/embeddings",
62
  response_model=EmbeddingResponse,
@@ -126,185 +86,3 @@ async def create_embeddings(
126
  )
127
 
128
 
129
- # @router.post( # DISABLED (OOM mitigation)
130
- # "/embeddings/vision/file",
131
- # response_model=EmbeddingResponse,
132
- # summary="Generate embeddings from uploaded images",
133
- # )
134
- # async def create_vision_embeddings_file(
135
- # files: Annotated[List[UploadFile], File(description="Image files to embed (max 5)")],
136
- # token: str = Depends(require_auth),
137
- # embedding_service: EmbeddingService = Depends(get_embeddings_service),
138
- # ) -> EmbeddingResponse:
139
- # if not files:
140
- # raise HTTPException(status_code=400, detail={"success": False, "message": "No files provided."})
141
- # if len(files) > _MAX_VISION_ITEMS:
142
- # raise HTTPException(status_code=400, detail={"success": False, "message": f"Maximum {_MAX_VISION_ITEMS} images per request."})
143
- #
144
- # if not embedding_service._vision_loaded:
145
- # raise HTTPException(status_code=503, detail={"success": False, "message": "Vision model not loaded."})
146
- #
147
- # _logger.info("Vision embedding file request: files=%s", len(files))
148
- #
149
- # dim = embedding_service.vision_dimension
150
- # start = time.perf_counter()
151
- # images: List[Image.Image] = []
152
- # item_results: List[EmbeddingItem] = []
153
- #
154
- # for f in files:
155
- # t0 = time.perf_counter()
156
- # try:
157
- # # FIX: Guarantee the file cursor is at the beginning before reading!
158
- # await f.seek(0)
159
- # raw = await f.read()
160
- #
161
- # img = await asyncio.get_running_loop().run_in_executor(_thread_pool, _validate_image, raw, f.filename or "unknown")
162
- # images.append(img)
163
- # except Exception as exc:
164
- # elapsed = (time.perf_counter() - t0) * 1000
165
- # item_results.append(EmbeddingItem(
166
- # success=False,
167
- # time_ms=round(elapsed, 3),
168
- # error_message=str(exc),
169
- # ))
170
- #
171
- # if not images:
172
- # total_ms = (time.perf_counter() - start) * 1000
173
- # return EmbeddingResponse(
174
- # success=False,
175
- # time_ms=round(total_ms, 3),
176
- # success_count=0,
177
- # failed_count=len(item_results),
178
- # error_message="No valid images could be processed.",
179
- # results=item_results,
180
- # )
181
- #
182
- # try:
183
- # loop = asyncio.get_running_loop()
184
- # vectors = await loop.run_in_executor(
185
- # _thread_pool,
186
- # embedding_service.generate_image_embedding,
187
- # images,
188
- # )
189
- # except Exception as exc:
190
- # elapsed = (time.perf_counter() - start) * 1000
191
- # _logger.error("Vision embedding error: %s", exc)
192
- # for _ in range(len(images) - len(item_results)):
193
- # item_results.append(EmbeddingItem(success=False, time_ms=0, error_message=str(exc)))
194
- # return EmbeddingResponse(
195
- # success=False,
196
- # time_ms=round(elapsed, 3),
197
- # success_count=0,
198
- # failed_count=len(item_results),
199
- # error_message=str(exc),
200
- # results=item_results,
201
- # )
202
- #
203
- # total_ms = (time.perf_counter() - start) * 1000
204
- # for i, vec in enumerate(vectors):
205
- # item_results.append(EmbeddingItem(
206
- # success=True,
207
- # time_ms=round(total_ms / len(vectors), 3),
208
- # embeddings=vec,
209
- # dimension=dim,
210
- # ))
211
- #
212
- # success_count = sum(1 for r in item_results if r.success)
213
- # failed_count = len(item_results) - success_count
214
- # _logger.info("Vision embedding success: items=%s, success=%s, failed=%s, total_ms=%s",
215
- # len(item_results), success_count, failed_count, round(total_ms, 3))
216
- # return EmbeddingResponse(
217
- # success=failed_count == 0,
218
- # time_ms=round(total_ms, 3),
219
- # success_count=success_count,
220
- # failed_count=failed_count,
221
- # results=item_results,
222
- # )
223
- #
224
- #
225
- # @router.post( # DISABLED (OOM mitigation)
226
- # "/embeddings/vision/url",
227
- # response_model=EmbeddingResponse,
228
- # summary="Generate embeddings from image URLs",
229
- # )
230
- # async def create_vision_embeddings_url(
231
- # body: VisionUrlRequest,
232
- # token: str = Depends(require_auth),
233
- # embedding_service: EmbeddingService = Depends(get_embeddings_service),
234
- # ) -> EmbeddingResponse:
235
- # if not embedding_service._vision_loaded:
236
- # raise HTTPException(status_code=503, detail={"success": False, "message": "Vision model not loaded."})
237
- #
238
- # _logger.info("Vision embedding URL request: urls=%s", len(body.urls))
239
- #
240
- # dim = embedding_service.vision_dimension
241
- # start = time.perf_counter()
242
- # images: List[Image.Image] = []
243
- # item_results: List[EmbeddingItem] = []
244
- #
245
- # for url in body.urls:
246
- # t0 = time.perf_counter()
247
- # try:
248
- # raw = await _download_image(url)
249
- # img = await asyncio.get_running_loop().run_in_executor(_thread_pool, _validate_image, raw, url)
250
- # images.append(img)
251
- # except Exception as exc:
252
- # elapsed = (time.perf_counter() - t0) * 1000
253
- # item_results.append(EmbeddingItem(
254
- # success=False,
255
- # time_ms=round(elapsed, 3),
256
- # error_message=str(exc),
257
- # ))
258
- #
259
- # if not images:
260
- # total_ms = (time.perf_counter() - start) * 1000
261
- # return EmbeddingResponse(
262
- # success=False,
263
- # time_ms=round(total_ms, 3),
264
- # success_count=0,
265
- # failed_count=len(item_results),
266
- # error_message="No valid images could be downloaded.",
267
- # results=item_results,
268
- # )
269
- #
270
- # try:
271
- # loop = asyncio.get_running_loop()
272
- # vectors = await loop.run_in_executor(
273
- # _thread_pool,
274
- # embedding_service.generate_image_embedding,
275
- # images,
276
- # )
277
- # except Exception as exc:
278
- # elapsed = (time.perf_counter() - start) * 1000
279
- # _logger.error("Vision embedding error: %s", exc)
280
- # for _ in range(len(images) - len(item_results)):
281
- # item_results.append(EmbeddingItem(success=False, time_ms=0, error_message=str(exc)))
282
- # return EmbeddingResponse(
283
- # success=False,
284
- # time_ms=round(elapsed, 3),
285
- # success_count=0,
286
- # failed_count=len(item_results),
287
- # error_message=str(exc),
288
- # results=item_results,
289
- # )
290
- #
291
- # total_ms = (time.perf_counter() - start) * 1000
292
- # for i, vec in enumerate(vectors):
293
- # item_results.append(EmbeddingItem(
294
- # success=True,
295
- # time_ms=round(total_ms / len(vectors), 3),
296
- # embeddings=vec,
297
- # dimension=dim,
298
- # ))
299
- #
300
- # success_count = sum(1 for r in item_results if r.success)
301
- # failed_count = len(item_results) - success_count
302
- # _logger.info("Vision embedding success: items=%s, success=%s, failed=%s, total_ms=%s",
303
- # len(item_results), success_count, failed_count, round(total_ms, 3))
304
- # return EmbeddingResponse(
305
- # success=failed_count == 0,
306
- # time_ms=round(total_ms, 3),
307
- # success_count=success_count,
308
- # failed_count=failed_count,
309
- # results=item_results,
310
- # )
 
2
 
3
  import asyncio
4
  import concurrent.futures
 
5
  import os
6
  import time
7
+ from fastapi import APIRouter, Depends, HTTPException
 
 
 
 
8
 
9
  from app.api.deps import require_auth, get_embeddings_service
10
  from app.config import get_settings
11
  from app.core.logger import get_logger
12
+ from app.models.schemas import EmbeddingItem, EmbeddingRequest, EmbeddingResponse
13
  from app.services.embeddings_service import EmbeddingService
14
 
15
  router = APIRouter()
 
17
  _settings = get_settings()
18
  _MAX_WORKERS = min(32, (os.cpu_count() or 1) + 4)
19
  _thread_pool = concurrent.futures.ThreadPoolExecutor(max_workers=_MAX_WORKERS)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  @router.post(
21
  "/embeddings",
22
  response_model=EmbeddingResponse,
 
86
  )
87
 
88
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/api/v1/reconcile.py CHANGED
@@ -2,7 +2,6 @@ from __future__ import annotations
2
 
3
  import asyncio
4
  import json
5
- import os
6
  import time
7
  import uuid
8
  from typing import Any, Dict, List, Optional
@@ -22,6 +21,7 @@ from app.services.reconciliation_service import (
22
  compare_rows,
23
  compare_schemas,
24
  download_file_with_retry,
 
25
  normalize_dataframe,
26
  read_to_dataframe,
27
  reconcile_columns,
@@ -73,10 +73,6 @@ class ReconciliationResponse(BaseModel):
73
  results: List[ReconciliationPairResult]
74
 
75
 
76
- def _extract_extension(filename: str) -> str:
77
- return os.path.splitext(filename)[1].lstrip(".").lower()
78
-
79
-
80
  def _failed_result(pair_index: int, error: str) -> Dict[str, Any]:
81
  return {
82
  "pair_index": pair_index,
@@ -246,8 +242,8 @@ async def reconcile_files(
246
  continue
247
  if src is None or dst is None:
248
  raise HTTPException(status_code=400, detail={"success": False, "message": f"Pair {idx}: Both source and destination files required."})
249
- src_ext = _extract_extension(src.filename or "")
250
- dst_ext = _extract_extension(dst.filename or "")
251
  _validate_extensions(src_ext, dst_ext, idx)
252
  pairs.append((src, dst, cm, idx, src_ext, dst_ext))
253
 
@@ -284,13 +280,13 @@ async def reconcile_urls(
284
  raise HTTPException(status_code=400, detail={"success": False, "message": "At least 1 pair is required."})
285
 
286
  for idx, pair in enumerate(body.pairs, 1):
287
- _validate_extensions(_extract_extension(pair.source), _extract_extension(pair.destination), idx)
288
 
289
  async with aiohttp.ClientSession() as session:
290
  async def process_url_pair(pair: ReconciliationPair, idx: int) -> Dict[str, Any]:
291
  async with semaphore_jobs:
292
- src_ext = _extract_extension(pair.source)
293
- dst_ext = _extract_extension(pair.destination)
294
 
295
  try:
296
  src_data, dst_data = await asyncio.gather(
 
2
 
3
  import asyncio
4
  import json
 
5
  import time
6
  import uuid
7
  from typing import Any, Dict, List, Optional
 
21
  compare_rows,
22
  compare_schemas,
23
  download_file_with_retry,
24
+ extract_extension,
25
  normalize_dataframe,
26
  read_to_dataframe,
27
  reconcile_columns,
 
73
  results: List[ReconciliationPairResult]
74
 
75
 
 
 
 
 
76
  def _failed_result(pair_index: int, error: str) -> Dict[str, Any]:
77
  return {
78
  "pair_index": pair_index,
 
242
  continue
243
  if src is None or dst is None:
244
  raise HTTPException(status_code=400, detail={"success": False, "message": f"Pair {idx}: Both source and destination files required."})
245
+ src_ext = extract_extension(src.filename or "")
246
+ dst_ext = extract_extension(dst.filename or "")
247
  _validate_extensions(src_ext, dst_ext, idx)
248
  pairs.append((src, dst, cm, idx, src_ext, dst_ext))
249
 
 
280
  raise HTTPException(status_code=400, detail={"success": False, "message": "At least 1 pair is required."})
281
 
282
  for idx, pair in enumerate(body.pairs, 1):
283
+ _validate_extensions(extract_extension(pair.source), extract_extension(pair.destination), idx)
284
 
285
  async with aiohttp.ClientSession() as session:
286
  async def process_url_pair(pair: ReconciliationPair, idx: int) -> Dict[str, Any]:
287
  async with semaphore_jobs:
288
+ src_ext = extract_extension(pair.source)
289
+ dst_ext = extract_extension(pair.destination)
290
 
291
  try:
292
  src_data, dst_data = await asyncio.gather(
app/api/v1/system.py CHANGED
@@ -8,7 +8,7 @@ import time
8
  from datetime import datetime, timezone
9
  from pathlib import Path
10
 
11
- from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status
12
  from fastapi.responses import StreamingResponse
13
 
14
  from app.api.deps import get_extraction_service, require_auth
@@ -40,8 +40,17 @@ _logger = get_logger(__name__)
40
  _MAINTENANCE_MODE: bool = False
41
 
42
 
 
 
 
 
43
  def is_maintenance() -> bool:
44
- return _MAINTENANCE_MODE or os.environ.get("APP_UNDER_MAINTENANCE", "").lower() in ("1", "true", "yes")
 
 
 
 
 
45
 
46
 
47
  @router.get("/health", response_model=HealthResponse, summary="Health check")
@@ -198,6 +207,12 @@ async def upload_and_restore(
198
  async def enable_maintenance(token: str = Depends(require_auth)):
199
  global _MAINTENANCE_MODE
200
  _MAINTENANCE_MODE = True
 
 
 
 
 
 
201
  _logger.warning("Maintenance mode ENABLED — all write operations blocked")
202
  return {"success": True, "message": "Maintenance mode enabled", "maintenance": True}
203
 
@@ -206,14 +221,27 @@ async def enable_maintenance(token: str = Depends(require_auth)):
206
  async def disable_maintenance(token: str = Depends(require_auth)):
207
  global _MAINTENANCE_MODE
208
  _MAINTENANCE_MODE = False
 
 
 
 
 
209
  _logger.warning("Maintenance mode DISABLED — write operations resumed")
210
  return {"success": True, "message": "Maintenance mode disabled", "maintenance": False}
211
 
212
 
213
  @router.get("/maintenance", summary="Check maintenance mode status")
214
  async def maintenance_status(token: str = Depends(require_auth)):
 
 
 
 
 
 
 
 
215
  return {
216
  "success": True,
217
  "maintenance": is_maintenance(),
218
- "source": "api_flag" if _MAINTENANCE_MODE else ("env_var" if os.environ.get("APP_UNDER_MAINTENANCE") else "off"),
219
  }
 
8
  from datetime import datetime, timezone
9
  from pathlib import Path
10
 
11
+ from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
12
  from fastapi.responses import StreamingResponse
13
 
14
  from app.api.deps import get_extraction_service, require_auth
 
40
  _MAINTENANCE_MODE: bool = False
41
 
42
 
43
+ def _maintenance_lock_path() -> Path:
44
+ return _get_data_dir() / ".maintenance.lock"
45
+
46
+
47
  def is_maintenance() -> bool:
48
+ if _MAINTENANCE_MODE:
49
+ return True
50
+ if os.environ.get("APP_UNDER_MAINTENANCE", "").lower() in ("1", "true", "yes"):
51
+ return True
52
+ lock_path = _maintenance_lock_path()
53
+ return lock_path.is_file()
54
 
55
 
56
  @router.get("/health", response_model=HealthResponse, summary="Health check")
 
207
  async def enable_maintenance(token: str = Depends(require_auth)):
208
  global _MAINTENANCE_MODE
209
  _MAINTENANCE_MODE = True
210
+ lock_path = _maintenance_lock_path()
211
+ try:
212
+ lock_path.parent.mkdir(parents=True, exist_ok=True)
213
+ lock_path.write_text(str(time.time()), encoding="utf-8")
214
+ except Exception as exc:
215
+ _logger.warning("Could not write maintenance lock file: %s", exc)
216
  _logger.warning("Maintenance mode ENABLED — all write operations blocked")
217
  return {"success": True, "message": "Maintenance mode enabled", "maintenance": True}
218
 
 
221
  async def disable_maintenance(token: str = Depends(require_auth)):
222
  global _MAINTENANCE_MODE
223
  _MAINTENANCE_MODE = False
224
+ lock_path = _maintenance_lock_path()
225
+ try:
226
+ lock_path.unlink(missing_ok=True)
227
+ except Exception as exc:
228
+ _logger.warning("Could not remove maintenance lock file: %s", exc)
229
  _logger.warning("Maintenance mode DISABLED — write operations resumed")
230
  return {"success": True, "message": "Maintenance mode disabled", "maintenance": False}
231
 
232
 
233
  @router.get("/maintenance", summary="Check maintenance mode status")
234
  async def maintenance_status(token: str = Depends(require_auth)):
235
+ lock_path = _maintenance_lock_path()
236
+ source = "off"
237
+ if _MAINTENANCE_MODE:
238
+ source = "api_flag"
239
+ elif os.environ.get("APP_UNDER_MAINTENANCE"):
240
+ source = "env_var"
241
+ elif lock_path.is_file():
242
+ source = "lock_file"
243
  return {
244
  "success": True,
245
  "maintenance": is_maintenance(),
246
+ "source": source,
247
  }
app/api/v1/vector_stores.py CHANGED
@@ -3,6 +3,7 @@ from __future__ import annotations
3
  import asyncio
4
  import time
5
 
 
6
  from fastapi import APIRouter, Depends, File, Form, HTTPException, Query, UploadFile, status
7
 
8
  from app.api.deps import get_vector_store_service, require_auth
@@ -128,7 +129,7 @@ async def get_vector_store(
128
  embedding_dimension=stats["embedding_dimension"],
129
  document_count=stats["document_count"],
130
  created_at=stats["created_at"],
131
- metadata=stats["metadata_json"],
132
  )
133
 
134
 
@@ -287,8 +288,6 @@ async def ingest_pdf_url(
287
  if record is None:
288
  raise HTTPException(status_code=404, detail=f"Vector store {store_id} not found")
289
 
290
- import httpx
291
-
292
  try:
293
  async with httpx.AsyncClient(timeout=30.0, follow_redirects=True) as client:
294
  resp = await client.get(body.url)
 
3
  import asyncio
4
  import time
5
 
6
+ import httpx
7
  from fastapi import APIRouter, Depends, File, Form, HTTPException, Query, UploadFile, status
8
 
9
  from app.api.deps import get_vector_store_service, require_auth
 
129
  embedding_dimension=stats["embedding_dimension"],
130
  document_count=stats["document_count"],
131
  created_at=stats["created_at"],
132
+ metadata=stats["metadata"],
133
  )
134
 
135
 
 
288
  if record is None:
289
  raise HTTPException(status_code=404, detail=f"Vector store {store_id} not found")
290
 
 
 
291
  try:
292
  async with httpx.AsyncClient(timeout=30.0, follow_redirects=True) as client:
293
  resp = await client.get(body.url)
app/api/v1/webhook_socket.py CHANGED
@@ -1,9 +1,10 @@
1
  from __future__ import annotations
2
 
3
  import asyncio
 
4
  import json
5
 
6
- from fastapi import APIRouter, Depends, Request, WebSocket, WebSocketDisconnect
7
 
8
  from app.api.deps import require_auth
9
  from app.models.schemas import (
@@ -29,7 +30,6 @@ async def create_channel(
29
  token: str = Depends(require_auth),
30
  ):
31
  if body.channel_id and manager.get_channel(body.channel_id):
32
- from fastapi import HTTPException
33
  raise HTTPException(status_code=409, detail=f"Channel '{body.channel_id}' already exists")
34
 
35
  ch = manager.create_channel(
@@ -73,7 +73,6 @@ async def channel_info(
73
  channel_id: str,
74
  token: str = Depends(require_auth),
75
  ):
76
- from fastapi import HTTPException
77
  ch = manager.get_channel(channel_id)
78
  if not ch:
79
  raise HTTPException(status_code=404, detail="Channel not found")
@@ -94,7 +93,6 @@ async def delete_channel(
94
  channel_id: str,
95
  token: str = Depends(require_auth),
96
  ):
97
- from fastapi import HTTPException
98
  if manager.delete_channel(channel_id):
99
  return ChannelDeleteResponse(deleted=channel_id)
100
  raise HTTPException(status_code=404, detail="Channel not found")
@@ -105,7 +103,6 @@ async def handle_webhook(
105
  channel_id: str,
106
  request: Request,
107
  ):
108
- from fastapi import HTTPException
109
  ch = manager.get_channel(channel_id)
110
  if not ch:
111
  raise HTTPException(status_code=404, detail="Channel not found")
@@ -174,7 +171,6 @@ async def websocket_endpoint(
174
  return
175
 
176
  if ch.secret:
177
- import hmac as hmac_mod
178
  if not hmac_mod.compare_digest(secret, ch.secret):
179
  await websocket.accept()
180
  await websocket.send_json({"event": "error", "message": "unauthorized"})
 
1
  from __future__ import annotations
2
 
3
  import asyncio
4
+ import hmac as hmac_mod
5
  import json
6
 
7
+ from fastapi import APIRouter, Depends, HTTPException, Request, WebSocket, WebSocketDisconnect
8
 
9
  from app.api.deps import require_auth
10
  from app.models.schemas import (
 
30
  token: str = Depends(require_auth),
31
  ):
32
  if body.channel_id and manager.get_channel(body.channel_id):
 
33
  raise HTTPException(status_code=409, detail=f"Channel '{body.channel_id}' already exists")
34
 
35
  ch = manager.create_channel(
 
73
  channel_id: str,
74
  token: str = Depends(require_auth),
75
  ):
 
76
  ch = manager.get_channel(channel_id)
77
  if not ch:
78
  raise HTTPException(status_code=404, detail="Channel not found")
 
93
  channel_id: str,
94
  token: str = Depends(require_auth),
95
  ):
 
96
  if manager.delete_channel(channel_id):
97
  return ChannelDeleteResponse(deleted=channel_id)
98
  raise HTTPException(status_code=404, detail="Channel not found")
 
103
  channel_id: str,
104
  request: Request,
105
  ):
 
106
  ch = manager.get_channel(channel_id)
107
  if not ch:
108
  raise HTTPException(status_code=404, detail="Channel not found")
 
171
  return
172
 
173
  if ch.secret:
 
174
  if not hmac_mod.compare_digest(secret, ch.secret):
175
  await websocket.accept()
176
  await websocket.send_json({"event": "error", "message": "unauthorized"})
app/config.py CHANGED
@@ -115,5 +115,3 @@ DEFAULT_STREAM = False
115
  @lru_cache(maxsize=1)
116
  def get_settings() -> Settings:
117
  return Settings()
118
-
119
- # Persistence test marker - v2
 
115
  @lru_cache(maxsize=1)
116
  def get_settings() -> Settings:
117
  return Settings()
 
 
app/core/auth/deps.py CHANGED
@@ -1,7 +1,6 @@
1
  from __future__ import annotations
2
 
3
  import logging
4
- from datetime import datetime, timedelta, timezone
5
  from typing import Annotated, Any, AsyncGenerator, Optional
6
 
7
  import jwt
@@ -38,10 +37,6 @@ AsyncSessionLocal = async_sessionmaker(engine, class_=AsyncSession, expire_on_co
38
  ph = PasswordHasher()
39
 
40
 
41
- def _now() -> datetime:
42
- return datetime.now(timezone.utc)
43
-
44
-
45
  class TempDatabaseWarning:
46
  code: str = "TEMP_DATABASE"
47
  message: str = (
 
1
  from __future__ import annotations
2
 
3
  import logging
 
4
  from typing import Annotated, Any, AsyncGenerator, Optional
5
 
6
  import jwt
 
37
  ph = PasswordHasher()
38
 
39
 
 
 
 
 
40
  class TempDatabaseWarning:
41
  code: str = "TEMP_DATABASE"
42
  message: str = (
app/core/banner.py DELETED
@@ -1 +0,0 @@
1
-
 
 
app/core/database/mongodb.py CHANGED
@@ -99,7 +99,4 @@ class MongoDBExecutor(BaseExecutor):
99
  async def _close_pool(self, pool: AsyncIOMotorClient) -> None:
100
  pool.close()
101
 
102
- async def _get_or_create_pool(self) -> AsyncIOMotorClient:
103
- if self._pool is not None:
104
- return self._pool
105
- return await super()._get_or_create_pool()
 
99
  async def _close_pool(self, pool: AsyncIOMotorClient) -> None:
100
  pool.close()
101
 
102
+
 
 
 
app/core/database/mysql.py CHANGED
@@ -1,7 +1,5 @@
1
  from __future__ import annotations
2
 
3
- import asyncio
4
- import sys
5
  from typing import Any
6
 
7
  import aiomysql
@@ -27,32 +25,6 @@ class MySQLExecutor(BaseExecutor):
27
  pool_recycle=3600,
28
  )
29
 
30
- async def _get_or_create_pool(self) -> aiomysql.Pool:
31
- async with self._lock:
32
- if self._pool is not None:
33
- return self._pool
34
-
35
- last_error: Exception | None = None
36
- for attempt in range(self._max_connection_retries):
37
- try:
38
- self._pool = await asyncio.wait_for(
39
- self._create_pool(),
40
- timeout=self._config.connection_timeout_seconds,
41
- )
42
- return self._pool
43
- except Exception as exc:
44
- last_error = exc
45
- _logger.warning(
46
- "MySQL connection failed (attempt %d): %s",
47
- attempt + 1, exc,
48
- )
49
- if attempt < self._max_connection_retries - 1:
50
- await asyncio.sleep(0.1 * (2**attempt))
51
-
52
- raise RuntimeError(
53
- f"Failed to connect to MySQL after {self._max_connection_retries} attempts"
54
- ) from last_error
55
-
56
  async def _execute_queries(
57
  self, pool: aiomysql.Pool, queries: list[str], use_transaction: bool
58
  ) -> list[StatementResult]:
 
1
  from __future__ import annotations
2
 
 
 
3
  from typing import Any
4
 
5
  import aiomysql
 
25
  pool_recycle=3600,
26
  )
27
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  async def _execute_queries(
29
  self, pool: aiomysql.Pool, queries: list[str], use_transaction: bool
30
  ) -> list[StatementResult]:
app/models/__init__.py CHANGED
@@ -15,7 +15,6 @@ from app.models.schemas import (
15
  SpacyLabelsResponse,
16
  SupportedFormatsResponse,
17
  UrlRequest,
18
- # VisionUrlRequest, # DISABLED (OOM mitigation)
19
  )
20
 
21
  __all__ = [
@@ -34,5 +33,4 @@ __all__ = [
34
  "InfoResponse",
35
  "SupportedFormatsResponse",
36
  "SpacyLabelsResponse",
37
- # "VisionUrlRequest", # DISABLED (OOM mitigation)
38
  ]
 
15
  SpacyLabelsResponse,
16
  SupportedFormatsResponse,
17
  UrlRequest,
 
18
  )
19
 
20
  __all__ = [
 
33
  "InfoResponse",
34
  "SupportedFormatsResponse",
35
  "SpacyLabelsResponse",
 
36
  ]
app/models/schemas.py CHANGED
@@ -334,20 +334,8 @@ class EmbeddingResponse(BaseModel):
334
  results: List[EmbeddingItem]
335
 
336
 
337
- # class VisionUrlRequest(BaseModel): # DISABLED (OOM mitigation)
338
- # urls: List[str] = Field(..., min_length=1, max_length=5, description="Array of image URLs to embed (max 5)")
339
- #
340
- # @field_validator("urls")
341
- # @classmethod
342
- # def validate_urls(cls, v: List[str]) -> List[str]:
343
- # for url in v:
344
- # if not url.startswith(("http://", "https://")):
345
- # raise ValueError(f"Invalid URL scheme: {url}")
346
- # return v
347
-
348
-
349
  class CodeItem(BaseModel):
350
- language: Literal["python", "javascript"] # "java" DISABLED (OOM mitigation)
351
  code: str = Field(..., min_length=1, max_length=65536, description="Source code to execute")
352
 
353
 
 
334
  results: List[EmbeddingItem]
335
 
336
 
 
 
 
 
 
 
 
 
 
 
 
 
337
  class CodeItem(BaseModel):
338
+ language: Literal["python", "javascript"]
339
  code: str = Field(..., min_length=1, max_length=65536, description="Source code to execute")
340
 
341
 
app/services/auth_service.py CHANGED
@@ -14,6 +14,7 @@ from sqlalchemy import select, update
14
  from sqlalchemy.ext.asyncio import AsyncSession
15
 
16
  from app.config import get_settings
 
17
  from app.core.auth.models import RefreshSession, Role, User
18
  from app.core.auth.schemas import (
19
  ChangePasswordSchema,
@@ -31,10 +32,6 @@ _settings = get_settings()
31
  ph = PasswordHasher()
32
 
33
 
34
- def _now() -> datetime:
35
- return datetime.now(timezone.utc)
36
-
37
-
38
  def _token_key(raw: str) -> str:
39
  return hashlib.sha256(raw.encode("utf-8")).hexdigest()
40
 
 
14
  from sqlalchemy.ext.asyncio import AsyncSession
15
 
16
  from app.config import get_settings
17
+ from app.core.auth.models import _utcnow as _now
18
  from app.core.auth.models import RefreshSession, Role, User
19
  from app.core.auth.schemas import (
20
  ChangePasswordSchema,
 
32
  ph = PasswordHasher()
33
 
34
 
 
 
 
 
35
  def _token_key(raw: str) -> str:
36
  return hashlib.sha256(raw.encode("utf-8")).hexdigest()
37
 
app/services/code_executor_service.py CHANGED
@@ -70,27 +70,6 @@ class CodeSanitizer:
70
  (r"worker_threads", "worker_threads not allowed"),
71
  (r"Buffer\s*\.", "Buffer not allowed"),
72
  ],
73
- # "java": [ # DISABLED (OOM mitigation)
74
- # (r"ProcessBuilder", "ProcessBuilder not allowed"),
75
- # (r"Runtime\.exec", "Runtime.exec() not allowed"),
76
- # (r"Runtime\.getRuntime\s*\(\s*\)", "Runtime.getRuntime() not allowed"),
77
- # (r"System\.exit\s*\(", "System.exit() not allowed"),
78
- # (r"System\.gc\s*\(", "System.gc() not allowed"),
79
- # (r"File\s*\(", "File operations not allowed"),
80
- # (r"FileInputStream", "FileInputStream not allowed"),
81
- # (r"FileOutputStream", "FileOutputStream not allowed"),
82
- # (r"FileReader", "FileReader not allowed"),
83
- # (r"FileWriter", "FileWriter not allowed"),
84
- # (r"Socket\s*\(", "Socket not allowed"),
85
- # (r"ServerSocket\s*\(", "ServerSocket not allowed"),
86
- # (r"URL\s*\(", "URL not allowed"),
87
- # (r"Class\.forName", "Class.forName() not allowed"),
88
- # (r"Thread\s*\(", "Thread not allowed"),
89
- # (r"ThreadPoolExecutor", "ThreadPoolExecutor not allowed"),
90
- # (r"Runtime\.", "Runtime not allowed"),
91
- # (r"System\.getProperty", "System.getProperty not allowed"),
92
- # (r"System\.getenv", "System.getenv not allowed"),
93
- # ],
94
  }
95
 
96
  @classmethod
@@ -101,11 +80,6 @@ class CodeSanitizer:
101
  return False, f"Forbidden: {message}"
102
  return True, None
103
 
104
- # @classmethod # DISABLED (OOM mitigation)
105
- # def validate_java_class(cls, code: str) -> tuple[bool, Optional[str]]:
106
- # if not re.search(r"public\s+class\s+Main\s*\{", code):
107
- # return False, "Java code must have 'public class Main' with 'public static void main(String[] args)'"
108
- # return True, None
109
 
110
 
111
  class CodeExecutorService:
@@ -131,15 +105,6 @@ class CodeExecutorService:
131
  "language": language, "timed_out": False,
132
  }
133
 
134
- # if language == "java": # DISABLED (OOM mitigation)
135
- # valid, err = CodeSanitizer.validate_java_class(code)
136
- # if not valid:
137
- # return {
138
- # "success": False, "output": "", "error": err,
139
- # "exit_code": None, "execution_time_ms": None,
140
- # "language": language, "timed_out": False,
141
- # }
142
-
143
  exec_timeout = min(timeout or self._max_execution_time, self._max_execution_time)
144
 
145
  async with self._semaphore:
@@ -233,14 +198,14 @@ class CodeExecutorService:
233
 
234
  async def check_runtimes(self) -> dict[str, str]:
235
  status = {}
236
- for lang, runtime in [("python", "python3"), ("javascript", "node")]: # "java" DISABLED (OOM mitigation)
237
  path = shutil.which(runtime)
238
  status[lang] = f"found at {path}" if path else "missing"
239
  return status
240
 
241
  @staticmethod
242
  def _filename_for(language: str) -> str:
243
- return {"python": "code.py", "javascript": "code.js" # "java": "Main.java" DISABLED (OOM mitigation)
244
  }[language]
245
 
246
  @staticmethod
@@ -251,7 +216,4 @@ class CodeExecutorService:
251
  return ["python3", str(sandbox_py), str(run_dir / filename)]
252
  elif language == "javascript":
253
  return ["node", str(sandbox_js), str(run_dir / filename)]
254
- # elif language == "java": # DISABLED (OOM mitigation)
255
- # return ["sh", "-c",
256
- # f"cd {run_dir} && javac Main.java 2>&1 && java -XX:CompressedClassSpaceSize=64m -Xmx96m Main 2>&1"]
257
  return []
 
70
  (r"worker_threads", "worker_threads not allowed"),
71
  (r"Buffer\s*\.", "Buffer not allowed"),
72
  ],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  }
74
 
75
  @classmethod
 
80
  return False, f"Forbidden: {message}"
81
  return True, None
82
 
 
 
 
 
 
83
 
84
 
85
  class CodeExecutorService:
 
105
  "language": language, "timed_out": False,
106
  }
107
 
 
 
 
 
 
 
 
 
 
108
  exec_timeout = min(timeout or self._max_execution_time, self._max_execution_time)
109
 
110
  async with self._semaphore:
 
198
 
199
  async def check_runtimes(self) -> dict[str, str]:
200
  status = {}
201
+ for lang, runtime in [("python", "python3"), ("javascript", "node")]:
202
  path = shutil.which(runtime)
203
  status[lang] = f"found at {path}" if path else "missing"
204
  return status
205
 
206
  @staticmethod
207
  def _filename_for(language: str) -> str:
208
+ return {"python": "code.py", "javascript": "code.js"
209
  }[language]
210
 
211
  @staticmethod
 
216
  return ["python3", str(sandbox_py), str(run_dir / filename)]
217
  elif language == "javascript":
218
  return ["node", str(sandbox_js), str(run_dir / filename)]
 
 
 
219
  return []
app/services/converter_service.py CHANGED
@@ -8,7 +8,6 @@ import time
8
  from pathlib import Path
9
  from urllib.parse import urlparse
10
 
11
- # import httpx # DISABLED (OOM mitigation) — only used by YouTube
12
  from markitdown import MarkItDown
13
 
14
  from app.core.constants import IMAGE_EXTENSIONS, IMAGE_MIME_PREFIXES
@@ -19,101 +18,6 @@ from app.services.ocr_service import ocr_image, ocr_pdf
19
  _logger = get_logger(__name__)
20
 
21
 
22
- # def _extract_youtube_video_id(url_or_id: str) -> str: # DISABLED (OOM mitigation)
23
- # if len(url_or_id) == 11 and not url_or_id.startswith("http"):
24
- # return url_or_id
25
- # pattern = r"(?:v=|\/)([0-9A-Za-z_-]{11}).*"
26
- # match = re.search(pattern, url_or_id)
27
- # if match:
28
- # return match.group(1)
29
- # raise ValueError(f"Could not extract a valid YouTube ID from: {url_or_id}")
30
- #
31
- #
32
- # def _fetch_youtube_oembed(video_id: str) -> dict | None: # DISABLED (OOM mitigation)
33
- # oembed_url = f"https://www.youtube.com/oembed?url=https://www.youtube.com/watch?v={video_id}&format=json"
34
- # try:
35
- # resp = httpx.get(oembed_url, timeout=10.0)
36
- # resp.raise_for_status()
37
- # return resp.json()
38
- # except Exception as exc:
39
- # _logger.warning("YouTube oEmbed failed for %s: %s", video_id, exc)
40
- # return None
41
- #
42
- #
43
- # def _fetch_youtube_transcript(video_id: str) -> str | None: # DISABLED (OOM mitigation)
44
- # try:
45
- # import json as _json
46
- # import os as _os
47
- #
48
- # from youtube_transcript_api import YouTubeTranscriptApi
49
- # from youtube_transcript_api.formatters import TextFormatter
50
- # from youtube_transcript_api._errors import (
51
- # TranscriptsDisabled,
52
- # NoTranscriptFound,
53
- # VideoUnavailable,
54
- # )
55
- #
56
- # kwargs: dict = {}
57
- # cookies_raw = _os.environ.get("YOUTUBE_COOKIES", "").strip()
58
- # if cookies_raw:
59
- # try:
60
- # import requests as _requests
61
- # session = _requests.Session()
62
- # session.cookies.update(_json.loads(cookies_raw))
63
- # kwargs["http_client"] = session
64
- # except Exception as exc:
65
- # _logger.warning("Failed to apply YOUTUBE_COOKIES: %s", exc)
66
- #
67
- # ytt_api = YouTubeTranscriptApi(**kwargs)
68
- # transcript_data = ytt_api.fetch(video_id, languages=["en"])
69
- # formatter = TextFormatter()
70
- # return formatter.format_transcript(transcript_data)
71
- # except TranscriptsDisabled:
72
- # _logger.warning("Transcripts disabled for video %s", video_id)
73
- # except NoTranscriptFound:
74
- # _logger.warning("No transcript found for video %s in language 'en'", video_id)
75
- # except VideoUnavailable:
76
- # _logger.warning("Video %s is unavailable, deleted, or private", video_id)
77
- # except ValueError as ve:
78
- # _logger.warning("Invalid video ID %s: %s", video_id, ve)
79
- # except Exception as exc:
80
- # _logger.warning("YouTube transcript failed for %s: %s", video_id, exc)
81
- # return None
82
- #
83
- #
84
- # def _convert_youtube(url: str) -> ConversionResult | None: # DISABLED (OOM mitigation)
85
- # try:
86
- # video_id = _extract_youtube_video_id(url)
87
- # except ValueError:
88
- # return None
89
- #
90
- # oembed = _fetch_youtube_oembed(video_id)
91
- # transcript = _fetch_youtube_transcript(video_id)
92
- #
93
- # lines = ["# YouTube\n"]
94
- # title = (oembed or {}).get("title", "")
95
- # if title:
96
- # lines.append(f"\n## {title}\n")
97
- #
98
- # author = (oembed or {}).get("author_name", "")
99
- # if author:
100
- # lines.append(f"\n- **Channel:** {author}\n")
101
- #
102
- # desc = (oembed or {}).get("description", "")
103
- # if desc:
104
- # lines.append(f"\n### Description\n{desc}\n")
105
- #
106
- # if transcript:
107
- # lines.append(f"\n### Transcript\n{transcript}\n")
108
- # else:
109
- # if not title and not desc:
110
- # return None
111
- # lines.append("\n> No transcript available.\n")
112
- #
113
- # markdown = "".join(lines)
114
- # return _build_result(url, markdown, 0, "text/html", 0.0)
115
-
116
-
117
  def _is_image(ext: str, mime: str) -> bool:
118
  return ext.lower() in IMAGE_EXTENSIONS or any(mime.startswith(p) for p in IMAGE_MIME_PREFIXES)
119
 
@@ -193,27 +97,6 @@ class ConverterService:
193
 
194
  start = time.perf_counter()
195
 
196
- # try: # DISABLED (OOM mitigation) — YouTube transcription
197
- # _extract_youtube_video_id(url)
198
- # except ValueError:
199
- # pass
200
- # else:
201
- # result = _convert_youtube(url)
202
- # if result is not None:
203
- # result = ConversionResult(
204
- # source=result.source,
205
- # markdown=result.markdown,
206
- # char_count=result.char_count,
207
- # word_count=result.word_count,
208
- # line_count=result.line_count,
209
- # duration_ms=(time.perf_counter() - start) * 1000,
210
- # file_size_bytes=result.file_size_bytes,
211
- # mime_type=result.mime_type,
212
- # content_hash=result.content_hash,
213
- # )
214
- # return result
215
- # _logger.warning("YouTube conversion returned None, falling back to markitdown: %s", url)
216
-
217
  try:
218
  url_ext = Path(urlparse(url).path).suffix.lower()
219
  if url_ext in IMAGE_EXTENSIONS:
 
8
  from pathlib import Path
9
  from urllib.parse import urlparse
10
 
 
11
  from markitdown import MarkItDown
12
 
13
  from app.core.constants import IMAGE_EXTENSIONS, IMAGE_MIME_PREFIXES
 
18
  _logger = get_logger(__name__)
19
 
20
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  def _is_image(ext: str, mime: str) -> bool:
22
  return ext.lower() in IMAGE_EXTENSIONS or any(mime.startswith(p) for p in IMAGE_MIME_PREFIXES)
23
 
 
97
 
98
  start = time.perf_counter()
99
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
  try:
101
  url_ext = Path(urlparse(url).path).suffix.lower()
102
  if url_ext in IMAGE_EXTENSIONS:
app/services/dataset_metadata_service.py CHANGED
@@ -614,23 +614,4 @@ async def extract_metadata(
614
  return result
615
 
616
 
617
- # ---------------------------------------------------------------------------
618
- # CLI convenience
619
- # ---------------------------------------------------------------------------
620
-
621
- # if __name__ == "__main__":
622
- # import json
623
- # import sys
624
-
625
- # async def _main() -> None:
626
- # if len(sys.argv) < 2:
627
- # print("Usage: python solution.py <file_path_or_url>")
628
- # sys.exit(1)
629
-
630
- # arg = sys.argv[1]
631
- # src: FileSource = arg if _is_url(arg) else Path(arg) # type: ignore[assignment]
632
-
633
- # result = await extract_metadata(src)
634
- # print(json.dumps(result, indent=2, default=str))
635
 
636
- # asyncio.run(_main())
 
614
  return result
615
 
616
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
617
 
 
app/services/embeddings_service.py CHANGED
@@ -5,25 +5,15 @@ import os
5
  from typing import Dict, List, Optional
6
 
7
  import numpy as np
8
- # import torch # DISABLED (OOM mitigation) — only used by vision
9
- # import torch.nn.functional as F # DISABLED (OOM mitigation)
10
- # from PIL import Image # DISABLED (OOM mitigation)
11
  from sentence_transformers import SentenceTransformer
12
- # from transformers import AutoImageProcessor, AutoModel # DISABLED (OOM mitigation)
13
 
14
  _logger = logging.getLogger(__name__)
15
 
16
  # Only 384-dim embedding is enabled. 768 and 1024 are disabled to reduce memory usage.
17
  _MODEL_MAP: Dict[int, str] = {
18
  384: "ibm-granite/granite-embedding-small-english-r2",
19
- # 768: "nomic-ai/nomic-embed-text-v1.5", # DISABLED (OOM mitigation)
20
- # 1024: "lightonai/modernbert-embed-large", # DISABLED (OOM mitigation)
21
  }
22
 
23
- # _VISION_MODEL_NAME = "nomic-ai/nomic-embed-vision-v1.5" # DISABLED (OOM mitigation)
24
- # _VISION_DIMENSION = 768
25
-
26
-
27
  class EmbeddingService:
28
  def __init__(self, models_dir: Optional[str] = None) -> None:
29
  self._models: Dict[int, SentenceTransformer] = {}
@@ -37,9 +27,6 @@ class EmbeddingService:
37
  self._device = "cpu"
38
 
39
  self._loaded_dimensions: List[int] = []
40
- # self._vision_processor: Optional[AutoImageProcessor] = None # DISABLED (OOM mitigation)
41
- # self._vision_model: Optional[AutoModel] = None # DISABLED (OOM mitigation)
42
- # self._vision_loaded = False
43
 
44
  def load_model(self, dimension: int) -> None:
45
  if dimension in self._models:
@@ -65,35 +52,6 @@ class EmbeddingService:
65
  for dim in _MODEL_MAP:
66
  self.load_model(dim)
67
 
68
- # def load_vision_model(self) -> None: # DISABLED (OOM mitigation)
69
- # if self._vision_loaded:
70
- # return
71
- # local_path = os.path.join(self._models_dir, "vision")
72
- # source = local_path if os.path.isdir(local_path) else _VISION_MODEL_NAME
73
- #
74
- # cfg_path = os.path.join(local_path, "config.json")
75
- # if os.path.exists(cfg_path):
76
- # import json
77
- # with open(cfg_path) as f:
78
- # d = json.load(f)
79
- # if isinstance(d.get("n_inner"), float):
80
- # d["n_inner"] = int(d["n_inner"])
81
- # with open(cfg_path, "w") as f:
82
- # json.dump(d, f, indent=2)
83
- # _logger.info("Patched vision model config: n_inner float -> int")
84
- #
85
- # _logger.info("Loading vision embedding model from %s", source)
86
- # self._vision_processor = AutoImageProcessor.from_pretrained(source)
87
- # self._vision_model = AutoModel.from_pretrained(
88
- # source,
89
- # trust_remote_code=True,
90
- # _fast_init=False,
91
- # )
92
- # self._vision_model.eval()
93
- # self._vision_model.to(self._device)
94
- # self._vision_loaded = True
95
- # _logger.info("Loaded vision embedding model (device=%s)", self._device)
96
-
97
  def generate_embedding(self, text: List[str], dimension: int) -> List[List[float]]:
98
  if dimension not in self._models:
99
  raise ValueError(f"Model for dimension {dimension} not loaded")
@@ -109,20 +67,6 @@ class EmbeddingService:
109
  )
110
  return result.tolist()
111
 
112
- # def generate_image_embedding(self, images: List[Image.Image]) -> List[List[float]]: # DISABLED (OOM mitigation)
113
- # if not self._vision_loaded or self._vision_model is None or self._vision_processor is None:
114
- # raise ValueError("Vision model not loaded")
115
- # all_embeddings: List[List[float]] = []
116
- # with torch.no_grad():
117
- # for image in images:
118
- # inputs = self._vision_processor(image, return_tensors="pt")
119
- # inputs = {k: v.to(self._device) for k, v in inputs.items()}
120
- # outputs = self._vision_model(**inputs)
121
- # emb = outputs.last_hidden_state[:, 0]
122
- # emb = F.normalize(emb, p=2, dim=1)
123
- # all_embeddings.append(emb.cpu().numpy().flatten().tolist())
124
- # return all_embeddings
125
-
126
  @property
127
  def loaded_dimensions(self) -> List[int]:
128
  return list(self._loaded_dimensions)
@@ -130,6 +74,3 @@ class EmbeddingService:
130
  def is_loaded(self, dimension: int) -> bool:
131
  return dimension in self._models
132
 
133
- # @property # DISABLED (OOM mitigation)
134
- # def vision_dimension(self) -> int:
135
- # return _VISION_DIMENSION
 
5
  from typing import Dict, List, Optional
6
 
7
  import numpy as np
 
 
 
8
  from sentence_transformers import SentenceTransformer
 
9
 
10
  _logger = logging.getLogger(__name__)
11
 
12
  # Only 384-dim embedding is enabled. 768 and 1024 are disabled to reduce memory usage.
13
  _MODEL_MAP: Dict[int, str] = {
14
  384: "ibm-granite/granite-embedding-small-english-r2",
 
 
15
  }
16
 
 
 
 
 
17
  class EmbeddingService:
18
  def __init__(self, models_dir: Optional[str] = None) -> None:
19
  self._models: Dict[int, SentenceTransformer] = {}
 
27
  self._device = "cpu"
28
 
29
  self._loaded_dimensions: List[int] = []
 
 
 
30
 
31
  def load_model(self, dimension: int) -> None:
32
  if dimension in self._models:
 
52
  for dim in _MODEL_MAP:
53
  self.load_model(dim)
54
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  def generate_embedding(self, text: List[str], dimension: int) -> List[List[float]]:
56
  if dimension not in self._models:
57
  raise ValueError(f"Model for dimension {dimension} not loaded")
 
67
  )
68
  return result.tolist()
69
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
  @property
71
  def loaded_dimensions(self) -> List[int]:
72
  return list(self._loaded_dimensions)
 
74
  def is_loaded(self, dimension: int) -> bool:
75
  return dimension in self._models
76
 
 
 
 
app/services/vector_store_service.py CHANGED
@@ -522,3 +522,6 @@ class VectorStoreService:
522
  self._collections.clear()
523
  self._stores.clear()
524
  self._thread_pool.shutdown(wait=True)
 
 
 
 
522
  self._collections.clear()
523
  self._stores.clear()
524
  self._thread_pool.shutdown(wait=True)
525
+ self._thread_pool = concurrent.futures.ThreadPoolExecutor(
526
+ max_workers=_MAX_WORKERS, thread_name_prefix="zvec"
527
+ )
tests/test_webhook_socket.py CHANGED
@@ -20,10 +20,6 @@ AUTH_HEADER = {"Authorization": f"Bearer {API_KEY}"}
20
  BASE = "http://localhost:7860/api/v1"
21
 
22
 
23
- # ---------------------------------------------------------------------------
24
- # Unit tests for ChannelManager
25
- # ---------------------------------------------------------------------------
26
-
27
  class TestChannelManager:
28
  def test_create_channel(self):
29
  mgr = ChannelManager()
@@ -61,7 +57,6 @@ class TestChannelManager:
61
  mgr = ChannelManager()
62
  mgr.create_channel(channel_id="dup")
63
  ch2 = mgr.create_channel(channel_id="dup")
64
- # Should overwrite
65
  assert mgr.get_channel("dup") is ch2
66
 
67
  def test_default_buffer_from_manager(self):
@@ -113,10 +108,43 @@ class TestChannelManager:
113
  sig2 = sign_payload(secret, body)
114
  assert sig1 == sig2
115
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
 
117
- # ---------------------------------------------------------------------------
118
- # Integration tests (requires running server @ localhost:7860)
119
- # ---------------------------------------------------------------------------
120
 
121
  pytestmark_integration = pytest.mark.skipif(
122
  not os.environ.get("RUN_INTEGRATION_TESTS"),
@@ -169,7 +197,7 @@ class TestWebhookSocketIntegration:
169
 
170
  async def test_create_channel_no_auth(self):
171
  resp = await self.client.post("/channels", json={})
172
- assert resp.status_code == 403
173
 
174
  async def test_create_duplicate_channel(self):
175
  await self._create_channel(channel_id="dup-test")
@@ -405,10 +433,6 @@ class TestWebhookSocketIntegration:
405
  assert not_found.status_code == 404
406
 
407
 
408
- # ---------------------------------------------------------------------------
409
- # Runner (standalone)
410
- # ---------------------------------------------------------------------------
411
-
412
  if __name__ == "__main__":
413
  import subprocess
414
  import sys as _sys
 
20
  BASE = "http://localhost:7860/api/v1"
21
 
22
 
 
 
 
 
23
  class TestChannelManager:
24
  def test_create_channel(self):
25
  mgr = ChannelManager()
 
57
  mgr = ChannelManager()
58
  mgr.create_channel(channel_id="dup")
59
  ch2 = mgr.create_channel(channel_id="dup")
 
60
  assert mgr.get_channel("dup") is ch2
61
 
62
  def test_default_buffer_from_manager(self):
 
108
  sig2 = sign_payload(secret, body)
109
  assert sig1 == sig2
110
 
111
+ def test_channel_info_fields(self):
112
+ mgr = ChannelManager()
113
+ mgr.create_channel(channel_id="info-test", buffer_size=5)
114
+ ch = mgr.get_channel("info-test")
115
+ assert ch.channel_id == "info-test"
116
+ assert ch.buffer_size == 5
117
+ assert ch.message_count == 0
118
+ assert ch.created_at > 0
119
+ assert ch.last_activity > 0
120
+
121
+ def test_publish_with_subscriber(self):
122
+ mgr = ChannelManager()
123
+ mgr.create_channel(channel_id="sub-test")
124
+ ch = mgr.get_channel("sub-test")
125
+
126
+ async def dummy():
127
+ return ch.channel_id
128
+ q = asyncio.Queue()
129
+ ch.subscribers[dummy] = q
130
+
131
+ result = asyncio.run(mgr.publish("sub-test", {"msg": "hello"}))
132
+ assert result == 1
133
+ assert ch.message_count == 1
134
+
135
+ def test_multiple_publishes(self):
136
+ mgr = ChannelManager()
137
+ ch = mgr.create_channel(channel_id="multi-pub", buffer_size=10)
138
+ for i in range(5):
139
+ asyncio.run(mgr.publish("multi-pub", {"n": i}))
140
+ assert ch.message_count == 5
141
+ assert len(ch.history) == 5
142
+
143
+ def test_channel_manager_defaults(self):
144
+ mgr = ChannelManager()
145
+ assert mgr.default_buffer == 0
146
+ assert mgr.channels == {}
147
 
 
 
 
148
 
149
  pytestmark_integration = pytest.mark.skipif(
150
  not os.environ.get("RUN_INTEGRATION_TESTS"),
 
197
 
198
  async def test_create_channel_no_auth(self):
199
  resp = await self.client.post("/channels", json={})
200
+ assert resp.status_code in (401, 403)
201
 
202
  async def test_create_duplicate_channel(self):
203
  await self._create_channel(channel_id="dup-test")
 
433
  assert not_found.status_code == 404
434
 
435
 
 
 
 
 
436
  if __name__ == "__main__":
437
  import subprocess
438
  import sys as _sys