unique-search-proxy-core 2026.44.0.dev7__tar.gz → 2026.44.0.dev8__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 (68) hide show
  1. {unique_search_proxy_core-2026.44.0.dev7 → unique_search_proxy_core-2026.44.0.dev8}/PKG-INFO +1 -1
  2. {unique_search_proxy_core-2026.44.0.dev7 → unique_search_proxy_core-2026.44.0.dev8}/pyproject.toml +1 -1
  3. {unique_search_proxy_core-2026.44.0.dev7 → unique_search_proxy_core-2026.44.0.dev8}/unique_search_proxy_core/url_safety/__init__.py +2 -0
  4. unique_search_proxy_core-2026.44.0.dev8/unique_search_proxy_core/url_safety/egress.py +150 -0
  5. unique_search_proxy_core-2026.44.0.dev7/unique_search_proxy_core/url_safety/egress.py +0 -96
  6. {unique_search_proxy_core-2026.44.0.dev7 → unique_search_proxy_core-2026.44.0.dev8}/README.md +0 -0
  7. {unique_search_proxy_core-2026.44.0.dev7 → unique_search_proxy_core-2026.44.0.dev8}/unique_search_proxy_core/__init__.py +0 -0
  8. {unique_search_proxy_core-2026.44.0.dev7 → unique_search_proxy_core-2026.44.0.dev8}/unique_search_proxy_core/agent_engines/__init__.py +0 -0
  9. {unique_search_proxy_core-2026.44.0.dev7 → unique_search_proxy_core-2026.44.0.dev8}/unique_search_proxy_core/agent_engines/base.py +0 -0
  10. {unique_search_proxy_core-2026.44.0.dev7 → unique_search_proxy_core-2026.44.0.dev8}/unique_search_proxy_core/agent_engines/bing/__init__.py +0 -0
  11. {unique_search_proxy_core-2026.44.0.dev7 → unique_search_proxy_core-2026.44.0.dev8}/unique_search_proxy_core/agent_engines/bing/enums.py +0 -0
  12. {unique_search_proxy_core-2026.44.0.dev7 → unique_search_proxy_core-2026.44.0.dev8}/unique_search_proxy_core/agent_engines/bing/grounding.py +0 -0
  13. {unique_search_proxy_core-2026.44.0.dev7 → unique_search_proxy_core-2026.44.0.dev8}/unique_search_proxy_core/agent_engines/bing/schema.py +0 -0
  14. {unique_search_proxy_core-2026.44.0.dev7 → unique_search_proxy_core-2026.44.0.dev8}/unique_search_proxy_core/agent_engines/bing/settings.py +0 -0
  15. {unique_search_proxy_core-2026.44.0.dev7 → unique_search_proxy_core-2026.44.0.dev8}/unique_search_proxy_core/agent_engines/config_types.py +0 -0
  16. {unique_search_proxy_core-2026.44.0.dev7 → unique_search_proxy_core-2026.44.0.dev8}/unique_search_proxy_core/agent_engines/output_schema.py +0 -0
  17. {unique_search_proxy_core-2026.44.0.dev7 → unique_search_proxy_core-2026.44.0.dev8}/unique_search_proxy_core/agent_engines/resolve.py +0 -0
  18. {unique_search_proxy_core-2026.44.0.dev7 → unique_search_proxy_core-2026.44.0.dev8}/unique_search_proxy_core/agent_engines/response_parsing.py +0 -0
  19. {unique_search_proxy_core-2026.44.0.dev7 → unique_search_proxy_core-2026.44.0.dev8}/unique_search_proxy_core/agent_engines/vertexai/__init__.py +0 -0
  20. {unique_search_proxy_core-2026.44.0.dev7 → unique_search_proxy_core-2026.44.0.dev8}/unique_search_proxy_core/agent_engines/vertexai/schema.py +0 -0
  21. {unique_search_proxy_core-2026.44.0.dev7 → unique_search_proxy_core-2026.44.0.dev8}/unique_search_proxy_core/agent_engines/vertexai/settings.py +0 -0
  22. {unique_search_proxy_core-2026.44.0.dev7 → unique_search_proxy_core-2026.44.0.dev8}/unique_search_proxy_core/context.py +0 -0
  23. {unique_search_proxy_core-2026.44.0.dev7 → unique_search_proxy_core-2026.44.0.dev8}/unique_search_proxy_core/crawlers/__init__.py +0 -0
  24. {unique_search_proxy_core-2026.44.0.dev7 → unique_search_proxy_core-2026.44.0.dev8}/unique_search_proxy_core/crawlers/base.py +0 -0
  25. {unique_search_proxy_core-2026.44.0.dev7 → unique_search_proxy_core-2026.44.0.dev8}/unique_search_proxy_core/crawlers/basic/__init__.py +0 -0
  26. {unique_search_proxy_core-2026.44.0.dev7 → unique_search_proxy_core-2026.44.0.dev8}/unique_search_proxy_core/crawlers/basic/content_types.py +0 -0
  27. {unique_search_proxy_core-2026.44.0.dev7 → unique_search_proxy_core-2026.44.0.dev8}/unique_search_proxy_core/crawlers/basic/processing/policy.py +0 -0
  28. {unique_search_proxy_core-2026.44.0.dev7 → unique_search_proxy_core-2026.44.0.dev8}/unique_search_proxy_core/crawlers/basic/schema.py +0 -0
  29. {unique_search_proxy_core-2026.44.0.dev7 → unique_search_proxy_core-2026.44.0.dev8}/unique_search_proxy_core/crawlers/config_types.py +0 -0
  30. {unique_search_proxy_core-2026.44.0.dev7 → unique_search_proxy_core-2026.44.0.dev8}/unique_search_proxy_core/crawlers/firecrawl/schema.py +0 -0
  31. {unique_search_proxy_core-2026.44.0.dev7 → unique_search_proxy_core-2026.44.0.dev8}/unique_search_proxy_core/crawlers/jina/schema.py +0 -0
  32. {unique_search_proxy_core-2026.44.0.dev7 → unique_search_proxy_core-2026.44.0.dev8}/unique_search_proxy_core/crawlers/tavily/schema.py +0 -0
  33. {unique_search_proxy_core-2026.44.0.dev7 → unique_search_proxy_core-2026.44.0.dev8}/unique_search_proxy_core/errors.py +0 -0
  34. {unique_search_proxy_core-2026.44.0.dev7 → unique_search_proxy_core-2026.44.0.dev8}/unique_search_proxy_core/http_client/__init__.py +0 -0
  35. {unique_search_proxy_core-2026.44.0.dev7 → unique_search_proxy_core-2026.44.0.dev8}/unique_search_proxy_core/http_client/client.py +0 -0
  36. {unique_search_proxy_core-2026.44.0.dev7 → unique_search_proxy_core-2026.44.0.dev8}/unique_search_proxy_core/http_client/credentials.py +0 -0
  37. {unique_search_proxy_core-2026.44.0.dev7 → unique_search_proxy_core-2026.44.0.dev8}/unique_search_proxy_core/http_client/response_cap.py +0 -0
  38. {unique_search_proxy_core-2026.44.0.dev7 → unique_search_proxy_core-2026.44.0.dev8}/unique_search_proxy_core/http_client/secrets.py +0 -0
  39. {unique_search_proxy_core-2026.44.0.dev7 → unique_search_proxy_core-2026.44.0.dev8}/unique_search_proxy_core/http_client/settings.py +0 -0
  40. {unique_search_proxy_core-2026.44.0.dev7 → unique_search_proxy_core-2026.44.0.dev8}/unique_search_proxy_core/logging.py +0 -0
  41. {unique_search_proxy_core-2026.44.0.dev7 → unique_search_proxy_core-2026.44.0.dev8}/unique_search_proxy_core/param_policy/__init__.py +0 -0
  42. {unique_search_proxy_core-2026.44.0.dev7 → unique_search_proxy_core-2026.44.0.dev8}/unique_search_proxy_core/param_policy/annotations.py +0 -0
  43. {unique_search_proxy_core-2026.44.0.dev7 → unique_search_proxy_core-2026.44.0.dev8}/unique_search_proxy_core/param_policy/derive.py +0 -0
  44. {unique_search_proxy_core-2026.44.0.dev7 → unique_search_proxy_core-2026.44.0.dev8}/unique_search_proxy_core/param_policy/exposable_config.py +0 -0
  45. {unique_search_proxy_core-2026.44.0.dev7 → unique_search_proxy_core-2026.44.0.dev8}/unique_search_proxy_core/param_policy/exposable_param.py +0 -0
  46. {unique_search_proxy_core-2026.44.0.dev7 → unique_search_proxy_core-2026.44.0.dev8}/unique_search_proxy_core/param_policy/exposed_params.py +0 -0
  47. {unique_search_proxy_core-2026.44.0.dev7 → unique_search_proxy_core-2026.44.0.dev8}/unique_search_proxy_core/param_policy/request_base.py +0 -0
  48. {unique_search_proxy_core-2026.44.0.dev7 → unique_search_proxy_core-2026.44.0.dev8}/unique_search_proxy_core/providers/schema.py +0 -0
  49. {unique_search_proxy_core-2026.44.0.dev7 → unique_search_proxy_core-2026.44.0.dev8}/unique_search_proxy_core/schema.py +0 -0
  50. {unique_search_proxy_core-2026.44.0.dev7 → unique_search_proxy_core-2026.44.0.dev8}/unique_search_proxy_core/search_engines/__init__.py +0 -0
  51. {unique_search_proxy_core-2026.44.0.dev7 → unique_search_proxy_core-2026.44.0.dev8}/unique_search_proxy_core/search_engines/base.py +0 -0
  52. {unique_search_proxy_core-2026.44.0.dev7 → unique_search_proxy_core-2026.44.0.dev8}/unique_search_proxy_core/search_engines/brave/__init__.py +0 -0
  53. {unique_search_proxy_core-2026.44.0.dev7 → unique_search_proxy_core-2026.44.0.dev8}/unique_search_proxy_core/search_engines/brave/schema.py +0 -0
  54. {unique_search_proxy_core-2026.44.0.dev7 → unique_search_proxy_core-2026.44.0.dev8}/unique_search_proxy_core/search_engines/config_types.py +0 -0
  55. {unique_search_proxy_core-2026.44.0.dev7 → unique_search_proxy_core-2026.44.0.dev8}/unique_search_proxy_core/search_engines/custom_api/__init__.py +0 -0
  56. {unique_search_proxy_core-2026.44.0.dev7 → unique_search_proxy_core-2026.44.0.dev8}/unique_search_proxy_core/search_engines/custom_api/schema.py +0 -0
  57. {unique_search_proxy_core-2026.44.0.dev7 → unique_search_proxy_core-2026.44.0.dev8}/unique_search_proxy_core/search_engines/google/__init__.py +0 -0
  58. {unique_search_proxy_core-2026.44.0.dev7 → unique_search_proxy_core-2026.44.0.dev8}/unique_search_proxy_core/search_engines/google/schema.py +0 -0
  59. {unique_search_proxy_core-2026.44.0.dev7 → unique_search_proxy_core-2026.44.0.dev8}/unique_search_proxy_core/search_engines/perplexity/__init__.py +0 -0
  60. {unique_search_proxy_core-2026.44.0.dev7 → unique_search_proxy_core-2026.44.0.dev8}/unique_search_proxy_core/search_engines/perplexity/schema.py +0 -0
  61. {unique_search_proxy_core-2026.44.0.dev7 → unique_search_proxy_core-2026.44.0.dev8}/unique_search_proxy_core/url_safety/dns.py +0 -0
  62. {unique_search_proxy_core-2026.44.0.dev7 → unique_search_proxy_core-2026.44.0.dev8}/unique_search_proxy_core/url_safety/models.py +0 -0
  63. {unique_search_proxy_core-2026.44.0.dev7 → unique_search_proxy_core-2026.44.0.dev8}/unique_search_proxy_core/url_safety/netloc.py +0 -0
  64. {unique_search_proxy_core-2026.44.0.dev7 → unique_search_proxy_core-2026.44.0.dev8}/unique_search_proxy_core/url_safety/policy.py +0 -0
  65. {unique_search_proxy_core-2026.44.0.dev7 → unique_search_proxy_core-2026.44.0.dev8}/unique_search_proxy_core/url_safety/redirect.py +0 -0
  66. {unique_search_proxy_core-2026.44.0.dev7 → unique_search_proxy_core-2026.44.0.dev8}/unique_search_proxy_core/url_safety/resolver.py +0 -0
  67. {unique_search_proxy_core-2026.44.0.dev7 → unique_search_proxy_core-2026.44.0.dev8}/unique_search_proxy_core/url_safety/service.py +0 -0
  68. {unique_search_proxy_core-2026.44.0.dev7 → unique_search_proxy_core-2026.44.0.dev8}/unique_search_proxy_core/url_safety/settings.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: unique-search-proxy-core
3
- Version: 2026.44.0.dev7
3
+ Version: 2026.44.0.dev8
4
4
  Summary: Shared Pydantic types for the Unique Search Proxy API
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-core"
3
- version = "2026.44.0.dev7"
3
+ version = "2026.44.0.dev8"
4
4
  description = "Shared Pydantic types for the Unique Search Proxy API"
5
5
  authors = [{ name = "ThePhilAz", email = "rami.azouz@philico.com" }]
6
6
  readme = "README.md"
@@ -1,6 +1,7 @@
1
1
  from unique_search_proxy_core.url_safety.egress import (
2
2
  pinned_httpx_get_args,
3
3
  safe_pinned_httpx_get,
4
+ safe_pinned_httpx_stream,
4
5
  )
5
6
  from unique_search_proxy_core.url_safety.models import (
6
7
  BlockedCrawlTarget,
@@ -29,5 +30,6 @@ __all__ = [
29
30
  "bypass_crawl_target",
30
31
  "pinned_httpx_get_args",
31
32
  "safe_pinned_httpx_get",
33
+ "safe_pinned_httpx_stream",
32
34
  "url_safety_settings",
33
35
  ]
@@ -0,0 +1,150 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import AsyncIterator, Mapping
4
+ from contextlib import asynccontextmanager
5
+ from urllib.parse import urlsplit
6
+
7
+ import httpx
8
+
9
+ from unique_search_proxy_core.url_safety.models import (
10
+ BlockedCrawlTarget,
11
+ CrawlTargetValidationError,
12
+ ResolvedCrawlTarget,
13
+ bypass_crawl_target,
14
+ )
15
+ from unique_search_proxy_core.url_safety.redirect import redirect_target_url
16
+ from unique_search_proxy_core.url_safety.resolver import resolve_crawl_target
17
+
18
+
19
+ def pinned_httpx_get_args(
20
+ target: ResolvedCrawlTarget,
21
+ ) -> tuple[str, dict[str, str], dict[str, str]]:
22
+ """Build httpx GET url, headers, and extensions for DNS-pinned egress."""
23
+ headers: dict[str, str] = {}
24
+ if target.host_header is not None:
25
+ headers["Host"] = target.host_header
26
+
27
+ extensions: dict[str, str] = {}
28
+ if target.sni_hostname is not None:
29
+ extensions["sni_hostname"] = target.sni_hostname
30
+
31
+ return target.request_url, headers, extensions
32
+
33
+
34
+ def _blocked_redirect(target: ResolvedCrawlTarget, reason: str) -> BlockedCrawlTarget:
35
+ return BlockedCrawlTarget(
36
+ hostname=target.hostname or None,
37
+ category="redirect",
38
+ reason=reason,
39
+ )
40
+
41
+
42
+ def _next_redirect_url(
43
+ response: httpx.Response,
44
+ current_target: ResolvedCrawlTarget,
45
+ ) -> str | None:
46
+ try:
47
+ next_url = redirect_target_url(response, current_target.normalized_url)
48
+ if next_url is not None:
49
+ parsed_next_url = urlsplit(next_url)
50
+ _ = parsed_next_url.hostname
51
+ _ = parsed_next_url.port
52
+ except ValueError as exc:
53
+ raise CrawlTargetValidationError(
54
+ [
55
+ _blocked_redirect(
56
+ current_target,
57
+ "Redirect target URL is missing or malformed",
58
+ )
59
+ ]
60
+ ) from exc
61
+ return next_url
62
+
63
+
64
+ @asynccontextmanager
65
+ async def safe_pinned_httpx_stream(
66
+ client: httpx.AsyncClient,
67
+ target: ResolvedCrawlTarget,
68
+ *,
69
+ headers: Mapping[str, str] | None = None,
70
+ timeout: float | httpx.Timeout,
71
+ max_redirect_hops: int,
72
+ enforce_url_safety: bool,
73
+ ) -> AsyncIterator[httpx.Response]:
74
+ """Open a streamed GET, applying the original target's safety mode to redirects.
75
+
76
+ Redirect hops are closed without reading their bodies. The final response
77
+ is yielded unread so the caller decides how much of the body to consume;
78
+ it is closed when the context exits.
79
+
80
+ Raises:
81
+ CrawlTargetValidationError: When a redirect is malformed, blocked, or
82
+ exceeds ``max_redirect_hops``.
83
+ """
84
+ if max_redirect_hops < 0:
85
+ raise ValueError("max_redirect_hops must be non-negative")
86
+
87
+ current_target = target
88
+ for redirects_followed in range(max_redirect_hops + 1):
89
+ request_url, pin_headers, extensions = pinned_httpx_get_args(current_target)
90
+ request = client.build_request(
91
+ "GET",
92
+ request_url,
93
+ headers={**(headers or {}), **pin_headers},
94
+ extensions=extensions or None,
95
+ timeout=timeout,
96
+ )
97
+ response = await client.send(request, stream=True, follow_redirects=False)
98
+ try:
99
+ next_url = _next_redirect_url(response, current_target)
100
+ if next_url is None:
101
+ yield response
102
+ return
103
+ if redirects_followed == max_redirect_hops:
104
+ raise CrawlTargetValidationError(
105
+ [
106
+ _blocked_redirect(
107
+ current_target,
108
+ "Maximum redirect hop count exceeded",
109
+ )
110
+ ]
111
+ )
112
+ finally:
113
+ await response.aclose()
114
+
115
+ current_target = (
116
+ await resolve_crawl_target(next_url)
117
+ if enforce_url_safety
118
+ else bypass_crawl_target(next_url)
119
+ )
120
+
121
+ raise AssertionError("redirect loop must return or raise")
122
+
123
+
124
+ async def safe_pinned_httpx_get(
125
+ client: httpx.AsyncClient,
126
+ target: ResolvedCrawlTarget,
127
+ *,
128
+ headers: Mapping[str, str] | None = None,
129
+ timeout: float | httpx.Timeout,
130
+ max_redirect_hops: int,
131
+ enforce_url_safety: bool,
132
+ ) -> httpx.Response:
133
+ """GET a target, applying the original target's safety mode to redirects."""
134
+ async with safe_pinned_httpx_stream(
135
+ client,
136
+ target,
137
+ headers=headers,
138
+ timeout=timeout,
139
+ max_redirect_hops=max_redirect_hops,
140
+ enforce_url_safety=enforce_url_safety,
141
+ ) as response:
142
+ await response.aread()
143
+ return response
144
+
145
+
146
+ __all__ = [
147
+ "pinned_httpx_get_args",
148
+ "safe_pinned_httpx_get",
149
+ "safe_pinned_httpx_stream",
150
+ ]
@@ -1,96 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from collections.abc import Mapping
4
- from urllib.parse import urlsplit
5
-
6
- import httpx
7
-
8
- from unique_search_proxy_core.url_safety.models import (
9
- BlockedCrawlTarget,
10
- CrawlTargetValidationError,
11
- ResolvedCrawlTarget,
12
- bypass_crawl_target,
13
- )
14
- from unique_search_proxy_core.url_safety.redirect import redirect_target_url
15
- from unique_search_proxy_core.url_safety.resolver import resolve_crawl_target
16
-
17
-
18
- def pinned_httpx_get_args(
19
- target: ResolvedCrawlTarget,
20
- ) -> tuple[str, dict[str, str], dict[str, str]]:
21
- """Build httpx GET url, headers, and extensions for DNS-pinned egress."""
22
- headers: dict[str, str] = {}
23
- if target.host_header is not None:
24
- headers["Host"] = target.host_header
25
-
26
- extensions: dict[str, str] = {}
27
- if target.sni_hostname is not None:
28
- extensions["sni_hostname"] = target.sni_hostname
29
-
30
- return target.request_url, headers, extensions
31
-
32
-
33
- async def safe_pinned_httpx_get(
34
- client: httpx.AsyncClient,
35
- target: ResolvedCrawlTarget,
36
- *,
37
- headers: Mapping[str, str] | None = None,
38
- timeout: float | httpx.Timeout,
39
- max_redirect_hops: int,
40
- enforce_url_safety: bool,
41
- ) -> httpx.Response:
42
- """GET a target, applying the original target's safety mode to redirects."""
43
- if max_redirect_hops < 0:
44
- raise ValueError("max_redirect_hops must be non-negative")
45
-
46
- current_target = target
47
- for redirects_followed in range(max_redirect_hops + 1):
48
- request_url, pin_headers, extensions = pinned_httpx_get_args(current_target)
49
- response = await client.get(
50
- request_url,
51
- headers={**(headers or {}), **pin_headers},
52
- extensions=extensions or None,
53
- timeout=timeout,
54
- follow_redirects=False,
55
- )
56
-
57
- try:
58
- next_url = redirect_target_url(response, current_target.normalized_url)
59
- if next_url is not None:
60
- parsed_next_url = urlsplit(next_url)
61
- _ = parsed_next_url.hostname
62
- _ = parsed_next_url.port
63
- except ValueError as exc:
64
- raise CrawlTargetValidationError(
65
- [
66
- BlockedCrawlTarget(
67
- hostname=current_target.hostname or None,
68
- category="redirect",
69
- reason="Redirect target URL is missing or malformed",
70
- )
71
- ]
72
- ) from exc
73
- if next_url is None:
74
- return response
75
-
76
- if redirects_followed == max_redirect_hops:
77
- raise CrawlTargetValidationError(
78
- [
79
- BlockedCrawlTarget(
80
- hostname=current_target.hostname or None,
81
- category="redirect",
82
- reason="Maximum redirect hop count exceeded",
83
- )
84
- ]
85
- )
86
-
87
- current_target = (
88
- await resolve_crawl_target(next_url)
89
- if enforce_url_safety
90
- else bypass_crawl_target(next_url)
91
- )
92
-
93
- raise AssertionError("redirect loop must return or raise")
94
-
95
-
96
- __all__ = ["pinned_httpx_get_args", "safe_pinned_httpx_get"]