validops-east-1 commited on
Commit
aaec145
·
1 Parent(s): 83d6851

feat: settings config

Browse files
Files changed (8) hide show
  1. .env.example +6 -0
  2. .gitignore +1 -0
  3. app/api/server.py +17 -6
  4. app/api/v1/router.py +41 -32
  5. app/config.py +76 -1
  6. pyproject.toml +1 -0
  7. requirements.txt +1 -0
  8. services.yaml +107 -0
.env.example CHANGED
@@ -27,6 +27,12 @@ ADMIN_PASSWORD=
27
  # Per-client-IP global rate limit (requests / minute). 0 disables.
28
  RATE_LIMIT_PER_MINUTE=60
29
 
 
 
 
 
 
 
30
  # --- URL shortener ---
31
  URL_SHORTENER_SECRET=change-me
32
  URL_SHORTENER_BASE=http://localhost:7860/api/v1
 
27
  # Per-client-IP global rate limit (requests / minute). 0 disables.
28
  RATE_LIMIT_PER_MINUTE=60
29
 
30
+ # --- Service enablement ---
31
+ # Which API groups are mounted is controlled by services.yaml at the project
32
+ # root (set enabled: true/false per service, e.g. google:sheets_api). Point
33
+ # this at a different file to override.
34
+ # SERVICES_CONFIG_PATH=./services.yaml
35
+
36
  # --- URL shortener ---
37
  URL_SHORTENER_SECRET=change-me
38
  URL_SHORTENER_BASE=http://localhost:7860/api/v1
.gitignore CHANGED
@@ -137,3 +137,4 @@ ddl/
137
  scripts/
138
  docs/API_DESCRIPTION.md
139
  docs/planning/
 
 
137
  scripts/
138
  docs/API_DESCRIPTION.md
139
  docs/planning/
140
+ postman
app/api/server.py CHANGED
@@ -18,7 +18,12 @@ from app.api.whatsapp import (
18
  get_whatsapp_health,
19
  router as whatsapp_router,
20
  )
21
- from app.config import get_settings
 
 
 
 
 
22
  from app.core.auth.deps import init_auth_db
23
  from app.core.database import pool_manager
24
  from app.core.logger import get_logger
@@ -91,6 +96,11 @@ async def _self_ping():
91
 
92
  @asynccontextmanager
93
  async def lifespan(app: FastAPI):
 
 
 
 
 
94
  if not _settings.supabase_url or not _settings.supabase_service_role_key:
95
  _logger.error(
96
  "Supabase not configured! Set SUPABASE_URL and "
@@ -269,11 +279,12 @@ def create_application() -> FastAPI:
269
  dependencies=[Depends(_api_rate_limit)],
270
  )
271
 
272
- app.include_router(
273
- whatsapp_router,
274
- prefix="/api/whatsapp",
275
- dependencies=[Depends(_api_rate_limit)],
276
- )
 
277
 
278
  @app.get("/", include_in_schema=False)
279
  async def root(request: Request):
 
18
  get_whatsapp_health,
19
  router as whatsapp_router,
20
  )
21
+ from app.config import (
22
+ disabled_services,
23
+ enabled_services,
24
+ get_settings,
25
+ is_service_enabled,
26
+ )
27
  from app.core.auth.deps import init_auth_db
28
  from app.core.database import pool_manager
29
  from app.core.logger import get_logger
 
96
 
97
  @asynccontextmanager
98
  async def lifespan(app: FastAPI):
99
+ for service in enabled_services():
100
+ _logger.info("%s-service is enabled", service.replace(":", "-"))
101
+ for service in disabled_services():
102
+ _logger.warning("%s-service is disabled", service.replace(":", "-"))
103
+
104
  if not _settings.supabase_url or not _settings.supabase_service_role_key:
105
  _logger.error(
106
  "Supabase not configured! Set SUPABASE_URL and "
 
279
  dependencies=[Depends(_api_rate_limit)],
280
  )
281
 
282
+ if is_service_enabled("whatsapp:service"):
283
+ app.include_router(
284
+ whatsapp_router,
285
+ prefix="/api/whatsapp",
286
+ dependencies=[Depends(_api_rate_limit)],
287
+ )
288
 
289
  @app.get("/", include_in_schema=False)
290
  async def root(request: Request):
app/api/v1/router.py CHANGED
@@ -36,37 +36,46 @@ from app.api.v1 import (
36
  webhook_socket,
37
  )
38
  from app.api.verify import router as verify_router
 
39
 
40
  api_v1_router = APIRouter()
41
- api_v1_router.include_router(auth.router, tags=["Authentication"])
42
- api_v1_router.include_router(convert.router, tags=["Convert"])
43
- api_v1_router.include_router(batch.router, tags=["Batch"])
44
- api_v1_router.include_router(system.router, tags=["System"])
45
- api_v1_router.include_router(database.router, tags=["Database"])
46
- api_v1_router.include_router(embeddings.router, tags=["Embeddings"])
47
- api_v1_router.include_router(code_executor.router, tags=["Code Executor"])
48
- api_v1_router.include_router(verify_router, prefix="/verify", tags=["Verify"])
49
- api_v1_router.include_router(reconcile.router, tags=["Reconcile"])
50
- api_v1_router.include_router(scheduler.router, tags=["Scheduler"])
51
- api_v1_router.include_router(scraper.router, tags=["Web Scraping"])
52
- api_v1_router.include_router(web_search.router, tags=["Web Search"])
53
- api_v1_router.include_router(sql_validator.router, tags=["SQL Validator"])
54
- api_v1_router.include_router(semantic_router.router, tags=["Semantic Router"])
55
- api_v1_router.include_router(token_counter.router, tags=["Token Counter"])
56
- api_v1_router.include_router(token_generator.router, tags=["Token Generator"])
57
- api_v1_router.include_router(chat.router, tags=["Chat"])
58
- api_v1_router.include_router(vector_stores.router, tags=["Vector Stores"])
59
- api_v1_router.include_router(webhook_socket.router, tags=["Webhook / Socket"])
60
- api_v1_router.include_router(csv_analysis.router, tags=["CSV Analysis"])
61
- api_v1_router.include_router(google_maps.router, tags=["Google Maps"])
62
- api_v1_router.include_router(google_oauth.router, tags=["Google OAuth"])
63
- api_v1_router.include_router(scopes.router, tags=["Google Scopes"])
64
- api_v1_router.include_router(gcs.router, tags=["Google Cloud Storage"])
65
- api_v1_router.include_router(gmail.router, tags=["Gmail"])
66
- api_v1_router.include_router(sheets.router, tags=["Google Sheets"])
67
- api_v1_router.include_router(media_convert.router, tags=["Media-to-Media Conversion"])
68
- api_v1_router.include_router(json_extract.router, tags=["JSON Extractor"])
69
- api_v1_router.include_router(keys_extract.router, prefix="/json", tags=["Keys Extractor"])
70
- api_v1_router.include_router(qr_decoder.router, tags=["QR Decoder"])
71
- api_v1_router.include_router(qr_generator.router, tags=["QR Generator"])
72
- api_v1_router.include_router(url_shortener.router, tags=["URL Shortener"])
 
 
 
 
 
 
 
 
 
36
  webhook_socket,
37
  )
38
  from app.api.verify import router as verify_router
39
+ from app.config import is_service_enabled
40
 
41
  api_v1_router = APIRouter()
42
+
43
+
44
+ def _include(router: APIRouter, service_path: str, **kwargs) -> None:
45
+ """Mount a router only when its service is enabled in services.yaml."""
46
+ if is_service_enabled(service_path):
47
+ api_v1_router.include_router(router, **kwargs)
48
+
49
+
50
+ _include(auth.router, "auth", tags=["Authentication"])
51
+ _include(convert.router, "convert", tags=["Convert"])
52
+ _include(batch.router, "batch", tags=["Batch"])
53
+ _include(system.router, "system", tags=["System"])
54
+ _include(database.router, "database", tags=["Database"])
55
+ _include(embeddings.router, "embeddings", tags=["Embeddings"])
56
+ _include(code_executor.router, "code_executor", tags=["Code Executor"])
57
+ _include(verify_router, "verify", prefix="/verify", tags=["Verify"])
58
+ _include(reconcile.router, "reconcile", tags=["Reconcile"])
59
+ _include(scheduler.router, "scheduler", tags=["Scheduler"])
60
+ _include(scraper.router, "scraper", tags=["Web Scraping"])
61
+ _include(web_search.router, "web_search", tags=["Web Search"])
62
+ _include(sql_validator.router, "sql_validator", tags=["SQL Validator"])
63
+ _include(semantic_router.router, "semantic_router", tags=["Semantic Router"])
64
+ _include(token_counter.router, "token_counter", tags=["Token Counter"])
65
+ _include(token_generator.router, "token_generator", tags=["Token Generator"])
66
+ _include(chat.router, "chat", tags=["Chat"])
67
+ _include(vector_stores.router, "vector_stores", tags=["Vector Stores"])
68
+ _include(webhook_socket.router, "webhook_socket", tags=["Webhook / Socket"])
69
+ _include(csv_analysis.router, "csv_analysis", tags=["CSV Analysis"])
70
+ _include(google_maps.router, "google:maps", tags=["Google Maps"])
71
+ _include(google_oauth.router, "google:oauth", tags=["Google OAuth"])
72
+ _include(scopes.router, "google:scopes", tags=["Google Scopes"])
73
+ _include(gcs.router, "google:cloud_storage", tags=["Google Cloud Storage"])
74
+ _include(gmail.router, "google:gmail", tags=["Gmail"])
75
+ _include(sheets.router, "google:sheets_api", tags=["Google Sheets"])
76
+ _include(media_convert.router, "media_convert", tags=["Media-to-Media Conversion"])
77
+ _include(json_extract.router, "json_extract", tags=["JSON Extractor"])
78
+ _include(keys_extract.router, "keys_extract", prefix="/json", tags=["Keys Extractor"])
79
+ _include(qr_decoder.router, "qr_decoder", tags=["QR Decoder"])
80
+ _include(qr_generator.router, "qr_generator", tags=["QR Generator"])
81
+ _include(url_shortener.router, "url_shortener", tags=["URL Shortener"])
app/config.py CHANGED
@@ -1,7 +1,11 @@
1
  from __future__ import annotations
2
 
 
3
  from functools import lru_cache
4
- from typing import Optional
 
 
 
5
 
6
  from pydantic import Field
7
  from pydantic_settings import BaseSettings, SettingsConfigDict
@@ -272,3 +276,74 @@ def get_settings() -> Settings:
272
  + ". Set strong values in your environment (see .env.example)."
273
  )
274
  return settings
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  from __future__ import annotations
2
 
3
+ import os
4
  from functools import lru_cache
5
+ from pathlib import Path
6
+ from typing import Dict, Optional
7
+
8
+ import yaml
9
 
10
  from pydantic import Field
11
  from pydantic_settings import BaseSettings, SettingsConfigDict
 
276
  + ". Set strong values in your environment (see .env.example)."
277
  )
278
  return settings
279
+
280
+
281
+ # ---------------------------------------------------------------------------
282
+ # Service / API enablement
283
+ #
284
+ # Per-service on/off switches are defined in `services.yaml` at the project
285
+ # root (override the location with SERVICES_CONFIG_PATH). The YAML is nested,
286
+ # e.g.:
287
+ #
288
+ # google:
289
+ # sheets_api:
290
+ # enabled: true
291
+ #
292
+ # and is addressed in code by its colon-joined path, e.g.
293
+ # `is_service_enabled("google:sheets_api")`. Entries missing from the file
294
+ # default to enabled so the API surface never silently shrinks.
295
+ # ---------------------------------------------------------------------------
296
+
297
+ _DEFAULT_SERVICES_CONFIG = Path(__file__).resolve().parent.parent / "services.yaml"
298
+
299
+
300
+ def _services_config_path() -> Path:
301
+ return Path(os.environ.get("SERVICES_CONFIG_PATH") or _DEFAULT_SERVICES_CONFIG)
302
+
303
+
304
+ def _flatten_service_config(node: Dict[str, object], prefix: str = "") -> Dict[str, bool]:
305
+ """Flatten nested YAML {group: {service: {enabled: bool}}} into paths.
306
+
307
+ Returns a map of "group:service" -> enabled flag. Keys without an
308
+ `enabled` field are skipped (so arbitrary metadata can live alongside).
309
+ """
310
+ flattened: Dict[str, bool] = {}
311
+ for key, value in node.items():
312
+ path = f"{prefix}:{key}" if prefix else key
313
+ if isinstance(value, dict):
314
+ if "enabled" in value:
315
+ flattened[path] = bool(value.get("enabled", True))
316
+ else:
317
+ flattened.update(_flatten_service_config(value, path))
318
+ return flattened
319
+
320
+
321
+ @lru_cache(maxsize=1)
322
+ def get_services_config() -> Dict[str, bool]:
323
+ """Load and flatten services.yaml into {"google:sheets_api": True, ...}."""
324
+ config_path = _services_config_path()
325
+ if not config_path.exists():
326
+ return {}
327
+ try:
328
+ with open(config_path, "r", encoding="utf-8") as handle:
329
+ raw = yaml.safe_load(handle) or {}
330
+ except Exception:
331
+ return {}
332
+ if not isinstance(raw, dict):
333
+ return {}
334
+ return _flatten_service_config(raw)
335
+
336
+
337
+ def is_service_enabled(service_path: str) -> bool:
338
+ """Return True if the named service is enabled (missing entries default on)."""
339
+ return get_services_config().get(service_path, True)
340
+
341
+
342
+ def enabled_services() -> list[str]:
343
+ """Colon-joined paths of all explicitly-enabled services, sorted."""
344
+ return sorted(path for path, enabled in get_services_config().items() if enabled)
345
+
346
+
347
+ def disabled_services() -> list[str]:
348
+ """Colon-joined paths of all explicitly-disabled services, sorted."""
349
+ return sorted(path for path, enabled in get_services_config().items() if not enabled)
pyproject.toml CHANGED
@@ -16,6 +16,7 @@ dependencies = [
16
  "uvicorn[standard]>=0.30",
17
  "pydantic>=2.7",
18
  "pydantic-settings>=2.0.0",
 
19
  "python-multipart>=0.0.9",
20
  "httpx>=0.27",
21
  "youtube-transcript-api>=1.2.4",
 
16
  "uvicorn[standard]>=0.30",
17
  "pydantic>=2.7",
18
  "pydantic-settings>=2.0.0",
19
+ "PyYAML>=6.0.0",
20
  "python-multipart>=0.0.9",
21
  "httpx>=0.27",
22
  "youtube-transcript-api>=1.2.4",
requirements.txt CHANGED
@@ -6,6 +6,7 @@ pydantic-settings==2.6.1
6
  python-multipart==0.0.20
7
  httpx==0.28.1
8
  aiohttp==3.11.13
 
9
 
10
  # --- Media / file processing ---
11
  numpy==2.2.6
 
6
  python-multipart==0.0.20
7
  httpx==0.28.1
8
  aiohttp==3.11.13
9
+ PyYAML==6.0.2
10
 
11
  # --- Media / file processing ---
12
  numpy==2.2.6
services.yaml ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ------------------------------------------------------------------
2
+ # AgentDeck Backend - service / API enablement config
3
+ #
4
+ # This file controls which API service groups are enabled at startup.
5
+ # Services marked `enabled: true` are mounted and their routes registered;
6
+ # services marked `enabled: false` are NOT mounted (requests return 404)
7
+ # and are logged as disabled. Missing entries default to enabled.
8
+ #
9
+ # The path style used by code is e.g. "google:sheets_api" (colons join the
10
+ # nested keys). Override the file location with SERVICES_CONFIG_PATH.
11
+ # ------------------------------------------------------------------
12
+
13
+ auth:
14
+ enabled: true
15
+
16
+ convert:
17
+ enabled: true
18
+
19
+ batch:
20
+ enabled: true
21
+
22
+ system:
23
+ enabled: true
24
+
25
+ database:
26
+ enabled: true
27
+
28
+ embeddings:
29
+ enabled: true
30
+
31
+ code_executor:
32
+ enabled: true
33
+
34
+ verify:
35
+ enabled: true
36
+
37
+ reconcile:
38
+ enabled: true
39
+
40
+ scheduler:
41
+ enabled: true
42
+
43
+ scraper:
44
+ enabled: true
45
+
46
+ web_search:
47
+ enabled: true
48
+
49
+ sql_validator:
50
+ enabled: true
51
+
52
+ semantic_router:
53
+ enabled: true
54
+
55
+ token_counter:
56
+ enabled: true
57
+
58
+ token_generator:
59
+ enabled: true
60
+
61
+ chat:
62
+ enabled: true
63
+
64
+ vector_stores:
65
+ enabled: true
66
+
67
+ webhook_socket:
68
+ enabled: true
69
+
70
+ csv_analysis:
71
+ enabled: true
72
+
73
+ google:
74
+ maps:
75
+ enabled: true
76
+ oauth:
77
+ enabled: true
78
+ scopes:
79
+ enabled: true
80
+ cloud_storage:
81
+ enabled: true
82
+ gmail:
83
+ enabled: true
84
+ sheets_api:
85
+ enabled: true
86
+
87
+ media_convert:
88
+ enabled: true
89
+
90
+ json_extract:
91
+ enabled: true
92
+
93
+ keys_extract:
94
+ enabled: true
95
+
96
+ qr_decoder:
97
+ enabled: true
98
+
99
+ qr_generator:
100
+ enabled: true
101
+
102
+ url_shortener:
103
+ enabled: true
104
+
105
+ whatsapp:
106
+ service:
107
+ enabled: true