validops-east-1 commited on
Commit
b7dddbe
·
1 Parent(s): 9db3128

common middlewre for /api/v1

Browse files
app/api/deps.py CHANGED
@@ -2,9 +2,8 @@ from __future__ import annotations
2
 
3
  from typing import Any, Dict, Optional, Tuple
4
 
5
- from fastapi import Depends, Request
6
 
7
- from app.core.security import require_api_key
8
  from app.services.auth_service import AuthService
9
  from app.services.converter_service import ConverterService
10
  from app.services.database_service import DatabaseService
@@ -60,10 +59,6 @@ def get_scheduler_service() -> SchedulerService:
60
  return _scheduler_service
61
 
62
 
63
- def require_auth(token: str = Depends(require_api_key)) -> str:
64
- return token
65
-
66
-
67
  def get_redis_scripts(request: Request) -> Tuple[Optional[Any], Dict[str, str]]:
68
  """Extract Redis client and Lua scripts from app state.
69
 
 
2
 
3
  from typing import Any, Dict, Optional, Tuple
4
 
5
+ from fastapi import Request
6
 
 
7
  from app.services.auth_service import AuthService
8
  from app.services.converter_service import ConverterService
9
  from app.services.database_service import DatabaseService
 
59
  return _scheduler_service
60
 
61
 
 
 
 
 
62
  def get_redis_scripts(request: Request) -> Tuple[Optional[Any], Dict[str, str]]:
63
  """Extract Redis client and Lua scripts from app state.
64
 
app/api/server.py CHANGED
@@ -22,6 +22,21 @@ from app.services.vector_store_service import VectorStoreService
22
  _logger = get_logger(__name__)
23
  _settings = get_settings()
24
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  _embedding_service: EmbeddingService = EmbeddingService()
26
  _vector_store_service: VectorStoreService = VectorStoreService(_embedding_service)
27
  _scheduler_service: SchedulerService = SchedulerService()
@@ -144,6 +159,26 @@ def create_application() -> FastAPI:
144
  )
145
  return await call_next(request)
146
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
  app.include_router(api_v1_router, prefix="/api/v1")
148
 
149
  @app.get("/", include_in_schema=False)
 
22
  _logger = get_logger(__name__)
23
  _settings = get_settings()
24
 
25
+ # Public /api/v1/ paths that do not require authentication
26
+ _PUBLIC_API_PREFIXES = (
27
+ "/api/v1/auth/register",
28
+ "/api/v1/auth/login",
29
+ "/api/v1/auth/refresh",
30
+ "/api/v1/auth/forgot-password",
31
+ "/api/v1/auth/reset-password",
32
+ "/api/v1/url-shortener/",
33
+ )
34
+
35
+
36
+ def _is_public_path(path: str) -> bool:
37
+ return path.startswith(_PUBLIC_API_PREFIXES)
38
+
39
+
40
  _embedding_service: EmbeddingService = EmbeddingService()
41
  _vector_store_service: VectorStoreService = VectorStoreService(_embedding_service)
42
  _scheduler_service: SchedulerService = SchedulerService()
 
159
  )
160
  return await call_next(request)
161
 
162
+ @app.middleware("http")
163
+ async def auth_middleware(request: Request, call_next):
164
+ path = request.url.path
165
+ if path.startswith("/api/v1/") and not _is_public_path(path):
166
+ auth_header = request.headers.get("Authorization", "")
167
+ if not auth_header.startswith("Bearer "):
168
+ from starlette.responses import JSONResponse
169
+ return JSONResponse(
170
+ status_code=401,
171
+ content={"success": False, "detail": "Missing Authorization header"},
172
+ )
173
+ token = auth_header.removeprefix("Bearer ")
174
+ if token != _settings.api_key:
175
+ from starlette.responses import JSONResponse
176
+ return JSONResponse(
177
+ status_code=401,
178
+ content={"success": False, "detail": "Invalid API key"},
179
+ )
180
+ return await call_next(request)
181
+
182
  app.include_router(api_v1_router, prefix="/api/v1")
183
 
184
  @app.get("/", include_in_schema=False)
app/api/v1/batch.py CHANGED
@@ -13,7 +13,6 @@ from app.api.deps import (
13
  get_converter_service,
14
  get_extraction_service,
15
  get_text_cleaner_service,
16
- require_auth,
17
  )
18
  from app.api.v1.convert import _build_metadata, _thread_pool
19
  from app.config import get_settings
@@ -64,7 +63,6 @@ async def batch_files(
64
  return_json: bool = Form(False),
65
  clean_content: bool = Query(False),
66
  mappings: Optional[str] = Form(None, description="JSON string with field mappings"),
67
- token: str = Depends(require_auth),
68
  converter_service: ConverterService = Depends(get_converter_service),
69
  extraction_service: ExtractionService = Depends(get_extraction_service),
70
  text_cleaner_service: TextCleanerService = Depends(get_text_cleaner_service),
@@ -145,7 +143,6 @@ async def batch_files(
145
  async def batch_urls(
146
  body: BatchUrlRequest,
147
  clean_content: bool = Query(False),
148
- token: str = Depends(require_auth),
149
  converter_service: ConverterService = Depends(get_converter_service),
150
  extraction_service: ExtractionService = Depends(get_extraction_service),
151
  text_cleaner_service: TextCleanerService = Depends(get_text_cleaner_service),
 
13
  get_converter_service,
14
  get_extraction_service,
15
  get_text_cleaner_service,
 
16
  )
17
  from app.api.v1.convert import _build_metadata, _thread_pool
18
  from app.config import get_settings
 
63
  return_json: bool = Form(False),
64
  clean_content: bool = Query(False),
65
  mappings: Optional[str] = Form(None, description="JSON string with field mappings"),
 
66
  converter_service: ConverterService = Depends(get_converter_service),
67
  extraction_service: ExtractionService = Depends(get_extraction_service),
68
  text_cleaner_service: TextCleanerService = Depends(get_text_cleaner_service),
 
143
  async def batch_urls(
144
  body: BatchUrlRequest,
145
  clean_content: bool = Query(False),
 
146
  converter_service: ConverterService = Depends(get_converter_service),
147
  extraction_service: ExtractionService = Depends(get_extraction_service),
148
  text_cleaner_service: TextCleanerService = Depends(get_text_cleaner_service),
app/api/v1/chat.py CHANGED
@@ -8,7 +8,7 @@ from typing import Any, AsyncGenerator, Dict, List, Optional
8
  from fastapi import APIRouter, Depends, HTTPException, Request
9
  from fastapi.responses import StreamingResponse
10
 
11
- from app.api.deps import get_redis_scripts, require_auth
12
  from app.services.chat_service import _stream_chat_completion, chat_completion
13
  from app.utils.schema_utils import validate_response_format
14
 
@@ -61,7 +61,6 @@ async def _stream_events(
61
  async def create_chat_completion(
62
  body: Dict[str, Any],
63
  request: Request,
64
- token: str = Depends(require_auth),
65
  ):
66
  start = time.monotonic()
67
  req_id = hex(int(time.time() * 1_000_000))[-8:]
 
8
  from fastapi import APIRouter, Depends, HTTPException, Request
9
  from fastapi.responses import StreamingResponse
10
 
11
+ from app.api.deps import get_redis_scripts
12
  from app.services.chat_service import _stream_chat_completion, chat_completion
13
  from app.utils.schema_utils import validate_response_format
14
 
 
61
  async def create_chat_completion(
62
  body: Dict[str, Any],
63
  request: Request,
 
64
  ):
65
  start = time.monotonic()
66
  req_id = hex(int(time.time() * 1_000_000))[-8:]
app/api/v1/code_executor.py CHANGED
@@ -5,7 +5,6 @@ from typing import Optional
5
 
6
  from fastapi import APIRouter, Depends
7
 
8
- from app.api.deps import require_auth
9
  from app.core.logger import get_logger
10
  from app.models.schemas import (
11
  CodeExecutionItemResult,
@@ -34,7 +33,6 @@ def get_executor() -> CodeExecutorService:
34
  )
35
  async def execute_code(
36
  body: CodeExecutionRequest,
37
- token: str = Depends(require_auth),
38
  executor: CodeExecutorService = Depends(get_executor),
39
  ) -> CodeExecutionResponse:
40
  _logger.info("Code execution request: items=%s", len(body.items))
 
5
 
6
  from fastapi import APIRouter, Depends
7
 
 
8
  from app.core.logger import get_logger
9
  from app.models.schemas import (
10
  CodeExecutionItemResult,
 
33
  )
34
  async def execute_code(
35
  body: CodeExecutionRequest,
 
36
  executor: CodeExecutorService = Depends(get_executor),
37
  ) -> CodeExecutionResponse:
38
  _logger.info("Code execution request: items=%s", len(body.items))
app/api/v1/convert.py CHANGED
@@ -23,7 +23,6 @@ from app.api.deps import (
23
  get_converter_service,
24
  get_extraction_service,
25
  get_text_cleaner_service,
26
- require_auth,
27
  )
28
  from app.config import get_settings
29
  from app.core.logger import get_logger
@@ -149,7 +148,6 @@ async def convert_file(
149
  mappings: Optional[str] = Form(None, description="JSON string with field mappings"),
150
  json_schema: Optional[str] = Form(None, description="JSON Schema for AI-based structured extraction"),
151
  schema_name: str = Form("extraction", description="Name for the JSON Schema"),
152
- token: str = Depends(require_auth),
153
  converter_service: ConverterService = Depends(get_converter_service),
154
  extraction_service: ExtractionService = Depends(get_extraction_service),
155
  text_cleaner_service: TextCleanerService = Depends(get_text_cleaner_service),
@@ -216,7 +214,6 @@ async def convert_file(
216
  async def convert_url(
217
  body: UrlRequest,
218
  clean_content: bool = Query(False),
219
- token: str = Depends(require_auth),
220
  converter_service: ConverterService = Depends(get_converter_service),
221
  extraction_service: ExtractionService = Depends(get_extraction_service),
222
  text_cleaner_service: TextCleanerService = Depends(get_text_cleaner_service),
 
23
  get_converter_service,
24
  get_extraction_service,
25
  get_text_cleaner_service,
 
26
  )
27
  from app.config import get_settings
28
  from app.core.logger import get_logger
 
148
  mappings: Optional[str] = Form(None, description="JSON string with field mappings"),
149
  json_schema: Optional[str] = Form(None, description="JSON Schema for AI-based structured extraction"),
150
  schema_name: str = Form("extraction", description="Name for the JSON Schema"),
 
151
  converter_service: ConverterService = Depends(get_converter_service),
152
  extraction_service: ExtractionService = Depends(get_extraction_service),
153
  text_cleaner_service: TextCleanerService = Depends(get_text_cleaner_service),
 
214
  async def convert_url(
215
  body: UrlRequest,
216
  clean_content: bool = Query(False),
 
217
  converter_service: ConverterService = Depends(get_converter_service),
218
  extraction_service: ExtractionService = Depends(get_extraction_service),
219
  text_cleaner_service: TextCleanerService = Depends(get_text_cleaner_service),
app/api/v1/csv_analysis.py CHANGED
@@ -3,10 +3,10 @@ from __future__ import annotations
3
  import json
4
  from typing import Annotated, Any, Dict, List, Optional
5
 
6
- from fastapi import APIRouter, Depends, File, Form, HTTPException, Request, UploadFile
7
  from pydantic import BaseModel, ValidationError
8
 
9
- from app.api.deps import get_redis_scripts, require_auth
10
  from app.config import get_settings
11
  from app.services.chat_service import chat_completion
12
  from app.services.csv_analysis_service import (
@@ -32,7 +32,7 @@ class _AIResponse(BaseModel):
32
  visualization: List[_VisualizationBlock] = []
33
  message: str = ""
34
 
35
- router = APIRouter(dependencies=[Depends(require_auth)])
36
  _settings = get_settings()
37
  _MAX_UPLOAD_BYTES = _settings.max_upload_bytes
38
 
 
3
  import json
4
  from typing import Annotated, Any, Dict, List, Optional
5
 
6
+ from fastapi import APIRouter, File, Form, HTTPException, Request, UploadFile
7
  from pydantic import BaseModel, ValidationError
8
 
9
+ from app.api.deps import get_redis_scripts
10
  from app.config import get_settings
11
  from app.services.chat_service import chat_completion
12
  from app.services.csv_analysis_service import (
 
32
  visualization: List[_VisualizationBlock] = []
33
  message: str = ""
34
 
35
+ router = APIRouter()
36
  _settings = get_settings()
37
  _MAX_UPLOAD_BYTES = _settings.max_upload_bytes
38
 
app/api/v1/database.py CHANGED
@@ -4,7 +4,6 @@ from typing import Annotated
4
 
5
  from fastapi import APIRouter, Depends, HTTPException, status
6
 
7
- from app.api.deps import require_auth
8
  from app.core.logger import get_logger
9
  from app.models.schemas import (
10
  DatabaseQueryRequest,
@@ -25,7 +24,6 @@ _logger = get_logger(__name__)
25
  )
26
  async def validate_database(
27
  body: DatabaseValidateRequest,
28
- token: Annotated[str, Depends(require_auth)],
29
  db_service: Annotated[DatabaseService, Depends()] = None,
30
  ) -> DatabaseValidateResponse:
31
  if db_service is None:
@@ -48,7 +46,6 @@ async def validate_database(
48
  )
49
  async def execute_database_query(
50
  body: DatabaseQueryRequest,
51
- token: Annotated[str, Depends(require_auth)],
52
  db_service: Annotated[DatabaseService, Depends()] = None,
53
  ) -> DatabaseQueryResponse:
54
  if db_service is None:
 
4
 
5
  from fastapi import APIRouter, Depends, HTTPException, status
6
 
 
7
  from app.core.logger import get_logger
8
  from app.models.schemas import (
9
  DatabaseQueryRequest,
 
24
  )
25
  async def validate_database(
26
  body: DatabaseValidateRequest,
 
27
  db_service: Annotated[DatabaseService, Depends()] = None,
28
  ) -> DatabaseValidateResponse:
29
  if db_service is None:
 
46
  )
47
  async def execute_database_query(
48
  body: DatabaseQueryRequest,
 
49
  db_service: Annotated[DatabaseService, Depends()] = None,
50
  ) -> DatabaseQueryResponse:
51
  if db_service is None:
app/api/v1/embeddings.py CHANGED
@@ -5,7 +5,7 @@ import time
5
 
6
  from fastapi import APIRouter, Depends, HTTPException
7
 
8
- from app.api.deps import get_embeddings_service, require_auth
9
  from app.config import get_settings
10
  from app.core.logger import get_logger
11
  from app.core.thread_pool import thread_pool as _thread_pool
@@ -22,7 +22,6 @@ _settings = get_settings()
22
  )
23
  async def create_embeddings(
24
  body: EmbeddingRequest,
25
- token: str = Depends(require_auth),
26
  embedding_service: EmbeddingService = Depends(get_embeddings_service),
27
  ) -> EmbeddingResponse:
28
  _logger.info("Embedding request: dim=%s, items=%s", body.dimension, len(body.content))
 
5
 
6
  from fastapi import APIRouter, Depends, HTTPException
7
 
8
+ from app.api.deps import get_embeddings_service
9
  from app.config import get_settings
10
  from app.core.logger import get_logger
11
  from app.core.thread_pool import thread_pool as _thread_pool
 
22
  )
23
  async def create_embeddings(
24
  body: EmbeddingRequest,
 
25
  embedding_service: EmbeddingService = Depends(get_embeddings_service),
26
  ) -> EmbeddingResponse:
27
  _logger.info("Embedding request: dim=%s, items=%s", body.dimension, len(body.content))
app/api/v1/google_maps.py CHANGED
@@ -5,7 +5,6 @@ from typing import Any, Optional
5
 
6
  from fastapi import APIRouter, Depends, Header, Query, Response
7
 
8
- from app.api.deps import require_auth
9
  from app.core.logger import get_logger
10
  from app.models.schemas import (
11
  GoogleAutocompleteMatchedSubstring,
@@ -231,7 +230,6 @@ def _build_autocomplete_predictions_legacy(data: dict) -> list[GoogleAutocomplet
231
  async def place_autocomplete(
232
  body: GooglePlaceAutocompleteRequest,
233
  x_goog_api_key: str = Header(..., alias="X-Goog-Api-Key", description="Your Google API key"),
234
- token: str = Depends(require_auth),
235
  service: GoogleMapsService = Depends(get_maps_service),
236
  ):
237
  start = time.perf_counter()
@@ -271,7 +269,6 @@ async def place_autocomplete_get(
271
  strictbounds: Optional[bool] = Query(None),
272
  sessiontoken: Optional[str] = Query(None),
273
  x_goog_api_key: str = Header(..., alias="X-Goog-Api-Key", description="Your Google API key"),
274
- token: str = Depends(require_auth),
275
  service: GoogleMapsService = Depends(get_maps_service),
276
  ):
277
  start = time.perf_counter()
@@ -295,7 +292,6 @@ async def place_autocomplete_get(
295
  async def query_autocomplete(
296
  body: GoogleQueryAutocompleteRequest,
297
  x_goog_api_key: str = Header(..., alias="X-Goog-Api-Key", description="Your Google API key"),
298
- token: str = Depends(require_auth),
299
  service: GoogleMapsService = Depends(get_maps_service),
300
  ):
301
  start = time.perf_counter()
@@ -325,7 +321,6 @@ async def query_autocomplete_get(
325
  radius: Optional[int] = Query(None, ge=1, le=50000),
326
  language: Optional[str] = Query(None, max_length=10),
327
  x_goog_api_key: str = Header(..., alias="X-Goog-Api-Key", description="Your Google API key"),
328
- token: str = Depends(require_auth),
329
  service: GoogleMapsService = Depends(get_maps_service),
330
  ):
331
  start = time.perf_counter()
@@ -351,7 +346,6 @@ async def query_autocomplete_get(
351
  async def geocode(
352
  body: GoogleGeocodeRequest,
353
  x_goog_api_key: str = Header(..., alias="X-Goog-Api-Key", description="Your Google API key"),
354
- token: str = Depends(require_auth),
355
  service: GoogleMapsService = Depends(get_maps_service),
356
  ):
357
  start = time.perf_counter()
@@ -378,7 +372,6 @@ async def geocode_get(
378
  language: Optional[str] = Query(None, max_length=10),
379
  bounds: Optional[str] = Query(None),
380
  x_goog_api_key: str = Header(..., alias="X-Goog-Api-Key", description="Your Google API key"),
381
- token: str = Depends(require_auth),
382
  service: GoogleMapsService = Depends(get_maps_service),
383
  ):
384
  start = time.perf_counter()
@@ -396,7 +389,6 @@ async def geocode_get(
396
  async def reverse_geocode(
397
  body: GoogleReverseGeocodeRequest,
398
  x_goog_api_key: str = Header(..., alias="X-Goog-Api-Key", description="Your Google API key"),
399
- token: str = Depends(require_auth),
400
  service: GoogleMapsService = Depends(get_maps_service),
401
  ):
402
  start = time.perf_counter()
@@ -423,7 +415,6 @@ async def reverse_geocode_get(
423
  result_type: Optional[str] = Query(None),
424
  location_type: Optional[str] = Query(None),
425
  x_goog_api_key: str = Header(..., alias="X-Goog-Api-Key", description="Your Google API key"),
426
- token: str = Depends(require_auth),
427
  service: GoogleMapsService = Depends(get_maps_service),
428
  ):
429
  start = time.perf_counter()
@@ -449,7 +440,6 @@ async def reverse_geocode_get(
449
  async def places_search(
450
  body: GooglePlacesSearchRequest,
451
  x_goog_api_key: str = Header(..., alias="X-Goog-Api-Key", description="Your Google API key"),
452
- token: str = Depends(require_auth),
453
  service: GoogleMapsService = Depends(get_maps_service),
454
  ):
455
  start = time.perf_counter()
@@ -483,7 +473,6 @@ async def places_search(
483
  async def places_nearby(
484
  body: GooglePlacesNearbyRequest,
485
  x_goog_api_key: str = Header(..., alias="X-Goog-Api-Key", description="Your Google API key"),
486
- token: str = Depends(require_auth),
487
  service: GoogleMapsService = Depends(get_maps_service),
488
  ):
489
  start = time.perf_counter()
@@ -528,7 +517,6 @@ async def places_search_get(
528
  page_size: Optional[int] = Query(None, ge=1, le=200, description="Number of results per page"),
529
  min_rating: Optional[float] = Query(None, ge=0.0, le=5.0, description="Minimum rating filter"),
530
  x_goog_api_key: str = Header(..., alias="X-Goog-Api-Key", description="Your Google API key"),
531
- token: str = Depends(require_auth),
532
  service: GoogleMapsService = Depends(get_maps_service),
533
  ):
534
  start = time.perf_counter()
@@ -565,7 +553,6 @@ async def places_nearby_get(
565
  page_size: Optional[int] = Query(None, ge=1, le=200, description="Number of results per page"),
566
  rank_preference: Optional[str] = Query(None, pattern="^(POPULARITY|DISTANCE)$", description="Ranking preference"),
567
  x_goog_api_key: str = Header(..., alias="X-Goog-Api-Key", description="Your Google API key"),
568
- token: str = Depends(require_auth),
569
  service: GoogleMapsService = Depends(get_maps_service),
570
  ):
571
  start = time.perf_counter()
@@ -593,7 +580,6 @@ async def places_nearby_get(
593
  async def place_details(
594
  body: GooglePlaceDetailsRequest,
595
  x_goog_api_key: str = Header(..., alias="X-Goog-Api-Key", description="Your Google API key"),
596
- token: str = Depends(require_auth),
597
  service: GoogleMapsService = Depends(get_maps_service),
598
  ):
599
  start = time.perf_counter()
@@ -621,7 +607,6 @@ async def place_details_get(
621
  language: Optional[str] = Query(None, max_length=10),
622
  fields: Optional[str] = Query(None),
623
  x_goog_api_key: str = Header(..., alias="X-Goog-Api-Key", description="Your Google API key"),
624
- token: str = Depends(require_auth),
625
  service: GoogleMapsService = Depends(get_maps_service),
626
  ):
627
  start = time.perf_counter()
@@ -645,10 +630,10 @@ async def place_photo(
645
  max_width_px: int = Query(400, ge=1, le=4800),
646
  max_height_px: int = Query(400, ge=1, le=4800),
647
  x_goog_api_key: str = Header(..., alias="X-Goog-Api-Key", description="Your Google API key"),
648
- token: str = Depends(require_auth),
649
  service: GoogleMapsService = Depends(get_maps_service),
650
  ):
651
  from app.config import get_settings
 
652
  import httpx
653
 
654
  cfg = get_settings()
@@ -659,9 +644,22 @@ async def place_photo(
659
  async with httpx.AsyncClient(timeout=cfg.google_maps_timeout, follow_redirects=True) as client:
660
  resp = await client.get(url, headers=headers, params=params)
661
  if resp.status_code != 200:
662
- error_body = resp.text[:300]
 
 
 
 
 
 
 
 
 
 
 
 
 
663
  return Response(
664
- content='{"success":false,"error":"HTTP %d: %s"}' % (resp.status_code, error_body),
665
  media_type="application/json",
666
  status_code=200,
667
  )
 
5
 
6
  from fastapi import APIRouter, Depends, Header, Query, Response
7
 
 
8
  from app.core.logger import get_logger
9
  from app.models.schemas import (
10
  GoogleAutocompleteMatchedSubstring,
 
230
  async def place_autocomplete(
231
  body: GooglePlaceAutocompleteRequest,
232
  x_goog_api_key: str = Header(..., alias="X-Goog-Api-Key", description="Your Google API key"),
 
233
  service: GoogleMapsService = Depends(get_maps_service),
234
  ):
235
  start = time.perf_counter()
 
269
  strictbounds: Optional[bool] = Query(None),
270
  sessiontoken: Optional[str] = Query(None),
271
  x_goog_api_key: str = Header(..., alias="X-Goog-Api-Key", description="Your Google API key"),
 
272
  service: GoogleMapsService = Depends(get_maps_service),
273
  ):
274
  start = time.perf_counter()
 
292
  async def query_autocomplete(
293
  body: GoogleQueryAutocompleteRequest,
294
  x_goog_api_key: str = Header(..., alias="X-Goog-Api-Key", description="Your Google API key"),
 
295
  service: GoogleMapsService = Depends(get_maps_service),
296
  ):
297
  start = time.perf_counter()
 
321
  radius: Optional[int] = Query(None, ge=1, le=50000),
322
  language: Optional[str] = Query(None, max_length=10),
323
  x_goog_api_key: str = Header(..., alias="X-Goog-Api-Key", description="Your Google API key"),
 
324
  service: GoogleMapsService = Depends(get_maps_service),
325
  ):
326
  start = time.perf_counter()
 
346
  async def geocode(
347
  body: GoogleGeocodeRequest,
348
  x_goog_api_key: str = Header(..., alias="X-Goog-Api-Key", description="Your Google API key"),
 
349
  service: GoogleMapsService = Depends(get_maps_service),
350
  ):
351
  start = time.perf_counter()
 
372
  language: Optional[str] = Query(None, max_length=10),
373
  bounds: Optional[str] = Query(None),
374
  x_goog_api_key: str = Header(..., alias="X-Goog-Api-Key", description="Your Google API key"),
 
375
  service: GoogleMapsService = Depends(get_maps_service),
376
  ):
377
  start = time.perf_counter()
 
389
  async def reverse_geocode(
390
  body: GoogleReverseGeocodeRequest,
391
  x_goog_api_key: str = Header(..., alias="X-Goog-Api-Key", description="Your Google API key"),
 
392
  service: GoogleMapsService = Depends(get_maps_service),
393
  ):
394
  start = time.perf_counter()
 
415
  result_type: Optional[str] = Query(None),
416
  location_type: Optional[str] = Query(None),
417
  x_goog_api_key: str = Header(..., alias="X-Goog-Api-Key", description="Your Google API key"),
 
418
  service: GoogleMapsService = Depends(get_maps_service),
419
  ):
420
  start = time.perf_counter()
 
440
  async def places_search(
441
  body: GooglePlacesSearchRequest,
442
  x_goog_api_key: str = Header(..., alias="X-Goog-Api-Key", description="Your Google API key"),
 
443
  service: GoogleMapsService = Depends(get_maps_service),
444
  ):
445
  start = time.perf_counter()
 
473
  async def places_nearby(
474
  body: GooglePlacesNearbyRequest,
475
  x_goog_api_key: str = Header(..., alias="X-Goog-Api-Key", description="Your Google API key"),
 
476
  service: GoogleMapsService = Depends(get_maps_service),
477
  ):
478
  start = time.perf_counter()
 
517
  page_size: Optional[int] = Query(None, ge=1, le=200, description="Number of results per page"),
518
  min_rating: Optional[float] = Query(None, ge=0.0, le=5.0, description="Minimum rating filter"),
519
  x_goog_api_key: str = Header(..., alias="X-Goog-Api-Key", description="Your Google API key"),
 
520
  service: GoogleMapsService = Depends(get_maps_service),
521
  ):
522
  start = time.perf_counter()
 
553
  page_size: Optional[int] = Query(None, ge=1, le=200, description="Number of results per page"),
554
  rank_preference: Optional[str] = Query(None, pattern="^(POPULARITY|DISTANCE)$", description="Ranking preference"),
555
  x_goog_api_key: str = Header(..., alias="X-Goog-Api-Key", description="Your Google API key"),
 
556
  service: GoogleMapsService = Depends(get_maps_service),
557
  ):
558
  start = time.perf_counter()
 
580
  async def place_details(
581
  body: GooglePlaceDetailsRequest,
582
  x_goog_api_key: str = Header(..., alias="X-Goog-Api-Key", description="Your Google API key"),
 
583
  service: GoogleMapsService = Depends(get_maps_service),
584
  ):
585
  start = time.perf_counter()
 
607
  language: Optional[str] = Query(None, max_length=10),
608
  fields: Optional[str] = Query(None),
609
  x_goog_api_key: str = Header(..., alias="X-Goog-Api-Key", description="Your Google API key"),
 
610
  service: GoogleMapsService = Depends(get_maps_service),
611
  ):
612
  start = time.perf_counter()
 
630
  max_width_px: int = Query(400, ge=1, le=4800),
631
  max_height_px: int = Query(400, ge=1, le=4800),
632
  x_goog_api_key: str = Header(..., alias="X-Goog-Api-Key", description="Your Google API key"),
 
633
  service: GoogleMapsService = Depends(get_maps_service),
634
  ):
635
  from app.config import get_settings
636
+ import json
637
  import httpx
638
 
639
  cfg = get_settings()
 
644
  async with httpx.AsyncClient(timeout=cfg.google_maps_timeout, follow_redirects=True) as client:
645
  resp = await client.get(url, headers=headers, params=params)
646
  if resp.status_code != 200:
647
+ # Try to extract a clean error message from the response
648
+ try:
649
+ error_body = resp.json()
650
+ err = error_body.get("error", {})
651
+ msg = err.get("message", "")
652
+ if "API key not valid" in msg or err.get("status") == "API_KEY_INVALID":
653
+ clean_error = "Invalid Google API key. Please provide a valid API key via the X-Goog-Api-Key header."
654
+ elif msg:
655
+ clean_error = msg.rstrip(".") + "."
656
+ else:
657
+ clean_error = f"Failed to fetch photo (HTTP {resp.status_code})."
658
+ except Exception:
659
+ clean_error = f"Failed to fetch photo (HTTP {resp.status_code})."
660
+
661
  return Response(
662
+ content=json.dumps({"success": False, "error": clean_error}),
663
  media_type="application/json",
664
  status_code=200,
665
  )
app/api/v1/json_extract.py CHANGED
@@ -3,10 +3,9 @@ from __future__ import annotations
3
  import time
4
  from typing import Any, Optional
5
 
6
- from fastapi import APIRouter, Depends, HTTPException
7
  from pydantic import BaseModel, Field
8
 
9
- from app.api.deps import require_auth
10
  from app.core.logger import get_logger
11
  from app.services.json_service import extract_json
12
 
@@ -57,7 +56,6 @@ class ExtractJsonResponse(BaseModel):
57
  )
58
  async def extract_json_endpoint(
59
  body: ExtractJsonRequest,
60
- token: str = Depends(require_auth),
61
  ) -> ExtractJsonResponse:
62
  start = time.perf_counter()
63
 
 
3
  import time
4
  from typing import Any, Optional
5
 
6
+ from fastapi import APIRouter, HTTPException
7
  from pydantic import BaseModel, Field
8
 
 
9
  from app.core.logger import get_logger
10
  from app.services.json_service import extract_json
11
 
 
56
  )
57
  async def extract_json_endpoint(
58
  body: ExtractJsonRequest,
 
59
  ) -> ExtractJsonResponse:
60
  start = time.perf_counter()
61
 
app/api/v1/keys_extract.py CHANGED
@@ -4,10 +4,9 @@ import asyncio
4
  import time
5
  from typing import Any, Dict, List, Optional
6
 
7
- from fastapi import APIRouter, Depends
8
  from pydantic import BaseModel, Field, field_validator
9
 
10
- from app.api.deps import require_auth
11
  from app.core.logger import get_logger
12
  from app.core.thread_pool import thread_pool
13
  from app.services.keys_extractor_service import KeysExtractor
@@ -133,7 +132,6 @@ async def _extract_single(body: KeysExtractRequest) -> KeysExtractResponse:
133
  @router.post(
134
  "/keys/extract",
135
  response_model=KeysExtractBatchResponse,
136
- dependencies=[Depends(require_auth)],
137
  tags=["Keys Extractor"],
138
  summary="Extract values from nested JSON objects (batch of up to 50 requests processed concurrently)",
139
  )
 
4
  import time
5
  from typing import Any, Dict, List, Optional
6
 
7
+ from fastapi import APIRouter
8
  from pydantic import BaseModel, Field, field_validator
9
 
 
10
  from app.core.logger import get_logger
11
  from app.core.thread_pool import thread_pool
12
  from app.services.keys_extractor_service import KeysExtractor
 
132
  @router.post(
133
  "/keys/extract",
134
  response_model=KeysExtractBatchResponse,
 
135
  tags=["Keys Extractor"],
136
  summary="Extract values from nested JSON objects (batch of up to 50 requests processed concurrently)",
137
  )
app/api/v1/qr_decoder.py CHANGED
@@ -3,10 +3,9 @@ from __future__ import annotations
3
  import time
4
  from typing import List, Optional
5
 
6
- from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
7
  from pydantic import BaseModel, Field
8
 
9
- from app.api.deps import require_auth
10
  from app.core.logger import get_logger
11
  from app.services.qr_decoder_service import QRDecoderService
12
 
@@ -49,7 +48,6 @@ class QRDecodeResponse(BaseModel):
49
  )
50
  async def decode_qr_file(
51
  file: UploadFile = File(..., description="Image file containing a QR code"),
52
- token: str = Depends(require_auth),
53
  ) -> QRDecodeResponse:
54
  start = time.perf_counter()
55
 
@@ -115,7 +113,6 @@ async def decode_qr_file(
115
  )
116
  async def decode_qr_url(
117
  body: QRDecodeUrlRequest,
118
- token: str = Depends(require_auth),
119
  ) -> QRDecodeResponse:
120
  start = time.perf_counter()
121
 
 
3
  import time
4
  from typing import List, Optional
5
 
6
+ from fastapi import APIRouter, File, HTTPException, UploadFile
7
  from pydantic import BaseModel, Field
8
 
 
9
  from app.core.logger import get_logger
10
  from app.services.qr_decoder_service import QRDecoderService
11
 
 
48
  )
49
  async def decode_qr_file(
50
  file: UploadFile = File(..., description="Image file containing a QR code"),
 
51
  ) -> QRDecodeResponse:
52
  start = time.perf_counter()
53
 
 
113
  )
114
  async def decode_qr_url(
115
  body: QRDecodeUrlRequest,
 
116
  ) -> QRDecodeResponse:
117
  start = time.perf_counter()
118
 
app/api/v1/reconcile.py CHANGED
@@ -7,10 +7,10 @@ import uuid
7
  from typing import Any, Dict, List, Optional
8
 
9
  import aiohttp
10
- from fastapi import APIRouter, Depends, File, Form, HTTPException, UploadFile
11
  from pydantic import BaseModel
12
 
13
- from app.api.deps import require_auth
14
  from app.config import get_settings
15
  from app.core.logger import get_logger
16
  from app.services.reconciliation_service import (
@@ -237,7 +237,6 @@ async def reconcile_files(
237
  source_10: UploadFile = File(None),
238
  destination_10: UploadFile = File(None),
239
  column_mapping_10: Optional[str] = Form(None),
240
- token: str = Depends(require_auth),
241
  ) -> ReconciliationResponse:
242
  start_time = time.time()
243
 
@@ -294,7 +293,6 @@ async def reconcile_files(
294
  )
295
  async def reconcile_urls(
296
  body: ReconciliationUrlRequest,
297
- token: str = Depends(require_auth),
298
  ) -> ReconciliationResponse:
299
  start_time = time.time()
300
 
 
7
  from typing import Any, Dict, List, Optional
8
 
9
  import aiohttp
10
+ from fastapi import APIRouter, File, Form, HTTPException, UploadFile
11
  from pydantic import BaseModel
12
 
13
+
14
  from app.config import get_settings
15
  from app.core.logger import get_logger
16
  from app.services.reconciliation_service import (
 
237
  source_10: UploadFile = File(None),
238
  destination_10: UploadFile = File(None),
239
  column_mapping_10: Optional[str] = Form(None),
 
240
  ) -> ReconciliationResponse:
241
  start_time = time.time()
242
 
 
293
  )
294
  async def reconcile_urls(
295
  body: ReconciliationUrlRequest,
 
296
  ) -> ReconciliationResponse:
297
  start_time = time.time()
298
 
app/api/v1/scheduler.py CHANGED
@@ -4,7 +4,7 @@ from typing import Any
4
 
5
  from fastapi import APIRouter, Depends, HTTPException, Query
6
 
7
- from app.api.deps import get_scheduler_service, require_auth
8
  from app.core.logger import get_logger
9
  from app.services.scheduler_service import (
10
  SchedulerService,
@@ -36,7 +36,6 @@ def _paginate(items: list[Any], total: int, page: int, page_size: int) -> dict[s
36
 
37
  @router.get("/scheduler/status", summary="Get scheduler status")
38
  async def get_scheduler_status(
39
- token: str = Depends(require_auth),
40
  scheduler_service: SchedulerService = Depends(get_scheduler_service),
41
  ):
42
  status = scheduler_service.get_scheduler_status()
@@ -46,7 +45,6 @@ async def get_scheduler_status(
46
 
47
  @router.get("/scheduler/metrics", summary="Get scheduler metrics")
48
  async def get_scheduler_metrics(
49
- token: str = Depends(require_auth),
50
  scheduler_service: SchedulerService = Depends(get_scheduler_service),
51
  ):
52
  metrics = await scheduler_service.get_metrics()
@@ -68,7 +66,6 @@ async def get_scheduler_health(
68
  @router.post("/scheduler/jobs", summary="Create a new scheduled job", status_code=201)
69
  async def create_job(
70
  body: dict[str, Any],
71
- token: str = Depends(require_auth),
72
  scheduler_service: SchedulerService = Depends(get_scheduler_service),
73
  ):
74
  errors = _validate_job_create(body)
@@ -88,7 +85,6 @@ async def list_jobs(
88
  tags: str | None = Query(None, description="Comma-separated tags to filter by"),
89
  page: int = Query(1, ge=1, description="Page number"),
90
  page_size: int = Query(20, ge=1, le=100, description="Items per page"),
91
- token: str = Depends(require_auth),
92
  scheduler_service: SchedulerService = Depends(get_scheduler_service),
93
  ):
94
  tag_list = [t.strip() for t in tags.split(",") if t.strip()] if tags else None
@@ -107,7 +103,6 @@ async def list_jobs(
107
  @router.get("/scheduler/jobs/{job_id}", summary="Get a scheduled job by ID")
108
  async def get_job(
109
  job_id: str,
110
- token: str = Depends(require_auth),
111
  scheduler_service: SchedulerService = Depends(get_scheduler_service),
112
  ):
113
  job = await scheduler_service.get_job(job_id)
@@ -120,7 +115,6 @@ async def get_job(
120
  async def update_job(
121
  job_id: str,
122
  body: dict[str, Any],
123
- token: str = Depends(require_auth),
124
  scheduler_service: SchedulerService = Depends(get_scheduler_service),
125
  ):
126
  try:
@@ -135,7 +129,6 @@ async def update_job(
135
  @router.delete("/scheduler/jobs/{job_id}", summary="Delete a scheduled job (soft delete)")
136
  async def delete_job(
137
  job_id: str,
138
- token: str = Depends(require_auth),
139
  scheduler_service: SchedulerService = Depends(get_scheduler_service),
140
  ):
141
  try:
@@ -148,7 +141,6 @@ async def delete_job(
148
  @router.delete("/scheduler/jobs/{job_id}/hard", summary="Permanently delete a scheduled job")
149
  async def hard_delete_job(
150
  job_id: str,
151
- token: str = Depends(require_auth),
152
  scheduler_service: SchedulerService = Depends(get_scheduler_service),
153
  ):
154
  try:
@@ -165,7 +157,6 @@ async def hard_delete_job(
165
  @router.post("/scheduler/jobs/{job_id}/pause", summary="Pause a scheduled job")
166
  async def pause_job(
167
  job_id: str,
168
- token: str = Depends(require_auth),
169
  scheduler_service: SchedulerService = Depends(get_scheduler_service),
170
  ):
171
  try:
@@ -180,7 +171,6 @@ async def pause_job(
180
  @router.post("/scheduler/jobs/{job_id}/resume", summary="Resume a paused job")
181
  async def resume_job(
182
  job_id: str,
183
- token: str = Depends(require_auth),
184
  scheduler_service: SchedulerService = Depends(get_scheduler_service),
185
  ):
186
  try:
@@ -195,7 +185,6 @@ async def resume_job(
195
  @router.post("/scheduler/jobs/{job_id}/run", summary="Trigger a job execution immediately")
196
  async def run_job_now(
197
  job_id: str,
198
- token: str = Depends(require_auth),
199
  scheduler_service: SchedulerService = Depends(get_scheduler_service),
200
  ):
201
  try:
@@ -216,7 +205,6 @@ async def get_job_history(
216
  job_id: str,
217
  page: int = Query(1, ge=1, description="Page number"),
218
  page_size: int = Query(20, ge=1, le=100, description="Items per page"),
219
- token: str = Depends(require_auth),
220
  scheduler_service: SchedulerService = Depends(get_scheduler_service),
221
  ):
222
  job = await scheduler_service.get_job(job_id)
@@ -240,7 +228,6 @@ async def get_execution_history(
240
  page: int = Query(1, ge=1, description="Page number"),
241
  page_size: int = Query(50, ge=1, le=200, description="Items per page"),
242
  status: str | None = Query(None, description="Filter by execution status"),
243
- token: str = Depends(require_auth),
244
  scheduler_service: SchedulerService = Depends(get_scheduler_service),
245
  ):
246
  history, total = await scheduler_service.get_execution_history(
 
4
 
5
  from fastapi import APIRouter, Depends, HTTPException, Query
6
 
7
+ from app.api.deps import get_scheduler_service
8
  from app.core.logger import get_logger
9
  from app.services.scheduler_service import (
10
  SchedulerService,
 
36
 
37
  @router.get("/scheduler/status", summary="Get scheduler status")
38
  async def get_scheduler_status(
 
39
  scheduler_service: SchedulerService = Depends(get_scheduler_service),
40
  ):
41
  status = scheduler_service.get_scheduler_status()
 
45
 
46
  @router.get("/scheduler/metrics", summary="Get scheduler metrics")
47
  async def get_scheduler_metrics(
 
48
  scheduler_service: SchedulerService = Depends(get_scheduler_service),
49
  ):
50
  metrics = await scheduler_service.get_metrics()
 
66
  @router.post("/scheduler/jobs", summary="Create a new scheduled job", status_code=201)
67
  async def create_job(
68
  body: dict[str, Any],
 
69
  scheduler_service: SchedulerService = Depends(get_scheduler_service),
70
  ):
71
  errors = _validate_job_create(body)
 
85
  tags: str | None = Query(None, description="Comma-separated tags to filter by"),
86
  page: int = Query(1, ge=1, description="Page number"),
87
  page_size: int = Query(20, ge=1, le=100, description="Items per page"),
 
88
  scheduler_service: SchedulerService = Depends(get_scheduler_service),
89
  ):
90
  tag_list = [t.strip() for t in tags.split(",") if t.strip()] if tags else None
 
103
  @router.get("/scheduler/jobs/{job_id}", summary="Get a scheduled job by ID")
104
  async def get_job(
105
  job_id: str,
 
106
  scheduler_service: SchedulerService = Depends(get_scheduler_service),
107
  ):
108
  job = await scheduler_service.get_job(job_id)
 
115
  async def update_job(
116
  job_id: str,
117
  body: dict[str, Any],
 
118
  scheduler_service: SchedulerService = Depends(get_scheduler_service),
119
  ):
120
  try:
 
129
  @router.delete("/scheduler/jobs/{job_id}", summary="Delete a scheduled job (soft delete)")
130
  async def delete_job(
131
  job_id: str,
 
132
  scheduler_service: SchedulerService = Depends(get_scheduler_service),
133
  ):
134
  try:
 
141
  @router.delete("/scheduler/jobs/{job_id}/hard", summary="Permanently delete a scheduled job")
142
  async def hard_delete_job(
143
  job_id: str,
 
144
  scheduler_service: SchedulerService = Depends(get_scheduler_service),
145
  ):
146
  try:
 
157
  @router.post("/scheduler/jobs/{job_id}/pause", summary="Pause a scheduled job")
158
  async def pause_job(
159
  job_id: str,
 
160
  scheduler_service: SchedulerService = Depends(get_scheduler_service),
161
  ):
162
  try:
 
171
  @router.post("/scheduler/jobs/{job_id}/resume", summary="Resume a paused job")
172
  async def resume_job(
173
  job_id: str,
 
174
  scheduler_service: SchedulerService = Depends(get_scheduler_service),
175
  ):
176
  try:
 
185
  @router.post("/scheduler/jobs/{job_id}/run", summary="Trigger a job execution immediately")
186
  async def run_job_now(
187
  job_id: str,
 
188
  scheduler_service: SchedulerService = Depends(get_scheduler_service),
189
  ):
190
  try:
 
205
  job_id: str,
206
  page: int = Query(1, ge=1, description="Page number"),
207
  page_size: int = Query(20, ge=1, le=100, description="Items per page"),
 
208
  scheduler_service: SchedulerService = Depends(get_scheduler_service),
209
  ):
210
  job = await scheduler_service.get_job(job_id)
 
228
  page: int = Query(1, ge=1, description="Page number"),
229
  page_size: int = Query(50, ge=1, le=200, description="Items per page"),
230
  status: str | None = Query(None, description="Filter by execution status"),
 
231
  scheduler_service: SchedulerService = Depends(get_scheduler_service),
232
  ):
233
  history, total = await scheduler_service.get_execution_history(
app/api/v1/scraper.py CHANGED
@@ -4,7 +4,6 @@ import time
4
 
5
  from fastapi import APIRouter, Depends
6
 
7
- from app.api.deps import require_auth
8
  from app.core.logger import get_logger
9
  from app.models.schemas import (
10
  ScrapeHealthResponse,
@@ -27,7 +26,6 @@ def get_scraper_service() -> ScraperService:
27
  summary="Check Scrapling framework health and available fetchers",
28
  )
29
  async def scrape_health(
30
- token: str = Depends(require_auth),
31
  svc: ScraperService = Depends(get_scraper_service),
32
  ) -> ScrapeHealthResponse:
33
  _logger.info("Scrape health check")
@@ -47,7 +45,6 @@ async def scrape_health(
47
  )
48
  async def scrape_extract(
49
  body: ScrapeRequest,
50
- token: str = Depends(require_auth),
51
  svc: ScraperService = Depends(get_scraper_service),
52
  ) -> ScrapeResponse:
53
  _logger.info(
 
4
 
5
  from fastapi import APIRouter, Depends
6
 
 
7
  from app.core.logger import get_logger
8
  from app.models.schemas import (
9
  ScrapeHealthResponse,
 
26
  summary="Check Scrapling framework health and available fetchers",
27
  )
28
  async def scrape_health(
 
29
  svc: ScraperService = Depends(get_scraper_service),
30
  ) -> ScrapeHealthResponse:
31
  _logger.info("Scrape health check")
 
45
  )
46
  async def scrape_extract(
47
  body: ScrapeRequest,
 
48
  svc: ScraperService = Depends(get_scraper_service),
49
  ) -> ScrapeResponse:
50
  _logger.info(
app/api/v1/semantic_router.py CHANGED
@@ -4,7 +4,7 @@ import time
4
 
5
  from fastapi import APIRouter, Depends
6
 
7
- from app.api.deps import get_embeddings_service, require_auth
8
  from app.models.schemas import SemanticRouterRequest, SemanticRouterResponse
9
  from app.services.embeddings_service import EmbeddingService
10
  from app.services.semantic_router_service import SemanticRouterService
@@ -19,7 +19,6 @@ router = APIRouter()
19
  )
20
  async def route_query(
21
  body: SemanticRouterRequest,
22
- token: str = Depends(require_auth),
23
  embedding_service: EmbeddingService = Depends(get_embeddings_service),
24
  ) -> SemanticRouterResponse:
25
  start = time.perf_counter()
 
4
 
5
  from fastapi import APIRouter, Depends
6
 
7
+ from app.api.deps import get_embeddings_service
8
  from app.models.schemas import SemanticRouterRequest, SemanticRouterResponse
9
  from app.services.embeddings_service import EmbeddingService
10
  from app.services.semantic_router_service import SemanticRouterService
 
19
  )
20
  async def route_query(
21
  body: SemanticRouterRequest,
 
22
  embedding_service: EmbeddingService = Depends(get_embeddings_service),
23
  ) -> SemanticRouterResponse:
24
  start = time.perf_counter()
app/api/v1/sql_validator.py CHANGED
@@ -2,9 +2,8 @@ from __future__ import annotations
2
 
3
  import time
4
 
5
- from fastapi import APIRouter, Depends
6
 
7
- from app.api.deps import require_auth
8
  from app.models.schemas import SqlValidationRequest, SqlValidationResponse
9
  from app.services.sql_validator_service import SqlValidatorService
10
 
@@ -19,7 +18,6 @@ _service = SqlValidatorService()
19
  )
20
  async def validate_sql(
21
  body: SqlValidationRequest,
22
- token: str = Depends(require_auth),
23
  ):
24
  start = time.perf_counter()
25
  result = _service.validate(body.query, body.dialect)
 
2
 
3
  import time
4
 
5
+ from fastapi import APIRouter
6
 
 
7
  from app.models.schemas import SqlValidationRequest, SqlValidationResponse
8
  from app.services.sql_validator_service import SqlValidatorService
9
 
 
18
  )
19
  async def validate_sql(
20
  body: SqlValidationRequest,
 
21
  ):
22
  start = time.perf_counter()
23
  result = _service.validate(body.query, body.dialect)
app/api/v1/system.py CHANGED
@@ -11,7 +11,7 @@ from pathlib import Path
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
15
  from app.config import get_settings
16
  from app.core.constants import (
17
  ARCHIVE_EXTENSIONS,
@@ -54,9 +54,7 @@ def is_maintenance() -> bool:
54
 
55
 
56
  @router.get("/health", response_model=HealthResponse, summary="Health check")
57
- async def health(
58
- token: str = Depends(require_auth),
59
- ):
60
  return HealthResponse(
61
  success=True,
62
  status="ok",
@@ -67,9 +65,7 @@ async def health(
67
 
68
 
69
  @router.get("/info", response_model=InfoResponse, summary="Server and environment information")
70
- async def info(
71
- token: str = Depends(require_auth),
72
- ):
73
  return InfoResponse(
74
  success=True,
75
  app=_settings.app_name,
@@ -84,9 +80,7 @@ async def info(
84
 
85
 
86
  @router.get("/formats", response_model=SupportedFormatsResponse, summary="List supported file formats")
87
- async def list_formats(
88
- token: str = Depends(require_auth),
89
- ):
90
  by_category = {
91
  "documents": [e for e in SUPPORTED_EXTENSIONS if e in DOCUMENT_EXTENSIONS],
92
  "office": [e for e in SUPPORTED_EXTENSIONS if e in OFFICE_EXTENSIONS],
@@ -107,7 +101,6 @@ async def list_formats(
107
 
108
  @router.get("/spacy-labels", response_model=SpacyLabelsResponse, summary="List available spaCy NER labels")
109
  async def list_spacy_labels(
110
- token: str = Depends(require_auth),
111
  extraction_service: ExtractionService = Depends(get_extraction_service),
112
  ):
113
  return SpacyLabelsResponse(
@@ -152,7 +145,7 @@ def _get_data_dir() -> Path:
152
 
153
 
154
  @router.get("/backup", summary="Download a full data backup archive")
155
- async def download_backup(token: str = Depends(require_auth)):
156
  data_dir = _get_data_dir()
157
  if not data_dir.is_dir():
158
  raise HTTPException(status_code=404, detail="Data directory not found")
@@ -172,7 +165,6 @@ async def download_backup(token: str = Depends(require_auth)):
172
  @router.post("/backup/restore", summary="Upload and restore a data backup archive")
173
  async def upload_and_restore(
174
  file: UploadFile = File(...),
175
- token: str = Depends(require_auth),
176
  ):
177
  data_dir = _get_data_dir()
178
  data_dir.mkdir(parents=True, exist_ok=True)
@@ -204,7 +196,7 @@ async def upload_and_restore(
204
 
205
 
206
  @router.post("/maintenance/enable", summary="Enable maintenance mode (blocks write operations)")
207
- async def enable_maintenance(token: str = Depends(require_auth)):
208
  global _MAINTENANCE_MODE
209
  _MAINTENANCE_MODE = True
210
  lock_path = _maintenance_lock_path()
@@ -218,7 +210,7 @@ async def enable_maintenance(token: str = Depends(require_auth)):
218
 
219
 
220
  @router.post("/maintenance/disable", summary="Disable maintenance mode")
221
- async def disable_maintenance(token: str = Depends(require_auth)):
222
  global _MAINTENANCE_MODE
223
  _MAINTENANCE_MODE = False
224
  lock_path = _maintenance_lock_path()
@@ -231,7 +223,7 @@ async def disable_maintenance(token: str = Depends(require_auth)):
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:
 
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
15
  from app.config import get_settings
16
  from app.core.constants import (
17
  ARCHIVE_EXTENSIONS,
 
54
 
55
 
56
  @router.get("/health", response_model=HealthResponse, summary="Health check")
57
+ async def health():
 
 
58
  return HealthResponse(
59
  success=True,
60
  status="ok",
 
65
 
66
 
67
  @router.get("/info", response_model=InfoResponse, summary="Server and environment information")
68
+ async def info():
 
 
69
  return InfoResponse(
70
  success=True,
71
  app=_settings.app_name,
 
80
 
81
 
82
  @router.get("/formats", response_model=SupportedFormatsResponse, summary="List supported file formats")
83
+ async def list_formats():
 
 
84
  by_category = {
85
  "documents": [e for e in SUPPORTED_EXTENSIONS if e in DOCUMENT_EXTENSIONS],
86
  "office": [e for e in SUPPORTED_EXTENSIONS if e in OFFICE_EXTENSIONS],
 
101
 
102
  @router.get("/spacy-labels", response_model=SpacyLabelsResponse, summary="List available spaCy NER labels")
103
  async def list_spacy_labels(
 
104
  extraction_service: ExtractionService = Depends(get_extraction_service),
105
  ):
106
  return SpacyLabelsResponse(
 
145
 
146
 
147
  @router.get("/backup", summary="Download a full data backup archive")
148
+ async def download_backup():
149
  data_dir = _get_data_dir()
150
  if not data_dir.is_dir():
151
  raise HTTPException(status_code=404, detail="Data directory not found")
 
165
  @router.post("/backup/restore", summary="Upload and restore a data backup archive")
166
  async def upload_and_restore(
167
  file: UploadFile = File(...),
 
168
  ):
169
  data_dir = _get_data_dir()
170
  data_dir.mkdir(parents=True, exist_ok=True)
 
196
 
197
 
198
  @router.post("/maintenance/enable", summary="Enable maintenance mode (blocks write operations)")
199
+ async def enable_maintenance():
200
  global _MAINTENANCE_MODE
201
  _MAINTENANCE_MODE = True
202
  lock_path = _maintenance_lock_path()
 
210
 
211
 
212
  @router.post("/maintenance/disable", summary="Disable maintenance mode")
213
+ async def disable_maintenance():
214
  global _MAINTENANCE_MODE
215
  _MAINTENANCE_MODE = False
216
  lock_path = _maintenance_lock_path()
 
223
 
224
 
225
  @router.get("/maintenance", summary="Check maintenance mode status")
226
+ async def maintenance_status():
227
  lock_path = _maintenance_lock_path()
228
  source = "off"
229
  if _MAINTENANCE_MODE:
app/api/v1/token_counter.py CHANGED
@@ -2,9 +2,8 @@ from __future__ import annotations
2
 
3
  import time
4
 
5
- from fastapi import APIRouter, Depends
6
 
7
- from app.api.deps import require_auth
8
  from app.models.domain import count_tokens
9
  from app.models.schemas import TokenCountRequest, TokenCountResponse
10
 
@@ -18,7 +17,6 @@ router = APIRouter()
18
  )
19
  async def count_text_tokens(
20
  body: TokenCountRequest,
21
- token: str = Depends(require_auth),
22
  ) -> TokenCountResponse:
23
  start = time.perf_counter()
24
  try:
 
2
 
3
  import time
4
 
5
+ from fastapi import APIRouter
6
 
 
7
  from app.models.domain import count_tokens
8
  from app.models.schemas import TokenCountRequest, TokenCountResponse
9
 
 
17
  )
18
  async def count_text_tokens(
19
  body: TokenCountRequest,
 
20
  ) -> TokenCountResponse:
21
  start = time.perf_counter()
22
  try:
app/api/v1/token_generator.py CHANGED
@@ -2,9 +2,8 @@ from __future__ import annotations
2
 
3
  import time
4
 
5
- from fastapi import APIRouter, Depends
6
 
7
- from app.api.deps import require_auth
8
  from app.models.schemas import (
9
  TokenGenerateRequest,
10
  TokenGenerateResponse,
@@ -24,7 +23,6 @@ _service = JWTService()
24
  )
25
  async def generate_token(
26
  body: TokenGenerateRequest,
27
- auth: str = Depends(require_auth),
28
  ) -> TokenGenerateResponse:
29
  start = time.perf_counter()
30
  try:
@@ -67,7 +65,6 @@ async def generate_token(
67
  )
68
  async def validate_token(
69
  body: TokenValidateRequest,
70
- auth: str = Depends(require_auth),
71
  ) -> TokenValidateResponse:
72
  start = time.perf_counter()
73
  try:
 
2
 
3
  import time
4
 
5
+ from fastapi import APIRouter
6
 
 
7
  from app.models.schemas import (
8
  TokenGenerateRequest,
9
  TokenGenerateResponse,
 
23
  )
24
  async def generate_token(
25
  body: TokenGenerateRequest,
 
26
  ) -> TokenGenerateResponse:
27
  start = time.perf_counter()
28
  try:
 
65
  )
66
  async def validate_token(
67
  body: TokenValidateRequest,
 
68
  ) -> TokenValidateResponse:
69
  start = time.perf_counter()
70
  try:
app/api/v1/vector_stores.py CHANGED
@@ -15,7 +15,7 @@ from fastapi import (
15
  status,
16
  )
17
 
18
- from app.api.deps import get_vector_store_service, require_auth
19
  from app.core.logger import get_logger
20
  from app.models.domain import ConversionError
21
  from app.models.schemas import (
@@ -61,7 +61,6 @@ async def _process_pdf_bytes(raw: bytes, source: str, clean_content: bool) -> st
61
  )
62
  async def create_vector_store(
63
  body: VectorStoreCreate,
64
- token: str = Depends(require_auth),
65
  vector_store_service: VectorStoreService = Depends(get_vector_store_service),
66
  ) -> VectorStoreResponse:
67
  store_id = await vector_store_service.create_store(
@@ -88,7 +87,6 @@ async def create_vector_store(
88
  summary="List all vector stores",
89
  )
90
  async def list_vector_stores(
91
- token: str = Depends(require_auth),
92
  vector_store_service: VectorStoreService = Depends(get_vector_store_service),
93
  ) -> VectorStoreListResponse:
94
  records = vector_store_service.list_stores()
@@ -118,7 +116,6 @@ async def list_vector_stores(
118
  )
119
  async def get_vector_store(
120
  store_id: str,
121
- token: str = Depends(require_auth),
122
  vector_store_service: VectorStoreService = Depends(get_vector_store_service),
123
  ) -> VectorStoreResponse:
124
  try:
@@ -144,7 +141,6 @@ async def get_vector_store(
144
  )
145
  async def delete_vector_store(
146
  store_id: str,
147
- token: str = Depends(require_auth),
148
  vector_store_service: VectorStoreService = Depends(get_vector_store_service),
149
  ) -> VectorStoreResponse:
150
  record = vector_store_service.get_store(store_id)
@@ -171,7 +167,6 @@ async def delete_vector_store(
171
  async def ingest_document(
172
  store_id: str,
173
  body: DocumentIngestRequest,
174
- token: str = Depends(require_auth),
175
  vector_store_service: VectorStoreService = Depends(get_vector_store_service),
176
  ) -> DocumentIngestResponse:
177
  record = vector_store_service.get_store(store_id)
@@ -218,7 +213,6 @@ async def ingest_pdf_document(
218
  chunk_size: int = Form(512, ge=64, le=4096),
219
  chunk_overlap: int = Form(64, ge=0, le=512),
220
  clean_content: bool = Query(True, description="Clean markdown text after PDF conversion"),
221
- token: str = Depends(require_auth),
222
  vector_store_service: VectorStoreService = Depends(get_vector_store_service),
223
  ) -> DocumentIngestResponse:
224
  record = vector_store_service.get_store(store_id)
@@ -284,7 +278,6 @@ async def ingest_pdf_url(
284
  store_id: str,
285
  body: DocumentIngestUrlRequest,
286
  clean_content: bool = Query(True, description="Clean markdown text after PDF conversion"),
287
- token: str = Depends(require_auth),
288
  vector_store_service: VectorStoreService = Depends(get_vector_store_service),
289
  ) -> DocumentIngestResponse:
290
  record = vector_store_service.get_store(store_id)
@@ -363,7 +356,6 @@ async def ingest_pdf_url(
363
  async def search_vector_store(
364
  store_id: str,
365
  body: SearchRequest,
366
- token: str = Depends(require_auth),
367
  vector_store_service: VectorStoreService = Depends(get_vector_store_service),
368
  ) -> SearchResponse:
369
  record = vector_store_service.get_store(store_id)
@@ -408,7 +400,6 @@ async def search_vector_store(
408
  async def delete_documents(
409
  store_id: str,
410
  body: DeleteRequest,
411
- token: str = Depends(require_auth),
412
  vector_store_service: VectorStoreService = Depends(get_vector_store_service),
413
  ) -> DeleteResponse:
414
  record = vector_store_service.get_store(store_id)
 
15
  status,
16
  )
17
 
18
+ from app.api.deps import get_vector_store_service
19
  from app.core.logger import get_logger
20
  from app.models.domain import ConversionError
21
  from app.models.schemas import (
 
61
  )
62
  async def create_vector_store(
63
  body: VectorStoreCreate,
 
64
  vector_store_service: VectorStoreService = Depends(get_vector_store_service),
65
  ) -> VectorStoreResponse:
66
  store_id = await vector_store_service.create_store(
 
87
  summary="List all vector stores",
88
  )
89
  async def list_vector_stores(
 
90
  vector_store_service: VectorStoreService = Depends(get_vector_store_service),
91
  ) -> VectorStoreListResponse:
92
  records = vector_store_service.list_stores()
 
116
  )
117
  async def get_vector_store(
118
  store_id: str,
 
119
  vector_store_service: VectorStoreService = Depends(get_vector_store_service),
120
  ) -> VectorStoreResponse:
121
  try:
 
141
  )
142
  async def delete_vector_store(
143
  store_id: str,
 
144
  vector_store_service: VectorStoreService = Depends(get_vector_store_service),
145
  ) -> VectorStoreResponse:
146
  record = vector_store_service.get_store(store_id)
 
167
  async def ingest_document(
168
  store_id: str,
169
  body: DocumentIngestRequest,
 
170
  vector_store_service: VectorStoreService = Depends(get_vector_store_service),
171
  ) -> DocumentIngestResponse:
172
  record = vector_store_service.get_store(store_id)
 
213
  chunk_size: int = Form(512, ge=64, le=4096),
214
  chunk_overlap: int = Form(64, ge=0, le=512),
215
  clean_content: bool = Query(True, description="Clean markdown text after PDF conversion"),
 
216
  vector_store_service: VectorStoreService = Depends(get_vector_store_service),
217
  ) -> DocumentIngestResponse:
218
  record = vector_store_service.get_store(store_id)
 
278
  store_id: str,
279
  body: DocumentIngestUrlRequest,
280
  clean_content: bool = Query(True, description="Clean markdown text after PDF conversion"),
 
281
  vector_store_service: VectorStoreService = Depends(get_vector_store_service),
282
  ) -> DocumentIngestResponse:
283
  record = vector_store_service.get_store(store_id)
 
356
  async def search_vector_store(
357
  store_id: str,
358
  body: SearchRequest,
 
359
  vector_store_service: VectorStoreService = Depends(get_vector_store_service),
360
  ) -> SearchResponse:
361
  record = vector_store_service.get_store(store_id)
 
400
  async def delete_documents(
401
  store_id: str,
402
  body: DeleteRequest,
 
403
  vector_store_service: VectorStoreService = Depends(get_vector_store_service),
404
  ) -> DeleteResponse:
405
  record = vector_store_service.get_store(store_id)
app/api/v1/web_search.py CHANGED
@@ -5,7 +5,6 @@ from typing import Optional
5
 
6
  from fastapi import APIRouter, Depends, Query
7
 
8
- from app.api.deps import require_auth
9
  from app.core.logger import get_logger
10
  from app.models.schemas import (
11
  WebSearchAutocompleteRequest,
@@ -33,7 +32,6 @@ def get_search_service() -> WebSearchService:
33
  )
34
  async def web_search_post(
35
  body: WebSearchRequest,
36
- token: str = Depends(require_auth),
37
  svc: WebSearchService = Depends(get_search_service),
38
  ) -> WebSearchResponse:
39
  _logger.info("Web search (POST): q=%s, cat=%s", body.q, body.categories)
@@ -77,7 +75,6 @@ async def web_search_get(
77
  safesearch: int = Query(0, ge=0, le=2, description="Safe search level"),
78
  engines: Optional[str] = Query(None, description="Comma-separated engines"),
79
  max_results: int = Query(10, ge=1, le=50, description="Max results"),
80
- token: str = Depends(require_auth),
81
  svc: WebSearchService = Depends(get_search_service),
82
  ) -> WebSearchResponse:
83
  _logger.info("Web search (GET): q=%s, cat=%s", q, categories)
@@ -113,7 +110,6 @@ async def web_search_get(
113
  summary="Get SearXNG instance configuration (engines, categories, plugins)",
114
  )
115
  async def web_search_config(
116
- token: str = Depends(require_auth),
117
  svc: WebSearchService = Depends(get_search_service),
118
  ) -> WebSearchConfigResponse:
119
  _logger.info("Web search config request")
@@ -141,7 +137,6 @@ async def web_search_config(
141
  )
142
  async def web_search_autocomplete(
143
  q: str = Query(..., min_length=1, max_length=200, description="Query prefix"),
144
- token: str = Depends(require_auth),
145
  svc: WebSearchService = Depends(get_search_service),
146
  ) -> WebSearchAutocompleteResponse:
147
  _logger.info("Web search autocomplete: q=%s", q)
@@ -164,7 +159,6 @@ async def web_search_autocomplete(
164
  )
165
  async def web_search_autocomplete_post(
166
  body: WebSearchAutocompleteRequest,
167
- token: str = Depends(require_auth),
168
  svc: WebSearchService = Depends(get_search_service),
169
  ) -> WebSearchAutocompleteResponse:
170
  _logger.info("Web search autocomplete (POST): q=%s", body.q)
@@ -186,7 +180,6 @@ async def web_search_autocomplete_post(
186
  summary="Get all SearXNG engine descriptions",
187
  )
188
  async def web_search_engine_descriptions(
189
- token: str = Depends(require_auth),
190
  svc: WebSearchService = Depends(get_search_service),
191
  ) -> WebSearchEngineDescriptionsResponse:
192
  _logger.info("Web search engine descriptions")
@@ -207,7 +200,6 @@ async def web_search_engine_descriptions(
207
  summary="Get SearXNG instance statistics",
208
  )
209
  async def web_search_stats(
210
- token: str = Depends(require_auth),
211
  svc: WebSearchService = Depends(get_search_service),
212
  ) -> WebSearchStatsResponse:
213
  _logger.info("Web search stats request")
@@ -227,7 +219,6 @@ async def web_search_stats(
227
  summary="Check SearXNG instance health",
228
  )
229
  async def web_search_health(
230
- token: str = Depends(require_auth),
231
  svc: WebSearchService = Depends(get_search_service),
232
  ) -> dict:
233
  _logger.info("Web search health check")
 
5
 
6
  from fastapi import APIRouter, Depends, Query
7
 
 
8
  from app.core.logger import get_logger
9
  from app.models.schemas import (
10
  WebSearchAutocompleteRequest,
 
32
  )
33
  async def web_search_post(
34
  body: WebSearchRequest,
 
35
  svc: WebSearchService = Depends(get_search_service),
36
  ) -> WebSearchResponse:
37
  _logger.info("Web search (POST): q=%s, cat=%s", body.q, body.categories)
 
75
  safesearch: int = Query(0, ge=0, le=2, description="Safe search level"),
76
  engines: Optional[str] = Query(None, description="Comma-separated engines"),
77
  max_results: int = Query(10, ge=1, le=50, description="Max results"),
 
78
  svc: WebSearchService = Depends(get_search_service),
79
  ) -> WebSearchResponse:
80
  _logger.info("Web search (GET): q=%s, cat=%s", q, categories)
 
110
  summary="Get SearXNG instance configuration (engines, categories, plugins)",
111
  )
112
  async def web_search_config(
 
113
  svc: WebSearchService = Depends(get_search_service),
114
  ) -> WebSearchConfigResponse:
115
  _logger.info("Web search config request")
 
137
  )
138
  async def web_search_autocomplete(
139
  q: str = Query(..., min_length=1, max_length=200, description="Query prefix"),
 
140
  svc: WebSearchService = Depends(get_search_service),
141
  ) -> WebSearchAutocompleteResponse:
142
  _logger.info("Web search autocomplete: q=%s", q)
 
159
  )
160
  async def web_search_autocomplete_post(
161
  body: WebSearchAutocompleteRequest,
 
162
  svc: WebSearchService = Depends(get_search_service),
163
  ) -> WebSearchAutocompleteResponse:
164
  _logger.info("Web search autocomplete (POST): q=%s", body.q)
 
180
  summary="Get all SearXNG engine descriptions",
181
  )
182
  async def web_search_engine_descriptions(
 
183
  svc: WebSearchService = Depends(get_search_service),
184
  ) -> WebSearchEngineDescriptionsResponse:
185
  _logger.info("Web search engine descriptions")
 
200
  summary="Get SearXNG instance statistics",
201
  )
202
  async def web_search_stats(
 
203
  svc: WebSearchService = Depends(get_search_service),
204
  ) -> WebSearchStatsResponse:
205
  _logger.info("Web search stats request")
 
219
  summary="Check SearXNG instance health",
220
  )
221
  async def web_search_health(
 
222
  svc: WebSearchService = Depends(get_search_service),
223
  ) -> dict:
224
  _logger.info("Web search health check")
app/api/v1/webhook_socket.py CHANGED
@@ -6,14 +6,12 @@ import json
6
 
7
  from fastapi import (
8
  APIRouter,
9
- Depends,
10
  HTTPException,
11
  Request,
12
  WebSocket,
13
  WebSocketDisconnect,
14
  )
15
 
16
- from app.api.deps import require_auth
17
  from app.models.schemas import (
18
  ChannelCreateRequest,
19
  ChannelCreateResponse,
@@ -34,7 +32,6 @@ manager = get_manager()
34
  async def create_channel(
35
  body: ChannelCreateRequest,
36
  request: Request,
37
- token: str = Depends(require_auth),
38
  ):
39
  if body.channel_id and manager.get_channel(body.channel_id):
40
  raise HTTPException(status_code=409, detail=f"Channel '{body.channel_id}' already exists")
@@ -59,9 +56,7 @@ async def create_channel(
59
 
60
 
61
  @router.get("/channels", response_model=ChannelListResponse, summary="List all webhook channels")
62
- async def list_channels(
63
- token: str = Depends(require_auth),
64
- ):
65
  channels = []
66
  for cid, ch in manager.channels.items():
67
  channels.append(ChannelListItem(
@@ -78,7 +73,6 @@ async def list_channels(
78
  @router.get("/channels/{channel_id}", response_model=ChannelInfoResponse, summary="Get channel info")
79
  async def channel_info(
80
  channel_id: str,
81
- token: str = Depends(require_auth),
82
  ):
83
  ch = manager.get_channel(channel_id)
84
  if not ch:
@@ -98,7 +92,6 @@ async def channel_info(
98
  @router.delete("/channels/{channel_id}", response_model=ChannelDeleteResponse, summary="Delete a channel")
99
  async def delete_channel(
100
  channel_id: str,
101
- token: str = Depends(require_auth),
102
  ):
103
  if manager.delete_channel(channel_id):
104
  return ChannelDeleteResponse(deleted=channel_id)
@@ -247,7 +240,5 @@ async def websocket_endpoint(
247
 
248
 
249
  @router.get("/webhook-socket/stats", response_model=WebhookSocketStatsResponse, summary="Webhook/socket server stats")
250
- async def ws_stats(
251
- token: str = Depends(require_auth),
252
- ):
253
  return manager.stats()
 
6
 
7
  from fastapi import (
8
  APIRouter,
 
9
  HTTPException,
10
  Request,
11
  WebSocket,
12
  WebSocketDisconnect,
13
  )
14
 
 
15
  from app.models.schemas import (
16
  ChannelCreateRequest,
17
  ChannelCreateResponse,
 
32
  async def create_channel(
33
  body: ChannelCreateRequest,
34
  request: Request,
 
35
  ):
36
  if body.channel_id and manager.get_channel(body.channel_id):
37
  raise HTTPException(status_code=409, detail=f"Channel '{body.channel_id}' already exists")
 
56
 
57
 
58
  @router.get("/channels", response_model=ChannelListResponse, summary="List all webhook channels")
59
+ async def list_channels():
 
 
60
  channels = []
61
  for cid, ch in manager.channels.items():
62
  channels.append(ChannelListItem(
 
73
  @router.get("/channels/{channel_id}", response_model=ChannelInfoResponse, summary="Get channel info")
74
  async def channel_info(
75
  channel_id: str,
 
76
  ):
77
  ch = manager.get_channel(channel_id)
78
  if not ch:
 
92
  @router.delete("/channels/{channel_id}", response_model=ChannelDeleteResponse, summary="Delete a channel")
93
  async def delete_channel(
94
  channel_id: str,
 
95
  ):
96
  if manager.delete_channel(channel_id):
97
  return ChannelDeleteResponse(deleted=channel_id)
 
240
 
241
 
242
  @router.get("/webhook-socket/stats", response_model=WebhookSocketStatsResponse, summary="Webhook/socket server stats")
243
+ async def ws_stats():
 
 
244
  return manager.stats()
app/services/google_maps_service.py CHANGED
@@ -63,8 +63,8 @@ class GoogleMapsService:
63
  async def geocode(self, address: str, region: Optional[str] = None,
64
  language: Optional[str] = None, bounds: Optional[str] = None,
65
  api_key: Optional[str] = None) -> Dict[str, Any]:
66
- if not api_key:
67
- raise ValueError("Google API key is missing")
68
  params: Dict[str, Any] = {
69
  "address": address,
70
  "key": api_key,
@@ -82,8 +82,8 @@ class GoogleMapsService:
82
  result_type: Optional[str] = None,
83
  location_type: Optional[str] = None,
84
  api_key: Optional[str] = None) -> Dict[str, Any]:
85
- if not api_key:
86
- raise ValueError("Google API key is missing")
87
  params: Dict[str, Any] = {
88
  "latlng": latlng,
89
  "key": api_key,
@@ -271,6 +271,20 @@ class GoogleMapsService:
271
  # Internal — Legacy API (for Geocoding)
272
  # -----------------------------------------------------------------------
273
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
274
  async def _call_api(self, path: str, params: Dict[str, Any]) -> Dict[str, Any]:
275
  url = f"{self._base_url}{path}"
276
  last_error: Optional[str] = None
@@ -287,7 +301,7 @@ class GoogleMapsService:
287
  last_error = "Request timed out"
288
  _logger.warning("Google Maps API timeout on %s (attempt %d/%d)", path, attempt + 1, 1 + self._max_retries)
289
  except httpx.HTTPStatusError as e:
290
- last_error = f"HTTP {e.response.status_code}: {e.response.text[:500]}"
291
  _logger.warning("Google Maps API HTTP error on %s: %s (attempt %d/%d)", path, last_error, attempt + 1, 1 + self._max_retries)
292
  if 400 <= e.response.status_code < 500:
293
  break
@@ -331,13 +345,42 @@ class GoogleMapsService:
331
  # Internal — Places API (New)
332
  # -----------------------------------------------------------------------
333
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
334
  async def _call_places_api(self, method: str, path: str,
335
  body: Optional[Dict[str, Any]] = None,
336
  params: Optional[Dict[str, Any]] = None,
337
  field_mask: Optional[str] = None,
338
  api_key: Optional[str] = None) -> Dict[str, Any]:
339
- if not api_key:
340
- raise ValueError("Google API key is missing")
341
  url = f"{self._places_base_url}{path}"
342
  headers: Dict[str, str] = {
343
  "X-Goog-Api-Key": api_key,
@@ -360,16 +403,11 @@ class GoogleMapsService:
360
  data: Dict[str, Any] = response.json()
361
  return {"success": True, "data": data, "error": None}
362
 
363
- try:
364
- error_body = response.json()
365
- error_msg = error_body.get("error", {}).get("message", response.text[:500])
366
- error_detail = error_body.get("error", {})
367
- _logger.warning("Places API error on %s: status=%s code=%s message=%s",
368
- path, response.status_code, error_detail.get("code"), error_detail.get("message"))
369
- except Exception:
370
- error_msg = response.text[:500]
371
- _logger.warning("Places API error on %s: HTTP %s body=%s", path, response.status_code, error_msg)
372
- last_error = f"HTTP {response.status_code}: {error_msg}"
373
 
374
  if response.status_code == 429:
375
  backoff = 2.0 ** (attempt + 1)
 
63
  async def geocode(self, address: str, region: Optional[str] = None,
64
  language: Optional[str] = None, bounds: Optional[str] = None,
65
  api_key: Optional[str] = None) -> Dict[str, Any]:
66
+ if not api_key or not api_key.strip():
67
+ return {"success": False, "error": "Google API key is missing. Provide a valid API key via the X-Goog-Api-Key header."}
68
  params: Dict[str, Any] = {
69
  "address": address,
70
  "key": api_key,
 
82
  result_type: Optional[str] = None,
83
  location_type: Optional[str] = None,
84
  api_key: Optional[str] = None) -> Dict[str, Any]:
85
+ if not api_key or not api_key.strip():
86
+ return {"success": False, "error": "Google API key is missing. Provide a valid API key via the X-Goog-Api-Key header."}
87
  params: Dict[str, Any] = {
88
  "latlng": latlng,
89
  "key": api_key,
 
271
  # Internal — Legacy API (for Geocoding)
272
  # -----------------------------------------------------------------------
273
 
274
+ @staticmethod
275
+ def _format_legacy_api_error(response: httpx.Response) -> str:
276
+ """Extract a clean error message from a legacy Geocoding API error response."""
277
+ try:
278
+ body = response.json()
279
+ msg = body.get("error_message", "") or body.get("status", "")
280
+ if msg:
281
+ if "invalid" in msg.lower() or "denied" in msg.lower():
282
+ return "Invalid Google API key. Please provide a valid API key via the X-Goog-Api-Key header."
283
+ return msg.rstrip(".") + "."
284
+ except Exception:
285
+ pass
286
+ return f"Geocoding API error (HTTP {response.status_code})."
287
+
288
  async def _call_api(self, path: str, params: Dict[str, Any]) -> Dict[str, Any]:
289
  url = f"{self._base_url}{path}"
290
  last_error: Optional[str] = None
 
301
  last_error = "Request timed out"
302
  _logger.warning("Google Maps API timeout on %s (attempt %d/%d)", path, attempt + 1, 1 + self._max_retries)
303
  except httpx.HTTPStatusError as e:
304
+ last_error = self._format_legacy_api_error(e.response)
305
  _logger.warning("Google Maps API HTTP error on %s: %s (attempt %d/%d)", path, last_error, attempt + 1, 1 + self._max_retries)
306
  if 400 <= e.response.status_code < 500:
307
  break
 
345
  # Internal — Places API (New)
346
  # -----------------------------------------------------------------------
347
 
348
+ @staticmethod
349
+ def _format_places_api_error(status_code: int, response: httpx.Response) -> str:
350
+ """Extract a clean, human-readable error message from a Places API error response."""
351
+ try:
352
+ error_body = response.json()
353
+ err = error_body.get("error", {})
354
+ msg = err.get("message", "") or ""
355
+ api_status = err.get("status", "")
356
+
357
+ # Build a clean message without dumping raw JSON
358
+ if "API key not valid" in msg or "API_KEY_INVALID" in api_status:
359
+ return "Invalid Google API key. Please provide a valid API key via the X-Goog-Api-Key header."
360
+ if "API key expired" in msg or "API_KEY_EXPIRED" in api_status:
361
+ return "Google API key has expired. Please renew your API key."
362
+ if status_code == 403:
363
+ return "Access forbidden. The API key may not have the required APIs enabled."
364
+ if status_code == 404:
365
+ return "The requested resource was not found."
366
+ if status_code == 429:
367
+ return "API rate limit exceeded. Please wait and retry."
368
+
369
+ # Return just the human-readable message, not the raw JSON
370
+ if msg:
371
+ return msg.rstrip(".") + "."
372
+ except Exception:
373
+ pass
374
+
375
+ return f"Google Places API error (HTTP {status_code})."
376
+
377
  async def _call_places_api(self, method: str, path: str,
378
  body: Optional[Dict[str, Any]] = None,
379
  params: Optional[Dict[str, Any]] = None,
380
  field_mask: Optional[str] = None,
381
  api_key: Optional[str] = None) -> Dict[str, Any]:
382
+ if not api_key or not api_key.strip():
383
+ return {"success": False, "error": "Google API key is missing. Provide a valid API key via the X-Goog-Api-Key header."}
384
  url = f"{self._places_base_url}{path}"
385
  headers: Dict[str, str] = {
386
  "X-Goog-Api-Key": api_key,
 
403
  data: Dict[str, Any] = response.json()
404
  return {"success": True, "data": data, "error": None}
405
 
406
+ last_error = self._format_places_api_error(response.status_code, response)
407
+ _logger.warning(
408
+ "Places API error on %s: HTTP %s -> %s",
409
+ path, response.status_code, last_error,
410
+ )
 
 
 
 
 
411
 
412
  if response.status_code == 429:
413
  backoff = 2.0 ** (attempt + 1)