unique-search-proxy 2026.44.0.dev11__tar.gz → 2026.44.0.dev13__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.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/PKG-INFO +4 -4
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/README.md +2 -2
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/pyproject.toml +2 -2
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/api/v1/crawl.py +4 -2
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/api/v1/search.py +4 -1
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/app.py +11 -0
- unique_search_proxy-2026.44.0.dev13/unique_search_proxy_client/web/core/crawl_limiter.py +51 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/core/crawlers/basic/service.py +6 -1
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/helm/generator/schema.py +10 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/helm/generator/template.py +66 -7
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/helm/metadata.py +9 -1
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/settings/app.py +1 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/settings/client.py +2 -2
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/settings/request_limits.py +32 -1
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/api/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/api/health.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/api/v1/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/api/v1/agent_search.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/api/v1/configuration.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/api/v1/context_headers.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/api/v1/openapi_examples.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/context.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/core/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/core/agent_engines/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/core/agent_engines/bing/cleanup.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/core/agent_engines/bing/client.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/core/agent_engines/bing/runner.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/core/agent_engines/bing/service.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/core/agent_engines/factory.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/core/agent_engines/service_base.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/core/agent_engines/structured_output.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/core/agent_engines/vertexai/client.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/core/agent_engines/vertexai/gemini.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/core/agent_engines/vertexai/redirects.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/core/agent_engines/vertexai/service.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/core/client/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/core/client/azure_transport.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/core/client/credentials.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/core/client/service.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/core/crawlers/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/core/crawlers/basic/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/core/crawlers/basic/processing/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/core/crawlers/basic/processing/errors.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/core/crawlers/basic/processing/html_markdown.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/core/crawlers/basic/processing/processors/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/core/crawlers/basic/processing/processors/html.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/core/crawlers/basic/processing/processors/pdf.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/core/crawlers/basic/processing/processors/plain_text.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/core/crawlers/basic/processing/registry.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/core/crawlers/basic/settings.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/core/crawlers/basic/user_agent.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/core/crawlers/factory.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/core/crawlers/firecrawl/polling.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/core/crawlers/firecrawl/request_body.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/core/crawlers/firecrawl/service.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/core/crawlers/jina/request_body.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/core/crawlers/jina/service.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/core/crawlers/pinned_egress.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/core/crawlers/tavily/request_body.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/core/crawlers/tavily/service.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/core/provider_response.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/core/providers.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/core/registry.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/core/search_engines/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/core/search_engines/brave/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/core/search_engines/brave/pagination.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/core/search_engines/brave/query_params.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/core/search_engines/brave/service.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/core/search_engines/custom_api/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/core/search_engines/custom_api/service.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/core/search_engines/descriptor.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/core/search_engines/factory.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/core/search_engines/google/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/core/search_engines/google/pagination.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/core/search_engines/google/query_params.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/core/search_engines/google/service.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/core/search_engines/pagination.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/core/search_engines/perplexity/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/core/search_engines/perplexity/request_body.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/core/search_engines/perplexity/service.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/core/search_engines/service_base.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/core/url_safety/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/core/url_safety/gate.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/error_handlers.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/helm/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/helm/generator/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/helm/generator/introspect.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/helm/generator/templates/provider_values_yaml.j2 +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/helm/generator/values_yaml.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/helm/registry.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/helm/url_safety_registration.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/logging_config.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/middleware/context.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/monitoring/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/monitoring/metrics.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/monitoring/setup.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/presets/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/presets/common.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/presets/crawl.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/presets/search.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/presets/types.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/settings/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/settings/base.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/settings/monitoring.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/settings/providers/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/settings/providers/base.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/settings/providers/bing_agent.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/settings/providers/brave.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/settings/providers/firecrawl.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/settings/providers/google.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/settings/providers/jina.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/settings/providers/perplexity.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/settings/providers/tavily.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/settings/providers/vertexai_agent.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/settings/secret_str.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/startup_report.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/unique_search_proxy_client/web/utils/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev11 → unique_search_proxy-2026.44.0.dev13}/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.
|
|
3
|
+
Version: 2026.44.0.dev13
|
|
4
4
|
Summary: Web Search Proxy implementation
|
|
5
5
|
Author: ThePhilAz
|
|
6
6
|
Author-email: ThePhilAz <rami.azouz@philico.com>
|
|
@@ -20,7 +20,7 @@ Requires-Dist: certifi>=2025.11.12,<2027
|
|
|
20
20
|
Requires-Dist: google-genai>=1.73.0,<2
|
|
21
21
|
Requires-Dist: google-auth>=2.43.0,<3
|
|
22
22
|
Requires-Dist: jinja2>=3.1.6,<4
|
|
23
|
-
Requires-Dist: unique-toolkit[monitoring]>=2026.44.0.
|
|
23
|
+
Requires-Dist: unique-toolkit[monitoring]>=2026.44.0.dev10,<2026.44.0rc0
|
|
24
24
|
Requires-Dist: unique-search-proxy-core>=2026.44.0.dev9,<2026.44.0rc0
|
|
25
25
|
Requires-Dist: openai>=1.105.0,<3
|
|
26
26
|
Requires-Python: >=3.12
|
|
@@ -120,7 +120,7 @@ Built-in providers register in `register_builtin_providers()` during `create_app
|
|
|
120
120
|
| `POST /v1/agent-search/stream` | Same, streamed as SSE (`delta` + `done`) |
|
|
121
121
|
| `POST /v1/crawl` | Crawl URLs (flat body: `crawler`, `urls`, `timeout`, …) |
|
|
122
122
|
| `GET /metrics` | Prometheus (when enabled) |
|
|
123
|
-
| `/docs` | Swagger UI with preset examples |
|
|
123
|
+
| `/docs` | Swagger UI with preset examples (only when `EXPOSE_DOCS=true`) |
|
|
124
124
|
|
|
125
125
|
OpenAPI spec: `openapi.json` (input for [SDK codegen](../unique_search_proxy_sdk/README.md)).
|
|
126
126
|
|
|
@@ -310,7 +310,7 @@ uv run python scripts/generate_sdk.py
|
|
|
310
310
|
## 8. Dev testing
|
|
311
311
|
|
|
312
312
|
1. Start the server and configure `.env` (`.env.example` sets `REQUIRE_CONTEXT_HEADERS=false` for local use; production keeps enforcement enabled).
|
|
313
|
-
2. **Swagger** — `/docs` → **Try it out** on `/v1/search` or `/v1/crawl` → pick an **Examples** preset. Context headers default to `local` in the UI.
|
|
313
|
+
2. **Swagger** (if `EXPOSE_DOCS=true`) — `/docs` → **Try it out** on `/v1/search` or `/v1/crawl` → pick an **Examples** preset. Context headers default to `local` in the UI.
|
|
314
314
|
3. **CLI** — same presets from the terminal (`scripts/try_presets.py` sends the same local context headers):
|
|
315
315
|
|
|
316
316
|
```bash
|
|
@@ -92,7 +92,7 @@ Built-in providers register in `register_builtin_providers()` during `create_app
|
|
|
92
92
|
| `POST /v1/agent-search/stream` | Same, streamed as SSE (`delta` + `done`) |
|
|
93
93
|
| `POST /v1/crawl` | Crawl URLs (flat body: `crawler`, `urls`, `timeout`, …) |
|
|
94
94
|
| `GET /metrics` | Prometheus (when enabled) |
|
|
95
|
-
| `/docs` | Swagger UI with preset examples |
|
|
95
|
+
| `/docs` | Swagger UI with preset examples (only when `EXPOSE_DOCS=true`) |
|
|
96
96
|
|
|
97
97
|
OpenAPI spec: `openapi.json` (input for [SDK codegen](../unique_search_proxy_sdk/README.md)).
|
|
98
98
|
|
|
@@ -282,7 +282,7 @@ uv run python scripts/generate_sdk.py
|
|
|
282
282
|
## 8. Dev testing
|
|
283
283
|
|
|
284
284
|
1. Start the server and configure `.env` (`.env.example` sets `REQUIRE_CONTEXT_HEADERS=false` for local use; production keeps enforcement enabled).
|
|
285
|
-
2. **Swagger** — `/docs` → **Try it out** on `/v1/search` or `/v1/crawl` → pick an **Examples** preset. Context headers default to `local` in the UI.
|
|
285
|
+
2. **Swagger** (if `EXPOSE_DOCS=true`) — `/docs` → **Try it out** on `/v1/search` or `/v1/crawl` → pick an **Examples** preset. Context headers default to `local` in the UI.
|
|
286
286
|
3. **CLI** — same presets from the terminal (`scripts/try_presets.py` sends the same local context headers):
|
|
287
287
|
|
|
288
288
|
```bash
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "unique-search-proxy"
|
|
3
|
-
version = "2026.44.0.
|
|
3
|
+
version = "2026.44.0.dev13"
|
|
4
4
|
description = "Web Search Proxy implementation"
|
|
5
5
|
authors = [{ name = "ThePhilAz", email = "rami.azouz@philico.com" }]
|
|
6
6
|
readme = "README.md"
|
|
@@ -22,7 +22,7 @@ dependencies = [
|
|
|
22
22
|
"google-genai>=1.73.0,<2",
|
|
23
23
|
"google-auth>=2.43.0,<3",
|
|
24
24
|
"jinja2>=3.1.6,<4",
|
|
25
|
-
"unique-toolkit[monitoring]>=2026.44.0.
|
|
25
|
+
"unique-toolkit[monitoring]>=2026.44.0.dev10,<2026.44.0rc0",
|
|
26
26
|
"unique-search-proxy-core>=2026.44.0.dev9,<2026.44.0rc0",
|
|
27
27
|
"openai>=1.105.0,<3",
|
|
28
28
|
]
|
|
@@ -23,6 +23,7 @@ from unique_search_proxy_client.web.api.v1.openapi_examples import (
|
|
|
23
23
|
)
|
|
24
24
|
from unique_search_proxy_client.web.context import get_request_context
|
|
25
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
|
|
26
27
|
from unique_search_proxy_client.web.core.crawlers.factory import get_crawler_service
|
|
27
28
|
from unique_search_proxy_client.web.core.crawlers.pinned_egress import (
|
|
28
29
|
PinnedEgressCrawler,
|
|
@@ -93,7 +94,7 @@ async def crawl(
|
|
|
93
94
|
body: CrawlRequest = Body(openapi_examples=CRAWL_OPENAPI_EXAMPLES), # type: ignore[valid-type]
|
|
94
95
|
) -> CrawlResponse:
|
|
95
96
|
crawler_id = body.crawler
|
|
96
|
-
timeout = body.timeout
|
|
97
|
+
timeout = min(body.timeout, request_limits_settings.max_timeout_seconds)
|
|
97
98
|
started = time.perf_counter()
|
|
98
99
|
_LOGGER.info(
|
|
99
100
|
"crawl start crawler=%s urls=%d timeout=%ss",
|
|
@@ -104,7 +105,7 @@ async def crawl(
|
|
|
104
105
|
|
|
105
106
|
try:
|
|
106
107
|
_enforce_url_limits(body.urls)
|
|
107
|
-
async with asyncio.timeout(timeout):
|
|
108
|
+
async with get_crawl_limiter(request.app).slot(), asyncio.timeout(timeout):
|
|
108
109
|
registry = get_http_client_registry(request.app)
|
|
109
110
|
client = await registry.client_for(get_request_context())
|
|
110
111
|
gate = await apply_url_safety_gate(
|
|
@@ -142,6 +143,7 @@ async def crawl(
|
|
|
142
143
|
crawl_body = body.model_copy(
|
|
143
144
|
update={
|
|
144
145
|
"urls": [target.display_url for target in gate.allowed_targets],
|
|
146
|
+
"timeout": timeout,
|
|
145
147
|
},
|
|
146
148
|
)
|
|
147
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,10 @@ 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.app import app_settings
|
|
32
|
+
from unique_search_proxy_client.web.settings.request_limits import (
|
|
33
|
+
request_limits_settings,
|
|
34
|
+
)
|
|
30
35
|
from unique_search_proxy_client.web.startup_report import (
|
|
31
36
|
log_startup_settings_report,
|
|
32
37
|
)
|
|
@@ -61,6 +66,9 @@ async def lifespan(app: FastAPI):
|
|
|
61
66
|
log_startup_settings_report(_LOGGER)
|
|
62
67
|
registry = await create_http_client_registry()
|
|
63
68
|
app.state.http_client_registry = registry
|
|
69
|
+
app.state.crawl_limiter = CrawlLimiter(
|
|
70
|
+
request_limits_settings.max_concurrent_crawls,
|
|
71
|
+
)
|
|
64
72
|
try:
|
|
65
73
|
await maybe_cleanup_auto_provisioned_bing_agents_on_start(registry)
|
|
66
74
|
yield
|
|
@@ -85,6 +93,9 @@ def create_app() -> FastAPI:
|
|
|
85
93
|
),
|
|
86
94
|
version="0.2.0",
|
|
87
95
|
lifespan=lifespan,
|
|
96
|
+
docs_url="/docs" if app_settings.expose_docs else None,
|
|
97
|
+
redoc_url="/redoc" if app_settings.expose_docs else None,
|
|
98
|
+
openapi_url="/openapi.json" if app_settings.expose_docs else None,
|
|
88
99
|
swagger_ui_parameters={
|
|
89
100
|
"defaultModelsExpandDepth": 1,
|
|
90
101
|
"defaultModelExpandDepth": 2,
|
|
@@ -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(
|
|
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(
|
|
@@ -129,6 +129,16 @@ def build_additional_schema(groups: tuple[HelmSettingsGroup, ...]) -> dict[str,
|
|
|
129
129
|
for group in groups
|
|
130
130
|
if group.helm_key is not None
|
|
131
131
|
}
|
|
132
|
+
properties["networkPolicy"] = {
|
|
133
|
+
"properties": {
|
|
134
|
+
"enabled": {"type": "boolean", "default": True},
|
|
135
|
+
"allowDisabled": {
|
|
136
|
+
"type": "boolean",
|
|
137
|
+
"default": False,
|
|
138
|
+
"description": "Allow networkPolicy.enabled=false. Set only if another control restricts who can reach search-proxy.",
|
|
139
|
+
},
|
|
140
|
+
}
|
|
141
|
+
}
|
|
132
142
|
return {
|
|
133
143
|
"$schema": "https://json-schema.org/draft-07/schema#",
|
|
134
144
|
"properties": properties,
|
|
@@ -7,6 +7,7 @@ from unique_search_proxy_client.web.helm.generator.introspect import (
|
|
|
7
7
|
from unique_search_proxy_client.web.helm.metadata import (
|
|
8
8
|
EgressDomainWildcard,
|
|
9
9
|
EgressEndpointField,
|
|
10
|
+
EgressProxyHost,
|
|
10
11
|
)
|
|
11
12
|
from unique_search_proxy_client.web.helm.registry import HelmSettingsGroup
|
|
12
13
|
|
|
@@ -199,25 +200,83 @@ def _emit_domain_wildcard_egress(
|
|
|
199
200
|
]
|
|
200
201
|
|
|
201
202
|
|
|
203
|
+
def _proxy_values_paths(
|
|
204
|
+
group: HelmSettingsGroup, egress: EgressProxyHost
|
|
205
|
+
) -> tuple[str, str] | None:
|
|
206
|
+
host = _find_helm_field(group, egress.host_field)
|
|
207
|
+
port = _find_helm_field(group, egress.port_field)
|
|
208
|
+
if host is None or port is None:
|
|
209
|
+
return None
|
|
210
|
+
root = f".Values.{group.helm_key}"
|
|
211
|
+
return (
|
|
212
|
+
f"{root}.{host.section}.{host.helm_name}",
|
|
213
|
+
f"{root}.{port.section}.{port.helm_name}",
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _emit_proxy_host_egress(
|
|
218
|
+
group: HelmSettingsGroup,
|
|
219
|
+
egress: EgressProxyHost,
|
|
220
|
+
) -> list[str]:
|
|
221
|
+
paths = _proxy_values_paths(group, egress)
|
|
222
|
+
if paths is None:
|
|
223
|
+
return []
|
|
224
|
+
|
|
225
|
+
host, port = paths
|
|
226
|
+
ipv4_octet = r"(25[0-5]|2[0-4][0-9]|1?[0-9]{1,2})"
|
|
227
|
+
return [
|
|
228
|
+
f"{{{{ if and {host} {port} }}}}",
|
|
229
|
+
f"{{{{- $host := {host} -}}}}",
|
|
230
|
+
f"{{{{- $port := {port} -}}}}",
|
|
231
|
+
r'{{- if not (and (kindIs "string" $host) (regexMatch "^[A-Za-z0-9]([A-Za-z0-9.-]*[A-Za-z0-9])?$" $host)) -}}',
|
|
232
|
+
'{{- fail "httpClient.connection.proxyHost must be a plain hostname or IPv4 address (no scheme, port, path or IPv6 literal)." -}}',
|
|
233
|
+
"{{- end -}}",
|
|
234
|
+
'{{- if not (regexMatch "^[0-9]+$" (toString $port)) -}}',
|
|
235
|
+
'{{- fail "httpClient.connection.proxyPort must be a number." -}}',
|
|
236
|
+
"{{- else if or (lt (int $port) 1) (gt (int $port) 65535) -}}",
|
|
237
|
+
'{{- fail "httpClient.connection.proxyPort must be between 1 and 65535." -}}',
|
|
238
|
+
"{{- end -}}",
|
|
239
|
+
f'{{{{- if regexMatch "^({ipv4_octet}\\\\.){{3}}{ipv4_octet}$" $host }}}}',
|
|
240
|
+
"- toCIDR:",
|
|
241
|
+
' - {{ printf "%s/32" $host | quote }}',
|
|
242
|
+
"{{- else }}",
|
|
243
|
+
'{{- if regexMatch "^[0-9.]+$" $host -}}',
|
|
244
|
+
'{{- fail "httpClient.connection.proxyHost is not a valid IPv4 address." -}}',
|
|
245
|
+
"{{- end }}",
|
|
246
|
+
"- toFQDNs:",
|
|
247
|
+
" - matchName: {{ $host | quote }}",
|
|
248
|
+
"{{- end }}",
|
|
249
|
+
" toPorts:",
|
|
250
|
+
" - ports:",
|
|
251
|
+
" - port: {{ $port | quote }}",
|
|
252
|
+
" protocol: TCP",
|
|
253
|
+
"{{- end }}",
|
|
254
|
+
]
|
|
255
|
+
|
|
256
|
+
|
|
202
257
|
def _emit_egress_block(group: HelmSettingsGroup) -> list[str]:
|
|
203
258
|
egress = group.egress
|
|
204
259
|
if isinstance(egress, EgressEndpointField):
|
|
205
260
|
return _emit_endpoint_field_egress(group, egress)
|
|
206
261
|
if isinstance(egress, EgressDomainWildcard):
|
|
207
262
|
return _emit_domain_wildcard_egress(group, egress)
|
|
263
|
+
if isinstance(egress, EgressProxyHost):
|
|
264
|
+
return _emit_proxy_host_egress(group, egress)
|
|
208
265
|
return []
|
|
209
266
|
|
|
210
267
|
|
|
268
|
+
def _egress_active_check(group: HelmSettingsGroup) -> str:
|
|
269
|
+
if isinstance(group.egress, EgressProxyHost):
|
|
270
|
+
paths = _proxy_values_paths(group, group.egress)
|
|
271
|
+
if paths is not None:
|
|
272
|
+
return f"(and {paths[0]} {paths[1]})"
|
|
273
|
+
return f"(and .Values.{group.helm_key} .Values.{group.helm_key}.enabled)"
|
|
274
|
+
|
|
275
|
+
|
|
211
276
|
def _emit_has_rules_block(groups: tuple[HelmSettingsGroup, ...]) -> list[str]:
|
|
212
277
|
checks = [
|
|
213
|
-
|
|
214
|
-
for group in groups
|
|
215
|
-
if group.egress is not None
|
|
278
|
+
_egress_active_check(group) for group in groups if group.egress is not None
|
|
216
279
|
]
|
|
217
|
-
if not checks:
|
|
218
|
-
return ["{{/* no auto egress rules */}}"]
|
|
219
|
-
if len(checks) == 1:
|
|
220
|
-
return [f"{{{{- if {checks[0]} -}}}}true{{{{- end -}}}}"]
|
|
221
280
|
joined = " ".join(checks)
|
|
222
281
|
return [f"{{{{- if or {joined} -}}}}true{{{{- end -}}}}"]
|
|
223
282
|
|
|
@@ -43,7 +43,15 @@ class EgressDomainWildcard:
|
|
|
43
43
|
port: str = "443"
|
|
44
44
|
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
@dataclass(frozen=True)
|
|
47
|
+
class EgressProxyHost:
|
|
48
|
+
"""Proxy egress when host and port are set: /32 ``toCIDR`` for IPv4, else ``toFQDNs``."""
|
|
49
|
+
|
|
50
|
+
host_field: str = "proxy_host"
|
|
51
|
+
port_field: str = "proxy_port"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
EgressRule = EgressEndpointField | EgressDomainWildcard | EgressProxyHost
|
|
47
55
|
"""How a settings group's outbound egress hosts are derived from its settings."""
|
|
48
56
|
|
|
49
57
|
_DEFAULT_EGRESS: EgressRule = EgressEndpointField()
|
|
@@ -7,7 +7,7 @@ from unique_search_proxy_core.http_client import (
|
|
|
7
7
|
ProxyUsernameSource,
|
|
8
8
|
)
|
|
9
9
|
|
|
10
|
-
from unique_search_proxy_client.web.helm.metadata import helm_settings
|
|
10
|
+
from unique_search_proxy_client.web.helm.metadata import EgressProxyHost, helm_settings
|
|
11
11
|
from unique_search_proxy_client.web.settings.base import get_settings
|
|
12
12
|
|
|
13
13
|
HTTP_CLIENT_ENV_PREFIX = "HTTP_CLIENT_"
|
|
@@ -21,7 +21,7 @@ HTTP_CLIENT_ENV_PREFIX = "HTTP_CLIENT_"
|
|
|
21
21
|
# the proxy fields, not by a chart-only toggle. Gating it would silently drop
|
|
22
22
|
# proxy config from overlays that set it without ``enabled: true``.
|
|
23
23
|
gated=False,
|
|
24
|
-
egress=
|
|
24
|
+
egress=EgressProxyHost(),
|
|
25
25
|
env_prefix=HTTP_CLIENT_ENV_PREFIX,
|
|
26
26
|
sections={
|
|
27
27
|
"tuning": [
|
|
@@ -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": [
|
|
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):
|
|
@@ -59,6 +66,30 @@ class RequestLimitsSettings(BaseSettings):
|
|
|
59
66
|
"longer URL are rejected with a 422."
|
|
60
67
|
),
|
|
61
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
|
+
)
|
|
62
93
|
|
|
63
94
|
@property
|
|
64
95
|
def max_crawl_response_bytes(self) -> int:
|
|
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
|