unique-search-proxy 2026.44.0.dev10__tar.gz → 2026.44.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.
Files changed (120) hide show
  1. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/PKG-INFO +1 -1
  2. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/pyproject.toml +1 -1
  3. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/api/v1/crawl.py +24 -2
  4. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/api/v1/search.py +4 -1
  5. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/app.py +7 -0
  6. unique_search_proxy-2026.44.0.dev12/unique_search_proxy_client/web/core/crawl_limiter.py +51 -0
  7. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/core/crawlers/basic/service.py +6 -1
  8. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/settings/request_limits.py +51 -1
  9. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/README.md +0 -0
  10. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/__init__.py +0 -0
  11. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/__init__.py +0 -0
  12. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/api/__init__.py +0 -0
  13. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/api/health.py +0 -0
  14. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/api/v1/__init__.py +0 -0
  15. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/api/v1/agent_search.py +0 -0
  16. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/api/v1/configuration.py +0 -0
  17. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/api/v1/context_headers.py +0 -0
  18. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/api/v1/openapi_examples.py +0 -0
  19. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/context.py +0 -0
  20. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/core/__init__.py +0 -0
  21. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/core/agent_engines/__init__.py +0 -0
  22. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/core/agent_engines/bing/cleanup.py +0 -0
  23. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/core/agent_engines/bing/client.py +0 -0
  24. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/core/agent_engines/bing/runner.py +0 -0
  25. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/core/agent_engines/bing/service.py +0 -0
  26. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/core/agent_engines/factory.py +0 -0
  27. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/core/agent_engines/service_base.py +0 -0
  28. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/core/agent_engines/structured_output.py +0 -0
  29. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/core/agent_engines/vertexai/client.py +0 -0
  30. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/core/agent_engines/vertexai/gemini.py +0 -0
  31. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/core/agent_engines/vertexai/redirects.py +0 -0
  32. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/core/agent_engines/vertexai/service.py +0 -0
  33. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/core/client/__init__.py +0 -0
  34. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/core/client/azure_transport.py +0 -0
  35. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/core/client/credentials.py +0 -0
  36. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/core/client/service.py +0 -0
  37. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/core/crawlers/__init__.py +0 -0
  38. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/core/crawlers/basic/__init__.py +0 -0
  39. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/core/crawlers/basic/processing/__init__.py +0 -0
  40. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/core/crawlers/basic/processing/errors.py +0 -0
  41. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/core/crawlers/basic/processing/html_markdown.py +0 -0
  42. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/core/crawlers/basic/processing/processors/__init__.py +0 -0
  43. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/core/crawlers/basic/processing/processors/html.py +0 -0
  44. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/core/crawlers/basic/processing/processors/pdf.py +0 -0
  45. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/core/crawlers/basic/processing/processors/plain_text.py +0 -0
  46. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/core/crawlers/basic/processing/registry.py +0 -0
  47. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/core/crawlers/basic/settings.py +0 -0
  48. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/core/crawlers/basic/user_agent.py +0 -0
  49. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/core/crawlers/factory.py +0 -0
  50. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/core/crawlers/firecrawl/polling.py +0 -0
  51. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/core/crawlers/firecrawl/request_body.py +0 -0
  52. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/core/crawlers/firecrawl/service.py +0 -0
  53. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/core/crawlers/jina/request_body.py +0 -0
  54. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/core/crawlers/jina/service.py +0 -0
  55. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/core/crawlers/pinned_egress.py +0 -0
  56. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/core/crawlers/tavily/request_body.py +0 -0
  57. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/core/crawlers/tavily/service.py +0 -0
  58. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/core/provider_response.py +0 -0
  59. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/core/providers.py +0 -0
  60. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/core/registry.py +0 -0
  61. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/core/search_engines/__init__.py +0 -0
  62. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/core/search_engines/brave/__init__.py +0 -0
  63. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/core/search_engines/brave/pagination.py +0 -0
  64. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/core/search_engines/brave/query_params.py +0 -0
  65. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/core/search_engines/brave/service.py +0 -0
  66. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/core/search_engines/custom_api/__init__.py +0 -0
  67. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/core/search_engines/custom_api/service.py +0 -0
  68. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/core/search_engines/descriptor.py +0 -0
  69. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/core/search_engines/factory.py +0 -0
  70. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/core/search_engines/google/__init__.py +0 -0
  71. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/core/search_engines/google/pagination.py +0 -0
  72. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/core/search_engines/google/query_params.py +0 -0
  73. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/core/search_engines/google/service.py +0 -0
  74. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/core/search_engines/pagination.py +0 -0
  75. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/core/search_engines/perplexity/__init__.py +0 -0
  76. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/core/search_engines/perplexity/request_body.py +0 -0
  77. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/core/search_engines/perplexity/service.py +0 -0
  78. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/core/search_engines/service_base.py +0 -0
  79. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/core/url_safety/__init__.py +0 -0
  80. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/core/url_safety/gate.py +0 -0
  81. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/error_handlers.py +0 -0
  82. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/helm/__init__.py +0 -0
  83. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/helm/generator/__init__.py +0 -0
  84. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/helm/generator/introspect.py +0 -0
  85. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/helm/generator/schema.py +0 -0
  86. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/helm/generator/template.py +0 -0
  87. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/helm/generator/templates/provider_values_yaml.j2 +0 -0
  88. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/helm/generator/values_yaml.py +0 -0
  89. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/helm/metadata.py +0 -0
  90. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/helm/registry.py +0 -0
  91. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/helm/url_safety_registration.py +0 -0
  92. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/logging_config.py +0 -0
  93. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/middleware/context.py +0 -0
  94. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/monitoring/__init__.py +0 -0
  95. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/monitoring/metrics.py +0 -0
  96. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/monitoring/setup.py +0 -0
  97. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/presets/__init__.py +0 -0
  98. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/presets/common.py +0 -0
  99. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/presets/crawl.py +0 -0
  100. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/presets/search.py +0 -0
  101. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/presets/types.py +0 -0
  102. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/settings/__init__.py +0 -0
  103. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/settings/app.py +0 -0
  104. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/settings/base.py +0 -0
  105. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/settings/client.py +0 -0
  106. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/settings/monitoring.py +0 -0
  107. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/settings/providers/__init__.py +0 -0
  108. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/settings/providers/base.py +0 -0
  109. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/settings/providers/bing_agent.py +0 -0
  110. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/settings/providers/brave.py +0 -0
  111. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/settings/providers/firecrawl.py +0 -0
  112. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/settings/providers/google.py +0 -0
  113. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/settings/providers/jina.py +0 -0
  114. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/settings/providers/perplexity.py +0 -0
  115. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/settings/providers/tavily.py +0 -0
  116. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/settings/providers/vertexai_agent.py +0 -0
  117. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/settings/secret_str.py +0 -0
  118. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/startup_report.py +0 -0
  119. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.0.dev12}/unique_search_proxy_client/web/utils/__init__.py +0 -0
  120. {unique_search_proxy-2026.44.0.dev10 → unique_search_proxy-2026.44.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.44.0.dev10
3
+ Version: 2026.44.0.dev12
4
4
  Summary: Web Search Proxy implementation
5
5
  Author: ThePhilAz
6
6
  Author-email: ThePhilAz <rami.azouz@philico.com>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "unique-search-proxy"
3
- version = "2026.44.0.dev10"
3
+ version = "2026.44.0.dev12"
4
4
  description = "Web Search Proxy implementation"
5
5
  authors = [{ name = "ThePhilAz", email = "rami.azouz@philico.com" }]
6
6
  readme = "README.md"
@@ -9,6 +9,7 @@ from unique_search_proxy_core.crawlers.config_types import CrawlRequest
9
9
  from unique_search_proxy_core.errors import (
10
10
  ProxyError,
11
11
  UpstreamTimeoutError,
12
+ ValidationProxyError,
12
13
  attach_request_context,
13
14
  )
14
15
  from unique_search_proxy_core.schema import (
@@ -22,6 +23,7 @@ from unique_search_proxy_client.web.api.v1.openapi_examples import (
22
23
  )
23
24
  from unique_search_proxy_client.web.context import get_request_context
24
25
  from unique_search_proxy_client.web.core.client import get_http_client_registry
26
+ from unique_search_proxy_client.web.core.crawl_limiter import get_crawl_limiter
25
27
  from unique_search_proxy_client.web.core.crawlers.factory import get_crawler_service
26
28
  from unique_search_proxy_client.web.core.crawlers.pinned_egress import (
27
29
  PinnedEgressCrawler,
@@ -35,6 +37,9 @@ from unique_search_proxy_client.web.monitoring.metrics import (
35
37
  record_crawl_success,
36
38
  record_crawl_url_outcomes,
37
39
  )
40
+ from unique_search_proxy_client.web.settings.request_limits import (
41
+ request_limits_settings,
42
+ )
38
43
 
39
44
  router = APIRouter(tags=["crawl"])
40
45
  _LOGGER = logging.getLogger(__name__)
@@ -48,6 +53,21 @@ def _crawl_request_context(exc: ProxyError, *, crawler_id: str) -> ProxyError:
48
53
  )
49
54
 
50
55
 
56
+ def _enforce_url_limits(urls: list[str]) -> None:
57
+ limits = request_limits_settings
58
+ if len(urls) > limits.max_urls_per_crawl:
59
+ raise ValidationProxyError(
60
+ f"Crawl request lists {len(urls)} URLs; "
61
+ f"at most {limits.max_urls_per_crawl} are allowed",
62
+ )
63
+ for index, url in enumerate(urls):
64
+ if len(url) > limits.max_url_length:
65
+ raise ValidationProxyError(
66
+ f"URL at index {index} is {len(url)} characters long; "
67
+ f"at most {limits.max_url_length} are allowed",
68
+ )
69
+
70
+
51
71
  def _url_outcomes(results: list[CrawlUrlResult]) -> list[tuple[str, str, str]]:
52
72
  """Derive ``(outcome, error_code, http_status)`` triples from merged results."""
53
73
  outcomes: list[tuple[str, str, str]] = []
@@ -74,7 +94,7 @@ async def crawl(
74
94
  body: CrawlRequest = Body(openapi_examples=CRAWL_OPENAPI_EXAMPLES), # type: ignore[valid-type]
75
95
  ) -> CrawlResponse:
76
96
  crawler_id = body.crawler
77
- timeout = body.timeout
97
+ timeout = min(body.timeout, request_limits_settings.max_timeout_seconds)
78
98
  started = time.perf_counter()
79
99
  _LOGGER.info(
80
100
  "crawl start crawler=%s urls=%d timeout=%ss",
@@ -84,7 +104,8 @@ async def crawl(
84
104
  )
85
105
 
86
106
  try:
87
- async with asyncio.timeout(timeout):
107
+ _enforce_url_limits(body.urls)
108
+ async with get_crawl_limiter(request.app).slot(), asyncio.timeout(timeout):
88
109
  registry = get_http_client_registry(request.app)
89
110
  client = await registry.client_for(get_request_context())
90
111
  gate = await apply_url_safety_gate(
@@ -122,6 +143,7 @@ async def crawl(
122
143
  crawl_body = body.model_copy(
123
144
  update={
124
145
  "urls": [target.display_url for target in gate.allowed_targets],
146
+ "timeout": timeout,
125
147
  },
126
148
  )
127
149
 
@@ -31,6 +31,9 @@ from unique_search_proxy_client.web.monitoring.metrics import (
31
31
  record_search_error,
32
32
  record_search_success,
33
33
  )
34
+ from unique_search_proxy_client.web.settings.request_limits import (
35
+ request_limits_settings,
36
+ )
34
37
 
35
38
  router = APIRouter(tags=["search"])
36
39
  _LOGGER = logging.getLogger(__name__)
@@ -63,7 +66,7 @@ async def search(
63
66
  ) -> SearchResponse:
64
67
  engine = body.engine
65
68
  engine_id = engine.value if hasattr(engine, "value") else str(engine)
66
- timeout = body.timeout
69
+ timeout = min(body.timeout, request_limits_settings.max_timeout_seconds)
67
70
  started = time.perf_counter()
68
71
  _LOGGER.info("search start engine=%s timeout=%ss", engine_id, timeout)
69
72
 
@@ -19,6 +19,7 @@ from unique_search_proxy_client.web.core.agent_engines.bing.client import (
19
19
  from unique_search_proxy_client.web.core.client.service import (
20
20
  create_http_client_registry,
21
21
  )
22
+ from unique_search_proxy_client.web.core.crawl_limiter import CrawlLimiter
22
23
  from unique_search_proxy_client.web.core.providers import register_builtin_providers
23
24
  from unique_search_proxy_client.web.error_handlers import register_exception_handlers
24
25
  from unique_search_proxy_client.web.logging_config import (
@@ -27,6 +28,9 @@ from unique_search_proxy_client.web.logging_config import (
27
28
  )
28
29
  from unique_search_proxy_client.web.middleware.context import RequestContextMiddleware
29
30
  from unique_search_proxy_client.web.monitoring import setup_prometheus
31
+ from unique_search_proxy_client.web.settings.request_limits import (
32
+ request_limits_settings,
33
+ )
30
34
  from unique_search_proxy_client.web.startup_report import (
31
35
  log_startup_settings_report,
32
36
  )
@@ -61,6 +65,9 @@ async def lifespan(app: FastAPI):
61
65
  log_startup_settings_report(_LOGGER)
62
66
  registry = await create_http_client_registry()
63
67
  app.state.http_client_registry = registry
68
+ app.state.crawl_limiter = CrawlLimiter(
69
+ request_limits_settings.max_concurrent_crawls,
70
+ )
64
71
  try:
65
72
  await maybe_cleanup_auto_provisioned_bing_agents_on_start(registry)
66
73
  yield
@@ -0,0 +1,51 @@
1
+ """Per-worker limit on crawl requests running at once."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from collections.abc import AsyncIterator
7
+ from contextlib import asynccontextmanager
8
+
9
+ from fastapi import FastAPI
10
+ from unique_search_proxy_core.errors import RateLimitedError
11
+
12
+ _RETRY_AFTER_SECONDS = 1
13
+
14
+
15
+ class CrawlLimiter:
16
+ """Admits at most ``max_concurrent`` crawls and rejects the rest.
17
+
18
+ Rejecting instead of queueing keeps a burst from piling bodies up in one
19
+ worker; callers retry on the 429.
20
+ """
21
+
22
+ def __init__(self, max_concurrent: int) -> None:
23
+ self._max_concurrent = max_concurrent
24
+ self._semaphore = asyncio.Semaphore(max_concurrent)
25
+
26
+ @asynccontextmanager
27
+ async def slot(self) -> AsyncIterator[None]:
28
+ """Hold one crawl slot for the duration of the block.
29
+
30
+ Raises:
31
+ RateLimitedError: Every slot is taken.
32
+ """
33
+ if self._semaphore.locked():
34
+ raise RateLimitedError(
35
+ f"Crawl capacity is in use ({self._max_concurrent} concurrent "
36
+ "crawls per worker)",
37
+ retry_after_seconds=_RETRY_AFTER_SECONDS,
38
+ )
39
+ async with self._semaphore:
40
+ yield
41
+
42
+
43
+ def get_crawl_limiter(app: FastAPI) -> CrawlLimiter:
44
+ """Return the application-owned crawl limiter."""
45
+ limiter = getattr(app.state, "crawl_limiter", None)
46
+ if limiter is None:
47
+ raise RuntimeError("Crawl limiter is not initialized")
48
+ return limiter
49
+
50
+
51
+ __all__ = ["CrawlLimiter", "get_crawl_limiter"]
@@ -108,7 +108,12 @@ class BasicCrawlerService(BaseCrawler[BasicCrawlRequest]):
108
108
  raise ValueError(msg)
109
109
 
110
110
  timeout = request.timeout
111
- semaphore = asyncio.Semaphore(request.max_concurrent_requests)
111
+ semaphore = asyncio.Semaphore(
112
+ min(
113
+ request.max_concurrent_requests,
114
+ request_limits_settings.max_concurrent_fetches,
115
+ ),
116
+ )
112
117
  budget = ByteBudget(request_limits_settings.max_crawl_response_bytes)
113
118
  return list(
114
119
  await asyncio.gather(
@@ -17,7 +17,14 @@ REQUEST_LIMITS_ENV_PREFIX = "REQUEST_LIMITS_"
17
17
  egress=None,
18
18
  env_prefix=REQUEST_LIMITS_ENV_PREFIX,
19
19
  sections={
20
- "crawl": ["max_crawl_response_kib"],
20
+ "crawl": [
21
+ "max_crawl_response_kib",
22
+ "max_urls_per_crawl",
23
+ "max_url_length",
24
+ "max_concurrent_fetches",
25
+ "max_concurrent_crawls",
26
+ ],
27
+ "timeouts": ["max_timeout_seconds"],
21
28
  },
22
29
  )
23
30
  class RequestLimitsSettings(BaseSettings):
@@ -41,6 +48,49 @@ class RequestLimitsSettings(BaseSettings):
41
48
  ),
42
49
  )
43
50
 
51
+ # Matches the search fetch_size ceiling: the web search tool crawls a whole
52
+ # result page in one request.
53
+ max_urls_per_crawl: int = Field(
54
+ default=100,
55
+ ge=1,
56
+ description=(
57
+ "Maximum number of URLs in one crawl request. Requests with more "
58
+ "are rejected with a 422."
59
+ ),
60
+ )
61
+ max_url_length: int = Field(
62
+ default=2048,
63
+ ge=1,
64
+ description=(
65
+ "Maximum length of each URL, in characters. Requests containing a "
66
+ "longer URL are rejected with a 422."
67
+ ),
68
+ )
69
+ max_concurrent_fetches: int = Field(
70
+ default=10,
71
+ ge=1,
72
+ description=(
73
+ "Ceiling on a basic crawl's maxConcurrentRequests; higher values "
74
+ "are lowered to this."
75
+ ),
76
+ )
77
+ max_concurrent_crawls: int = Field(
78
+ default=2,
79
+ ge=1,
80
+ description=(
81
+ "Crawl requests one worker process runs at once. Requests beyond "
82
+ "this get a 429 instead of queueing."
83
+ ),
84
+ )
85
+ max_timeout_seconds: int = Field(
86
+ default=120,
87
+ ge=1,
88
+ description=(
89
+ "Ceiling on the timeout of crawl and search requests; longer "
90
+ "timeouts are lowered to this."
91
+ ),
92
+ )
93
+
44
94
  @property
45
95
  def max_crawl_response_bytes(self) -> int:
46
96
  return self.max_crawl_response_kib * 1024