diff --git a/pyproject.toml b/pyproject.toml index 3c1d515..d8c9b24 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ requires-python = "==3.12.*" dependencies = [ "fastmcp~=3.2", "pydantic~=2.12.5", - "urllib3>=2.1.0,<3.0.0", + "urllib3[socks]>=2.1.0,<3.0.0", "python-dateutil>=2.8.2", "typing-extensions>=4.7.1", ] @@ -37,3 +37,11 @@ build-backend = "hatchling.build" [project.scripts] kagimcp = "kagimcp:main" + +[tool.pytest.ini_options] +pythonpath = ["src"] + +[dependency-groups] +dev = [ + "pytest>=8.0", +] diff --git a/src/openapi_client/rest.py b/src/openapi_client/rest.py index bc1a6f2..76b44b9 100644 --- a/src/openapi_client/rest.py +++ b/src/openapi_client/rest.py @@ -17,6 +17,8 @@ import json import re import ssl +import urllib.request +from urllib.parse import urlparse import urllib3 @@ -105,14 +107,24 @@ def __init__(self, configuration) -> None: # https pool manager self.pool_manager: urllib3.PoolManager - if configuration.proxy: - if is_socks_proxy_url(configuration.proxy): + proxies = urllib.request.getproxies() + parsed_host = urlparse(configuration.host) + proxy_url = None + if parsed_host.hostname and not urllib.request.proxy_bypass(parsed_host.hostname): + proxy_url = ( + proxies.get(parsed_host.scheme) + or proxies.get('all') + or proxies.get('http') + ) + + if proxy_url: + if is_socks_proxy_url(proxy_url): from urllib3.contrib.socks import SOCKSProxyManager - pool_args["proxy_url"] = configuration.proxy + pool_args["proxy_url"] = proxy_url pool_args["headers"] = configuration.proxy_headers self.pool_manager = SOCKSProxyManager(**pool_args) else: - pool_args["proxy_url"] = configuration.proxy + pool_args["proxy_url"] = proxy_url pool_args["proxy_headers"] = configuration.proxy_headers self.pool_manager = urllib3.ProxyManager(**pool_args) else: diff --git a/tests/test_proxy.py b/tests/test_proxy.py new file mode 100644 index 0000000..aa37558 --- /dev/null +++ b/tests/test_proxy.py @@ -0,0 +1,72 @@ +"""Tests for proxy support in the REST client.""" + +import pytest +import urllib3 +from urllib3.contrib.socks import SOCKSProxyManager + +from openapi_client.configuration import Configuration +from openapi_client.rest import RESTClientObject + + +class TestProxyFromEnv: + """RESTClientObject should use proxy from environment variables.""" + + @pytest.fixture(autouse=True) + def clean_proxy_env(self, monkeypatch): + """Clear all proxy environment variables before each test.""" + for key in [ + "HTTP_PROXY", + "http_proxy", + "HTTPS_PROXY", + "https_proxy", + "ALL_PROXY", + "all_proxy", + "NO_PROXY", + "no_proxy", + ]: + monkeypatch.delenv(key, raising=False) + + def test_no_proxy_uses_pool_manager(self): + """Without any proxy config, PoolManager is used (no proxy).""" + config = Configuration() + client = RESTClientObject(config) + assert isinstance(client.pool_manager, urllib3.PoolManager) + assert not isinstance(client.pool_manager, urllib3.ProxyManager) + + def test_https_proxy_env_uses_proxy_manager(self, monkeypatch): + """HTTPS_PROXY env var should cause ProxyManager to be used.""" + monkeypatch.setenv("HTTPS_PROXY", "http://localhost:8080") + config = Configuration() + client = RESTClientObject(config) + assert isinstance(client.pool_manager, urllib3.ProxyManager) + + def test_http_proxy_env_uses_proxy_manager(self, monkeypatch): + """HTTP_PROXY env var should cause ProxyManager to be used.""" + monkeypatch.setenv("HTTP_PROXY", "http://localhost:8080") + config = Configuration() + client = RESTClientObject(config) + assert isinstance(client.pool_manager, urllib3.ProxyManager) + + def test_all_proxy_env_uses_proxy_manager(self, monkeypatch): + """ALL_PROXY env var should cause ProxyManager to be used.""" + monkeypatch.setenv("ALL_PROXY", "http://localhost:8080") + config = Configuration() + client = RESTClientObject(config) + assert isinstance(client.pool_manager, urllib3.ProxyManager) + + def test_all_proxy_socks_uses_socks_proxy_manager(self, monkeypatch): + """ALL_PROXY with socks5h should use SOCKSProxyManager.""" + monkeypatch.setenv("ALL_PROXY", "socks5h://localhost:1080") + config = Configuration() + client = RESTClientObject(config) + assert isinstance(client.pool_manager, SOCKSProxyManager) + + def test_no_proxy_env_bypasses_proxy(self, monkeypatch): + """NO_PROXY env var should cause ProxyManager NOT to be used for matching host.""" + monkeypatch.setenv("HTTPS_PROXY", "http://localhost:8080") + monkeypatch.setenv("NO_PROXY", "kagi.com") + config = Configuration() + # default host is https://kagi.com/api/v1, hostname is kagi.com + client = RESTClientObject(config) + assert isinstance(client.pool_manager, urllib3.PoolManager) + assert not isinstance(client.pool_manager, urllib3.ProxyManager) diff --git a/uv.lock b/uv.lock index a3ce266..23761dc 100644 --- a/uv.lock +++ b/uv.lock @@ -357,6 +357,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, ] +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + [[package]] name = "jaraco-classes" version = "3.4.0" @@ -463,14 +472,19 @@ wheels = [ [[package]] name = "kagimcp" -version = "1.0.0rc4" +version = "1.0.0" source = { editable = "." } dependencies = [ { name = "fastmcp" }, { name = "pydantic" }, { name = "python-dateutil" }, { name = "typing-extensions" }, - { name = "urllib3" }, + { name = "urllib3", extra = ["socks"] }, +] + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, ] [package.metadata] @@ -479,9 +493,12 @@ requires-dist = [ { name = "pydantic", specifier = "~=2.12.5" }, { name = "python-dateutil", specifier = ">=2.8.2" }, { name = "typing-extensions", specifier = ">=4.7.1" }, - { name = "urllib3", specifier = ">=2.1.0,<3.0.0" }, + { name = "urllib3", extras = ["socks"], specifier = ">=2.1.0,<3.0.0" }, ] +[package.metadata.requires-dev] +dev = [{ name = "pytest", specifier = ">=8.0" }] + [[package]] name = "keyring" version = "25.7.0" @@ -606,6 +623,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" }, ] +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + [[package]] name = "py-key-value-aio" version = "0.4.4" @@ -735,6 +761,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/df/80/fc9d01d5ed37ba4c42ca2b55b4339ae6e200b456be3a1aaddf4a9fa99b8c/pyperclip-1.11.0-py3-none-any.whl", hash = "sha256:299403e9ff44581cb9ba2ffeed69c7aa96a008622ad0c46cb575ca75b5b84273", size = 11063, upload-time = "2025-09-26T14:40:36.069Z" }, ] +[[package]] +name = "pysocks" +version = "1.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/11/293dd436aea955d45fc4e8a35b6ae7270f5b8e00b53cf6c024c83b657a11/PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0", size = 284429, upload-time = "2019-09-20T02:07:35.714Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/59/b4572118e098ac8e46e399a1dd0f2d85403ce8bbaad9ec79373ed6badaf9/PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5", size = 16725, upload-time = "2019-09-20T02:06:22.938Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -952,6 +1003,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, ] +[package.optional-dependencies] +socks = [ + { name = "pysocks" }, +] + [[package]] name = "uvicorn" version = "0.41.0"