├── .gitignore ├── README.md ├── __init__.py ├── _facebook.py ├── _foursquare.py ├── _stripe.py ├── _twitter.py └── tests └── stripe_test.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## What is tornado_api 2 | 3 | tornado_api is a collection of Mixins and asynchronous HTTP libraries for [Tornado Web Framework](http://www.tornadoweb.org/). 4 | 5 | 6 | ## FacebookGraphMixin 7 | 8 | Re-implementation of Tornado's OAuth2 Mixin. 9 | 10 | 11 | ## FoursquareMixin 12 | 13 | OAuth2 Mixin for Foursquare. Once authorized via authorize_redirect(), you can call Foursquare API using foursquare_request() 14 | 15 | 16 | ## tornado_api.Stripe 17 | 18 | A complete implementation of Stripe v1 API using Tornado AsyncHTTPClient. 19 | 20 | ### Initialization 21 | 22 | ```python 23 | # 24 | # By default, blocking is set to False. 25 | # If blocking is set to True, then it uses Tornado blocking HTTP client. 26 | # 27 | stripe = tornado_api.Stripe(YOUR_STRIPE_API_KEY, blocking=True) 28 | ``` 29 | 30 | ### Building URL 31 | 32 | tornado_api.Stripe maps to Stripe Curl URL exactly one-to-one. 33 | 34 | /v1/charges 35 | 36 | ```python 37 | stripe = tornado_api.Stripe(YOUR_STRIPE_API_KEY) 38 | stripe.charges 39 | ``` 40 | 41 | /v1/charges/{CHARGE_ID} 42 | 43 | ```python 44 | stripe = tornado_api.Stripe(YOUR_STRIPE_API_KEY) 45 | stripe.charges.id(CHARGE_ID) 46 | ``` 47 | 48 | /v1/customers 49 | 50 | ```python 51 | stripe = tornado_api.Stripe(YOUR_STRIPE_API_KEY) 52 | stripe.customers 53 | ``` 54 | 55 | /v1/customers/{CUSTOMER_ID} 56 | 57 | ```python 58 | stripe = tornado_api.Stripe(YOUR_STRIPE_API_KEY) 59 | stripe.customers.id(CUSTOMER_ID) 60 | ``` 61 | 62 | /v1/customers/{CUSTOMER_ID}/subscription 63 | 64 | ```python 65 | stripe = tornado_api.Stripe(YOUR_STRIPE_API_KEY) 66 | stripe.customers.id(CUSTOMER_ID).subscription 67 | ``` 68 | 69 | /v1/invoices 70 | 71 | ```python 72 | stripe = tornado_api.Stripe(YOUR_STRIPE_API_KEY) 73 | stripe.invoices 74 | ``` 75 | 76 | /v1/invoices/{INVOICE_ID} 77 | 78 | ```python 79 | stripe = tornado_api.Stripe(YOUR_STRIPE_API_KEY) 80 | stripe.invoices.id(INVOICE_ID) 81 | ``` 82 | 83 | /v1/invoiceitems 84 | 85 | ```python 86 | stripe = tornado_api.Stripe(YOUR_STRIPE_API_KEY) 87 | stripe.invoiceitems 88 | ``` 89 | 90 | /v1/invoiceitems/{INVOICEITEM_ID} 91 | 92 | ```python 93 | stripe = tornado_api.Stripe(YOUR_STRIPE_API_KEY) 94 | stripe.invoiceitems.id(INVOICEITEM_ID) 95 | ``` 96 | 97 | /v1/tokens 98 | 99 | ```python 100 | stripe = tornado_api.Stripe(YOUR_STRIPE_API_KEY) 101 | stripe.tokens 102 | ``` 103 | 104 | /v1/tokens/{TOKEN_ID} 105 | 106 | ```python 107 | stripe = tornado_api.Stripe(YOUR_STRIPE_API_KEY) 108 | stripe.tokens.id(TOKEN_ID) 109 | ``` 110 | 111 | /v1/events 112 | 113 | ```python 114 | stripe = tornado_api.Stripe(YOUR_STRIPE_API_KEY) 115 | stripe.events 116 | ``` 117 | 118 | /v1/events/{EVENT_ID} 119 | 120 | ```python 121 | stripe = tornado_api.Stripe(YOUR_STRIPE_API_KEY) 122 | stripe.events.id(EVENT_ID) 123 | ``` 124 | 125 | ### Performing HTTP request 126 | 127 | GET 128 | 129 | ```python 130 | stripe = tornado_api.Stripe(YOUR_STRIPE_API_KEY) 131 | stripe.plans.get() 132 | stripe.plans.id(PLAN_ID).get() 133 | ``` 134 | 135 | POST 136 | 137 | ```python 138 | DUMMY_PLAN = { 139 | 'amount': 2000, 140 | 'interval': 'month', 141 | 'name': 'Amazing Gold Plan', 142 | 'currency': 'usd', 143 | 'id': 'stripe-test-gold' 144 | } 145 | stripe = tornado_api.Stripe(YOUR_STRIPE_API_KEY) 146 | stripe.plans.post(**DUMMY_PLAN) 147 | ``` 148 | 149 | DELETE 150 | 151 | ```python 152 | stripe = tornado_api.Stripe(YOUR_STRIPE_API_KEY) 153 | stripe.plans.id(DUMMY_PLAN['id']).delete() 154 | ``` 155 | 156 | ## tornado_api.Twitter 157 | 158 | Requirement: 159 | 160 | ``` 161 | pip install twitter 162 | ``` 163 | 164 | Based on [Twitter module](http://mike.verdone.ca/twitter/). The only 2 differences: 165 | 166 | * The HTTP client have been replaced by Tornado AsyncHTTPClient. 167 | 168 | * \_\_call\_\_() accepts _callback as keyword argument. 169 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | from _facebook import FacebookGraphMixin 2 | from _foursquare import FoursquareMixin 3 | from _stripe import Stripe 4 | from _twitter import Twitter 5 | 6 | __all__ = ['FoursquareMixin', 'FacebookGraphMixin', 'Twitter', 'Stripe'] -------------------------------------------------------------------------------- /_facebook.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import logging 16 | import urllib 17 | 18 | from tornado import httpclient 19 | from tornado import escape 20 | from tornado.httputil import url_concat 21 | 22 | class FacebookGraphMixin(object): 23 | """Facebook authentication using the new Graph API and OAuth2.""" 24 | 25 | _OAUTH_ACCESS_TOKEN_URL = "https://graph.facebook.com/oauth/access_token" 26 | _OAUTH_AUTHORIZE_URL = "https://graph.facebook.com/oauth/authorize" 27 | 28 | _BASE_URL = "https://graph.facebook.com" 29 | 30 | @property 31 | def httpclient_instance(self): 32 | return httpclient.AsyncHTTPClient() 33 | 34 | 35 | def authorize_redirect(self, redirect_uri=None, client_id=None, **kwargs): 36 | """Redirects the user to obtain OAuth authorization for this service. 37 | 38 | Some providers require that you register a Callback 39 | URL with your application. You should call this method to log the 40 | user in, and then call get_authenticated_user() in the handler 41 | you registered as your Callback URL to complete the authorization 42 | process. 43 | """ 44 | args = { 45 | "redirect_uri": redirect_uri, 46 | "client_id": client_id 47 | } 48 | if kwargs: args.update(kwargs) 49 | self.redirect(url_concat(self._OAUTH_AUTHORIZE_URL, args)) 50 | 51 | 52 | def get_authenticated_user(self, redirect_uri, client_id, client_secret, code, callback): 53 | """Handles the login for the Facebook user, returning a user object. Example usage:: 54 | 55 | class FacebookGraphLoginHandler(LoginHandler, tornado.auth.FacebookGraphMixin): 56 | @tornado.web.asynchronous 57 | def get(self): 58 | if self.get_argument("code", False): 59 | self.get_authenticated_user( 60 | redirect_uri='/auth/facebookgraph/', 61 | client_id=self.settings["facebook_api_key"], 62 | client_secret=self.settings["facebook_secret"], 63 | code=self.get_argument("code"), 64 | callback=self.async_callback(self._on_login) 65 | ) 66 | return 67 | self.authorize_redirect( 68 | redirect_uri='/auth/facebookgraph/', 69 | client_id=self.settings["facebook_api_key"], 70 | scope="read_stream,offline_access" 71 | ) 72 | 73 | def _on_login(self, user): 74 | logging.error(user) 75 | self.finish() 76 | """ 77 | args = { 78 | "redirect_uri": redirect_uri, 79 | "code": code, 80 | "client_id": client_id, 81 | "client_secret": client_secret, 82 | } 83 | 84 | self.httpclient_instance.fetch( 85 | url_concat(self._OAUTH_ACCESS_TOKEN_URL, args), 86 | self.async_callback(self._on_access_token, redirect_uri, client_id, client_secret, callback) 87 | ) 88 | 89 | 90 | def _on_access_token(self, redirect_uri, client_id, client_secret, callback, response): 91 | if response.error: 92 | logging.warning('Facebook auth error: %s' % str(response)) 93 | callback(None) 94 | return 95 | 96 | args = escape.parse_qs_bytes(escape.native_str(response.body)) 97 | session = { 98 | "access_token": args["access_token"][-1], 99 | "expires": args.get("expires") 100 | } 101 | 102 | self.facebook_request( 103 | path="/me", 104 | callback=self.async_callback(self._on_get_user_info, callback, session), 105 | access_token=session["access_token"] 106 | ) 107 | 108 | 109 | def _on_get_user_info(self, callback, session, user): 110 | if user is None: 111 | callback(None) 112 | return 113 | 114 | user.update({"access_token": session.get("access_token"), "session_expires": session.get("expires")}) 115 | callback(user) 116 | 117 | 118 | def facebook_request(self, path, callback, access_token=None, post_args=None, **args): 119 | """Fetches the given relative API path, e.g., "/btaylor/picture" 120 | 121 | If the request is a POST, post_args should be provided. Query 122 | string arguments should be given as keyword arguments. 123 | 124 | An introduction to the Facebook Graph API can be found at 125 | http://developers.facebook.com/docs/api 126 | 127 | Many methods require an OAuth access token which you can obtain 128 | through authorize_redirect() and get_authenticated_user(). The 129 | user returned through that process includes an 'access_token' 130 | attribute that can be used to make authenticated requests via 131 | this method. Example usage:: 132 | 133 | class MainHandler(tornado.web.RequestHandler, tornado.auth.FacebookGraphMixin): 134 | @tornado.web.authenticated 135 | @tornado.web.asynchronous 136 | def get(self): 137 | self.facebook_request( 138 | "/me/feed", 139 | post_args={"message": "I am posting from my Tornado application!"}, 140 | access_token=self.current_user["access_token"], 141 | callback=self.async_callback(self._on_post)) 142 | 143 | def _on_post(self, new_entry): 144 | if not new_entry: 145 | # Call failed; perhaps missing permission? 146 | self.authorize_redirect() 147 | return 148 | self.finish("Posted a message!") 149 | 150 | """ 151 | url = self.__class__._BASE_URL + path 152 | 153 | all_args = {} 154 | if access_token: 155 | all_args["access_token"] = access_token 156 | all_args.update(args) 157 | 158 | if all_args: url += "?" + urllib.urlencode(all_args) 159 | 160 | callback = self.async_callback(self._on_facebook_request, callback) 161 | if post_args is not None: 162 | self.httpclient_instance.fetch(url, method="POST", body=urllib.urlencode(post_args), callback=callback) 163 | else: 164 | self.httpclient_instance.fetch(url, callback=callback) 165 | 166 | 167 | def _on_facebook_request(self, callback, response): 168 | if response.error: 169 | logging.warning("Error response %s fetching %s", response.error, response.request.url) 170 | callback(None) 171 | return 172 | callback(escape.json_decode(response.body)) 173 | -------------------------------------------------------------------------------- /_foursquare.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright 2012 Didip Kerabat 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import logging 18 | import urllib 19 | 20 | from tornado import httpclient 21 | from tornado import escape 22 | from tornado.httputil import url_concat 23 | 24 | class FoursquareMixin(object): 25 | """Foursquare API using Oauth2""" 26 | 27 | _OAUTH_ACCESS_TOKEN_URL = "https://foursquare.com/oauth2/access_token" 28 | _OAUTH_AUTHORIZE_URL = "https://foursquare.com/oauth2/authorize" 29 | _OAUTH_AUTHENTICATE_URL = "https://foursquare.com/oauth2/authenticate" 30 | 31 | _BASE_URL = "https://api.foursquare.com/v2" 32 | 33 | @property 34 | def httpclient_instance(self): 35 | return httpclient.AsyncHTTPClient() 36 | 37 | 38 | def authorize_redirect(self, redirect_uri=None, client_id=None, **kwargs): 39 | """Redirects the user to obtain OAuth authorization for this service. 40 | 41 | Some providers require that you register a Callback 42 | URL with your application. You should call this method to log the 43 | user in, and then call get_authenticated_user() in the handler 44 | you registered as your Callback URL to complete the authorization 45 | process. 46 | """ 47 | args = { 48 | "redirect_uri": redirect_uri, 49 | "client_id": client_id, 50 | "response_type": "code" 51 | } 52 | if kwargs: args.update(kwargs) 53 | self.redirect(url_concat(self._OAUTH_AUTHENTICATE_URL, args)) # Why _OAUTH_AUTHORIZE_URL fails? 54 | 55 | 56 | def get_authenticated_user(self, redirect_uri, client_id, client_secret, code, callback): 57 | """ 58 | Handles the login for the Foursquare user, returning a user object. 59 | 60 | Example usage:: 61 | 62 | class FoursquareLoginHandler(LoginHandler, FoursquareMixin): 63 | @tornado.web.asynchronous 64 | def get(self): 65 | if self.get_argument("code", False): 66 | self.get_authenticated_user( 67 | redirect_uri='/auth/foursquare/connect', 68 | client_id=self.settings["foursquare_client_id"], 69 | client_secret=self.settings["foursquare_client_secret"], 70 | code=self.get_argument("code"), 71 | callback=self.async_callback(self._on_login) 72 | ) 73 | return 74 | 75 | self.authorize_redirect( 76 | redirect_uri='/auth/foursquare/connect', 77 | client_id=self.settings["foursquare_api_key"] 78 | ) 79 | 80 | def _on_login(self, user): 81 | logging.error(user) 82 | self.finish() 83 | """ 84 | args = { 85 | "redirect_uri": redirect_uri, 86 | "code": code, 87 | "client_id": client_id, 88 | "client_secret": client_secret, 89 | "grant_type": "authorization_code" 90 | } 91 | 92 | self.httpclient_instance.fetch( 93 | url_concat(self._OAUTH_ACCESS_TOKEN_URL, args), 94 | self.async_callback(self._on_access_token, redirect_uri, client_id, client_secret, callback) 95 | ) 96 | 97 | 98 | def _on_access_token(self, redirect_uri, client_id, client_secret, callback, response): 99 | if response.error: 100 | logging.warning('Foursquare auth error: %s' % str(response)) 101 | callback(None) 102 | return 103 | 104 | session = escape.json_decode(response.body) 105 | 106 | self.foursquare_request( 107 | path="/users/self", 108 | callback=self.async_callback(self._on_get_user_info, callback, session), 109 | access_token=session["access_token"] 110 | ) 111 | 112 | 113 | def _on_get_user_info(self, callback, session, user): 114 | if user is None: 115 | callback(None) 116 | return 117 | 118 | user.update({ 119 | 'first_name': user.get('firstName'), 120 | 'last_name': user.get('lastName'), 121 | 'home_city': user.get('homeCity'), 122 | 'access_token': session['access_token'] 123 | }) 124 | callback(user) 125 | 126 | 127 | def foursquare_request(self, path, callback, access_token=None, post_args=None, **args): 128 | """ 129 | If the request is a POST, post_args should be provided. Query 130 | string arguments should be given as keyword arguments. 131 | 132 | See: https://developer.foursquare.com/docs/ 133 | """ 134 | url = self.__class__._BASE_URL + path 135 | 136 | all_args = {} 137 | if access_token: 138 | all_args["access_token"] = access_token 139 | all_args["oauth_token"] = access_token 140 | all_args.update(args) 141 | 142 | if all_args: url += "?" + urllib.urlencode(all_args) 143 | 144 | callback = self.async_callback(self._on_foursquare_request, callback) 145 | if post_args is not None: 146 | self.httpclient_instance.fetch(url, method="POST", body=urllib.urlencode(post_args), callback=callback) 147 | else: 148 | self.httpclient_instance.fetch(url, callback=callback) 149 | 150 | 151 | def _on_foursquare_request(self, callback, response): 152 | response_body = escape.json_decode(response.body) 153 | if response.error: 154 | logging.warning( 155 | "Foursquare Error(%s) :: Detail: %s, Message: %s, URL: %s", 156 | response.error, response_body["meta"]["errorDetail"], response_body["meta"]["errorMessage"], response.request.url 157 | ) 158 | callback(None) 159 | return 160 | callback(response_body) 161 | -------------------------------------------------------------------------------- /_stripe.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright 2012 Didip Kerabat 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import logging 18 | import urllib 19 | import functools 20 | 21 | from tornado import httpclient, escape 22 | 23 | class Stripe(object): 24 | api_hostname = 'api.stripe.com' 25 | api_version = 'v1' 26 | 27 | resources = set([ 28 | 'charges', 29 | 'customers', 30 | 'invoices', 31 | 'invoiceitems', 32 | 'tokens', 33 | 'events', 34 | 'plans', 35 | 'coupons', 36 | 'subscription', 37 | 'incoming' 38 | ]) 39 | 40 | def __init__(self, api_key, blocking=False): 41 | self.api_key = api_key 42 | self.blocking = blocking 43 | self.url = None 44 | 45 | if blocking: 46 | self.httpclient_instance = httpclient.HTTPClient() 47 | else: 48 | self.httpclient_instance = httpclient.AsyncHTTPClient() 49 | 50 | 51 | def __getattr__(self, name): 52 | ''' 53 | Builds API URL. 54 | Example: 55 | tornado_api.Stripe('api_key').plans.get(callback=lambda x: x) 56 | ''' 57 | if name in self.__class__.resources: 58 | self.url = '/'.join([self.url or self.api_endpoint, name]) 59 | return self 60 | else: 61 | raise AttributeError(name) 62 | 63 | 64 | @property 65 | def api_endpoint(self): 66 | return 'https://%s:@%s/%s' % (self.api_key, self.__class__.api_hostname, self.__class__.api_version) 67 | 68 | 69 | def id(self, id): 70 | ''' 71 | Append ID to constructed URL. 72 | Example: 73 | customer_id = 'cus_xyz' 74 | tornado_api.Stripe('api_key').customers.id(customer_id).subscription.post(callback=lambda x: x) 75 | ''' 76 | self.url = '/'.join([self.url or self.api_endpoint, str(id)]) 77 | return self 78 | 79 | 80 | def reset_url(self): 81 | self.url = None 82 | 83 | 84 | def get(self, **kwargs): 85 | return self._call_check_blocking_first('GET', **kwargs) 86 | 87 | 88 | def post(self, **kwargs): 89 | return self._call_check_blocking_first('POST', **kwargs) 90 | 91 | 92 | def put(self, **kwargs): 93 | return self._call_check_blocking_first('PUT', **kwargs) 94 | 95 | 96 | def delete(self, **kwargs): 97 | return self._call_check_blocking_first('DELETE', **kwargs) 98 | 99 | 100 | def _call_check_blocking_first(self, http_method, **kwargs): 101 | if self.blocking: 102 | http_response = self._call(http_method, **kwargs) 103 | return self._parse_response(None, http_response) 104 | else: 105 | return self._call(http_method, **kwargs) 106 | 107 | 108 | def _call(self, http_method, callback=None, **kwargs): 109 | copy_of_url = self.url 110 | 111 | # reset self.url 112 | self.reset_url() 113 | 114 | httpclient_args = [copy_of_url] 115 | 116 | if not self.blocking: 117 | if not callback: 118 | callback = lambda x: x 119 | 120 | httpclient_args.append(functools.partial(self._parse_response, callback)) 121 | 122 | httpclient_kwargs = { 'method': http_method } 123 | 124 | if http_method != 'GET' and kwargs: 125 | httpclient_kwargs['body'] = urllib.urlencode(self._nested_dict_to_url(kwargs)) 126 | 127 | return self.httpclient_instance.fetch(*httpclient_args, **httpclient_kwargs) 128 | 129 | 130 | def _nested_dict_to_url(self, d): 131 | """ 132 | We want post vars of form: 133 | {'foo': 'bar', 'nested': {'a': 'b', 'c': 'd'}} 134 | to become (pre url-encoding): 135 | foo=bar&nested[a]=b&nested[c]=d 136 | """ 137 | stk = [] 138 | for key, value in d.items(): 139 | if isinstance(value, dict): 140 | n = {} 141 | for k, v in value.items(): 142 | n["%s[%s]" % (key, k)] = v 143 | stk.extend(self._nested_dict_to_url(n)) 144 | else: 145 | stk.append((key, value)) 146 | return stk 147 | 148 | 149 | def _parse_response(self, callback, response): 150 | """Parse a response from the API""" 151 | try: 152 | res = escape.json_decode(response.body) 153 | except Exception, e: 154 | e.args += ('API response was: %s' % response,) 155 | raise e 156 | 157 | if res.get('error'): 158 | raise Exception('Error(%s): %s' % (res['error']['type'], res['error']['message'])) 159 | 160 | if callback: 161 | callback(res) 162 | else: 163 | return res 164 | 165 | -------------------------------------------------------------------------------- /_twitter.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright 2012 Didip Kerabat 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import logging 18 | 19 | import twitter 20 | from tornado import httpclient, escape 21 | 22 | class Twitter(twitter.Twitter): 23 | """ 24 | Extension of twitter.Twitter to use tornado.httpclient(). 25 | Requirement: 26 | - twitter egg. See: http://mike.verdone.ca/twitter/ 27 | 28 | Why? 29 | I want to perform Twitter request outside Tornado's request life-cycle. 30 | Thus, the mixin is kind of useless. 31 | But at the same time, I don't want blocking library. 32 | """ 33 | def _http_protocol(self): 34 | return "https" if self.secure else "http" 35 | 36 | 37 | def _http_method_from_kwargs(self, kwargs): 38 | method = kwargs.pop('_method', "GET") 39 | for action in twitter.twitter_globals.POST_ACTIONS: 40 | if re.search("%s(/\d+)?$" % action, uri): 41 | method = "POST" 42 | return method, kwargs 43 | 44 | 45 | def _http_callback_from_kwargs(self, kwargs): 46 | callback = kwargs.pop('_callback', None) 47 | return callback, kwargs 48 | 49 | 50 | def _http_request_path_from_kwargs(self, kwargs): 51 | # If this part matches a keyword argument, use the 52 | # supplied value otherwise, just use the part. 53 | uri = '/'.join([ 54 | str(kwargs.pop(uripart, uripart)) 55 | for uripart in self.uriparts 56 | ]) 57 | 58 | # If an id kwarg is present and there is no id to fill in in 59 | # the list of uriparts, assume the id goes at the end. 60 | id = kwargs.pop('id', None) 61 | if id: uri += "/%s" %(id) 62 | 63 | return uri, kwargs 64 | 65 | 66 | def __call__(self, **kwargs): 67 | protocol = self._http_protocol() 68 | method, kwargs = self._http_method_from_kwargs(kwargs) 69 | callback, kwargs = self._http_callback_from_kwargs(kwargs) 70 | uri, kwargs = self._http_request_path_from_kwargs(kwargs) 71 | 72 | url = "%s://%s/%s" %(protocol, self.domain, uri) 73 | if self.format: 74 | url += ".%s" %(self.format) 75 | 76 | headers = self.auth.generate_headers() if self.auth else {} 77 | 78 | arg_data = self.auth.encode_params(url, method, kwargs) 79 | if method == 'GET': 80 | url += '?' + arg_data 81 | body = None 82 | else: 83 | body = arg_data.encode('utf8') 84 | 85 | return self._handle_response(url, headers, method=method, body=body, callback=callback) 86 | 87 | 88 | def _handle_response(self, url, headers, method="GET", body=None, callback=None): 89 | http = httpclient.AsyncHTTPClient() 90 | if req.method == "POST": 91 | http.fetch(url, headers=headers, method=method, body=body, callback=self._on_twitter_request(callback)) 92 | else: 93 | http.fetch(url, headers=req.headers, callback=self._on_twitter_request(callback)) 94 | 95 | 96 | def _on_twitter_request(self, callback): 97 | if not callback: callback = lambda x: x 98 | 99 | def call_me_later(response): 100 | if response.error: 101 | logging.warning("Error response %s fetching %s", response.error, response.request.url) 102 | callback(None) 103 | return 104 | 105 | if self.format == "json": 106 | callback(escape.json_decode(response.body)) 107 | else: 108 | callback(response.body) 109 | return call_me_later 110 | 111 | -------------------------------------------------------------------------------- /tests/stripe_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os, os.path, sys 4 | sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..')) 5 | 6 | import unittest 7 | 8 | from tornado_api import Stripe 9 | 10 | DUMMY_PLAN = { 11 | 'amount': 2000, 12 | 'interval': 'month', 13 | 'name': 'Amazing Gold Plan', 14 | 'currency': 'usd', 15 | 'id': 'stripe-test-gold' 16 | } 17 | 18 | class UrlGenerationTest(unittest.TestCase): 19 | def setUp(self): 20 | unittest.TestCase.setUp(self) 21 | self.stripe = Stripe('api_key', blocking=True) 22 | 23 | def resource_without_id_test(self): 24 | ''' 25 | self.stripe.charges.url should == https://api_key:@api.stripe.com/v1/charges 26 | self.stripe.customers.url should == https://api_key:@api.stripe.com/v1/customers 27 | self.stripe.invoices.url should == https://api_key:@api.stripe.com/v1/invoices 28 | self.stripe.invoiceitems.url should == https://api_key:@api.stripe.com/v1/invoiceitems 29 | self.stripe.tokens.url should == https://api_key:@api.stripe.com/v1/tokens 30 | self.stripe.events.url should == https://api_key:@api.stripe.com/v1/events 31 | self.stripe.plans.url should == https://api_key:@api.stripe.com/v1/plans 32 | self.stripe.coupons.url should == https://api_key:@api.stripe.com/v1/coupons 33 | ''' 34 | for resource in ['charges', 'customers', 'invoices', 'invoiceitems', 'tokens', 'events', 'plans', 'coupons']: 35 | expectation = '%s/%s' % (self.stripe.api_endpoint, resource) 36 | 37 | getattr(self.stripe, resource) # Equivalent of self.stripe.charges 38 | self.assertEqual(self.stripe.url, expectation) 39 | self.stripe.reset_url() 40 | 41 | 42 | def resource_with_id_test(self): 43 | ''' 44 | self.stripe.charges.id('charge_id').url should == https://api_key:@api.stripe.com/v1/charges/charge_id 45 | self.stripe.customers.id('customer_id').url should == https://api_key:@api.stripe.com/v1/customers/customer_id 46 | self.stripe.invoices.id('invoice_id').url should == https://api_key:@api.stripe.com/v1/invoices/invoice_id 47 | self.stripe.invoiceitems.id('invoiceitem').url should == https://api_key:@api.stripe.com/v1/invoiceitems/invoiceitem_id 48 | self.stripe.tokens.id('token_id').url should == https://api_key:@api.stripe.com/v1/tokens/token_id 49 | self.stripe.events.id('event_id').url should == https://api_key:@api.stripe.com/v1/events/event_id 50 | self.stripe.plans.id('plan_id').url should == https://api_key:@api.stripe.com/v1/plans/plan_id 51 | self.stripe.coupons.id('coupon_id').url should == https://api_key:@api.stripe.com/v1/coupons/coupon_id 52 | ''' 53 | for resource in ['charges', 'customers', 'invoices', 'invoiceitems', 'tokens', 'events']: 54 | id = resource[:-1] + '_id' 55 | expectation = '%s/%s/%s' % (self.stripe.api_endpoint, resource, id) 56 | 57 | getattr(self.stripe, resource) # Equivalent of self.stripe.charges 58 | self.stripe.id(id) 59 | 60 | self.assertEqual(self.stripe.url, expectation) 61 | self.stripe.reset_url() 62 | 63 | 64 | def resource_after_id_test(self): 65 | ''' 66 | self.stripe.customers.id('customer_id').subscription.url 67 | should == https://api_key:@api.stripe.com/v1/customers/customer_id/subscription 68 | ''' 69 | id = 'customer_id' 70 | expectation = '%s/customers/%s/subscription' % (self.stripe.api_endpoint, id) 71 | 72 | self.stripe.customers.id(id).subscription 73 | 74 | self.assertEqual(self.stripe.url, expectation) 75 | self.stripe.reset_url() 76 | 77 | 78 | def nested_resource_test(self): 79 | ''' 80 | self.stripe.invoices.incoming.url 81 | should == https://api_key:@api.stripe.com/v1/invoices/incoming 82 | ''' 83 | expectation = '%s/invoices/incoming' % (self.stripe.api_endpoint) 84 | 85 | self.stripe.invoices.incoming 86 | 87 | self.assertEqual(self.stripe.url, expectation) 88 | self.stripe.reset_url() 89 | 90 | 91 | class BadApiKeyTest(unittest.TestCase): 92 | def setUp(self): 93 | unittest.TestCase.setUp(self) 94 | self.stripe = Stripe('api_key', blocking=True) 95 | 96 | 97 | def bad_api_key_get_test(self): 98 | try: 99 | self.stripe.plans.get() 100 | except Exception, e: 101 | self.assertEqual(e.__class__.__name__, 'HTTPError') 102 | self.assertTrue(str(e).find('401') > -1) 103 | self.assertTrue(str(e).find('Unauthorized') > -1) 104 | 105 | 106 | class GoodApiKeyTest(unittest.TestCase): 107 | ''' 108 | To run this test on CLI: 109 | export STRIPE_API_KEY=your-stripe-api-key; nosetests 110 | ''' 111 | def setUp(self): 112 | unittest.TestCase.setUp(self) 113 | api_key = os.environ.get('STRIPE_API_KEY', None) 114 | if not api_key: 115 | raise KeyError("You must set STRIPE_API_KEY environment variable. Example: export STRIPE_API_KEY=your-stripe-api-key; nosetests") 116 | 117 | self.stripe = Stripe(api_key, blocking=True) 118 | 119 | 120 | class PlansTest(GoodApiKeyTest): 121 | def crud_test(self): 122 | # Test creating DUMMY_PLAN 123 | self.stripe.plans.post(**DUMMY_PLAN) 124 | 125 | # Test getting created DUMMY_PLAN 126 | plan = self.stripe.plans.id(DUMMY_PLAN['id']).get() 127 | for key in DUMMY_PLAN.keys(): 128 | self.assertEqual(plan[key], DUMMY_PLAN[key]) 129 | 130 | # Test deletion of DUMMY_PLAN 131 | self.stripe.plans.id(DUMMY_PLAN['id']).delete() 132 | 133 | # After deletion, such plan should not exists. 134 | try: 135 | plan = self.stripe.plans.id(DUMMY_PLAN['id']).get() 136 | except Exception, e: 137 | self.assertEqual(e.__class__.__name__, 'HTTPError') 138 | self.assertTrue(str(e).find('404') > -1) 139 | self.assertTrue(str(e).find('Not Found') > -1) 140 | 141 | 142 | --------------------------------------------------------------------------------