Skip to content

Commit

Permalink
Add proxy support
Browse files Browse the repository at this point in the history
  • Loading branch information
daniel-jones-dev committed Nov 12, 2021
1 parent 9e85f00 commit cb8b85e
Show file tree
Hide file tree
Showing 7 changed files with 97 additions and 11 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added
* Add glossary support for document translation.
* Add proxy support.
### Changed
### Deprecated
### Removed
Expand Down
22 changes: 18 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 105,14 @@ for language in translator.get_target_languages():
else:
print(f"{language.code} ({language.name})")
```
### Logging

### Exceptions
All module functions may raise `deepl.DeepLException` or one of its subclasses.
If invalid arguments are provided, they may raise the standard exceptions `ValueError` and `TypeError`.

### Configuration

#### Logging
Logging can be enabled to see the HTTP-requests sent and responses received by the library. Enable and control logging
using Python's logging module, for example:
```python
Expand All @@ -114,9 121,16 @@ logging.basicConfig()
logging.getLogger('deepl').setLevel(logging.DEBUG)
```

### Exceptions
All module functions may raise `deepl.DeepLException` or one of its subclasses.
If invalid arguments are provided, they may raise the standard exceptions `ValueError` and `TypeError`.
#### Proxy configuration
You can configure a proxy by specifying the `proxy` argument when creating a `deepl.Translator`:
```python
proxy = "http://user:[email protected]:3128"
translator = deepl.Translator(..., proxy=proxy)
```

The proxy argument is passed to the underlying `requests` session,
[see the documentation here](https://docs.python-requests.org/en/latest/user/advanced/#proxies); a dictionary of schemes
to proxy URLs is also accepted.

## Command Line Interface
The library can be run on the command line supporting all API functions. Use the `--help` option for
Expand Down
16 changes: 14 additions & 2 deletions deepl/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 15,7 @@

env_auth_key = "DEEPL_AUTH_KEY"
env_server_url = "DEEPL_SERVER_URL"
env_proxy_url = "DEEPL_PROXY_URL"


def action_usage(translator: deepl.Translator):
Expand Down Expand Up @@ -201,6 202,13 @@ def get_parser(prog_name):
help=f"alternative server URL for testing; the {env_server_url} "
f"environment variable may be used as secondary fallback",
)
parser.add_argument(
"--proxy-url",
default=None,
metavar="URL",
help=f"proxy server URL to use for all connections; the {env_proxy_url} "
f"environment variable may be used as secondary fallback",
)

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

server_url = args.server_url or os.getenv(env_server_url)
auth_key = args.auth_key or os.getenv(env_auth_key)
proxy_url = args.proxy_url or os.getenv(env_proxy_url)

try:
if auth_key is None:
Expand All @@ -478,7 487,10 @@ def main(args=None, prog_name=None):
# Note: the get_languages() call to verify language codes is skipped
# because the CLI makes one API call per execution.
translator = deepl.Translator(
auth_key=auth_key, server_url=server_url, skip_language_check=True
auth_key=auth_key,
server_url=server_url,
proxy=proxy_url,
skip_language_check=True,
)

if args.command == "text":
Expand All @@ -493,7 505,7 @@ def main(args=None, prog_name=None):
sys.exit(1)

# Remove global args so they are not unrecognised in action functions
del args.verbose, args.server_url, args.auth_key
del args.verbose, args.server_url, args.auth_key, args.proxy_url
args = vars(args)
# Call action function corresponding to command with remaining args
command = args.pop("command")
Expand Down
13 changes: 11 additions & 2 deletions deepl/http_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 8,7 @@
import random
import requests
import time
from typing import Optional, Tuple, Union
from typing import Dict, Optional, Tuple, Union
from .util import log_info


Expand Down Expand Up @@ -58,8 58,17 @@ def sleep_until_deadline(self):


class HttpClient:
def __init__(self):
def __init__(self, proxy: Union[Dict, str, None] = None):
self._session = requests.Session()
if proxy:
if isinstance(proxy, str):
proxy = {"http": proxy, "https": proxy}
if not isinstance(proxy, dict):
raise ValueError(
"proxy may be specified as a URL string or dictionary "
"containing URL strings for the http and https keys."
)
self._session.proxies.update(proxy)
self._session.headers = {"User-Agent": user_agent}
pass

Expand Down
7 changes: 6 additions & 1 deletion deepl/translator.py
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 396,10 @@ class Translator:
:param auth_key: Authentication key as found in your DeepL API account.
:param server_url: (Optional) Base URL of DeepL API, can be overridden e.g.
for testing purposes.
:param proxy: (Optional) Proxy server URL string or dictionary containing
URL strings for the 'http' and 'https' keys. This is passed to the
underlying requests session, see the requests proxy documentation for
more information.
:param skip_language_check: Deprecated, and now has no effect as the
corresponding internal functionality has been removed. This parameter
will be removed in a future version.
Expand All @@ -416,6 420,7 @@ def __init__(
auth_key: str,
*,
server_url: Optional[str] = None,
proxy: Union[Dict, str, None] = None,
skip_language_check: bool = False,
):
if not auth_key:
Expand All @@ -429,7 434,7 @@ def __init__(
)

self._server_url = server_url
self._client = http_client.HttpClient()
self._client = http_client.HttpClient(proxy)
self.headers = {"Authorization": f"DeepL-Auth-Key {auth_key}"}

def __del__(self):
Expand Down
35 changes: 33 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 15,8 @@
# Set environment variables to change this configuration.
# Example: export DEEPL_SERVER_URL=http://localhost:3000/
# export DEEPL_MOCK_SERVER_PORT=3000
# export DEEPL_PROXY_URL=http://localhost:3001/
# export DEEPL_MOCK_PROXY_SERVER_PORT=3001
#
# supported use cases:
# - using real API
Expand All @@ -28,6 30,8 @@ class Config(BaseSettings):
auth_key: str = None
server_url: str = None
mock_server_port: int = None
proxy_url: str = None
mock_proxy_server_port: int = None

class Config:
env_prefix = "DEEPL_"
Expand All @@ -49,9 53,11 @@ def __init__(self):
uu = str(uuid.uuid1())
session_uuid = f"{os.getenv('PYTEST_CURRENT_TEST')}/{uu}"
self.headers["mock-server-session"] = session_uuid
self.proxy = config.proxy_url
else:
self.auth_key = config.auth_key
self.server_url = config.server_url
self.proxy = config.proxy_url

def no_response(self, count):
"""Instructs the mock server to ignore N requests from this
Expand Down Expand Up @@ -113,15 119,24 @@ def set_doc_translate_time(self, milliseconds):
milliseconds
)

def expect_proxy(self, value: bool = True):
"""Instructs the mock server to only accept requests via the proxy."""
if config.mock_server_port is not None:
self.headers["mock-server-session-expect-proxy"] = (
"1" if value else "0"
)

return Server()


def _make_translator(server, auth_key=None):
def _make_translator(server, auth_key=None, proxy=None):
"""Returns a deepl.Translator for the specified server test fixture.
The server auth_key is used unless specifically overridden."""
if auth_key is None:
auth_key = server.auth_key
translator = deepl.Translator(auth_key, server_url=server.server_url)
translator = deepl.Translator(
auth_key, server_url=server.server_url, proxy=proxy
)

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


@pytest.fixture
def translator_with_random_auth_key_and_proxy(server):
"""Returns a deepl.Translator with randomized authentication key,
for use in mock-server tests."""
return _make_translator(
server, auth_key=str(uuid.uuid1()), proxy=server.proxy
)


@pytest.fixture
def cleanup_matching_glossaries(translator):
"""
Expand Down Expand Up @@ -302,6 326,13 @@ def output_document_path(tmpdir):
Config().mock_server_port is None,
reason="this test requires a mock server",
)
# Decorate test functions with "@needs_mock_proxy_server" to skip them if a real
# server is used or mock proxy server is not configured
needs_mock_proxy_server = pytest.mark.skipif(
Config().mock_proxy_server_port is None
or Config().mock_server_port is None,
reason="this test requires a mock proxy server",
)
# Decorate test functions with "@needs_real_server" to skip them if a mock
# server is used
needs_real_server = pytest.mark.skipif(
Expand Down
14 changes: 14 additions & 0 deletions tests/test_general.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 84,20 @@ def test_server_url_selected_based_on_auth_key(server):
assert translator_free.server_url == "https://api-free.deepl.com"


@needs_mock_proxy_server
def test_proxy_usage(
server,
translator_with_random_auth_key,
translator_with_random_auth_key_and_proxy,
):
server.expect_proxy()

translator_with_random_auth_key_and_proxy.get_usage()

with pytest.raises(deepl.DeepLException):
translator_with_random_auth_key.get_usage()


@needs_mock_server
def test_usage_no_response(translator, server, monkeypatch):
server.no_response(2)
Expand Down

0 comments on commit cb8b85e

Please sign in to comment.