Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
]
Expand All @@ -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",
]
20 changes: 16 additions & 4 deletions src/openapi_client/rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
import json
import re
import ssl
import urllib.request
from urllib.parse import urlparse

import urllib3

Expand Down Expand Up @@ -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:
Expand Down
72 changes: 72 additions & 0 deletions tests/test_proxy.py
Original file line number Diff line number Diff line change
@@ -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)
62 changes: 59 additions & 3 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.