Skip to content

Commit

Permalink
Support "app info" for plugins in Python
Browse files Browse the repository at this point in the history
  • Loading branch information
ob-stripe committed Apr 28, 2017
1 parent 18ed830 commit 56dc502
Show file tree
Hide file tree
Showing 4 changed files with 123 additions and 30 deletions.
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,18 @@ There are a few options for enabling it:
logging.getLogger('stripe').setLevel(logging.DEBUG)
```

### Writing a Plugin

If you're writing a plugin that uses the library, we'd appreciate it if you
identified using `stripe.set_app_info()`:

```py
stripe.set_app_info("MyAwesomePlugin", version="1.2.34", url="https://myawesomeplugin.info")
```

This information is passed along when the library makes calls to the Stripe
API.

## Development

Run all tests (modify `-e` according to your Python target):
Expand Down
15 changes: 15 additions & 0 deletions stripe/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
verify_ssl_certs = True
proxy = None
default_http_client = None
app_info = None

# Set to either 'debug' or 'info', controls console logging
log = None
Expand Down Expand Up @@ -92,3 +93,17 @@
UpdateableAPIResource,
convert_to_stripe_object)
from stripe.util import json, logger # noqa


# Sets some basic information about the running application that's sent along
# with API requests. Useful for plugin authors to identify their plugin when
# communicating with Stripe.
#
# Takes a name and optional version and plugin URL.
def set_app_info(name, version=None, url=None):
global app_info
app_info = {
'name': name,
'version': version,
'url': url,
}
78 changes: 49 additions & 29 deletions stripe/api_requestor.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,15 @@ def build_url(cls, url, params):
DeprecationWarning)
return _build_api_url(url, cls.encode(params))

@classmethod
def format_app_info(cls, info):
str = info['name']
if info['version']:
str += "/%s" % (info['version'],)
if info['url']:
str += " (%s)" % (info['url'],)
return str

def request(self, method, url, params=None, headers=None):
rbody, rcode, rheaders, my_api_key = self.request_raw(
method.lower(), url, params, headers)
Expand Down Expand Up @@ -175,6 +184,45 @@ def handle_api_error(self, rbody, rcode, resp, rheaders):
raise error.APIError(err.get('message'), rbody, rcode, resp,
rheaders)

def request_headers(self, api_key, method):
user_agent = 'Stripe/v1 PythonBindings/%s' % (version.VERSION,)
if stripe.app_info:
user_agent += " " + self.format_app_info(stripe.app_info)

ua = {
'bindings_version': version.VERSION,
'lang': 'python',
'publisher': 'stripe',
'httplib': self._client.name,
}
for attr, func in [['lang_version', platform.python_version],
['platform', platform.platform],
['uname', lambda: ' '.join(platform.uname())]]:
try:
val = func()
except Exception as e:
val = "!! %s" % (e,)
ua[attr] = val
if stripe.app_info:
ua['application'] = stripe.app_info

headers = {
'X-Stripe-Client-User-Agent': util.json.dumps(ua),
'User-Agent': user_agent,
'Authorization': 'Bearer %s' % (api_key,),
}

if self.stripe_account:
headers['Stripe-Account'] = self.stripe_account

if method == 'post':
headers['Content-Type'] = 'application/x-www-form-urlencoded'

if self.api_version is not None:
headers['Stripe-Version'] = self.api_version

return headers

def request_raw(self, method, url, params=None, supplied_headers=None):
"""
Mechanism for issuing an API call
Expand Down Expand Up @@ -219,35 +267,7 @@ def request_raw(self, method, url, params=None, supplied_headers=None):
'Stripe bindings. Please contact [email protected] for '
'assistance.' % (method,))

ua = {
'bindings_version': version.VERSION,
'lang': 'python',
'publisher': 'stripe',
'httplib': self._client.name,
}
for attr, func in [['lang_version', platform.python_version],
['platform', platform.platform],
['uname', lambda: ' '.join(platform.uname())]]:
try:
val = func()
except Exception as e:
val = "!! %s" % (e,)
ua[attr] = val

headers = {
'X-Stripe-Client-User-Agent': util.json.dumps(ua),
'User-Agent': 'Stripe/v1 PythonBindings/%s' % (version.VERSION,),
'Authorization': 'Bearer %s' % (my_api_key,)
}

if self.stripe_account:
headers['Stripe-Account'] = self.stripe_account

if method == 'post':
headers['Content-Type'] = 'application/x-www-form-urlencoded'

if self.api_version is not None:
headers['Stripe-Version'] = self.api_version
headers = self.request_headers(my_api_key, method)

if supplied_headers is not None:
for key, value in supplied_headers.items():
Expand Down
48 changes: 47 additions & 1 deletion stripe/test/test_requestor.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,19 @@ class APIHeaderMatcher(object):
]
METHOD_EXTRA_KEYS = {"post": ["Content-Type"]}

def __init__(self, api_key=None, extra={}, request_method=None):
def __init__(self, api_key=None, extra={}, request_method=None,
user_agent=None, app_info=None):
self.request_method = request_method
self.api_key = api_key or stripe.api_key
self.extra = extra
self.user_agent = user_agent
self.app_info = app_info

def __eq__(self, other):
return (self._keys_match(other) and
self._auth_match(other) and
self._user_agent_match(other) and
self._x_stripe_ua_contains_app_info(other) and
self._extra_match(other))

def _keys_match(self, other):
Expand All @@ -54,6 +59,21 @@ def _keys_match(self, other):
def _auth_match(self, other):
return other['Authorization'] == "Bearer %s" % (self.api_key,)

def _user_agent_match(self, other):
if self.user_agent is not None:
return other['User-Agent'] == self.user_agent

return True

def _x_stripe_ua_contains_app_info(self, other):
if self.app_info:
ua = stripe.util.json.loads(other['X-Stripe-Client-User-Agent'])
if 'application' not in ua:
return False
return ua['application'] == self.app_info

return True

def _extra_match(self, other):
for k, v in self.extra.iteritems():
if other[k] != v:
Expand Down Expand Up @@ -316,6 +336,32 @@ def test_uses_instance_account(self):
),
)

def test_uses_app_info(self):
try:
old = stripe.app_info
stripe.set_app_info(
'MyAwesomePlugin',
url='https://myawesomeplugin.info',
version='1.2.34'
)

self.mock_response('{}', 200)
self.requestor.request('get', self.valid_path, {})

ua = "Stripe/v1 PythonBindings/%s" % (stripe.version.VERSION,)
ua += " MyAwesomePlugin/1.2.34 (https://myawesomeplugin.info)"
header_matcher = APIHeaderMatcher(
user_agent=ua,
app_info={
'name': 'MyAwesomePlugin',
'url': 'https://myawesomeplugin.info',
'version': '1.2.34',
}
)
self.check_call('get', headers=header_matcher)
finally:
stripe.app_info = old

def test_fails_without_api_key(self):
stripe.api_key = None

Expand Down

0 comments on commit 56dc502

Please sign in to comment.