Skip to content

Commit ec14974

Browse files
No snow proxy precence fix (snowflakedb#2615)
1 parent 0ec0d7b commit ec14974

File tree

6 files changed

+261
-52
lines changed

6 files changed

+261
-52
lines changed

src/snowflake/connector/connection.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1082,22 +1082,23 @@ def connect(self, **kwargs) -> None:
10821082

10831083
self._crl_config: CRLConfig = CRLConfig.from_connection(self)
10841084

1085+
no_proxy_csv_str = (
1086+
",".join(str(x) for x in self.no_proxy)
1087+
if (
1088+
self.no_proxy is not None
1089+
and isinstance(self.no_proxy, Iterable)
1090+
and not isinstance(self.no_proxy, (str, bytes))
1091+
)
1092+
else self.no_proxy
1093+
)
10851094
self._http_config = HttpConfig(
10861095
adapter_factory=ProxySupportAdapterFactory(),
10871096
use_pooling=(not self.disable_request_pooling),
10881097
proxy_host=self.proxy_host,
10891098
proxy_port=self.proxy_port,
10901099
proxy_user=self.proxy_user,
10911100
proxy_password=self.proxy_password,
1092-
no_proxy=(
1093-
",".join(str(x) for x in self.no_proxy)
1094-
if (
1095-
self.no_proxy is not None
1096-
and isinstance(self.no_proxy, Iterable)
1097-
and not isinstance(self.no_proxy, (str, bytes))
1098-
)
1099-
else self.no_proxy
1100-
),
1101+
no_proxy=no_proxy_csv_str,
11011102
)
11021103
self._session_manager = SessionManagerFactory.get_manager(self._http_config)
11031104

src/snowflake/connector/session_manager.py

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ def close(self) -> None:
228228
self._idle_sessions.clear()
229229

230230

231-
class _ConfigDirectAccessMixin(abc.ABC):
231+
class _BaseConfigDirectAccessMixin(abc.ABC):
232232
@property
233233
@abc.abstractmethod
234234
def config(self) -> HttpConfig: ...
@@ -245,14 +245,6 @@ def use_pooling(self) -> bool:
245245
def use_pooling(self, value: bool) -> None:
246246
self.config = self.config.copy_with(use_pooling=value)
247247

248-
@property
249-
def adapter_factory(self) -> Callable[..., HTTPAdapter]:
250-
return self.config.adapter_factory
251-
252-
@adapter_factory.setter
253-
def adapter_factory(self, value: Callable[..., HTTPAdapter]) -> None:
254-
self.config = self.config.copy_with(adapter_factory=value)
255-
256248
@property
257249
def max_retries(self) -> Retry | int:
258250
return self.config.max_retries
@@ -262,6 +254,16 @@ def max_retries(self, value: Retry | int) -> None:
262254
self.config = self.config.copy_with(max_retries=value)
263255

264256

257+
class _HttpConfigDirectAccessMixin(_BaseConfigDirectAccessMixin):
258+
@property
259+
def adapter_factory(self) -> Callable[..., HTTPAdapter]:
260+
return self.config.adapter_factory
261+
262+
@adapter_factory.setter
263+
def adapter_factory(self, value: Callable[..., HTTPAdapter]) -> None:
264+
self.config = self.config.copy_with(adapter_factory=value)
265+
266+
265267
class _RequestVerbsUsingSessionMixin(abc.ABC):
266268
"""
267269
Mixin that provides HTTP methods (get, post, put, etc.) mirroring requests.Session, maintaining their default argument behavior (e.g., HEAD uses allow_redirects=False).
@@ -372,7 +374,7 @@ def delete(
372374
return session.delete(url, headers=headers, timeout=timeout, **kwargs)
373375

374376

375-
class SessionManager(_RequestVerbsUsingSessionMixin, _ConfigDirectAccessMixin):
377+
class SessionManager(_RequestVerbsUsingSessionMixin, _HttpConfigDirectAccessMixin):
376378
"""
377379
Central HTTP session manager that handles all external requests from the Snowflake driver.
378380
@@ -562,7 +564,7 @@ def clone(
562564
Optional kwargs (e.g. *use_pooling* / *adapter_factory* / max_retries etc.) - overrides to create a modified
563565
copy of the HttpConfig before instantiation.
564566
"""
565-
return SessionManager.from_config(self._cfg, **http_config_overrides)
567+
return self.from_config(self._cfg, **http_config_overrides)
566568

567569
def __getstate__(self):
568570
state = self.__dict__.copy()
@@ -626,12 +628,6 @@ def make_session(self, *, url: str | None = None) -> Session:
626628
session.proxies = proxies
627629
return session
628630

629-
def clone(
630-
self,
631-
**http_config_overrides,
632-
) -> SessionManager:
633-
return ProxySessionManager.from_config(self._cfg, **http_config_overrides)
634-
635631

636632
class SessionManagerFactory:
637633
@staticmethod

test/data/wiremock/mappings/generic/telemetry.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@
1010
"data": {
1111
"code": null,
1212
"data": "Log Received",
13-
"message": null,
14-
"success": true
15-
}
13+
"message": null
14+
},
15+
"success": true
1616
}
1717
}
1818
}

test/test_utils/cross_module_fixtures/wiremock_fixtures.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
WiremockClient,
1313
get_clients_for_proxy_and_target,
1414
get_clients_for_proxy_target_and_storage,
15+
get_clients_for_two_proxies_and_target,
1516
)
1617

1718

@@ -102,3 +103,23 @@ def wiremock_backend_storage_proxy(wiremock_generic_mappings_dir):
102103
proxy_mapping_template=wiremock_proxy_mapping_path
103104
) as triple:
104105
yield triple
106+
107+
108+
@pytest.fixture
109+
def wiremock_two_proxies_backend(wiremock_generic_mappings_dir):
110+
"""Starts backend (DB) and two proxy Wiremocks.
111+
112+
Returns a tuple ``(backend_wm, proxy1_wm, proxy2_wm)`` to make roles explicit.
113+
- proxy1_wm: Configured to forward to backend
114+
- proxy2_wm: Configured to forward to backend
115+
116+
Use when you need to test proxy selection logic with simple setup,
117+
such as connection parameters taking precedence over environment variables.
118+
"""
119+
wiremock_proxy_mapping_path = (
120+
wiremock_generic_mappings_dir / "proxy_forward_all.json"
121+
)
122+
with get_clients_for_two_proxies_and_target(
123+
proxy_mapping_template=wiremock_proxy_mapping_path
124+
) as triple:
125+
yield triple

test/test_utils/wiremock/wiremock_utils.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,3 +388,55 @@ def get_clients_for_proxy_target_and_storage(
388388
forbidden = [target_wm.wiremock_http_port, proxy_wm.wiremock_http_port]
389389
with WiremockClient(forbidden_ports=forbidden) as storage_wm:
390390
yield target_wm, storage_wm, proxy_wm
391+
392+
393+
@contextmanager
394+
def get_clients_for_two_proxies_and_target(
395+
proxy_mapping_template: Union[str, dict, pathlib.Path, None] = None,
396+
additional_proxy_placeholders: Optional[dict[str, object]] = None,
397+
additional_proxy_args: Optional[Iterable[str]] = None,
398+
):
399+
"""Context manager that starts three Wiremock instances – one *target* (DB) and two *proxies*.
400+
401+
Both proxies are configured to forward all traffic to *target* using the same
402+
mapping mechanism. This allows the test to verify which proxy was actually used
403+
by checking the request history.
404+
405+
Yields a tuple ``(target_wm, proxy1_wm, proxy2_wm)`` where:
406+
- target_wm: The backend/DB Wiremock
407+
- proxy1_wm: First proxy configured to forward to target
408+
- proxy2_wm: Second proxy configured to forward to target
409+
410+
All processes are shut down automatically on context exit.
411+
412+
Note:
413+
Use this helper for tests that need to verify proxy selection logic,
414+
such as connection parameters taking precedence over environment variables.
415+
"""
416+
# Reuse existing helper to set up target+proxy1
417+
with get_clients_for_proxy_and_target(
418+
proxy_mapping_template=proxy_mapping_template,
419+
additional_proxy_placeholders=additional_proxy_placeholders,
420+
additional_proxy_args=additional_proxy_args,
421+
) as (target_wm, proxy1_wm):
422+
# Start second proxy and configure it to forward to target as well
423+
forbidden = [target_wm.wiremock_http_port, proxy1_wm.wiremock_http_port]
424+
with WiremockClient(forbidden_ports=forbidden) as proxy2_wm:
425+
# Configure proxy2 to forward to target with the same mapping
426+
if proxy_mapping_template is None:
427+
proxy_mapping_template = (
428+
pathlib.Path(__file__).parent.parent.parent.parent
429+
/ "test"
430+
/ "data"
431+
/ "wiremock"
432+
/ "mappings"
433+
/ "generic"
434+
/ "proxy_forward_all.json"
435+
)
436+
placeholders: dict[str, object] = {
437+
"{{TARGET_HTTP_HOST_WITH_PORT}}": target_wm.http_host_with_port
438+
}
439+
if additional_proxy_placeholders:
440+
placeholders.update(additional_proxy_placeholders)
441+
proxy2_wm.add_mapping(proxy_mapping_template, placeholders=placeholders)
442+
yield target_wm, proxy1_wm, proxy2_wm

0 commit comments

Comments
 (0)