Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Try to reuse the HttpClient between requests by default #526

3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,8 @@ production use, but `debug` is also available for more verbosity.

There are a few options for enabling it:

1. Set the environment variable `STRIPE_LOG` to the value `debug` or `info`
1. Set the environment variable `STRIPE_LOG` to the value `debug`, `info` or
`warning`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I couldn't tell that easily from the changes whether this already happens, but warnings should be emitted by default (if STRIPE_LOG isn't set), because otherwise no one will ever see them. Does the code do that right now?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we have it respect either warn or warning? Especially in logging libraries these two are often interchanged that it'd be sort of a nice feature to support both. If we're supporting only one, I'd say that we might want to do warn instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@brandur-stripe My code currently requires a log level to be set in order to show warnings. I just did this to follow the existing pattern from info and debug - I'm fine with changing it.

We could simplify the interface and not allow warn (or warning) at all, and just always print warnings. Is there a case (STRIPE_LOG/stripe.log value) where you wouldn't expect warnings to be printed?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it might be preferable to keep the STRIPE_LOG interface unchanged and use the warnings module to output warnings, like here. It's more idiomatic and has a better change of being noticed by users.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds a whole lot simpler - I'll do that!


```
$ export STRIPE_LOG=debug
Expand Down
22 changes: 17 additions & 5 deletions stripe/api_requestor.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,16 +83,28 @@ def __init__(
self.api_version = api_version or stripe.api_version
self.stripe_account = account

self._default_proxy = None

from stripe import verify_ssl_certs as verify
from stripe import proxy

self._client = (
client
or stripe.default_http_client
or http_client.new_default_http_client(
if client:
self._client = client
elif stripe.default_http_client:
self._client = stripe.default_http_client
if proxy != self._default_proxy:
util.log_warning(
"stripe.proxy was updated after sending a request - this is a no-op. To use a different proxy, set stripe.default_http_client to a new client configured with the proxy."
)
else:
# If the stripe.default_http_client has not been set by the user
# yet, we'll set it here. This way, we aren't creating a new
# HttpClient for every request.
stripe.default_http_client = http_client.new_default_http_client(
verify_ssl_certs=verify, proxy=proxy
)
)
self._client = stripe.default_http_client
self._default_proxy = proxy

self._last_request_metrics = None

Expand Down
12 changes: 10 additions & 2 deletions stripe/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"io",
"parse_qsl",
"utf8",
"log_warning",
"log_info",
"log_debug",
"dashboard_link",
Expand All @@ -41,9 +42,9 @@ def is_appengine_dev():


def _console_log_level():
if stripe.log in ["debug", "info"]:
if stripe.log in ["debug", "info", "warning"]:
return stripe.log
elif STRIPE_LOG in ["debug", "info"]:
elif STRIPE_LOG in ["debug", "info", "warning"]:
return STRIPE_LOG
else:
return None
Expand All @@ -63,6 +64,13 @@ def log_info(message, **params):
logger.info(msg)


def log_warning(message, **params):
msg = logfmt(dict(message=message, **params))
if _console_log_level() in ["debug", "info", "warning"]:
print(msg, file=sys.stderr)
logger.warning(msg)


def _test_or_live_environment():
if stripe.api_key is None:
return
Expand Down
22 changes: 22 additions & 0 deletions tests/test_api_requestor.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,14 +231,17 @@ def setup_stripe(self):
orig_attrs = {
"api_key": stripe.api_key,
"api_version": stripe.api_version,
"default_http_client": stripe.default_http_client,
"enable_telemetry": stripe.enable_telemetry,
}
stripe.api_key = "sk_test_123"
stripe.api_version = "2017-12-14"
stripe.default_http_client = None
stripe.enable_telemetry = False
yield
stripe.api_key = orig_attrs["api_key"]
stripe.api_version = orig_attrs["api_version"]
stripe.default_http_client = orig_attrs["default_http_client"]
ob-stripe marked this conversation as resolved.
Show resolved Hide resolved
stripe.enable_telemetry = orig_attrs["enable_telemetry"]

@pytest.fixture
Expand Down Expand Up @@ -485,6 +488,25 @@ def test_uses_instance_account(
),
)

def test_sets_default_http_client(self, http_client):
assert not stripe.default_http_client

stripe.api_requestor.APIRequestor(client=http_client)

# default_http_client is not populated if a client is provided
assert not stripe.default_http_client

stripe.api_requestor.APIRequestor()

# default_http_client is set when no client is specified
assert stripe.default_http_client

new_default_client = stripe.default_http_client
stripe.api_requestor.APIRequestor()

# the newly created client is reused
assert stripe.default_http_client == new_default_client

def test_uses_app_info(self, requestor, mock_response, check_call):
try:
old = stripe.app_info
Expand Down
137 changes: 137 additions & 0 deletions tests/test_integration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import sys
import socket
from threading import Thread
import json

import stripe
import pytest

if sys.version_info[0] < 3:
from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
else:
from http.server import BaseHTTPRequestHandler, HTTPServer


def get_free_port():
s = socket.socket(socket.AF_INET, type=socket.SOCK_STREAM)
s.bind(("localhost", 0))
address, port = s.getsockname()
s.close()
return port


class TestIntegration(object):
@pytest.fixture(autouse=True)
def close_mock_server(self):
yield
if self.mock_server:
self.mock_server.shutdown()
self.mock_server_thread.join()

@pytest.fixture(autouse=True)
def setup_stripe(self):
orig_attrs = {
"api_base": stripe.api_base,
"api_key": stripe.api_key,
"default_http_client": stripe.default_http_client,
"log": stripe.log,
"proxy": stripe.proxy,
}
stripe.api_base = "http://localhost:12111" # stripe-mock
stripe.api_key = "sk_test_123"
stripe.default_http_client = None
stripe.log = "warning"
stripe.proxy = None
yield
stripe.api_base = orig_attrs["api_base"]
stripe.api_key = orig_attrs["api_key"]
stripe.default_http_client = orig_attrs["default_http_client"]
stripe.log = orig_attrs["log"]
stripe.proxy = orig_attrs["proxy"]

def setup_mock_server(self, handler):
# Configure mock server.
self.mock_server_port = get_free_port()
self.mock_server = HTTPServer(
("localhost", self.mock_server_port), handler
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeesh, I was surprised to find that Python's docs for this are astoundingly awful, but passing 0 here should have the server select a port, which allows you to remove get_free_port and a possible race condition.

Here's an example with ThreadedTCPServer:

if __name__ == "__main__":
    # Port 0 means to select an arbitrary unused port
    HOST, PORT = "localhost", 0

    server = ThreadedTCPServer((HOST, PORT), ThreadedTCPRequestHandler)
    ip, port = server.server_address

    # Start a thread with the server -- that thread will then start one
    # more thread for each request
    server_thread = threading.Thread(target=server.serve_forever)
    # Exit the server thread when the main thread terminates
    server_thread.daemon = True
    server_thread.start()
    print "Server loop running in thread:", server_thread.name

    client(ip, port, "Hello World 1")
    client(ip, port, "Hello World 2")
    client(ip, port, "Hello World 3")

    server.shutdown()
    server.server_close()


# Start running mock server in a separate thread.
# Daemon threads automatically shut down when the main process exits.
self.mock_server_thread = Thread(target=self.mock_server.serve_forever)
self.mock_server_thread.setDaemon(True)
self.mock_server_thread.start()

def test_hits_api_base(self):
class MockServerRequestHandler(BaseHTTPRequestHandler):
num_requests = 0

def do_GET(self):
self.__class__.num_requests += 1

self.send_response(200)
self.send_header(
"Content-Type", "application/json; charset=utf-8"
)
self.end_headers()
self.wfile.write(json.dumps({}).encode("utf-8"))
return

self.setup_mock_server(MockServerRequestHandler)

stripe.api_base = "http://localhost:%s" % self.mock_server_port
stripe.Balance.retrieve()
assert MockServerRequestHandler.num_requests == 1

def test_hits_stripe_proxy(self, mocker):
class MockServerRequestHandler(BaseHTTPRequestHandler):
num_requests = 0

def do_GET(self):
self.__class__.num_requests += 1

self.send_response(200)
self.send_header(
"Content-Type", "application/json; charset=utf-8"
)
self.end_headers()
self.wfile.write(json.dumps({}).encode("utf-8"))
return

self.setup_mock_server(MockServerRequestHandler)
logger_mock = mocker.patch("stripe.util.log_warning")

stripe.proxy = "http://localhost:%s" % self.mock_server_port
stripe.Balance.retrieve()
assert MockServerRequestHandler.num_requests == 1

stripe.proxy = "http://bad-url"
logger_mock.assert_not_called()
stripe.Balance.retrieve()
logger_mock.assert_called_with(
"stripe.proxy was updated after sending a request - this is a no-op. To use a different proxy, set stripe.default_http_client to a new client configured with the proxy."
)
assert MockServerRequestHandler.num_requests == 2

def test_hits_client_proxy(self):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we name this one something like test_hits_proxy_through_default_http_client?

class MockServerRequestHandler(BaseHTTPRequestHandler):
num_requests = 0

def do_GET(self):
self.__class__.num_requests += 1

self.send_response(200)
self.send_header(
"Content-Type", "application/json; charset=utf-8"
)
self.end_headers()
self.wfile.write(json.dumps({}).encode("utf-8"))
return

self.setup_mock_server(MockServerRequestHandler)

stripe.default_http_client = stripe.http_client.new_default_http_client(
proxy="http://localhost:%s" % self.mock_server_port
)
stripe.Balance.retrieve()
assert MockServerRequestHandler.num_requests == 1
26 changes: 26 additions & 0 deletions tests/test_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ def test_log_debug(self, mocker):
# (STRIPE_LOG, stripe.log): should_output?
test_cases = [
LogTestCase(env=None, flag=None, should_output=False),
LogTestCase(env="warning", flag=None, should_output=False),
LogTestCase(env=None, flag="warning", should_output=False),
LogTestCase(env=None, flag="debug", should_output=True),
LogTestCase(env=None, flag="info", should_output=False),
LogTestCase(env="debug", flag=None, should_output=True),
Expand All @@ -91,6 +93,8 @@ def test_log_info(self, mocker):
# (STRIPE_LOG, stripe.log): should_output?
test_cases = [
LogTestCase(env=None, flag=None, should_output=False),
LogTestCase(env="warning", flag=None, should_output=False),
LogTestCase(env=None, flag="warning", should_output=False),
LogTestCase(env=None, flag="debug", should_output=True),
LogTestCase(env=None, flag="info", should_output=True),
LogTestCase(env="debug", flag=None, should_output=True),
Expand All @@ -107,6 +111,28 @@ def test_log_info(self, mocker):
mocker=mocker,
)

def test_log_warning(self, mocker):
# (STRIPE_LOG, stripe.log): should_output?
test_cases = [
LogTestCase(env=None, flag=None, should_output=False),
LogTestCase(env="warning", flag=None, should_output=True),
LogTestCase(env=None, flag="warning", should_output=True),
LogTestCase(env=None, flag="debug", should_output=True),
LogTestCase(env=None, flag="info", should_output=True),
LogTestCase(env="debug", flag=None, should_output=True),
LogTestCase(env="debug", flag="debug", should_output=True),
LogTestCase(env="debug", flag="info", should_output=True),
LogTestCase(env="info", flag=None, should_output=True),
LogTestCase(env="info", flag="debug", should_output=True),
LogTestCase(env="info", flag="info", should_output=True),
]
self.log_test_loop(
test_cases,
logging_func=util.log_warning,
logger_name="stripe.util.logger.warning",
mocker=mocker,
)

def test_logfmt(self):
cases = [
FmtTestCase(
Expand Down