unique-search-proxy 2026.28.0.dev0__tar.gz → 2026.28.0.dev2__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.
Files changed (113) hide show
  1. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/PKG-INFO +4 -3
  2. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/pyproject.toml +6 -3
  3. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/app.py +1 -1
  4. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/core/agent_engines/bing/client.py +1 -1
  5. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/core/agent_engines/vertexai/client.py +12 -4
  6. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/core/client/service.py +4 -2
  7. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/core/crawlers/firecrawl/service.py +1 -1
  8. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/core/crawlers/jina/service.py +1 -1
  9. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/core/crawlers/tavily/service.py +1 -1
  10. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/core/search_engines/brave/service.py +1 -1
  11. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/core/search_engines/google/service.py +1 -1
  12. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/core/search_engines/perplexity/service.py +1 -1
  13. unique_search_proxy-2026.28.0.dev2/unique_search_proxy_client/web/helm/__init__.py +1 -0
  14. unique_search_proxy-2026.28.0.dev2/unique_search_proxy_client/web/helm/generator/__init__.py +73 -0
  15. unique_search_proxy-2026.28.0.dev2/unique_search_proxy_client/web/helm/generator/introspect.py +324 -0
  16. unique_search_proxy-2026.28.0.dev2/unique_search_proxy_client/web/helm/generator/schema.py +139 -0
  17. unique_search_proxy-2026.28.0.dev2/unique_search_proxy_client/web/helm/generator/template.py +308 -0
  18. unique_search_proxy-2026.28.0.dev2/unique_search_proxy_client/web/helm/generator/templates/provider_values_yaml.j2 +40 -0
  19. unique_search_proxy-2026.28.0.dev2/unique_search_proxy_client/web/helm/generator/values_yaml.py +205 -0
  20. unique_search_proxy-2026.28.0.dev2/unique_search_proxy_client/web/helm/metadata.py +175 -0
  21. unique_search_proxy-2026.28.0.dev2/unique_search_proxy_client/web/helm/registry.py +117 -0
  22. unique_search_proxy-2026.28.0.dev2/unique_search_proxy_client/web/helm/url_safety_registration.py +84 -0
  23. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/settings/client.py +19 -0
  24. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/settings/monitoring.py +8 -0
  25. unique_search_proxy-2026.28.0.dev2/unique_search_proxy_client/web/settings/providers/__init__.py +37 -0
  26. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/settings/providers/base.py +13 -30
  27. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/settings/providers/bing_agent.py +3 -1
  28. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/settings/providers/brave.py +6 -2
  29. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/settings/providers/firecrawl.py +6 -2
  30. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/settings/providers/google.py +6 -2
  31. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/settings/providers/jina.py +15 -2
  32. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/settings/providers/perplexity.py +6 -2
  33. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/settings/providers/tavily.py +6 -2
  34. unique_search_proxy-2026.28.0.dev2/unique_search_proxy_client/web/settings/providers/vertexai_agent.py +46 -0
  35. unique_search_proxy-2026.28.0.dev2/unique_search_proxy_client/web/settings/secret_str.py +96 -0
  36. {unique_search_proxy-2026.28.0.dev0/unique_search_proxy_client/web/settings → unique_search_proxy-2026.28.0.dev2/unique_search_proxy_client/web}/startup_report.py +7 -52
  37. unique_search_proxy-2026.28.0.dev0/unique_search_proxy_client/web/settings/providers/__init__.py +0 -17
  38. unique_search_proxy-2026.28.0.dev0/unique_search_proxy_client/web/settings/providers/vertexai_agent.py +0 -29
  39. unique_search_proxy-2026.28.0.dev0/unique_search_proxy_client/web/settings/secret_str.py +0 -41
  40. unique_search_proxy-2026.28.0.dev0/unique_search_proxy_client/web/settings/startup_log.py +0 -29
  41. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/README.md +0 -0
  42. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/__init__.py +0 -0
  43. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/__init__.py +0 -0
  44. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/api/__init__.py +0 -0
  45. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/api/health.py +0 -0
  46. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/api/v1/__init__.py +0 -0
  47. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/api/v1/agent_search.py +0 -0
  48. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/api/v1/configuration.py +0 -0
  49. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/api/v1/crawl.py +0 -0
  50. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/api/v1/openapi_examples.py +0 -0
  51. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/api/v1/search.py +0 -0
  52. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/core/__init__.py +0 -0
  53. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/core/agent_engines/__init__.py +0 -0
  54. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/core/agent_engines/bing/runner.py +0 -0
  55. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/core/agent_engines/bing/service.py +0 -0
  56. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/core/agent_engines/factory.py +0 -0
  57. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/core/agent_engines/serialization.py +0 -0
  58. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/core/agent_engines/service_base.py +0 -0
  59. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/core/agent_engines/structured_output.py +0 -0
  60. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/core/agent_engines/vertexai/gemini.py +0 -0
  61. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/core/agent_engines/vertexai/service.py +0 -0
  62. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/core/client/__init__.py +0 -0
  63. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/core/crawlers/__init__.py +0 -0
  64. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/core/crawlers/basic/__init__.py +0 -0
  65. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/core/crawlers/basic/processing/__init__.py +0 -0
  66. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/core/crawlers/basic/processing/errors.py +0 -0
  67. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/core/crawlers/basic/processing/html_markdown.py +0 -0
  68. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/core/crawlers/basic/processing/processors/__init__.py +0 -0
  69. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/core/crawlers/basic/processing/processors/html.py +0 -0
  70. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/core/crawlers/basic/processing/processors/pdf.py +0 -0
  71. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/core/crawlers/basic/processing/processors/plain_text.py +0 -0
  72. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/core/crawlers/basic/processing/registry.py +0 -0
  73. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/core/crawlers/basic/service.py +0 -0
  74. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/core/crawlers/basic/settings.py +0 -0
  75. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/core/crawlers/basic/user_agent.py +0 -0
  76. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/core/crawlers/factory.py +0 -0
  77. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/core/crawlers/firecrawl/polling.py +0 -0
  78. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/core/crawlers/firecrawl/request_body.py +0 -0
  79. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/core/crawlers/jina/request_body.py +0 -0
  80. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/core/crawlers/pinned_egress.py +0 -0
  81. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/core/crawlers/tavily/request_body.py +0 -0
  82. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/core/provider_response.py +0 -0
  83. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/core/providers.py +0 -0
  84. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/core/registry.py +0 -0
  85. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/core/search_engines/__init__.py +0 -0
  86. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/core/search_engines/brave/__init__.py +0 -0
  87. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/core/search_engines/brave/pagination.py +0 -0
  88. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/core/search_engines/brave/query_params.py +0 -0
  89. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/core/search_engines/descriptor.py +0 -0
  90. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/core/search_engines/factory.py +0 -0
  91. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/core/search_engines/google/__init__.py +0 -0
  92. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/core/search_engines/google/pagination.py +0 -0
  93. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/core/search_engines/google/query_params.py +0 -0
  94. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/core/search_engines/pagination.py +0 -0
  95. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/core/search_engines/perplexity/__init__.py +0 -0
  96. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/core/search_engines/perplexity/request_body.py +0 -0
  97. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/core/search_engines/service_base.py +0 -0
  98. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/core/url_safety/__init__.py +0 -0
  99. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/core/url_safety/gate.py +0 -0
  100. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/error_handlers.py +0 -0
  101. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/logging_config.py +0 -0
  102. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/monitoring/__init__.py +0 -0
  103. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/monitoring/metrics.py +0 -0
  104. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/monitoring/setup.py +0 -0
  105. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/presets/__init__.py +0 -0
  106. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/presets/common.py +0 -0
  107. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/presets/crawl.py +0 -0
  108. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/presets/search.py +0 -0
  109. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/presets/types.py +0 -0
  110. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/settings/__init__.py +0 -0
  111. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/settings/base.py +0 -0
  112. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/unique_search_proxy_client/web/utils/__init__.py +0 -0
  113. {unique_search_proxy-2026.28.0.dev0 → unique_search_proxy-2026.28.0.dev2}/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.28.0.dev0
3
+ Version: 2026.28.0.dev2
4
4
  Summary: Web Search Proxy implementation
5
5
  Author: ThePhilAz
6
6
  Author-email: ThePhilAz <rami.azouz@philico.com>
@@ -18,8 +18,9 @@ Requires-Dist: azure-core>=1.36.0,<2
18
18
  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
- Requires-Dist: unique-toolkit[monitoring]>=2026.28.0.dev0,<2026.28.0rc0
22
- Requires-Dist: unique-search-proxy-core>=2026.28.0.dev0,<2026.28.0rc0
21
+ Requires-Dist: jinja2>=3.1.6,<4
22
+ Requires-Dist: unique-toolkit[monitoring]>=2026.28.0.dev3,<2026.28.0rc0
23
+ Requires-Dist: unique-search-proxy-core>=2026.28.0.dev1,<2026.28.0rc0
23
24
  Requires-Python: >=3.12
24
25
  Description-Content-Type: text/markdown
25
26
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "unique-search-proxy"
3
- version = "2026.28.0.dev0"
3
+ version = "2026.28.0.dev2"
4
4
  description = "Web Search Proxy implementation"
5
5
  authors = [{ name = "ThePhilAz", email = "rami.azouz@philico.com" }]
6
6
  readme = "README.md"
@@ -20,8 +20,9 @@ dependencies = [
20
20
  "certifi>=2025.11.12,<2027",
21
21
  "google-genai>=1.73.0,<2",
22
22
  "google-auth>=2.43.0,<3",
23
- "unique-toolkit[monitoring]>=2026.28.0.dev0,<2026.28.0rc0",
24
- "unique-search-proxy-core>=2026.28.0.dev0,<2026.28.0rc0",
23
+ "jinja2>=3.1.6,<4",
24
+ "unique-toolkit[monitoring]>=2026.28.0.dev3,<2026.28.0rc0",
25
+ "unique-search-proxy-core>=2026.28.0.dev1,<2026.28.0rc0",
25
26
  ]
26
27
 
27
28
  [dependency-groups]
@@ -77,6 +78,8 @@ DEP003 = ["unique_toolkit", "unique_search_proxy_core"]
77
78
 
78
79
  [tool.poe.tasks]
79
80
  generate-sdk = "python scripts/generate_sdk.py"
81
+ generate-helm-config = "python scripts/generate_helm_config.py"
82
+ render-helm-fixtures = "bash scripts/render_helm_fixtures.sh"
80
83
  lint = "ruff check ."
81
84
  lint-fix = "ruff check . --fix"
82
85
  format = "ruff format ."
@@ -18,7 +18,7 @@ from unique_search_proxy_client.web.logging_config import (
18
18
  configure_logging,
19
19
  )
20
20
  from unique_search_proxy_client.web.monitoring import setup_prometheus
21
- from unique_search_proxy_client.web.settings.startup_report import (
21
+ from unique_search_proxy_client.web.startup_report import (
22
22
  log_startup_settings_report,
23
23
  )
24
24
 
@@ -8,10 +8,10 @@ from azure.core.credentials_async import AsyncTokenCredential
8
8
  from azure.core.pipeline.transport import AsyncioRequestsTransport
9
9
  from azure.identity.aio import DefaultAzureCredential, WorkloadIdentityCredential
10
10
 
11
- from unique_search_proxy_client.web.settings.providers.base import NOT_PROVIDED
12
11
  from unique_search_proxy_client.web.settings.providers.bing_agent import (
13
12
  bing_agent_credentials,
14
13
  )
14
+ from unique_search_proxy_client.web.settings.secret_str import NOT_PROVIDED
15
15
 
16
16
  _LOGGER = logging.getLogger(__name__)
17
17
 
@@ -8,16 +8,24 @@ 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
12
11
  from unique_search_proxy_client.web.settings.providers.vertexai_agent import (
13
12
  vertexai_agent_credentials,
14
13
  )
14
+ from unique_search_proxy_client.web.settings.secret_str import (
15
+ is_secret_configured,
16
+ read_secret,
17
+ )
15
18
 
16
19
  _LOGGER = logging.getLogger(__name__)
17
20
 
18
21
 
19
22
  def _get_base_api_client_from_service_account() -> BaseApiClient:
20
- assert vertexai_agent_credentials.service_account_credentials is not None
23
+ if not is_secret_configured(vertexai_agent_credentials.service_account_credentials):
24
+ msg = (
25
+ "VERTEXAI_AGENT_CREDENTIAL_TYPE is 'service_account' but "
26
+ "VERTEXAI_AGENT_SERVICE_ACCOUNT_CREDENTIALS is not set."
27
+ )
28
+ raise ValueError(msg)
21
29
 
22
30
  scopes = vertexai_agent_credentials.service_account_scopes or [
23
31
  "https://www.googleapis.com/auth/cloud-platform",
@@ -43,11 +51,11 @@ def _get_base_api_client_from_adc() -> BaseApiClient:
43
51
 
44
52
 
45
53
  def get_vertex_client() -> AsyncClient:
46
- if vertexai_agent_credentials.service_account_credentials is not None:
54
+ if vertexai_agent_credentials.credential_type == "service_account":
47
55
  _LOGGER.info("Using explicit service account credentials for VertexAI agent")
48
56
  base_api_client = _get_base_api_client_from_service_account()
49
57
  else:
50
- _LOGGER.info("Using ADC for VertexAI agent")
58
+ _LOGGER.info("Using workload identity (ADC) for VertexAI agent")
51
59
  base_api_client = _get_base_api_client_from_adc()
52
60
  return AsyncClient(api_client=base_api_client)
53
61
 
@@ -14,8 +14,10 @@ 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
+ from unique_search_proxy_client.web.settings.secret_str import (
18
+ read_secret,
19
+ read_secret_headers,
20
+ )
19
21
 
20
22
  if TYPE_CHECKING:
21
23
  from fastapi import FastAPI
@@ -25,10 +25,10 @@ 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
29
28
  from unique_search_proxy_client.web.settings.providers.firecrawl import (
30
29
  firecrawl_crawl_credentials as credentials,
31
30
  )
31
+ from unique_search_proxy_client.web.settings.secret_str import read_secret
32
32
  from unique_search_proxy_client.web.utils.url import join_url_path
33
33
 
34
34
  _LOGGER = logging.getLogger(__name__)
@@ -18,10 +18,10 @@ 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
22
21
  from unique_search_proxy_client.web.settings.providers.jina import (
23
22
  jina_crawl_credentials as credentials,
24
23
  )
24
+ from unique_search_proxy_client.web.settings.secret_str import read_secret
25
25
 
26
26
  _LOGGER = logging.getLogger(__name__)
27
27
 
@@ -19,10 +19,10 @@ 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
23
22
  from unique_search_proxy_client.web.settings.providers.tavily import (
24
23
  tavily_crawl_credentials as credentials,
25
24
  )
25
+ from unique_search_proxy_client.web.settings.secret_str import read_secret
26
26
 
27
27
  _LOGGER = logging.getLogger(__name__)
28
28
 
@@ -37,10 +37,10 @@ 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
41
40
  from unique_search_proxy_client.web.settings.providers.brave import (
42
41
  brave_search_credentials as credentials,
43
42
  )
43
+ from unique_search_proxy_client.web.settings.secret_str import read_secret
44
44
 
45
45
  _LOGGER = logging.getLogger(__name__)
46
46
  _BRAVE_PROVIDER_LABEL = "Brave Web Search API"
@@ -37,10 +37,10 @@ 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
41
40
  from unique_search_proxy_client.web.settings.providers.google import (
42
41
  google_search_credentials as credentials,
43
42
  )
43
+ from unique_search_proxy_client.web.settings.secret_str import read_secret
44
44
 
45
45
  _GOOGLE_PROVIDER_LABEL = "Google Custom Search API"
46
46
 
@@ -36,7 +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
+ from unique_search_proxy_client.web.settings.secret_str import read_secret
40
40
 
41
41
  _LOGGER = logging.getLogger(__name__)
42
42
  _PERPLEXITY_PROVIDER_LABEL = "Perplexity Search API"
@@ -0,0 +1 @@
1
+ """Helm chart metadata, registry, and artifact generation for the search proxy."""
@@ -0,0 +1,73 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from unique_search_proxy_client.web.helm.generator.schema import (
6
+ render_additional_schema,
7
+ )
8
+ from unique_search_proxy_client.web.helm.generator.template import (
9
+ render_generated_template,
10
+ )
11
+ from unique_search_proxy_client.web.helm.generator.values_yaml import (
12
+ patch_values_yaml,
13
+ )
14
+ from unique_search_proxy_client.web.helm.registry import (
15
+ HelmSettingsGroup,
16
+ helm_generated_groups,
17
+ )
18
+
19
+
20
+ def chart_paths(chart_dir: Path) -> dict[str, Path]:
21
+ return {
22
+ "additional_schema": chart_dir / "values.additional.schema.json",
23
+ "generated_tpl": chart_dir / "templates" / "_generated.tpl",
24
+ "values_yaml": chart_dir / "values.yaml",
25
+ }
26
+
27
+
28
+ def generated_groups_tuple() -> tuple[HelmSettingsGroup, ...]:
29
+ return tuple(helm_generated_groups())
30
+
31
+
32
+ def generate_artifacts(chart_dir: Path) -> dict[str, str]:
33
+ groups = generated_groups_tuple()
34
+ paths = chart_paths(chart_dir)
35
+ values_text = paths["values_yaml"].read_text(encoding="utf-8")
36
+
37
+ outputs = {
38
+ str(paths["additional_schema"]): render_additional_schema(groups),
39
+ str(paths["generated_tpl"]): render_generated_template(groups),
40
+ str(paths["values_yaml"]): patch_values_yaml(values_text, groups),
41
+ }
42
+ return outputs
43
+
44
+
45
+ def write_artifacts(chart_dir: Path) -> dict[str, Path]:
46
+ outputs = generate_artifacts(chart_dir)
47
+ written: dict[str, Path] = {}
48
+ for path_str, content in outputs.items():
49
+ path = Path(path_str)
50
+ path.write_text(content, encoding="utf-8")
51
+ written[path.name] = path
52
+ return written
53
+
54
+
55
+ def check_artifacts(chart_dir: Path) -> list[str]:
56
+ drift: list[str] = []
57
+ outputs = generate_artifacts(chart_dir)
58
+ for path_str, expected in outputs.items():
59
+ path = Path(path_str)
60
+ if not path.exists():
61
+ drift.append(f"missing -> {path}")
62
+ continue
63
+ actual = path.read_text(encoding="utf-8")
64
+ if actual != expected:
65
+ drift.append(f"drift -> {path}")
66
+ # Files emitted by earlier generator versions; flag them so they are removed
67
+ # rather than silently shadowing the current output. (_google.tpl predates the
68
+ # consolidated template; _providers.tpl was renamed to _generated.tpl.)
69
+ for stale_name in ("_google.tpl", "_providers.tpl"):
70
+ stale = chart_dir / "templates" / stale_name
71
+ if stale.exists():
72
+ drift.append(f"stale file should be removed -> {stale}")
73
+ return drift
@@ -0,0 +1,324 @@
1
+ from __future__ import annotations
2
+
3
+ import inspect
4
+ import re
5
+ from collections.abc import Callable
6
+ from dataclasses import dataclass
7
+ from typing import Any, Literal, cast, get_args, get_origin
8
+
9
+ from pydantic import SecretStr
10
+ from pydantic.fields import FieldInfo
11
+
12
+ from unique_search_proxy_client.web.settings.secret_str import (
13
+ NOT_PROVIDED,
14
+ LogSecretStr,
15
+ field_has_not_provided_default,
16
+ )
17
+
18
+ _URI_DEFAULT_RE = re.compile(r"^https?://", re.IGNORECASE)
19
+
20
+ DEFAULT_HELM_SECTION = "connection"
21
+
22
+
23
+ def snake_to_camel(name: str) -> str:
24
+ parts = name.split("_")
25
+ return parts[0] + "".join(part.capitalize() for part in parts[1:])
26
+
27
+
28
+ def env_var_name(field_name: str, env_prefix: str) -> str:
29
+ return f"{env_prefix}{field_name.upper()}"
30
+
31
+
32
+ def _helm_extra(field_info: FieldInfo) -> dict[str, Any]:
33
+ extra = field_info.json_schema_extra
34
+ if isinstance(extra, dict):
35
+ helm = extra.get("helm")
36
+ if isinstance(helm, dict):
37
+ return helm
38
+ return {}
39
+
40
+
41
+ def _unwrap_annotation(annotation: Any) -> Any:
42
+ origin = get_origin(annotation)
43
+ if origin is None:
44
+ return annotation
45
+ args = get_args(annotation)
46
+ non_none = [arg for arg in args if arg is not type(None)]
47
+ if len(non_none) == 1:
48
+ return non_none[0]
49
+ return annotation
50
+
51
+
52
+ def _is_secret_type(annotation: Any) -> bool:
53
+ inner = _unwrap_annotation(annotation)
54
+ return inner in {SecretStr, LogSecretStr}
55
+
56
+
57
+ def _is_scalar_type(annotation: Any) -> bool:
58
+ origin = get_origin(annotation)
59
+ if origin is Literal:
60
+ return True
61
+ inner = _unwrap_annotation(annotation)
62
+ if inner in {str, int, float, bool}:
63
+ return True
64
+ if isinstance(inner, type) and issubclass(inner, str):
65
+ return True
66
+ return False
67
+
68
+
69
+ def _is_list_str_type(annotation: Any) -> bool:
70
+ origin = get_origin(annotation)
71
+ if origin is not list:
72
+ return False
73
+ args = get_args(annotation)
74
+ if not args:
75
+ return False
76
+ inner = args[0]
77
+ return inner is str or (isinstance(inner, type) and issubclass(inner, str))
78
+
79
+
80
+ def _factory_takes_validated_data(factory: Callable[..., Any]) -> bool:
81
+ try:
82
+ params = inspect.signature(factory).parameters
83
+ except (TypeError, ValueError):
84
+ return False
85
+ return any(
86
+ p.kind
87
+ in (
88
+ inspect.Parameter.POSITIONAL_ONLY,
89
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
90
+ inspect.Parameter.VAR_POSITIONAL,
91
+ )
92
+ for p in params.values()
93
+ )
94
+
95
+
96
+ def _default_value(field_info: FieldInfo) -> Any:
97
+ factory = field_info.default_factory
98
+ if factory is not None:
99
+ factory = cast(Callable[..., Any], factory)
100
+ try:
101
+ if _factory_takes_validated_data(factory):
102
+ return factory({})
103
+ return factory()
104
+ except TypeError:
105
+ return None
106
+ return field_info.default
107
+
108
+
109
+ def _default_is_uri(default: Any) -> bool:
110
+ return isinstance(default, str) and bool(_URI_DEFAULT_RE.match(default))
111
+
112
+
113
+ @dataclass(frozen=True)
114
+ class HelmFieldSpec:
115
+ """One Helm-mapped field derived from a Pydantic model field."""
116
+
117
+ python_name: str
118
+ helm_name: str
119
+ env_var: str
120
+ sensitive: bool
121
+ required_when_enabled: bool
122
+ schema_ref: str | None
123
+ plain_type: str | None
124
+ format_uri: bool
125
+ default: Any
126
+ section: str
127
+ block_level: bool
128
+ container: bool
129
+ overridable: bool
130
+ emit_in_template: bool
131
+ emit_in_values: bool
132
+
133
+
134
+ def _schema_ref_for_field(
135
+ *,
136
+ field_info: FieldInfo,
137
+ sensitive: bool,
138
+ default: Any,
139
+ helm_extra: dict[str, Any],
140
+ ) -> tuple[str | None, str | None]:
141
+ override = helm_extra.get("value_source")
142
+ if isinstance(override, str):
143
+ return f"#/$defs/valueSource{override}", None
144
+
145
+ if sensitive:
146
+ return "#/$defs/valueSourceSensitive", None
147
+
148
+ inner = _unwrap_annotation(field_info.annotation)
149
+ if inner is bool:
150
+ return "#/$defs/valueSourceBoolean", None
151
+ if inner is int:
152
+ return "#/$defs/valueSourceInteger", None
153
+ if inner is float:
154
+ return None, "number"
155
+ if _default_is_uri(default):
156
+ return None, "string"
157
+ if inner is str or (isinstance(inner, type) and issubclass(inner, str)):
158
+ return "#/$defs/valueSourceString", None
159
+ return "#/$defs/valueSourceString", None
160
+
161
+
162
+ def iter_helm_fields(
163
+ model: type[Any],
164
+ *,
165
+ env_prefix: str,
166
+ ) -> tuple[HelmFieldSpec, ...]:
167
+ specs: list[HelmFieldSpec] = []
168
+ for field_name, field_info in model.model_fields.items():
169
+ helm_extra = _helm_extra(field_info)
170
+ if helm_extra.get("skip"):
171
+ continue
172
+
173
+ annotation = field_info.annotation
174
+ origin = get_origin(annotation)
175
+ container = False
176
+ if origin is dict:
177
+ continue
178
+ if origin is list:
179
+ if not _is_list_str_type(annotation):
180
+ continue
181
+ container = True
182
+ elif not _is_scalar_type(annotation) and not _is_secret_type(annotation):
183
+ continue
184
+
185
+ default = _default_value(field_info)
186
+ sensitive = bool(helm_extra.get("sensitive")) or _is_secret_type(annotation)
187
+ required_when_enabled = bool(
188
+ helm_extra.get("required_when_enabled")
189
+ ) or field_has_not_provided_default(field_info)
190
+ schema_ref, plain_type = _connection_property_types(
191
+ field_info=field_info,
192
+ sensitive=sensitive,
193
+ default=default,
194
+ helm_extra=helm_extra,
195
+ container=container,
196
+ )
197
+ helm_name = str(helm_extra.get("helm_name", snake_to_camel(field_name)))
198
+ section = str(helm_extra.get("section", DEFAULT_HELM_SECTION))
199
+ block_level = bool(helm_extra.get("block_level"))
200
+ overridable = bool(helm_extra.get("overridable"))
201
+ spec = HelmFieldSpec(
202
+ python_name=field_name,
203
+ helm_name=helm_name,
204
+ env_var=env_var_name(field_name, env_prefix),
205
+ sensitive=sensitive,
206
+ required_when_enabled=required_when_enabled,
207
+ schema_ref=schema_ref,
208
+ plain_type=plain_type,
209
+ format_uri=_default_is_uri(default),
210
+ default=default,
211
+ section=section,
212
+ block_level=block_level,
213
+ container=container,
214
+ overridable=overridable,
215
+ emit_in_template=False,
216
+ emit_in_values=True,
217
+ )
218
+ specs.append(
219
+ HelmFieldSpec(
220
+ python_name=spec.python_name,
221
+ helm_name=spec.helm_name,
222
+ env_var=spec.env_var,
223
+ sensitive=spec.sensitive,
224
+ required_when_enabled=spec.required_when_enabled,
225
+ schema_ref=spec.schema_ref,
226
+ plain_type=spec.plain_type,
227
+ format_uri=spec.format_uri,
228
+ default=spec.default,
229
+ section=spec.section,
230
+ block_level=spec.block_level,
231
+ container=spec.container,
232
+ overridable=spec.overridable,
233
+ emit_in_template=_should_emit_in_template(spec),
234
+ emit_in_values=spec.emit_in_values,
235
+ )
236
+ )
237
+ return tuple(specs)
238
+
239
+
240
+ def _connection_property_types(
241
+ *,
242
+ field_info: FieldInfo,
243
+ sensitive: bool,
244
+ default: Any,
245
+ helm_extra: dict[str, Any],
246
+ container: bool,
247
+ ) -> tuple[str | None, str | None]:
248
+ if container:
249
+ return None, None
250
+ return _schema_ref_for_field(
251
+ field_info=field_info,
252
+ sensitive=sensitive,
253
+ default=default,
254
+ helm_extra=helm_extra,
255
+ )
256
+
257
+
258
+ def _should_emit_in_template(field: HelmFieldSpec) -> bool:
259
+ if field.container:
260
+ # Lists are documentation-only unless explicitly marked overridable, in
261
+ # which case they are injected (JSON-encoded) so overlays can set them.
262
+ return field.overridable
263
+ if field.required_when_enabled or field.sensitive:
264
+ return True
265
+ # Optional fields default to ``None`` (no literal in values.yaml) but are
266
+ # still settable via overlays, so they must be emitted — guarded so the env
267
+ # var only renders when the overlay actually provides a value.
268
+ return field.default is not NOT_PROVIDED
269
+
270
+
271
+ def literal_default_for_values(field: HelmFieldSpec) -> str | int | float | bool | None:
272
+ if field.container:
273
+ return None
274
+ if field.sensitive or field.required_when_enabled:
275
+ return None
276
+ default = field.default
277
+ if isinstance(default, SecretStr):
278
+ default = default.get_secret_value()
279
+ if default is NOT_PROVIDED:
280
+ return None
281
+ if default is None:
282
+ return None
283
+ if isinstance(default, (str, int, float, bool)):
284
+ return default
285
+ return str(default)
286
+
287
+
288
+ def container_default_items(field: HelmFieldSpec) -> tuple[str, ...] | None:
289
+ if not field.container:
290
+ return None
291
+ default = field.default
292
+ if not isinstance(default, list):
293
+ return None
294
+ return tuple(str(item) for item in default)
295
+
296
+
297
+ def group_fields_by_section(
298
+ fields: tuple[HelmFieldSpec, ...],
299
+ ) -> tuple[tuple[str, tuple[HelmFieldSpec, ...]], ...]:
300
+ """Group fields by section; ``connection`` first, then declaration order."""
301
+ by_section: dict[str, list[HelmFieldSpec]] = {}
302
+ section_order: list[str] = []
303
+ for field in fields:
304
+ if field.block_level:
305
+ continue
306
+ if field.section not in by_section:
307
+ by_section[field.section] = []
308
+ section_order.append(field.section)
309
+ by_section[field.section].append(field)
310
+
311
+ ordered_names: list[str] = []
312
+ if DEFAULT_HELM_SECTION in by_section:
313
+ ordered_names.append(DEFAULT_HELM_SECTION)
314
+ for name in section_order:
315
+ if name != DEFAULT_HELM_SECTION:
316
+ ordered_names.append(name)
317
+
318
+ return tuple((name, tuple(by_section[name])) for name in ordered_names)
319
+
320
+
321
+ def block_level_fields(
322
+ fields: tuple[HelmFieldSpec, ...],
323
+ ) -> tuple[HelmFieldSpec, ...]:
324
+ return tuple(field for field in fields if field.block_level)