Skip to content
Merged
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
13 changes: 9 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,18 @@ pip install apimatic-core
|---------------------------------------------------------------------------|-----------------------------------------------------------------------------|
| [`HttpResponseFactory`](apimatic_core/factories/http_response_factory.py) | A factory class to create an HTTP Response |

## HTTP Configurations
| Name | Description |
|---------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------|
| [`HttpClientConfiguration`](apimatic_core/http/configurations/http_client_configuration.py) | A class used for configuring the SDK by a user |
| [`ProxySettings`](apimatic_core/http/configurations/proxy_settings.py) | ProxySettings encapsulates HTTP proxy configuration for Requests, e.g. address, port and optional basic authentication for HTTP and HTTPS |

## HTTP
| Name | Description |
|---------------------------------------------------------------------------------------------|-------------------------------------------------------------|
| [`HttpCallBack`](apimatic_core/factories/http_response_factory.py) | A factory class to create an HTTP Response |
| [`HttpClientConfiguration`](apimatic_core/http/configurations/http_client_configuration.py) | A class used for configuring the SDK by a user |
| [`HttpRequest`](apimatic_core/http/request/http_request.py) | A class which contains information about the HTTP Response |
| [`ApiResponse`](apimatic_core/http/response/api_response.py) | A wrapper class for Api Response |
| [`HttpCallBack`](apimatic_core/http/http_callback.py) | A class to register hooks for the API request and response |
| [`HttpRequest`](apimatic_core/http/request/http_request.py) | A class which contains information about the HTTP Request |
| [`ApiResponse`](apimatic_core/http/response/api_response.py) | A wrapper class for the complete Http Response including raw body and headers etc. |
| [`HttpResponse`](apimatic_core/http/response/http_response.py) | A class which contains information about the HTTP Response |

## Logging Configuration
Expand Down
3 changes: 2 additions & 1 deletion apimatic_core/http/configurations/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
__all__ = [
'http_client_configuration'
'http_client_configuration',
'proxy_settings'
]
11 changes: 9 additions & 2 deletions apimatic_core/http/configurations/http_client_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,15 @@ def retry_methods(self):
def logging_configuration(self):
return self._logging_configuration

@property
def proxy_settings(self):
return self._proxy_settings

def __init__(self, http_client_instance=None,
override_http_client_configuration=False, http_call_back=None,
timeout=60, max_retries=0, backoff_factor=2,
retry_statuses=None, retry_methods=None, logging_configuration=None):
retry_statuses=None, retry_methods=None, logging_configuration=None,
proxy_settings=None):
if retry_statuses is None:
retry_statuses = [408, 413, 429, 500, 502, 503, 504, 521, 522, 524]

Expand Down Expand Up @@ -93,6 +98,8 @@ def __init__(self, http_client_instance=None,

self._logging_configuration = logging_configuration

self._proxy_settings = proxy_settings

def set_http_client(self, http_client):
self._http_client = http_client

Expand All @@ -103,6 +110,6 @@ def clone(self, http_callback=None):
http_call_back=http_callback or self.http_callback,
timeout=self.timeout, max_retries=self.max_retries, backoff_factor=self.backoff_factor,
retry_statuses=self.retry_statuses, retry_methods=self.retry_methods,
logging_configuration=self.logging_configuration)
logging_configuration=self.logging_configuration, proxy_settings=self.proxy_settings)
http_client_instance.set_http_client(self.http_client)
return http_client_instance
81 changes: 81 additions & 0 deletions apimatic_core/http/configurations/proxy_settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
from typing import Dict, Optional
from urllib.parse import quote


class ProxySettings:
"""
A simple data model for configuring HTTP(S) proxy settings.
"""

HTTP_SCHEME: str = "http://"
HTTPS_SCHEME: str = "https://"

address: str
port: Optional[int]
username: Optional[str]
password: Optional[str]

def __init__(self, address: str, port: Optional[int] = None,
username: Optional[str] = None, password: Optional[str] = None) -> None:
"""
Parameters
----------
address : str
Hostname or IP of the proxy.
port : int, optional
Port of the proxy server.
username : str, optional
Username for authentication.
password : str, optional
Password for authentication.
"""
self.address = address
self.port = port
self.username = username
self.password = password

def __repr__(self) -> str:
"""
Developer-friendly representation.
"""
return (
f"ProxySettings(address={self.address!r}, "
f"port={self.port!r}, "
f"username={self.username!r}, "
f"password={'***' if self.password else None})"
)

def __str__(self) -> str:
"""
Human-friendly string for display/logging.
"""
user_info = f"{self.username}:***@" if self.username else ""
port = f":{self.port}" if self.port else ""
return f"{user_info}{self.address}{port}"

def _sanitize_address(self) -> str:
addr = (self.address or "").strip()
# Trim scheme if present
if addr.startswith(self.HTTP_SCHEME):
addr = addr[len(self.HTTP_SCHEME):]
elif addr.startswith(self.HTTPS_SCHEME):
addr = addr[len(self.HTTPS_SCHEME):]
# Drop trailing slash if user typed a URL-like form
return addr.rstrip("/")

def to_proxies(self) -> Dict[str, str]:
"""
Build a `requests`-compatible proxies dictionary.
"""
host = self._sanitize_address()
auth = ""
if self.username is not None:
# URL-encode in case of special chars
u = quote(self.username, safe="")
p = quote(self.password or "", safe="")
auth = f"{u}:{p}@"
port = f":{self.port}" if self.port is not None else ""
return {
"http": f"{self.HTTP_SCHEME}{auth}{host}{port}",
"https": f"{self.HTTPS_SCHEME}{auth}{host}{port}",
}
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

setup(
name='apimatic-core',
version='0.2.21',
version='0.2.22',
description='A library that contains core logic and utilities for '
'consuming REST APIs using Python SDKs generated by APIMatic.',
long_description=long_description,
Expand Down
Empty file.
94 changes: 94 additions & 0 deletions tests/apimatic_core/configuration/test_proxy_settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import pytest

from apimatic_core.http.configurations.proxy_settings import ProxySettings


class TestProxySettings:
def test_has_expected_keys(self):
ps = ProxySettings(address="proxy.local")
proxies = ps.to_proxies()
assert set(proxies.keys()) == {"http", "https"}

@pytest.mark.parametrize(
"address, port, username, password, exp_http, exp_https",
[
pytest.param(
"proxy.local", None, None, None,
"http://proxy.local",
"https://proxy.local",
id="no-auth-no-port",
),
pytest.param(
"proxy.local", 8080, None, None,
"http://proxy.local:8080",
"https://proxy.local:8080",
id="no-auth-with-port",
),
pytest.param(
"proxy.local", 8080, "user", "pass",
"http://user:pass@proxy.local:8080",
"https://user:pass@proxy.local:8080",
id="auth-with-port",
),
pytest.param(
"proxy.local", None, "user", None,
# password None -> empty string: "user:@"
"http://user:@proxy.local",
"https://user:@proxy.local",
id="auth-username-only-password-none",
),
pytest.param(
"proxy.local", None, "a b", "p@ss#",
# URL-encoding of space/@/#
"http://a%20b:p%40ss%23@proxy.local",
"https://a%20b:p%40ss%23@proxy.local",
id="auth-with-url-encoding",
),
pytest.param(
"localhost", None, "", "",
# empty username triggers auth block (since not None) -> ':@'
"http://:@localhost",
"https://:@localhost",
id="empty-username-and-password",
),
],
)
def test_formats(self, address, port, username, password, exp_http, exp_https):
ps = ProxySettings(address, port, username, password)
proxies = ps.to_proxies()
assert proxies["http"] == exp_http
assert proxies["https"] == exp_https

@pytest.mark.parametrize("address", ["proxy.local", "localhost"])
def test_no_trailing_colon_when_no_port(self, address):
ps = ProxySettings(address)
proxies = ps.to_proxies()
assert not proxies["http"].endswith(":")
assert not proxies["https"].endswith(":")
assert "::" not in proxies["http"]
assert "::" not in proxies["https"]

def test_single_colon_before_port(self):
ps = ProxySettings(address="proxy.local", port=3128)
proxies = ps.to_proxies()
assert proxies["http"].endswith(":3128")
assert proxies["https"].endswith(":3128")
assert "proxy.local::3128" not in proxies["http"]
assert "proxy.local::3128" not in proxies["https"]

# --- NEW: scheme trimming cases (reflecting the reverted, simpler behavior) ---

def test_trims_http_scheme_no_port(self):
ps = ProxySettings(address="http://proxy.local")
proxies = ps.to_proxies()
assert proxies["http"] == "http://proxy.local"
assert proxies["https"] == "https://proxy.local"

def test_trims_https_scheme_trailing_slash_with_port_and_auth(self):
ps = ProxySettings(address="https://proxy.local/", port=8080, username="user", password="secret")
proxies = ps.to_proxies()
assert proxies["http"] == "http://user:secret@proxy.local:8080"
assert proxies["https"] == "https://user:secret@proxy.local:8080"
assert not proxies["http"].endswith(":")
assert not proxies["https"].endswith(":")

Loading