unique-search-proxy 2026.26.0.dev10__tar.gz → 2026.26.0.dev12__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/PKG-INFO +2 -2
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/pyproject.toml +2 -2
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/app.py +18 -15
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/agent_engines/vertexai/client.py +4 -3
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/client/service.py +6 -3
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/crawlers/firecrawl/service.py +4 -3
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/crawlers/jina/service.py +2 -1
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/crawlers/tavily/service.py +2 -1
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/search_engines/brave/service.py +2 -1
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/search_engines/google/service.py +2 -1
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/search_engines/perplexity/service.py +2 -1
- unique_search_proxy-2026.26.0.dev12/unique_search_proxy_client/web/logging_config.py +39 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/settings/base.py +2 -3
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/settings/client.py +5 -4
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/settings/providers/base.py +20 -5
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/settings/providers/brave.py +2 -1
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/settings/providers/firecrawl.py +2 -1
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/settings/providers/google.py +2 -1
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/settings/providers/jina.py +2 -1
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/settings/providers/perplexity.py +2 -1
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/settings/providers/tavily.py +2 -1
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/settings/providers/vertexai_agent.py +2 -1
- unique_search_proxy-2026.26.0.dev12/unique_search_proxy_client/web/settings/secret_str.py +41 -0
- unique_search_proxy-2026.26.0.dev12/unique_search_proxy_client/web/settings/startup_log.py +29 -0
- unique_search_proxy-2026.26.0.dev12/unique_search_proxy_client/web/settings/startup_report.py +138 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/README.md +0 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/__init__.py +0 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/__init__.py +0 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/api/__init__.py +0 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/api/health.py +0 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/api/v1/__init__.py +0 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/api/v1/agent_search.py +0 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/api/v1/configuration.py +0 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/api/v1/crawl.py +0 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/api/v1/openapi_examples.py +0 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/api/v1/search.py +0 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/__init__.py +0 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/agent_engines/__init__.py +0 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/agent_engines/bing/client.py +0 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/agent_engines/bing/runner.py +0 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/agent_engines/bing/service.py +0 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/agent_engines/factory.py +0 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/agent_engines/serialization.py +0 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/agent_engines/service_base.py +0 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/agent_engines/structured_output.py +0 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/agent_engines/vertexai/gemini.py +0 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/agent_engines/vertexai/service.py +0 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/client/__init__.py +0 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/crawlers/__init__.py +0 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/crawlers/basic/__init__.py +0 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/crawlers/basic/processing/__init__.py +0 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/crawlers/basic/processing/errors.py +0 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/crawlers/basic/processing/html_markdown.py +0 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/crawlers/basic/processing/processors/__init__.py +0 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/crawlers/basic/processing/processors/html.py +0 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/crawlers/basic/processing/processors/pdf.py +0 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/crawlers/basic/processing/processors/plain_text.py +0 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/crawlers/basic/processing/registry.py +0 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/crawlers/basic/service.py +0 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/crawlers/basic/settings.py +0 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/crawlers/basic/user_agent.py +0 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/crawlers/factory.py +0 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/crawlers/firecrawl/polling.py +0 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/crawlers/firecrawl/request_body.py +0 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/crawlers/jina/request_body.py +0 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/crawlers/pinned_egress.py +0 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/crawlers/tavily/request_body.py +0 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/provider_response.py +0 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/providers.py +0 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/registry.py +0 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/search_engines/__init__.py +0 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/search_engines/brave/__init__.py +0 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/search_engines/brave/pagination.py +0 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/search_engines/brave/query_params.py +0 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/search_engines/descriptor.py +0 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/search_engines/factory.py +0 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/search_engines/google/__init__.py +0 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/search_engines/google/pagination.py +0 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/search_engines/google/query_params.py +0 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/search_engines/pagination.py +0 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/search_engines/perplexity/__init__.py +0 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/search_engines/perplexity/request_body.py +0 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/search_engines/service_base.py +0 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/url_safety/__init__.py +0 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/url_safety/gate.py +0 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/error_handlers.py +0 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/monitoring/__init__.py +0 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/monitoring/metrics.py +0 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/monitoring/setup.py +0 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/presets/__init__.py +0 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/presets/common.py +0 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/presets/crawl.py +0 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/presets/search.py +0 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/presets/types.py +0 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/settings/__init__.py +0 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/settings/monitoring.py +0 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/settings/providers/__init__.py +0 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/settings/providers/bing_agent.py +0 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/utils/__init__.py +0 -0
- {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/utils/url.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: unique-search-proxy
|
|
3
|
-
Version: 2026.26.0.
|
|
3
|
+
Version: 2026.26.0.dev12
|
|
4
4
|
Summary: Web Search Proxy implementation
|
|
5
5
|
Author: ThePhilAz
|
|
6
6
|
Author-email: ThePhilAz <rami.azouz@philico.com>
|
|
@@ -19,7 +19,7 @@ Requires-Dist: certifi>=2025.11.12,<2027
|
|
|
19
19
|
Requires-Dist: google-genai>=1.73.0,<2
|
|
20
20
|
Requires-Dist: google-auth>=2.43.0,<3
|
|
21
21
|
Requires-Dist: unique-toolkit[monitoring]>=2026.26.0.dev11,<2026.26.0rc0
|
|
22
|
-
Requires-Dist: unique-search-proxy-core>=2026.26.0.
|
|
22
|
+
Requires-Dist: unique-search-proxy-core>=2026.26.0.dev7,<2026.26.0rc0
|
|
23
23
|
Requires-Python: >=3.12
|
|
24
24
|
Description-Content-Type: text/markdown
|
|
25
25
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "unique-search-proxy"
|
|
3
|
-
version = "2026.26.0.
|
|
3
|
+
version = "2026.26.0.dev12"
|
|
4
4
|
description = "Web Search Proxy implementation"
|
|
5
5
|
authors = [{ name = "ThePhilAz", email = "rami.azouz@philico.com" }]
|
|
6
6
|
readme = "README.md"
|
|
@@ -21,7 +21,7 @@ dependencies = [
|
|
|
21
21
|
"google-genai>=1.73.0,<2",
|
|
22
22
|
"google-auth>=2.43.0,<3",
|
|
23
23
|
"unique-toolkit[monitoring]>=2026.26.0.dev11,<2026.26.0rc0",
|
|
24
|
-
"unique-search-proxy-core>=2026.26.0.
|
|
24
|
+
"unique-search-proxy-core>=2026.26.0.dev7,<2026.26.0rc0",
|
|
25
25
|
]
|
|
26
26
|
|
|
27
27
|
[dependency-groups]
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
-
import
|
|
4
|
+
import sys
|
|
5
5
|
from contextlib import asynccontextmanager
|
|
6
6
|
|
|
7
7
|
from dotenv import load_dotenv
|
|
@@ -13,22 +13,19 @@ from unique_search_proxy_client.web.api import health_router, v1_router
|
|
|
13
13
|
from unique_search_proxy_client.web.core.client.service import create_http_client_pool
|
|
14
14
|
from unique_search_proxy_client.web.core.providers import register_builtin_providers
|
|
15
15
|
from unique_search_proxy_client.web.error_handlers import register_exception_handlers
|
|
16
|
+
from unique_search_proxy_client.web.logging_config import (
|
|
17
|
+
build_logging_config,
|
|
18
|
+
configure_logging,
|
|
19
|
+
)
|
|
16
20
|
from unique_search_proxy_client.web.monitoring import setup_prometheus
|
|
21
|
+
from unique_search_proxy_client.web.settings.startup_report import (
|
|
22
|
+
log_startup_settings_report,
|
|
23
|
+
)
|
|
17
24
|
|
|
18
|
-
|
|
25
|
+
if "pytest" not in sys.modules:
|
|
26
|
+
load_dotenv()
|
|
19
27
|
|
|
20
|
-
|
|
21
|
-
def _configure_logging() -> None:
|
|
22
|
-
level_name = os.getenv("LOG_LEVEL", "INFO").upper()
|
|
23
|
-
level = getattr(logging, level_name, logging.INFO)
|
|
24
|
-
logging.basicConfig(
|
|
25
|
-
level=level,
|
|
26
|
-
format="%(levelname)s %(name)s: %(message)s",
|
|
27
|
-
force=True,
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
_configure_logging()
|
|
28
|
+
configure_logging()
|
|
32
29
|
suppress_httpx_request_logs()
|
|
33
30
|
|
|
34
31
|
_LOGGER = logging.getLogger(__name__)
|
|
@@ -52,6 +49,7 @@ logging.getLogger("uvicorn.access").addFilter(HealthCheckFilter())
|
|
|
52
49
|
@asynccontextmanager
|
|
53
50
|
async def lifespan(app: FastAPI):
|
|
54
51
|
_LOGGER.info("Starting Unique Search Proxy...")
|
|
52
|
+
log_startup_settings_report(_LOGGER)
|
|
55
53
|
pool = await create_http_client_pool()
|
|
56
54
|
app.state.http_client_pool = pool
|
|
57
55
|
try:
|
|
@@ -95,4 +93,9 @@ app = create_app()
|
|
|
95
93
|
if __name__ == "__main__":
|
|
96
94
|
import uvicorn
|
|
97
95
|
|
|
98
|
-
uvicorn.run(
|
|
96
|
+
uvicorn.run(
|
|
97
|
+
app,
|
|
98
|
+
host="0.0.0.0",
|
|
99
|
+
port=2349,
|
|
100
|
+
log_config=build_logging_config(),
|
|
101
|
+
)
|
|
@@ -8,6 +8,7 @@ from google.auth import load_credentials_from_dict
|
|
|
8
8
|
from google.genai._api_client import BaseApiClient
|
|
9
9
|
from google.genai.client import AsyncClient
|
|
10
10
|
|
|
11
|
+
from unique_search_proxy_client.web.settings.providers.base import read_secret
|
|
11
12
|
from unique_search_proxy_client.web.settings.providers.vertexai_agent import (
|
|
12
13
|
vertexai_agent_credentials,
|
|
13
14
|
)
|
|
@@ -22,9 +23,9 @@ def _get_base_api_client_from_service_account() -> BaseApiClient:
|
|
|
22
23
|
"https://www.googleapis.com/auth/cloud-platform",
|
|
23
24
|
]
|
|
24
25
|
service_account_info = json.loads(
|
|
25
|
-
b64decode(
|
|
26
|
-
|
|
27
|
-
),
|
|
26
|
+
b64decode(
|
|
27
|
+
read_secret(vertexai_agent_credentials.service_account_credentials)
|
|
28
|
+
).decode("utf-8"),
|
|
28
29
|
)
|
|
29
30
|
credentials, project_id = load_credentials_from_dict(
|
|
30
31
|
service_account_info,
|
|
@@ -14,6 +14,8 @@ from unique_search_proxy_client.web.settings.client import (
|
|
|
14
14
|
ProxyConfig,
|
|
15
15
|
http_client_settings,
|
|
16
16
|
)
|
|
17
|
+
from unique_search_proxy_client.web.settings.providers.base import read_secret
|
|
18
|
+
from unique_search_proxy_client.web.settings.secret_str import read_secret_headers
|
|
17
19
|
|
|
18
20
|
if TYPE_CHECKING:
|
|
19
21
|
from fastapi import FastAPI
|
|
@@ -51,7 +53,8 @@ def _build_proxy_url_with_username_password(settings: HttpClientSettings) -> str
|
|
|
51
53
|
raise ValueError("Proxy username and password are required")
|
|
52
54
|
return (
|
|
53
55
|
f"{settings.proxy_protocol}://"
|
|
54
|
-
f"{proxy_username}:{proxy_password}
|
|
56
|
+
f"{read_secret(proxy_username)}:{read_secret(proxy_password)}"
|
|
57
|
+
f"@{proxy_host}:{proxy_port}"
|
|
55
58
|
)
|
|
56
59
|
|
|
57
60
|
|
|
@@ -92,7 +95,7 @@ def _get_username_password_proxy_kwargs(settings: HttpClientSettings) -> ProxyCo
|
|
|
92
95
|
)
|
|
93
96
|
return ProxyConfig(
|
|
94
97
|
proxy=proxy_url,
|
|
95
|
-
headers=settings.proxy_headers,
|
|
98
|
+
headers=read_secret_headers(settings.proxy_headers) or None,
|
|
96
99
|
verify=settings.proxy_ssl_ca_bundle_path or True,
|
|
97
100
|
)
|
|
98
101
|
|
|
@@ -104,7 +107,7 @@ def _get_ssl_tls_proxy_kwargs(settings: HttpClientSettings) -> ProxyConfig:
|
|
|
104
107
|
return ProxyConfig(
|
|
105
108
|
proxy=proxy_url,
|
|
106
109
|
cert=cert_args,
|
|
107
|
-
headers=settings.proxy_headers,
|
|
110
|
+
headers=read_secret_headers(settings.proxy_headers) or None,
|
|
108
111
|
verify=settings.proxy_ssl_ca_bundle_path or True,
|
|
109
112
|
)
|
|
110
113
|
|
|
@@ -25,6 +25,7 @@ from unique_search_proxy_client.web.core.provider_response import (
|
|
|
25
25
|
upstream_error_message,
|
|
26
26
|
upstream_response_raw,
|
|
27
27
|
)
|
|
28
|
+
from unique_search_proxy_client.web.settings.providers.base import read_secret
|
|
28
29
|
from unique_search_proxy_client.web.settings.providers.firecrawl import (
|
|
29
30
|
firecrawl_crawl_credentials as credentials,
|
|
30
31
|
)
|
|
@@ -90,7 +91,7 @@ class FirecrawlCrawlerService(BaseCrawler[FirecrawlCrawlRequest]):
|
|
|
90
91
|
response = await client.post(
|
|
91
92
|
credentials.scrape_endpoint,
|
|
92
93
|
json=body,
|
|
93
|
-
headers=_firecrawl_headers(credentials.api_key),
|
|
94
|
+
headers=_firecrawl_headers(read_secret(credentials.api_key)),
|
|
94
95
|
timeout=timeout,
|
|
95
96
|
)
|
|
96
97
|
except httpx.TimeoutException as exc:
|
|
@@ -149,7 +150,7 @@ class FirecrawlCrawlerService(BaseCrawler[FirecrawlCrawlRequest]):
|
|
|
149
150
|
start_response = await client.post(
|
|
150
151
|
credentials.batch_scrape_endpoint,
|
|
151
152
|
json=body,
|
|
152
|
-
headers=_firecrawl_headers(credentials.api_key),
|
|
153
|
+
headers=_firecrawl_headers(read_secret(credentials.api_key)),
|
|
153
154
|
timeout=timeout,
|
|
154
155
|
)
|
|
155
156
|
except httpx.TimeoutException as exc:
|
|
@@ -200,7 +201,7 @@ class FirecrawlCrawlerService(BaseCrawler[FirecrawlCrawlRequest]):
|
|
|
200
201
|
final_payload = await poll_batch_scrape(
|
|
201
202
|
client,
|
|
202
203
|
status_url=status_url,
|
|
203
|
-
api_key=credentials.api_key,
|
|
204
|
+
api_key=read_secret(credentials.api_key),
|
|
204
205
|
deadline=deadline,
|
|
205
206
|
)
|
|
206
207
|
except TimeoutError as exc:
|
|
@@ -18,6 +18,7 @@ from unique_search_proxy_client.web.core.provider_response import (
|
|
|
18
18
|
transport_error_raw,
|
|
19
19
|
upstream_response_raw,
|
|
20
20
|
)
|
|
21
|
+
from unique_search_proxy_client.web.settings.providers.base import read_secret
|
|
21
22
|
from unique_search_proxy_client.web.settings.providers.jina import (
|
|
22
23
|
jina_crawl_credentials as credentials,
|
|
23
24
|
)
|
|
@@ -70,7 +71,7 @@ class JinaCrawlerService(BaseCrawler[JinaCrawlRequest]):
|
|
|
70
71
|
|
|
71
72
|
urls = list(request.urls)
|
|
72
73
|
timeout = request.timeout
|
|
73
|
-
headers = _jina_headers(credentials.api_key)
|
|
74
|
+
headers = _jina_headers(read_secret(credentials.api_key))
|
|
74
75
|
semaphore = asyncio.Semaphore(request.max_concurrent_requests)
|
|
75
76
|
|
|
76
77
|
async def crawl_one(url: str) -> CrawlUrlResult:
|
|
@@ -19,6 +19,7 @@ from unique_search_proxy_client.web.core.provider_response import (
|
|
|
19
19
|
upstream_error_message,
|
|
20
20
|
upstream_response_raw,
|
|
21
21
|
)
|
|
22
|
+
from unique_search_proxy_client.web.settings.providers.base import read_secret
|
|
22
23
|
from unique_search_proxy_client.web.settings.providers.tavily import (
|
|
23
24
|
tavily_crawl_credentials as credentials,
|
|
24
25
|
)
|
|
@@ -132,7 +133,7 @@ class TavilyCrawlerService(BaseCrawler[TavilyCrawlRequest]):
|
|
|
132
133
|
response = await client.post(
|
|
133
134
|
credentials.extract_endpoint,
|
|
134
135
|
json=body,
|
|
135
|
-
headers=_tavily_headers(credentials.api_key),
|
|
136
|
+
headers=_tavily_headers(read_secret(credentials.api_key)),
|
|
136
137
|
timeout=timeout,
|
|
137
138
|
)
|
|
138
139
|
except httpx.TimeoutException as exc:
|
|
@@ -37,6 +37,7 @@ from unique_search_proxy_client.web.core.search_engines.pagination import PageRe
|
|
|
37
37
|
from unique_search_proxy_client.web.core.search_engines.service_base import (
|
|
38
38
|
SearchEngineService,
|
|
39
39
|
)
|
|
40
|
+
from unique_search_proxy_client.web.settings.providers.base import read_secret
|
|
40
41
|
from unique_search_proxy_client.web.settings.providers.brave import (
|
|
41
42
|
brave_search_credentials as credentials,
|
|
42
43
|
)
|
|
@@ -71,7 +72,7 @@ class BraveSearchService(SearchEngineService[BraveSearchRequest]):
|
|
|
71
72
|
break
|
|
72
73
|
page = await self._fetch_page(
|
|
73
74
|
request=request,
|
|
74
|
-
api_key=credentials.api_key,
|
|
75
|
+
api_key=read_secret(credentials.api_key),
|
|
75
76
|
api_endpoint=credentials.api_endpoint,
|
|
76
77
|
page=page_request,
|
|
77
78
|
timeout=timeout,
|
|
@@ -37,6 +37,7 @@ from unique_search_proxy_client.web.core.search_engines.pagination import PageRe
|
|
|
37
37
|
from unique_search_proxy_client.web.core.search_engines.service_base import (
|
|
38
38
|
SearchEngineService,
|
|
39
39
|
)
|
|
40
|
+
from unique_search_proxy_client.web.settings.providers.base import read_secret
|
|
40
41
|
from unique_search_proxy_client.web.settings.providers.google import (
|
|
41
42
|
google_search_credentials as credentials,
|
|
42
43
|
)
|
|
@@ -71,7 +72,7 @@ class GoogleSearchService(SearchEngineService[GoogleSearchRequest]):
|
|
|
71
72
|
for page_request in iter_google_page_requests(fetch_size):
|
|
72
73
|
page = await self._fetch_page(
|
|
73
74
|
request=request,
|
|
74
|
-
api_key=credentials.api_key,
|
|
75
|
+
api_key=read_secret(credentials.api_key),
|
|
75
76
|
search_engine_id=search_engine_id,
|
|
76
77
|
api_endpoint=credentials.api_endpoint,
|
|
77
78
|
page=page_request,
|
|
@@ -36,6 +36,7 @@ from unique_search_proxy_client.web.core.search_engines.service_base import (
|
|
|
36
36
|
from unique_search_proxy_client.web.settings.providers import (
|
|
37
37
|
perplexity_search_credentials as credentials,
|
|
38
38
|
)
|
|
39
|
+
from unique_search_proxy_client.web.settings.providers.base import read_secret
|
|
39
40
|
|
|
40
41
|
_LOGGER = logging.getLogger(__name__)
|
|
41
42
|
_PERPLEXITY_PROVIDER_LABEL = "Perplexity Search API"
|
|
@@ -67,7 +68,7 @@ class PerplexitySearchService(SearchEngineService[PerplexitySearchRequest]):
|
|
|
67
68
|
response = await client.post(
|
|
68
69
|
credentials.api_endpoint,
|
|
69
70
|
json=body,
|
|
70
|
-
headers=_perplexity_headers(credentials.api_key),
|
|
71
|
+
headers=_perplexity_headers(read_secret(credentials.api_key)),
|
|
71
72
|
timeout=timeout,
|
|
72
73
|
)
|
|
73
74
|
except httpx.TimeoutException as exc:
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
import logging
|
|
5
|
+
import logging.config
|
|
6
|
+
import os
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from uvicorn.config import LOG_LEVELS, LOGGING_CONFIG
|
|
10
|
+
|
|
11
|
+
_APP_LOGGER_NAMES = (
|
|
12
|
+
"unique_search_proxy_client",
|
|
13
|
+
"unique_search_proxy_core",
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def build_logging_config(log_level: str | None = None) -> dict[str, Any]:
|
|
18
|
+
"""Extend Uvicorn's logging config with application loggers."""
|
|
19
|
+
level_key = (log_level or os.getenv("LOG_LEVEL", "info")).lower()
|
|
20
|
+
if level_key not in LOG_LEVELS:
|
|
21
|
+
level_key = "info"
|
|
22
|
+
level_name = level_key.upper()
|
|
23
|
+
|
|
24
|
+
config = copy.deepcopy(LOGGING_CONFIG)
|
|
25
|
+
for logger_name in _APP_LOGGER_NAMES:
|
|
26
|
+
config["loggers"][logger_name] = {
|
|
27
|
+
"handlers": ["default"],
|
|
28
|
+
"level": level_name,
|
|
29
|
+
"propagate": False,
|
|
30
|
+
}
|
|
31
|
+
config["loggers"]["uvicorn"]["level"] = level_name
|
|
32
|
+
config["loggers"]["uvicorn.error"]["level"] = level_name
|
|
33
|
+
config["loggers"]["uvicorn.access"]["level"] = level_name
|
|
34
|
+
return config
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def configure_logging(log_level: str | None = None) -> None:
|
|
38
|
+
"""Configure app and Uvicorn loggers with Uvicorn's colored formatter."""
|
|
39
|
+
logging.config.dictConfig(build_logging_config(log_level))
|
|
@@ -36,9 +36,8 @@ def _is_test_runtime() -> bool:
|
|
|
36
36
|
|
|
37
37
|
def get_settings(cls: type[T], *, env_prefix: str) -> T:
|
|
38
38
|
"""Load a settings model from env; uses ``tests/test.env`` under pytest."""
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
config = _settings_config(env_prefix=env_prefix, test=True)
|
|
39
|
+
use_test_env = _is_test_runtime()
|
|
40
|
+
config = _settings_config(env_prefix=env_prefix, test=use_test_env)
|
|
42
41
|
|
|
43
42
|
class Settings(cls):
|
|
44
43
|
model_config = config
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
from typing import Literal
|
|
2
2
|
|
|
3
|
-
from pydantic import BaseModel
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
4
|
from pydantic_settings import BaseSettings
|
|
5
5
|
|
|
6
6
|
from unique_search_proxy_client.web.settings.base import get_settings
|
|
7
|
+
from unique_search_proxy_client.web.settings.secret_str import LogSecretStr
|
|
7
8
|
|
|
8
9
|
HTTP_CLIENT_ENV_PREFIX = "HTTP_CLIENT_"
|
|
9
10
|
|
|
@@ -30,10 +31,10 @@ class HttpClientSettings(BaseSettings):
|
|
|
30
31
|
proxy_protocol: ProxyProtocol = "http"
|
|
31
32
|
proxy_host: str | None = None
|
|
32
33
|
proxy_port: int | None = None
|
|
33
|
-
proxy_headers: dict[str,
|
|
34
|
+
proxy_headers: dict[str, LogSecretStr] = Field(default_factory=dict)
|
|
34
35
|
proxy_ssl_ca_bundle_path: str | None = None
|
|
35
|
-
proxy_username:
|
|
36
|
-
proxy_password:
|
|
36
|
+
proxy_username: LogSecretStr | None = None
|
|
37
|
+
proxy_password: LogSecretStr | None = None
|
|
37
38
|
proxy_ssl_cert_path: str | None = None
|
|
38
39
|
proxy_ssl_key_path: str | None = None
|
|
39
40
|
|
|
@@ -4,7 +4,7 @@ from collections.abc import Callable, Sequence
|
|
|
4
4
|
from functools import lru_cache
|
|
5
5
|
from typing import ClassVar, TypeVar
|
|
6
6
|
|
|
7
|
-
from pydantic import BaseModel
|
|
7
|
+
from pydantic import BaseModel, SecretStr
|
|
8
8
|
from pydantic_settings import BaseSettings
|
|
9
9
|
from unique_search_proxy_core.errors import EngineNotConfiguredError
|
|
10
10
|
|
|
@@ -36,10 +36,25 @@ def provider_credentials(env_prefix: str) -> Callable[[T], T]:
|
|
|
36
36
|
return decorate
|
|
37
37
|
|
|
38
38
|
|
|
39
|
-
def
|
|
39
|
+
def read_secret(value: str | SecretStr | None) -> str:
|
|
40
40
|
if value is None:
|
|
41
|
-
return
|
|
42
|
-
|
|
41
|
+
return ""
|
|
42
|
+
if isinstance(value, SecretStr):
|
|
43
|
+
return value.get_secret_value()
|
|
44
|
+
return value
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _field_has_not_provided_default(field_info: object) -> bool:
|
|
48
|
+
default = getattr(field_info, "default", None)
|
|
49
|
+
if default is NOT_PROVIDED:
|
|
50
|
+
return True
|
|
51
|
+
if isinstance(default, SecretStr):
|
|
52
|
+
return default.get_secret_value() == NOT_PROVIDED
|
|
53
|
+
return False
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _is_secret_configured(value: str | SecretStr | None) -> bool:
|
|
57
|
+
normalized = read_secret(value).strip()
|
|
43
58
|
if not normalized:
|
|
44
59
|
return False
|
|
45
60
|
return normalized != NOT_PROVIDED
|
|
@@ -53,7 +68,7 @@ def _iter_not_provided_credential_fields(model_cls: type[BaseModel]) -> tuple[st
|
|
|
53
68
|
return tuple(
|
|
54
69
|
name
|
|
55
70
|
for name, field_info in model_cls.model_fields.items()
|
|
56
|
-
if field_info
|
|
71
|
+
if _field_has_not_provided_default(field_info)
|
|
57
72
|
)
|
|
58
73
|
|
|
59
74
|
|
|
@@ -8,6 +8,7 @@ from unique_search_proxy_client.web.settings.providers.base import (
|
|
|
8
8
|
ProviderCredentials,
|
|
9
9
|
provider_credentials,
|
|
10
10
|
)
|
|
11
|
+
from unique_search_proxy_client.web.settings.secret_str import LogSecretStr
|
|
11
12
|
|
|
12
13
|
_DEFAULT_BRAVE_API_ENDPOINT = "https://api.search.brave.com/res/v1/web/search"
|
|
13
14
|
_ENV_PREFIX = "BRAVE_SEARCH_"
|
|
@@ -17,7 +18,7 @@ _ENV_PREFIX = "BRAVE_SEARCH_"
|
|
|
17
18
|
class _BraveCredentials(ProviderCredentials):
|
|
18
19
|
"""Environment-backed credentials for Brave Web Search."""
|
|
19
20
|
|
|
20
|
-
api_key:
|
|
21
|
+
api_key: LogSecretStr = Field(default=LogSecretStr(NOT_PROVIDED))
|
|
21
22
|
api_endpoint: str = Field(default=_DEFAULT_BRAVE_API_ENDPOINT)
|
|
22
23
|
|
|
23
24
|
|
|
@@ -10,6 +10,7 @@ from unique_search_proxy_client.web.settings.providers.base import (
|
|
|
10
10
|
ProviderCredentials,
|
|
11
11
|
provider_credentials,
|
|
12
12
|
)
|
|
13
|
+
from unique_search_proxy_client.web.settings.secret_str import LogSecretStr
|
|
13
14
|
from unique_search_proxy_client.web.utils.url import join_url_path
|
|
14
15
|
|
|
15
16
|
_FIRECRAWL_API_BASE = "https://api.firecrawl.dev"
|
|
@@ -28,7 +29,7 @@ _ENV_PREFIX = "FIRECRAWL_"
|
|
|
28
29
|
class _FirecrawlCredentials(ProviderCredentials):
|
|
29
30
|
"""Environment-backed credentials for Firecrawl v2 APIs."""
|
|
30
31
|
|
|
31
|
-
api_key:
|
|
32
|
+
api_key: LogSecretStr = Field(default=LogSecretStr(NOT_PROVIDED))
|
|
32
33
|
api_endpoint: str = Field(default=_FIRECRAWL_API_BASE)
|
|
33
34
|
api_version: FirecrawlApiVersion = Field(default=_DEFAULT_FIRECRAWL_API_VERSION)
|
|
34
35
|
|
|
@@ -8,6 +8,7 @@ from unique_search_proxy_client.web.settings.providers.base import (
|
|
|
8
8
|
ProviderCredentials,
|
|
9
9
|
provider_credentials,
|
|
10
10
|
)
|
|
11
|
+
from unique_search_proxy_client.web.settings.secret_str import LogSecretStr
|
|
11
12
|
|
|
12
13
|
_DEFAULT_GOOGLE_API_ENDPOINT = "https://www.googleapis.com/customsearch/v1"
|
|
13
14
|
_ENV_PREFIX = "GOOGLE_SEARCH_"
|
|
@@ -17,7 +18,7 @@ _ENV_PREFIX = "GOOGLE_SEARCH_"
|
|
|
17
18
|
class _GoogleCredentials(ProviderCredentials):
|
|
18
19
|
"""Environment-backed credentials for Google Custom Search."""
|
|
19
20
|
|
|
20
|
-
api_key:
|
|
21
|
+
api_key: LogSecretStr = Field(default=LogSecretStr(NOT_PROVIDED))
|
|
21
22
|
api_endpoint: str = Field(default=_DEFAULT_GOOGLE_API_ENDPOINT)
|
|
22
23
|
engine_id: str = Field(default=NOT_PROVIDED)
|
|
23
24
|
|
|
@@ -10,6 +10,7 @@ from unique_search_proxy_client.web.settings.providers.base import (
|
|
|
10
10
|
ProviderCredentials,
|
|
11
11
|
provider_credentials,
|
|
12
12
|
)
|
|
13
|
+
from unique_search_proxy_client.web.settings.secret_str import LogSecretStr
|
|
13
14
|
|
|
14
15
|
_JINA_DOMAIN = "jina.ai"
|
|
15
16
|
_JINA_SUBDOMAINS: dict[str, dict[str, str]] = {
|
|
@@ -25,7 +26,7 @@ _ENV_PREFIX = "JINA_"
|
|
|
25
26
|
class _JinaCredentials(ProviderCredentials):
|
|
26
27
|
"""Environment-backed credentials for Jina Reader and Search."""
|
|
27
28
|
|
|
28
|
-
api_key:
|
|
29
|
+
api_key: LogSecretStr = Field(default=LogSecretStr(NOT_PROVIDED))
|
|
29
30
|
deployment: JinaDeployment = Field(default=_DEFAULT_JINA_DEPLOYMENT)
|
|
30
31
|
api_domain: str = Field(default=_JINA_DOMAIN)
|
|
31
32
|
|
|
@@ -8,6 +8,7 @@ from unique_search_proxy_client.web.settings.providers.base import (
|
|
|
8
8
|
ProviderCredentials,
|
|
9
9
|
provider_credentials,
|
|
10
10
|
)
|
|
11
|
+
from unique_search_proxy_client.web.settings.secret_str import LogSecretStr
|
|
11
12
|
|
|
12
13
|
_DEFAULT_PERPLEXITY_API_ENDPOINT = "https://api.perplexity.ai/search"
|
|
13
14
|
_ENV_PREFIX = "PERPLEXITY_SEARCH_"
|
|
@@ -17,7 +18,7 @@ _ENV_PREFIX = "PERPLEXITY_SEARCH_"
|
|
|
17
18
|
class _PerplexityCredentials(ProviderCredentials):
|
|
18
19
|
"""Environment-backed credentials for Perplexity Search."""
|
|
19
20
|
|
|
20
|
-
api_key:
|
|
21
|
+
api_key: LogSecretStr = Field(default=LogSecretStr(NOT_PROVIDED))
|
|
21
22
|
api_endpoint: str = Field(default=_DEFAULT_PERPLEXITY_API_ENDPOINT)
|
|
22
23
|
|
|
23
24
|
|
|
@@ -10,6 +10,7 @@ from unique_search_proxy_client.web.settings.providers.base import (
|
|
|
10
10
|
ProviderCredentials,
|
|
11
11
|
provider_credentials,
|
|
12
12
|
)
|
|
13
|
+
from unique_search_proxy_client.web.settings.secret_str import LogSecretStr
|
|
13
14
|
from unique_search_proxy_client.web.utils.url import join_url_path
|
|
14
15
|
|
|
15
16
|
_TAVILY_API_BASE = "https://api.tavily.com"
|
|
@@ -25,7 +26,7 @@ _ENV_PREFIX = "TAVILY_"
|
|
|
25
26
|
class _TavilyCredentials(ProviderCredentials):
|
|
26
27
|
"""Environment-backed credentials for Tavily APIs."""
|
|
27
28
|
|
|
28
|
-
api_key:
|
|
29
|
+
api_key: LogSecretStr = Field(default=LogSecretStr(NOT_PROVIDED))
|
|
29
30
|
api_endpoint: str = Field(default=_TAVILY_API_BASE)
|
|
30
31
|
|
|
31
32
|
def _endpoint(self, operation: TavilyOperation) -> str:
|
|
@@ -7,6 +7,7 @@ from unique_search_proxy_client.web.settings.providers.base import (
|
|
|
7
7
|
ProviderCredentials,
|
|
8
8
|
provider_credentials,
|
|
9
9
|
)
|
|
10
|
+
from unique_search_proxy_client.web.settings.secret_str import LogSecretStr
|
|
10
11
|
|
|
11
12
|
_ENV_PREFIX = "VERTEXAI_AGENT_"
|
|
12
13
|
|
|
@@ -15,7 +16,7 @@ _ENV_PREFIX = "VERTEXAI_AGENT_"
|
|
|
15
16
|
class _VertexAIAgentCredentials(ProviderCredentials):
|
|
16
17
|
"""Environment-backed credentials for Vertex AI grounding (Google GenAI)."""
|
|
17
18
|
|
|
18
|
-
service_account_credentials:
|
|
19
|
+
service_account_credentials: LogSecretStr | None = Field(default=None)
|
|
19
20
|
service_account_scopes: list[str] | None = Field(default=None)
|
|
20
21
|
|
|
21
22
|
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
|
|
5
|
+
from pydantic import SecretStr
|
|
6
|
+
|
|
7
|
+
_NOT_PROVIDED = "NOT_PROVIDED"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _secret_log_suffix_len() -> int:
|
|
11
|
+
from unique_search_proxy_client.web.settings.startup_log import (
|
|
12
|
+
startup_log_settings,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
return startup_log_settings.secret_suffix_len
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _mask_secret_for_display(value: str) -> str:
|
|
19
|
+
if not value:
|
|
20
|
+
return ""
|
|
21
|
+
if value == _NOT_PROVIDED:
|
|
22
|
+
return _NOT_PROVIDED
|
|
23
|
+
suffix_len = _secret_log_suffix_len()
|
|
24
|
+
if suffix_len <= 0:
|
|
25
|
+
return "*" * 10
|
|
26
|
+
# 20% of the value length is the max suffix length
|
|
27
|
+
if len(value) * 0.10 <= suffix_len:
|
|
28
|
+
return "*" * 10
|
|
29
|
+
return f"**********{value[-suffix_len:]}"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class LogSecretStr(SecretStr):
|
|
33
|
+
"""SecretStr that masks values in str()/repr() with a configurable suffix."""
|
|
34
|
+
|
|
35
|
+
def _display(self) -> str:
|
|
36
|
+
return _mask_secret_for_display(self._secret_value)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def read_secret_headers(headers: Mapping[str, LogSecretStr]) -> dict[str, str]:
|
|
40
|
+
"""Unwrap proxy header secrets for httpx."""
|
|
41
|
+
return {name: value.get_secret_value() for name, value in headers.items()}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pydantic import Field
|
|
4
|
+
from pydantic_settings import BaseSettings
|
|
5
|
+
|
|
6
|
+
from unique_search_proxy_client.web.settings.base import get_settings
|
|
7
|
+
|
|
8
|
+
STARTUP_LOG_ENV_PREFIX = "STARTUP_LOG_"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class StartupLogSettings(BaseSettings):
|
|
12
|
+
"""Startup log formatting options."""
|
|
13
|
+
|
|
14
|
+
secret_suffix_len: int = Field(
|
|
15
|
+
default=0,
|
|
16
|
+
ge=0,
|
|
17
|
+
le=32,
|
|
18
|
+
description=(
|
|
19
|
+
"Number of trailing characters to show when logging secret values "
|
|
20
|
+
"at startup (0 hides the secret entirely)."
|
|
21
|
+
),
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _get_startup_log_settings() -> StartupLogSettings:
|
|
26
|
+
return get_settings(StartupLogSettings, env_prefix=STARTUP_LOG_ENV_PREFIX)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
startup_log_settings = _get_startup_log_settings()
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
from collections import defaultdict
|
|
6
|
+
from collections.abc import Iterable
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
from unique_search_proxy_core.url_safety.settings import url_safety_settings
|
|
10
|
+
|
|
11
|
+
from unique_search_proxy_client.web.settings.client import http_client_settings
|
|
12
|
+
from unique_search_proxy_client.web.settings.monitoring import prometheus_settings
|
|
13
|
+
from unique_search_proxy_client.web.settings.providers.base import (
|
|
14
|
+
_field_has_not_provided_default,
|
|
15
|
+
_is_secret_configured,
|
|
16
|
+
)
|
|
17
|
+
from unique_search_proxy_client.web.settings.providers.bing_agent import (
|
|
18
|
+
bing_agent_credentials,
|
|
19
|
+
)
|
|
20
|
+
from unique_search_proxy_client.web.settings.providers.brave import (
|
|
21
|
+
brave_search_credentials,
|
|
22
|
+
)
|
|
23
|
+
from unique_search_proxy_client.web.settings.providers.firecrawl import (
|
|
24
|
+
firecrawl_crawl_credentials,
|
|
25
|
+
)
|
|
26
|
+
from unique_search_proxy_client.web.settings.providers.google import (
|
|
27
|
+
google_search_credentials,
|
|
28
|
+
)
|
|
29
|
+
from unique_search_proxy_client.web.settings.providers.jina import (
|
|
30
|
+
jina_crawl_credentials,
|
|
31
|
+
)
|
|
32
|
+
from unique_search_proxy_client.web.settings.providers.perplexity import (
|
|
33
|
+
perplexity_search_credentials,
|
|
34
|
+
)
|
|
35
|
+
from unique_search_proxy_client.web.settings.providers.tavily import (
|
|
36
|
+
tavily_crawl_credentials,
|
|
37
|
+
)
|
|
38
|
+
from unique_search_proxy_client.web.settings.providers.vertexai_agent import (
|
|
39
|
+
vertexai_agent_credentials,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
_LOGGER = logging.getLogger(__name__)
|
|
43
|
+
|
|
44
|
+
SettingsGroup = tuple[str, BaseModel, str]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _env_var_name(field_name: str, env_prefix: str) -> str:
|
|
48
|
+
return f"{env_prefix}{field_name.upper()}"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _field_status(
|
|
52
|
+
model: BaseModel,
|
|
53
|
+
field_name: str,
|
|
54
|
+
env_prefix: str,
|
|
55
|
+
) -> str:
|
|
56
|
+
value = getattr(model, field_name)
|
|
57
|
+
field_info = model.model_fields[field_name]
|
|
58
|
+
env_var = _env_var_name(field_name, env_prefix)
|
|
59
|
+
|
|
60
|
+
if _field_has_not_provided_default(field_info):
|
|
61
|
+
return "configured" if _is_secret_configured(value) else "missing"
|
|
62
|
+
|
|
63
|
+
if value is None:
|
|
64
|
+
return "unset"
|
|
65
|
+
|
|
66
|
+
if env_var in os.environ:
|
|
67
|
+
return "set"
|
|
68
|
+
|
|
69
|
+
return "default"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _group_fields_by_status(
|
|
73
|
+
model: BaseModel,
|
|
74
|
+
env_prefix: str,
|
|
75
|
+
) -> dict[str, list[str]]:
|
|
76
|
+
grouped: dict[str, list[str]] = defaultdict(list)
|
|
77
|
+
for field_name in model.model_fields:
|
|
78
|
+
env_var = _env_var_name(field_name, env_prefix)
|
|
79
|
+
status = _field_status(model, field_name, env_prefix)
|
|
80
|
+
grouped[status].append(env_var)
|
|
81
|
+
return grouped
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _group_summary(grouped: dict[str, list[str]]) -> str:
|
|
85
|
+
missing = grouped.get("missing", [])
|
|
86
|
+
if missing:
|
|
87
|
+
return f"incomplete ({len(missing)} missing)"
|
|
88
|
+
return "ready"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _format_settings_value(value: object) -> str:
|
|
92
|
+
if isinstance(value, dict):
|
|
93
|
+
if not value:
|
|
94
|
+
return "{}"
|
|
95
|
+
pairs = ", ".join(f"{key!r}: {secret}" for key, secret in value.items())
|
|
96
|
+
return "{" + pairs + "}"
|
|
97
|
+
return str(value)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _format_group(title: str, model: BaseModel, env_prefix: str) -> list[str]:
|
|
101
|
+
grouped = _group_fields_by_status(model, env_prefix)
|
|
102
|
+
lines = [f" [{title}] {_group_summary(grouped)}"]
|
|
103
|
+
for field_name in model.model_fields:
|
|
104
|
+
env_var = _env_var_name(field_name, env_prefix)
|
|
105
|
+
value = getattr(model, field_name)
|
|
106
|
+
lines.append(f" {env_var}={_format_settings_value(value)}")
|
|
107
|
+
return lines
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _settings_groups() -> Iterable[SettingsGroup]:
|
|
111
|
+
return (
|
|
112
|
+
("Google Search", google_search_credentials, "GOOGLE_SEARCH_"),
|
|
113
|
+
("Brave Search", brave_search_credentials, "BRAVE_SEARCH_"),
|
|
114
|
+
("Perplexity Search", perplexity_search_credentials, "PERPLEXITY_SEARCH_"),
|
|
115
|
+
("Bing Agent", bing_agent_credentials, "BING_AGENT_"),
|
|
116
|
+
("VertexAI Agent", vertexai_agent_credentials, "VERTEXAI_AGENT_"),
|
|
117
|
+
("Tavily", tavily_crawl_credentials, "TAVILY_"),
|
|
118
|
+
("Jina", jina_crawl_credentials, "JINA_"),
|
|
119
|
+
("Firecrawl", firecrawl_crawl_credentials, "FIRECRAWL_"),
|
|
120
|
+
("HTTP Client", http_client_settings, "HTTP_CLIENT_"),
|
|
121
|
+
("Prometheus", prometheus_settings, "PROMETHEUS_"),
|
|
122
|
+
("URL Safety", url_safety_settings, "URL_SAFETY_"),
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def build_startup_settings_report() -> str:
|
|
127
|
+
"""Build a multi-line startup settings summary for logging."""
|
|
128
|
+
lines = ["Search Proxy settings at startup:"]
|
|
129
|
+
for title, model, env_prefix in _settings_groups():
|
|
130
|
+
lines.extend(_format_group(title, model, env_prefix))
|
|
131
|
+
lines.append(f" [Runtime] LOG_LEVEL={os.getenv('LOG_LEVEL', 'INFO')}")
|
|
132
|
+
return "\n".join(lines)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def log_startup_settings_report(logger: logging.Logger | None = None) -> None:
|
|
136
|
+
"""Log configured vs missing/default env-backed settings at pod startup."""
|
|
137
|
+
log = logger or _LOGGER
|
|
138
|
+
log.info("%s", build_startup_settings_report())
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|