Skip to content

Commit cb8b85e

Browse files
Add proxy support
1 parent 9e85f00 commit cb8b85e

7 files changed

+97
-11
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
## [Unreleased]
99
### Added
1010
* Add glossary support for document translation.
11+
* Add proxy support.
1112
### Changed
1213
### Deprecated
1314
### Removed

README.md

+18-4
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,14 @@ for language in translator.get_target_languages():
105105
else:
106106
print(f"{language.code} ({language.name})")
107107
```
108-
### Logging
108+
109+
### Exceptions
110+
All module functions may raise `deepl.DeepLException` or one of its subclasses.
111+
If invalid arguments are provided, they may raise the standard exceptions `ValueError` and `TypeError`.
112+
113+
### Configuration
114+
115+
#### Logging
109116
Logging can be enabled to see the HTTP-requests sent and responses received by the library. Enable and control logging
110117
using Python's logging module, for example:
111118
```python
@@ -114,9 +121,16 @@ logging.basicConfig()
114121
logging.getLogger('deepl').setLevel(logging.DEBUG)
115122
```
116123

117-
### Exceptions
118-
All module functions may raise `deepl.DeepLException` or one of its subclasses.
119-
If invalid arguments are provided, they may raise the standard exceptions `ValueError` and `TypeError`.
124+
#### Proxy configuration
125+
You can configure a proxy by specifying the `proxy` argument when creating a `deepl.Translator`:
126+
```python
127+
proxy = "http://user:[email protected]:3128"
128+
translator = deepl.Translator(..., proxy=proxy)
129+
```
130+
131+
The proxy argument is passed to the underlying `requests` session,
132+
[see the documentation here](https://docs.python-requests.org/en/latest/user/advanced/#proxies); a dictionary of schemes
133+
to proxy URLs is also accepted.
120134

121135
## Command Line Interface
122136
The library can be run on the command line supporting all API functions. Use the `--help` option for

deepl/__main__.py

+14-2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
env_auth_key = "DEEPL_AUTH_KEY"
1717
env_server_url = "DEEPL_SERVER_URL"
18+
env_proxy_url = "DEEPL_PROXY_URL"
1819

1920

2021
def action_usage(translator: deepl.Translator):
@@ -201,6 +202,13 @@ def get_parser(prog_name):
201202
help=f"alternative server URL for testing; the {env_server_url} "
202203
f"environment variable may be used as secondary fallback",
203204
)
205+
parser.add_argument(
206+
"--proxy-url",
207+
default=None,
208+
metavar="URL",
209+
help=f"proxy server URL to use for all connections; the {env_proxy_url} "
210+
f"environment variable may be used as secondary fallback",
211+
)
204212

205213
# Note: add_subparsers param 'required' is not available in py36
206214
subparsers = parser.add_subparsers(metavar="command", dest="command")
@@ -467,6 +475,7 @@ def main(args=None, prog_name=None):
467475

468476
server_url = args.server_url or os.getenv(env_server_url)
469477
auth_key = args.auth_key or os.getenv(env_auth_key)
478+
proxy_url = args.proxy_url or os.getenv(env_proxy_url)
470479

471480
try:
472481
if auth_key is None:
@@ -478,7 +487,10 @@ def main(args=None, prog_name=None):
478487
# Note: the get_languages() call to verify language codes is skipped
479488
# because the CLI makes one API call per execution.
480489
translator = deepl.Translator(
481-
auth_key=auth_key, server_url=server_url, skip_language_check=True
490+
auth_key=auth_key,
491+
server_url=server_url,
492+
proxy=proxy_url,
493+
skip_language_check=True,
482494
)
483495

484496
if args.command == "text":
@@ -493,7 +505,7 @@ def main(args=None, prog_name=None):
493505
sys.exit(1)
494506

495507
# Remove global args so they are not unrecognised in action functions
496-
del args.verbose, args.server_url, args.auth_key
508+
del args.verbose, args.server_url, args.auth_key, args.proxy_url
497509
args = vars(args)
498510
# Call action function corresponding to command with remaining args
499511
command = args.pop("command")

deepl/http_client.py

+11-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import random
99
import requests
1010
import time
11-
from typing import Optional, Tuple, Union
11+
from typing import Dict, Optional, Tuple, Union
1212
from .util import log_info
1313

1414

@@ -58,8 +58,17 @@ def sleep_until_deadline(self):
5858

5959

6060
class HttpClient:
61-
def __init__(self):
61+
def __init__(self, proxy: Union[Dict, str, None] = None):
6262
self._session = requests.Session()
63+
if proxy:
64+
if isinstance(proxy, str):
65+
proxy = {"http": proxy, "https": proxy}
66+
if not isinstance(proxy, dict):
67+
raise ValueError(
68+
"proxy may be specified as a URL string or dictionary "
69+
"containing URL strings for the http and https keys."
70+
)
71+
self._session.proxies.update(proxy)
6372
self._session.headers = {"User-Agent": user_agent}
6473
pass
6574

deepl/translator.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,10 @@ class Translator:
396396
:param auth_key: Authentication key as found in your DeepL API account.
397397
:param server_url: (Optional) Base URL of DeepL API, can be overridden e.g.
398398
for testing purposes.
399+
:param proxy: (Optional) Proxy server URL string or dictionary containing
400+
URL strings for the 'http' and 'https' keys. This is passed to the
401+
underlying requests session, see the requests proxy documentation for
402+
more information.
399403
:param skip_language_check: Deprecated, and now has no effect as the
400404
corresponding internal functionality has been removed. This parameter
401405
will be removed in a future version.
@@ -416,6 +420,7 @@ def __init__(
416420
auth_key: str,
417421
*,
418422
server_url: Optional[str] = None,
423+
proxy: Union[Dict, str, None] = None,
419424
skip_language_check: bool = False,
420425
):
421426
if not auth_key:
@@ -429,7 +434,7 @@ def __init__(
429434
)
430435

431436
self._server_url = server_url
432-
self._client = http_client.HttpClient()
437+
self._client = http_client.HttpClient(proxy)
433438
self.headers = {"Authorization": f"DeepL-Auth-Key {auth_key}"}
434439

435440
def __del__(self):

tests/conftest.py

+33-2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
# Set environment variables to change this configuration.
1616
# Example: export DEEPL_SERVER_URL=http://localhost:3000/
1717
# export DEEPL_MOCK_SERVER_PORT=3000
18+
# export DEEPL_PROXY_URL=http://localhost:3001/
19+
# export DEEPL_MOCK_PROXY_SERVER_PORT=3001
1820
#
1921
# supported use cases:
2022
# - using real API
@@ -28,6 +30,8 @@ class Config(BaseSettings):
2830
auth_key: str = None
2931
server_url: str = None
3032
mock_server_port: int = None
33+
proxy_url: str = None
34+
mock_proxy_server_port: int = None
3135

3236
class Config:
3337
env_prefix = "DEEPL_"
@@ -49,9 +53,11 @@ def __init__(self):
4953
uu = str(uuid.uuid1())
5054
session_uuid = f"{os.getenv('PYTEST_CURRENT_TEST')}/{uu}"
5155
self.headers["mock-server-session"] = session_uuid
56+
self.proxy = config.proxy_url
5257
else:
5358
self.auth_key = config.auth_key
5459
self.server_url = config.server_url
60+
self.proxy = config.proxy_url
5561

5662
def no_response(self, count):
5763
"""Instructs the mock server to ignore N requests from this
@@ -113,15 +119,24 @@ def set_doc_translate_time(self, milliseconds):
113119
milliseconds
114120
)
115121

122+
def expect_proxy(self, value: bool = True):
123+
"""Instructs the mock server to only accept requests via the proxy."""
124+
if config.mock_server_port is not None:
125+
self.headers["mock-server-session-expect-proxy"] = (
126+
"1" if value else "0"
127+
)
128+
116129
return Server()
117130

118131

119-
def _make_translator(server, auth_key=None):
132+
def _make_translator(server, auth_key=None, proxy=None):
120133
"""Returns a deepl.Translator for the specified server test fixture.
121134
The server auth_key is used unless specifically overridden."""
122135
if auth_key is None:
123136
auth_key = server.auth_key
124-
translator = deepl.Translator(auth_key, server_url=server.server_url)
137+
translator = deepl.Translator(
138+
auth_key, server_url=server.server_url, proxy=proxy
139+
)
125140

126141
# If the server test fixture has custom headers defined, update the
127142
# translator headers and replace with the server headers dictionary.
@@ -146,6 +161,15 @@ def translator_with_random_auth_key(server):
146161
return _make_translator(server, auth_key=str(uuid.uuid1()))
147162

148163

164+
@pytest.fixture
165+
def translator_with_random_auth_key_and_proxy(server):
166+
"""Returns a deepl.Translator with randomized authentication key,
167+
for use in mock-server tests."""
168+
return _make_translator(
169+
server, auth_key=str(uuid.uuid1()), proxy=server.proxy
170+
)
171+
172+
149173
@pytest.fixture
150174
def cleanup_matching_glossaries(translator):
151175
"""
@@ -302,6 +326,13 @@ def output_document_path(tmpdir):
302326
Config().mock_server_port is None,
303327
reason="this test requires a mock server",
304328
)
329+
# Decorate test functions with "@needs_mock_proxy_server" to skip them if a real
330+
# server is used or mock proxy server is not configured
331+
needs_mock_proxy_server = pytest.mark.skipif(
332+
Config().mock_proxy_server_port is None
333+
or Config().mock_server_port is None,
334+
reason="this test requires a mock proxy server",
335+
)
305336
# Decorate test functions with "@needs_real_server" to skip them if a mock
306337
# server is used
307338
needs_real_server = pytest.mark.skipif(

tests/test_general.py

+14
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,20 @@ def test_server_url_selected_based_on_auth_key(server):
8484
assert translator_free.server_url == "https://api-free.deepl.com"
8585

8686

87+
@needs_mock_proxy_server
88+
def test_proxy_usage(
89+
server,
90+
translator_with_random_auth_key,
91+
translator_with_random_auth_key_and_proxy,
92+
):
93+
server.expect_proxy()
94+
95+
translator_with_random_auth_key_and_proxy.get_usage()
96+
97+
with pytest.raises(deepl.DeepLException):
98+
translator_with_random_auth_key.get_usage()
99+
100+
87101
@needs_mock_server
88102
def test_usage_no_response(translator, server, monkeypatch):
89103
server.no_response(2)

0 commit comments

Comments
 (0)