unique-search-proxy 2026.46.0.dev4__tar.gz → 2026.46.0.dev5__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.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/PKG-INFO +3 -1
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/README.md +2 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/pyproject.toml +1 -1
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/api/v1/crawl.py +19 -4
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/core/crawlers/basic/service.py +42 -7
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/core/crawlers/pinned_egress.py +2 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/core/provider_response.py +10 -0
- unique_search_proxy-2026.46.0.dev5/unique_search_proxy_client/web/core/url_safety/gate.py +142 -0
- unique_search_proxy-2026.46.0.dev4/unique_search_proxy_client/web/core/url_safety/gate.py +0 -87
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/__init__.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/__init__.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/api/__init__.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/api/health.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/api/v1/__init__.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/api/v1/agent_search.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/api/v1/configuration.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/api/v1/context_headers.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/api/v1/openapi_examples.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/api/v1/search.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/app.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/context.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/core/__init__.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/core/agent_engines/__init__.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/core/agent_engines/bing/citation_instructions.j2 +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/core/agent_engines/bing/cleanup.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/core/agent_engines/bing/client.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/core/agent_engines/bing/instructions.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/core/agent_engines/bing/runner.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/core/agent_engines/bing/service.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/core/agent_engines/factory.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/core/agent_engines/service_base.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/core/agent_engines/vertexai/client.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/core/agent_engines/vertexai/gemini.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/core/agent_engines/vertexai/redirects.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/core/agent_engines/vertexai/service.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/core/client/__init__.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/core/client/azure_transport.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/core/client/credentials.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/core/client/service.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/core/crawl_limiter.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/core/crawlers/__init__.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/core/crawlers/basic/__init__.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/core/crawlers/basic/processing/__init__.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/core/crawlers/basic/processing/errors.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/core/crawlers/basic/processing/html_markdown.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/core/crawlers/basic/processing/processors/__init__.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/core/crawlers/basic/processing/processors/html.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/core/crawlers/basic/processing/processors/pdf.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/core/crawlers/basic/processing/processors/plain_text.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/core/crawlers/basic/processing/registry.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/core/crawlers/basic/settings.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/core/crawlers/basic/user_agent.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/core/crawlers/factory.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/core/crawlers/firecrawl/polling.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/core/crawlers/firecrawl/request_body.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/core/crawlers/firecrawl/service.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/core/crawlers/jina/request_body.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/core/crawlers/jina/service.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/core/crawlers/tavily/request_body.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/core/crawlers/tavily/service.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/core/providers.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/core/registry.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/core/search_engines/__init__.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/core/search_engines/brave/__init__.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/core/search_engines/brave/pagination.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/core/search_engines/brave/query_params.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/core/search_engines/brave/service.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/core/search_engines/custom_api/__init__.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/core/search_engines/custom_api/service.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/core/search_engines/descriptor.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/core/search_engines/factory.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/core/search_engines/google/__init__.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/core/search_engines/google/pagination.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/core/search_engines/google/query_params.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/core/search_engines/google/service.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/core/search_engines/pagination.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/core/search_engines/perplexity/__init__.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/core/search_engines/perplexity/request_body.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/core/search_engines/perplexity/service.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/core/search_engines/service_base.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/core/url_safety/__init__.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/error_handlers.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/helm/__init__.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/helm/generator/__init__.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/helm/generator/introspect.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/helm/generator/schema.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/helm/generator/template.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/helm/generator/templates/provider_values_yaml.j2 +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/helm/generator/values_yaml.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/helm/metadata.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/helm/registry.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/helm/url_safety_registration.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/logging_config.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/middleware/context.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/monitoring/__init__.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/monitoring/metrics.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/monitoring/setup.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/presets/__init__.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/presets/common.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/presets/crawl.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/presets/search.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/presets/types.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/settings/__init__.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/settings/app.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/settings/base.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/settings/client.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/settings/monitoring.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/settings/providers/__init__.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/settings/providers/base.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/settings/providers/bing_agent.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/settings/providers/brave.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/settings/providers/firecrawl.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/settings/providers/google.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/settings/providers/jina.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/settings/providers/perplexity.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/settings/providers/tavily.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/settings/providers/vertexai_agent.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/settings/request_limits.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/settings/secret_str.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/startup_report.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/usage.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/unique_search_proxy_client/web/utils/__init__.py +0 -0
- {unique_search_proxy-2026.46.0.dev4 → unique_search_proxy-2026.46.0.dev5}/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.46.0.
|
|
3
|
+
Version: 2026.46.0.dev5
|
|
4
4
|
Summary: Web Search Proxy implementation
|
|
5
5
|
Author: ThePhilAz
|
|
6
6
|
Author-email: ThePhilAz <rami.azouz@philico.com>
|
|
@@ -240,6 +240,8 @@ Per-URL outcomes — one URL failing does not fail the batch:
|
|
|
240
240
|
|
|
241
241
|
Crawler discriminators: `Basic`, `Tavily`, `Jina`, `Firecrawl`.
|
|
242
242
|
|
|
243
|
+
**Timeout:** with `Basic`, URLs still loading just before `timeout` come back as per-URL `UPSTREAM_TIMEOUT` results next to the finished pages. A busy worker can send the response up to 5 s after `timeout`, so set your HTTP client timeout at least 5 s above it. The other crawlers fail the whole batch with a `504` at `timeout`.
|
|
244
|
+
|
|
243
245
|
### 5.5 Errors
|
|
244
246
|
|
|
245
247
|
Structured envelope on all non-2xx responses:
|
|
@@ -212,6 +212,8 @@ Per-URL outcomes — one URL failing does not fail the batch:
|
|
|
212
212
|
|
|
213
213
|
Crawler discriminators: `Basic`, `Tavily`, `Jina`, `Firecrawl`.
|
|
214
214
|
|
|
215
|
+
**Timeout:** with `Basic`, URLs still loading just before `timeout` come back as per-URL `UPSTREAM_TIMEOUT` results next to the finished pages. A busy worker can send the response up to 5 s after `timeout`, so set your HTTP client timeout at least 5 s above it. The other crawlers fail the whole batch with a `504` at `timeout`.
|
|
216
|
+
|
|
215
217
|
### 5.5 Errors
|
|
216
218
|
|
|
217
219
|
Structured envelope on all non-2xx responses:
|
|
@@ -45,6 +45,13 @@ from unique_search_proxy_client.web.usage import record_request_usage
|
|
|
45
45
|
router = APIRouter(tags=["crawl"])
|
|
46
46
|
_LOGGER = logging.getLogger(__name__)
|
|
47
47
|
|
|
48
|
+
# Callers often use the crawl timeout as their HTTP client timeout. Finishing
|
|
49
|
+
# this much earlier lets the response reach them before they give up.
|
|
50
|
+
_RESPONSE_HEADROOM_SECONDS = 0.5
|
|
51
|
+
# Only catches work that ignores the deadline. A busy worker that cancels pages
|
|
52
|
+
# late must delay the response, not drop the pages that finished.
|
|
53
|
+
_SAFETY_NET_GRACE_SECONDS = 5.0
|
|
54
|
+
|
|
48
55
|
|
|
49
56
|
def _crawl_request_context(exc: ProxyError, *, crawler_id: str) -> ProxyError:
|
|
50
57
|
return attach_request_context(
|
|
@@ -108,12 +115,18 @@ async def crawl(
|
|
|
108
115
|
|
|
109
116
|
try:
|
|
110
117
|
_enforce_url_limits(body.urls)
|
|
111
|
-
async with
|
|
118
|
+
async with (
|
|
119
|
+
get_crawl_limiter(request.app).slot(),
|
|
120
|
+
asyncio.timeout(timeout + _SAFETY_NET_GRACE_SECONDS),
|
|
121
|
+
):
|
|
122
|
+
request_deadline = asyncio.get_running_loop().time() + timeout
|
|
123
|
+
deadline = request_deadline - _RESPONSE_HEADROOM_SECONDS
|
|
112
124
|
registry = get_http_client_registry(request.app)
|
|
113
125
|
client = await registry.client_for(get_request_context())
|
|
114
126
|
gate = await apply_url_safety_gate(
|
|
115
127
|
body.urls,
|
|
116
128
|
redirect_http_client=client,
|
|
129
|
+
deadline=deadline,
|
|
117
130
|
)
|
|
118
131
|
units = len(gate.allowed_targets)
|
|
119
132
|
if not gate.allowed_targets:
|
|
@@ -126,7 +139,7 @@ async def crawl(
|
|
|
126
139
|
)
|
|
127
140
|
merged_results = merge_crawl_results(
|
|
128
141
|
body.urls,
|
|
129
|
-
|
|
142
|
+
uncrawled_by_index=gate.uncrawled_by_index,
|
|
130
143
|
crawler_results=[],
|
|
131
144
|
)
|
|
132
145
|
record_crawl_url_outcomes(
|
|
@@ -157,9 +170,11 @@ async def crawl(
|
|
|
157
170
|
crawler_results = await crawler.crawl_pinned(
|
|
158
171
|
crawl_body,
|
|
159
172
|
gate.allowed_targets,
|
|
173
|
+
deadline=deadline,
|
|
160
174
|
)
|
|
161
175
|
else:
|
|
162
|
-
|
|
176
|
+
async with asyncio.timeout_at(request_deadline):
|
|
177
|
+
crawler_results = await crawler.crawl(crawl_body)
|
|
163
178
|
succeeded = True
|
|
164
179
|
except TimeoutError as exc:
|
|
165
180
|
record_crawl_error(
|
|
@@ -217,7 +232,7 @@ async def crawl(
|
|
|
217
232
|
record_crawl_success(crawler_id, len(body.urls), duration)
|
|
218
233
|
merged_results = merge_crawl_results(
|
|
219
234
|
body.urls,
|
|
220
|
-
|
|
235
|
+
uncrawled_by_index=gate.uncrawled_by_index,
|
|
221
236
|
crawler_results=crawler_results,
|
|
222
237
|
)
|
|
223
238
|
record_crawl_url_outcomes(crawler_id, _url_outcomes(merged_results))
|
|
@@ -2,6 +2,8 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import logging
|
|
5
|
+
from collections.abc import Coroutine
|
|
6
|
+
from typing import Any
|
|
5
7
|
|
|
6
8
|
import httpx
|
|
7
9
|
from httpx import AsyncClient, Timeout
|
|
@@ -33,6 +35,7 @@ from unique_search_proxy_client.web.core.crawlers.basic.user_agent import (
|
|
|
33
35
|
random_user_agent,
|
|
34
36
|
)
|
|
35
37
|
from unique_search_proxy_client.web.core.provider_response import (
|
|
38
|
+
crawl_deadline_missed,
|
|
36
39
|
crawl_forbidden_target,
|
|
37
40
|
crawl_upstream_error,
|
|
38
41
|
transport_error_raw,
|
|
@@ -97,7 +100,10 @@ class BasicCrawlerService(BaseCrawler[BasicCrawlRequest]):
|
|
|
97
100
|
self,
|
|
98
101
|
request: BasicCrawlRequest, # type: ignore[valid-type]
|
|
99
102
|
allowed_targets: list[AllowedCrawlTarget],
|
|
103
|
+
*,
|
|
104
|
+
deadline: float | None = None,
|
|
100
105
|
) -> list[CrawlUrlResult]:
|
|
106
|
+
"""Crawl targets in order; one still running at ``deadline`` fails on its own."""
|
|
101
107
|
client = self._http_client
|
|
102
108
|
if client is None:
|
|
103
109
|
raise RuntimeError("HTTP client is required for Basic crawler")
|
|
@@ -115,9 +121,9 @@ class BasicCrawlerService(BaseCrawler[BasicCrawlRequest]):
|
|
|
115
121
|
),
|
|
116
122
|
)
|
|
117
123
|
budget = ByteBudget(request_limits_settings.max_crawl_response_bytes)
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
124
|
+
results = await asyncio.gather(
|
|
125
|
+
*[
|
|
126
|
+
self._crawl_one_before_deadline(
|
|
121
127
|
self._crawl_one(
|
|
122
128
|
client,
|
|
123
129
|
allowed_target.display_url,
|
|
@@ -126,11 +132,40 @@ class BasicCrawlerService(BaseCrawler[BasicCrawlRequest]):
|
|
|
126
132
|
semaphore=semaphore,
|
|
127
133
|
budget=budget,
|
|
128
134
|
content_type_handlers=request.content_types.to_handlers(),
|
|
129
|
-
)
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
135
|
+
),
|
|
136
|
+
deadline=deadline,
|
|
137
|
+
)
|
|
138
|
+
for allowed_target in allowed_targets
|
|
139
|
+
],
|
|
133
140
|
)
|
|
141
|
+
missed = sum(result is None for result in results)
|
|
142
|
+
if missed:
|
|
143
|
+
_LOGGER.warning(
|
|
144
|
+
"Basic crawl missed the request deadline timeout=%ss pages=%d",
|
|
145
|
+
timeout,
|
|
146
|
+
missed,
|
|
147
|
+
)
|
|
148
|
+
return [
|
|
149
|
+
result
|
|
150
|
+
if result is not None
|
|
151
|
+
else crawl_deadline_missed(
|
|
152
|
+
allowed_target.display_url,
|
|
153
|
+
f"Crawl did not finish before the {timeout}s request deadline",
|
|
154
|
+
)
|
|
155
|
+
for allowed_target, result in zip(allowed_targets, results, strict=True)
|
|
156
|
+
]
|
|
157
|
+
|
|
158
|
+
async def _crawl_one_before_deadline(
|
|
159
|
+
self,
|
|
160
|
+
crawl: Coroutine[Any, Any, CrawlUrlResult],
|
|
161
|
+
*,
|
|
162
|
+
deadline: float | None,
|
|
163
|
+
) -> CrawlUrlResult | None:
|
|
164
|
+
try:
|
|
165
|
+
async with asyncio.timeout_at(deadline):
|
|
166
|
+
return await crawl
|
|
167
|
+
except TimeoutError:
|
|
168
|
+
return None
|
|
134
169
|
|
|
135
170
|
async def _crawl_one(
|
|
136
171
|
self,
|
|
@@ -91,6 +91,16 @@ def crawl_forbidden_target(url: str, message: str) -> CrawlUrlResult:
|
|
|
91
91
|
)
|
|
92
92
|
|
|
93
93
|
|
|
94
|
+
def crawl_deadline_missed(url: str, message: str) -> CrawlUrlResult:
|
|
95
|
+
"""Build a per-URL crawl failure for a URL that missed the request deadline."""
|
|
96
|
+
return crawl_upstream_error(
|
|
97
|
+
url,
|
|
98
|
+
message,
|
|
99
|
+
code=ProxyErrorCode.UPSTREAM_TIMEOUT.value,
|
|
100
|
+
content_type=None,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
94
104
|
def raise_for_upstream_response(
|
|
95
105
|
response: httpx.Response,
|
|
96
106
|
*,
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from unique_search_proxy_core.schema import CrawlUrlResult
|
|
9
|
+
from unique_search_proxy_core.url_safety import (
|
|
10
|
+
ResolvedCrawlTarget,
|
|
11
|
+
UrlSafetyOutcome,
|
|
12
|
+
UrlSafetyService,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
from unique_search_proxy_client.web.core.provider_response import (
|
|
16
|
+
crawl_deadline_missed,
|
|
17
|
+
crawl_forbidden_target,
|
|
18
|
+
)
|
|
19
|
+
from unique_search_proxy_client.web.monitoring.metrics import record_crawl_blocked
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from httpx import AsyncClient
|
|
23
|
+
|
|
24
|
+
_LOGGER = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class AllowedCrawlTarget:
|
|
29
|
+
"""User-facing URL paired with the validated resolution for pinned egress."""
|
|
30
|
+
|
|
31
|
+
display_url: str
|
|
32
|
+
resolved: ResolvedCrawlTarget
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class UrlSafetyGateResult:
|
|
37
|
+
allowed_targets: list[AllowedCrawlTarget]
|
|
38
|
+
blocked_by_index: dict[int, CrawlUrlResult]
|
|
39
|
+
timed_out_by_index: dict[int, CrawlUrlResult] = field(default_factory=dict)
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def uncrawled_by_index(self) -> dict[int, CrawlUrlResult]:
|
|
43
|
+
return {**self.blocked_by_index, **self.timed_out_by_index}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
async def apply_url_safety_gate(
|
|
47
|
+
urls: list[str],
|
|
48
|
+
*,
|
|
49
|
+
redirect_http_client: AsyncClient,
|
|
50
|
+
deadline: float | None = None,
|
|
51
|
+
) -> UrlSafetyGateResult:
|
|
52
|
+
"""Partition crawl URLs into allowed, blocked and timed-out targets."""
|
|
53
|
+
allowed_targets: list[AllowedCrawlTarget] = []
|
|
54
|
+
blocked_by_index: dict[int, CrawlUrlResult] = {}
|
|
55
|
+
timed_out_by_index: dict[int, CrawlUrlResult] = {}
|
|
56
|
+
|
|
57
|
+
for index, url in enumerate(urls):
|
|
58
|
+
outcome = await _validate_before_deadline(
|
|
59
|
+
url,
|
|
60
|
+
redirect_http_client=redirect_http_client,
|
|
61
|
+
deadline=deadline,
|
|
62
|
+
)
|
|
63
|
+
if outcome is None:
|
|
64
|
+
timed_out_by_index = {
|
|
65
|
+
unchecked_index: crawl_deadline_missed(
|
|
66
|
+
unchecked_url.strip(),
|
|
67
|
+
"URL safety check did not finish before the crawl deadline",
|
|
68
|
+
)
|
|
69
|
+
for unchecked_index, unchecked_url in enumerate(
|
|
70
|
+
urls[index:],
|
|
71
|
+
start=index,
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
_LOGGER.warning(
|
|
75
|
+
"URL safety checks missed the crawl deadline unchecked=%d",
|
|
76
|
+
len(timed_out_by_index),
|
|
77
|
+
)
|
|
78
|
+
break
|
|
79
|
+
|
|
80
|
+
if outcome.blocked is not None:
|
|
81
|
+
record_crawl_blocked(outcome.blocked.category)
|
|
82
|
+
blocked_by_index[index] = crawl_forbidden_target(
|
|
83
|
+
outcome.url.strip(),
|
|
84
|
+
outcome.blocked.reason,
|
|
85
|
+
)
|
|
86
|
+
continue
|
|
87
|
+
|
|
88
|
+
if outcome.resolved is None:
|
|
89
|
+
msg = "URL safety allowed a crawl target without resolved metadata"
|
|
90
|
+
raise RuntimeError(msg)
|
|
91
|
+
|
|
92
|
+
allowed_targets.append(
|
|
93
|
+
AllowedCrawlTarget(
|
|
94
|
+
display_url=outcome.url.strip(),
|
|
95
|
+
resolved=outcome.resolved,
|
|
96
|
+
),
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
return UrlSafetyGateResult(
|
|
100
|
+
allowed_targets=allowed_targets,
|
|
101
|
+
blocked_by_index=blocked_by_index,
|
|
102
|
+
timed_out_by_index=timed_out_by_index,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
async def _validate_before_deadline(
|
|
107
|
+
url: str,
|
|
108
|
+
*,
|
|
109
|
+
redirect_http_client: AsyncClient,
|
|
110
|
+
deadline: float | None,
|
|
111
|
+
) -> UrlSafetyOutcome | None:
|
|
112
|
+
"""Return the URL's safety outcome, or ``None`` when the deadline passes first."""
|
|
113
|
+
try:
|
|
114
|
+
async with asyncio.timeout_at(deadline):
|
|
115
|
+
[outcome] = await UrlSafetyService.validate_urls_individually(
|
|
116
|
+
[url],
|
|
117
|
+
redirect_http_client=redirect_http_client,
|
|
118
|
+
)
|
|
119
|
+
except TimeoutError:
|
|
120
|
+
return None
|
|
121
|
+
return outcome
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def merge_crawl_results(
|
|
125
|
+
urls: list[str],
|
|
126
|
+
*,
|
|
127
|
+
uncrawled_by_index: dict[int, CrawlUrlResult],
|
|
128
|
+
crawler_results: list[CrawlUrlResult],
|
|
129
|
+
) -> list[CrawlUrlResult]:
|
|
130
|
+
"""Merge the gate's results for uncrawled URLs with crawler outcomes in order."""
|
|
131
|
+
merged: list[CrawlUrlResult] = []
|
|
132
|
+
crawler_index = 0
|
|
133
|
+
for index, _url in enumerate(urls):
|
|
134
|
+
uncrawled = uncrawled_by_index.get(index)
|
|
135
|
+
if uncrawled is not None:
|
|
136
|
+
merged.append(uncrawled)
|
|
137
|
+
continue
|
|
138
|
+
|
|
139
|
+
merged.append(crawler_results[crawler_index])
|
|
140
|
+
crawler_index += 1
|
|
141
|
+
|
|
142
|
+
return merged
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from dataclasses import dataclass
|
|
4
|
-
from typing import TYPE_CHECKING
|
|
5
|
-
|
|
6
|
-
from unique_search_proxy_core.schema import CrawlUrlResult
|
|
7
|
-
from unique_search_proxy_core.url_safety import ResolvedCrawlTarget, UrlSafetyService
|
|
8
|
-
|
|
9
|
-
from unique_search_proxy_client.web.core.provider_response import crawl_forbidden_target
|
|
10
|
-
from unique_search_proxy_client.web.monitoring.metrics import record_crawl_blocked
|
|
11
|
-
|
|
12
|
-
if TYPE_CHECKING:
|
|
13
|
-
from httpx import AsyncClient
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
@dataclass(frozen=True)
|
|
17
|
-
class AllowedCrawlTarget:
|
|
18
|
-
"""User-facing URL paired with the validated resolution for pinned egress."""
|
|
19
|
-
|
|
20
|
-
display_url: str
|
|
21
|
-
resolved: ResolvedCrawlTarget
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
@dataclass(frozen=True)
|
|
25
|
-
class UrlSafetyGateResult:
|
|
26
|
-
allowed_targets: list[AllowedCrawlTarget]
|
|
27
|
-
blocked_by_index: dict[int, CrawlUrlResult]
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
async def apply_url_safety_gate(
|
|
31
|
-
urls: list[str],
|
|
32
|
-
*,
|
|
33
|
-
redirect_http_client: AsyncClient,
|
|
34
|
-
) -> UrlSafetyGateResult:
|
|
35
|
-
"""Validate crawl URLs and partition them into allowed vs blocked targets."""
|
|
36
|
-
outcomes = await UrlSafetyService.validate_urls_individually(
|
|
37
|
-
urls,
|
|
38
|
-
redirect_http_client=redirect_http_client,
|
|
39
|
-
)
|
|
40
|
-
allowed_targets: list[AllowedCrawlTarget] = []
|
|
41
|
-
blocked_by_index: dict[int, CrawlUrlResult] = {}
|
|
42
|
-
|
|
43
|
-
for index, outcome in enumerate(outcomes):
|
|
44
|
-
if outcome.blocked is not None:
|
|
45
|
-
record_crawl_blocked(outcome.blocked.category)
|
|
46
|
-
blocked_by_index[index] = crawl_forbidden_target(
|
|
47
|
-
outcome.url.strip(),
|
|
48
|
-
outcome.blocked.reason,
|
|
49
|
-
)
|
|
50
|
-
continue
|
|
51
|
-
|
|
52
|
-
if outcome.resolved is None:
|
|
53
|
-
msg = "URL safety allowed a crawl target without resolved metadata"
|
|
54
|
-
raise RuntimeError(msg)
|
|
55
|
-
|
|
56
|
-
allowed_targets.append(
|
|
57
|
-
AllowedCrawlTarget(
|
|
58
|
-
display_url=outcome.url.strip(),
|
|
59
|
-
resolved=outcome.resolved,
|
|
60
|
-
),
|
|
61
|
-
)
|
|
62
|
-
|
|
63
|
-
return UrlSafetyGateResult(
|
|
64
|
-
allowed_targets=allowed_targets,
|
|
65
|
-
blocked_by_index=blocked_by_index,
|
|
66
|
-
)
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
def merge_crawl_results(
|
|
70
|
-
urls: list[str],
|
|
71
|
-
*,
|
|
72
|
-
blocked_by_index: dict[int, CrawlUrlResult],
|
|
73
|
-
crawler_results: list[CrawlUrlResult],
|
|
74
|
-
) -> list[CrawlUrlResult]:
|
|
75
|
-
"""Merge per-URL blocked results with crawler outcomes in request order."""
|
|
76
|
-
merged: list[CrawlUrlResult] = []
|
|
77
|
-
crawler_index = 0
|
|
78
|
-
for index, _url in enumerate(urls):
|
|
79
|
-
blocked = blocked_by_index.get(index)
|
|
80
|
-
if blocked is not None:
|
|
81
|
-
merged.append(blocked)
|
|
82
|
-
continue
|
|
83
|
-
|
|
84
|
-
merged.append(crawler_results[crawler_index])
|
|
85
|
-
crawler_index += 1
|
|
86
|
-
|
|
87
|
-
return merged
|
|
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
|
|
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
|