├── .gitignore ├── LICENSE.txt ├── README.md ├── django_braintree ├── __init__.py ├── forms.py └── odict.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py? 2 | .DS_Store 3 | *.egg-info -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) <2010> 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Info 2 | ==== 3 | 4 | This module provides an easy to use interface to Braintree using Django's built-in form system to allow Django developers to easily make use of the Braintree transparent redirect functionality to help with PCI DSS compliance issues. 5 | 6 | The django_braintree module supports all documented fields in the official transparent redirect documentation. You can selectively turn on/off fields as required by your use scenario (for example, hiding the shipping address in the transaction form). 7 | 8 | This module depends on the Braintree Python module, so please install it first. 9 | 10 | Braintree API Documentation 11 | --------------------------- 12 | 13 | * [Transparent redirect][1] 14 | * [Python module][2] 15 | * [Python module API documentation][3] 16 | 17 | [1]: http://www.braintreepaymentsolutions.com/gateway/transparent-redirect 18 | [2]: http://www.braintreepaymentsolutions.com/gateway/python 19 | [3]: http://www.braintreepaymentsolutions.com/gateway/python/docs/index.html 20 | 21 | Example 1: Simple 22 | ----------------- 23 | Download and install the django_braintree module, then create a form in one of your views. Start by installing the module in settings.py: 24 | 25 | import braintree 26 | 27 | INSTALLED_APPS = [ 28 | ... 29 | "django_braintree", 30 | ... 31 | ] 32 | 33 | # Braintree sandbox settings 34 | BRAINTREE_ENV = braintree.Environment.Sandbox 35 | BRAINTREE_MERCHANT = 'your_merchant_key' 36 | BRAINTREE_PUBLIC_KEY = 'your_public_key' 37 | BRAINTREE_PRIVATE_KEY = 'your_private_key' 38 | 39 | # If you cannot install M2Crypto (e.g. AppEngine): 40 | BRAINTREE_UNSAFE_SSL = True 41 | 42 | Next, create a view to use one of the transparent redirect forms: 43 | 44 | from django_braintree.forms import TransactionForm 45 | 46 | def myview(request): 47 | result = TransactionForm.get_result(request) 48 | 49 | # If successful redirect to a thank you page 50 | if result and result.is_success: 51 | return HttpResponseRedirect("/thanks") 52 | 53 | # Create the form. You MUST pass in the result to get error messages! 54 | myform = TransactionForm(result, redirect_url="http://mysite.com/myview") 55 | 56 | # Remove items we don't need 57 | myform.remove_section("transaction[shipping_address]") 58 | myform.remove_section("transaction[amount]") 59 | myform.remove_section("transaction[options]") 60 | 61 | # Set fields we want passed along 62 | myform.tr_fields["transaction"]["amount"] = "19.99" 63 | 64 | # Generate the tr_data signed field; this MUST be called! 65 | myform.generate_tr_data() 66 | 67 | return render("template.html", { 68 | "form": myform, 69 | }) 70 | 71 | Then, in your template rendering the form is easy: 72 | 73 |
74 | {{ form.as_table }} 75 | 76 |
77 | 78 | Example 2: Custom Form 79 | ---------------------- 80 | 81 | Creating a custom form like the one below provides an alternative to: 82 | 83 | myform.remove_section("transaction[amount]") 84 | myform.tr_fields["transaction"]["amount"] = "19.99" 85 | 86 | Follow steps shown in Example 1 above. In your Django application, create a 87 | forms.py module, and add the following to it: 88 | 89 | from django_braintree.forms import BraintreeForm 90 | from django_braintree.odict import OrderedDict 91 | 92 | class ProtectedAmountForm(BraintreeForm): 93 | 94 | tr_type = "Transaction" 95 | tr_fields = OrderedDict([ 96 | ("transaction", OrderedDict([ 97 | ("customer", OrderedDict([ 98 | ("first_name", None), 99 | ("last_name", None), 100 | ("email", None), 101 | ("phone", None),]), 102 | ), 103 | ("credit_card", OrderedDict([ 104 | ("cardholder_name", None), 105 | ("number", None), 106 | ("expiration_month", None), 107 | ("expiration_year", None), 108 | ("cvv", None)]), 109 | ), 110 | ("billing", OrderedDict([ 111 | ("postal_code", None), 112 | ("country_name", None)]), 113 | ), 114 | ])), 115 | ]) 116 | tr_labels = { 117 | "transaction": { 118 | "credit_card": { 119 | "cvv": "CVV", 120 | "expiration_month": "Expiration Month", 121 | "expiration_year": "Expiration Year", 122 | }, 123 | }, 124 | } 125 | tr_protected = { 126 | "transaction": { 127 | "amount": None, 128 | "type": None, 129 | "order_id": None, 130 | "customer_id": None, 131 | "payment_method_token": None, 132 | "customer": { 133 | "id": None, 134 | }, 135 | "credit_card": { 136 | "token": None, 137 | }, 138 | "options": { 139 | "store_in_vault": True 140 | }, 141 | }, 142 | } 143 | 144 | def __init__(self, amount, result=None, redirect_url=None, *args, **kwargs): 145 | self.tr_protected["transaction"]["amount"] = amount 146 | super(ProtectedAmountForm, self).__init__(result, redirect_url=redirect_url, *args, **kwargs) 147 | 148 | License 149 | ------- 150 | Django Braintree uses the MIT license. Please see the LICENSE file for full details. 151 | -------------------------------------------------------------------------------- /django_braintree/__init__.py: -------------------------------------------------------------------------------- 1 | import braintree 2 | 3 | from django.conf import settings 4 | 5 | if not hasattr(braintree.Configuration, "merchant_id"): 6 | braintree.Configuration.configure( 7 | getattr(settings, "BRAINTREE_ENV", braintree.Environment.Sandbox), 8 | getattr(settings, "BRAINTREE_MERCHANT", ""), 9 | getattr(settings, "BRAINTREE_PUBLIC_KEY", ""), 10 | getattr(settings, "BRAINTREE_PRIVATE_KEY", ""), 11 | ) 12 | 13 | if getattr(settings, "BRAINTREE_UNSAFE_SSL", False): 14 | braintree.Configuration.use_unsafe_ssl = True 15 | -------------------------------------------------------------------------------- /django_braintree/forms.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from copy import deepcopy 3 | 4 | from django import forms 5 | from django.forms.util import ErrorList 6 | from django.forms import widgets 7 | 8 | import braintree 9 | 10 | from .odict import OrderedDict 11 | 12 | class BraintreeForm(forms.Form): 13 | """ 14 | A base Braintree form that defines common behaviors for all the 15 | various forms in this file for implementing Braintree transparent 16 | redirects. 17 | 18 | When creating a new instance of a Braintree form you MUST pass in a 19 | result object as returned by BraintreeForm.get_result(...). You SHOULD 20 | also pass in a redirect_url keyword parameter. 21 | 22 | >>> result = MyForm.get_result(request) 23 | >>> form = MyForm(result, redirect_url="http://mysite.com/foo") 24 | 25 | Note that result may be None. 26 | 27 | Each BraintreeForm subclass must define a set of fields and its type, 28 | and can optionally define a set of labels and protected data. This is 29 | all dependent on the type of transparent redirect, which is documented 30 | here: 31 | 32 | http://www.braintreepaymentsolutions.com/gateway/transparent-redirect 33 | 34 | You can set any protected data easily: 35 | 36 | >>> form.tr_protected["options"]["submit_for_settlement"] = True 37 | 38 | Before rendering the form you MUST always generate the signed, hidden 39 | special data via: 40 | 41 | >>> form.generate_tr_data() 42 | 43 | To get the location to post data to you can use the action property in 44 | your templates: 45 | 46 |
47 | {{ form.as_table }} 48 | 49 |
50 | 51 | """ 52 | tr_type = "" 53 | 54 | # Order of fields matters so we used an ordered dictionary 55 | tr_fields = OrderedDict() 56 | tr_labels = {} 57 | tr_help = {} 58 | tr_protected = {} 59 | 60 | # A list of fields that should be boolean (checkbox) options 61 | tr_boolean_fields = [] 62 | 63 | @classmethod 64 | def get_result(cls, request): 65 | """ 66 | Get the result (or None) of a transparent redirect given a Django 67 | Request object. 68 | 69 | >>> result = MyForm.get_result(request) 70 | >>> if result.is_success: 71 | take_some_action() 72 | 73 | This method uses the request.META["QUERY_STRING"] parameter to get 74 | the HTTP query string. 75 | """ 76 | try: 77 | result = braintree.TransparentRedirect.confirm(request.META["QUERY_STRING"]) 78 | except (KeyError, braintree.exceptions.not_found_error.NotFoundError): 79 | result = None 80 | 81 | return result 82 | 83 | def __init__(self, result, *args, **kwargs): 84 | self.redirect_url = kwargs.pop("redirect_url", "") 85 | self.update_id = kwargs.pop("update_id", None) 86 | 87 | # Create the form instance, with initial data if it was given 88 | if result: 89 | self.result = result 90 | data = self._flatten_dictionary(result.params) 91 | 92 | super(BraintreeForm, self).__init__(data, *args, **kwargs) 93 | 94 | # Are there any errors we should display? 95 | errors = self._flatten_errors(result.errors.errors.data) 96 | self.errors.update(errors) 97 | 98 | else: 99 | super(BraintreeForm, self).__init__(*args, **kwargs) 100 | 101 | # Dynamically setup all the required form fields 102 | # This is required because of the strange naming scheme that uses 103 | # characters not supported in Python variable names. 104 | labels = self._flatten_dictionary(self.tr_labels) 105 | helptext = self._flatten_dictionary(self.tr_help) 106 | for key in self._flatten_dictionary(self.tr_fields).keys(): 107 | if key in labels: 108 | label = labels[key] 109 | else: 110 | label = key.split("[")[-1].strip("]").replace("_", " ").title() 111 | 112 | #override for default field 113 | if isinstance(self._flatten_dictionary(self.tr_fields)[key], forms.Field): 114 | self.fields[key] = self._flatten_dictionary(self.tr_fields)[key] 115 | self.fields[key].label = label 116 | continue 117 | 118 | if key in self.tr_boolean_fields: 119 | # A checkbox MUST set value="true" for Braintree to pick 120 | # it up properly, refer to Braintree ticket #26438 121 | field = forms.BooleanField(label=label, required=False, widget=widgets.CheckboxInput(attrs={"checked": True, "value": "true", "class": "checkbox"})) 122 | elif key.endswith("[expiration_month]"): 123 | # Month selection should be a simple dropdown 124 | field = forms.ChoiceField(choices=[(x,x) for x in range(1, 13)], required=False, label=label) 125 | elif key.endswith("[expiration_year]"): 126 | # Year selection should be a simple dropdown 127 | year = datetime.date.today().year 128 | field = forms.ChoiceField(choices=[(x,x) for x in range(year, year + 16)], required=False, label=label) 129 | else: 130 | field = forms.CharField(label=label, required=False) 131 | 132 | if key in helptext: 133 | field.help_text = helptext[key] 134 | 135 | self.fields[key] = field 136 | 137 | def _flatten_dictionary(self, params, parent=None): 138 | """ 139 | Flatten a hierarchical dictionary into a simple dictionary. 140 | 141 | >>> self._flatten_dictionary({ 142 | "test": { 143 | "foo": 12, 144 | "bar": "hello", 145 | }, 146 | "baz": False 147 | }) 148 | { 149 | "test[foo]": 12, 150 | "test[bar]": hello, 151 | "baz": False 152 | } 153 | 154 | """ 155 | data = OrderedDict() 156 | for key, val in params.items(): 157 | full_key = parent + "[" + key + "]" if parent else key 158 | if isinstance(val, dict): 159 | data.update(self._flatten_dictionary(val, full_key)) 160 | else: 161 | data[full_key] = val 162 | return data 163 | 164 | def _flatten_errors(self, params, parent=None): 165 | """ 166 | A modified version of the flatten_dictionary method above used 167 | to coerce the structure holding errors returned by Braintree into 168 | a flattened dictionary where the keys are the names of the fields 169 | and the values the error messages, which can be directly used to 170 | set the field errors on the Django form object for display in 171 | templates. 172 | """ 173 | data = OrderedDict() 174 | for key, val in params.items(): 175 | full_key = parent + "[" + key + "]" if parent else key 176 | if full_key.endswith("[errors]"): 177 | full_key = full_key[:-len("[errors]")] 178 | if isinstance(val, dict): 179 | data.update(self._flatten_errors(val, full_key)) 180 | elif key == "errors": 181 | for error in val: 182 | data[full_key + "[" + error["attribute"] + "]"] = [error["message"]] 183 | else: 184 | data[full_key] = [val] 185 | return data 186 | 187 | def _remove_none(self, data): 188 | """ 189 | Remove all items from a nested dictionary whose value is None. 190 | """ 191 | for key, value in data.items(): 192 | if value is None or isinstance(value, forms.Field): 193 | del data[key] 194 | if isinstance(value, dict): 195 | self._remove_none(data[key]) 196 | 197 | def generate_tr_data(self): 198 | """ 199 | Generate the special signed tr_data field required to properly 200 | render and submit the form to Braintree. This MUST be called 201 | prior to rendering the form! 202 | """ 203 | tr_data = deepcopy(self.tr_fields) 204 | 205 | if self._errors: 206 | tr_data.update(self.tr_protected) 207 | else: 208 | tr_data.recursive_update(self.tr_protected) 209 | 210 | self._remove_none(tr_data) 211 | 212 | if self.update_id: 213 | tr_data.update({self.update_key:self.update_id}) 214 | signed = getattr(braintree, self.tr_type).tr_data_for_update(tr_data, self.redirect_url) 215 | 216 | elif hasattr(getattr(braintree, self.tr_type), "tr_data_for_sale"): 217 | signed = getattr(braintree, self.tr_type).tr_data_for_sale(tr_data, self.redirect_url) 218 | 219 | else: 220 | signed = getattr(braintree, self.tr_type).tr_data_for_create(tr_data, self.redirect_url) 221 | 222 | self.fields["tr_data"] = forms.CharField(widget=widgets.HiddenInput({'value': signed})) 223 | 224 | def remove_section(self, section): 225 | """ 226 | Remove a section of fields from the form, e.g. allowing you to 227 | hide all shipping address information in one quick call if you 228 | don't care about it. 229 | """ 230 | for key in self.fields.keys(): 231 | if key.startswith(section): 232 | del self.fields[key] 233 | 234 | def clean(self): 235 | if isinstance(self.result, braintree.error_result.ErrorResult) and self.result.transaction: 236 | raise forms.ValidationError(u"Error Processing Credit Card: %s" % self.result.transaction.processor_response_text) 237 | 238 | @property 239 | def action(self): 240 | """ 241 | Get the location to post data to. Use this property in your 242 | templates, e.g.
. 243 | """ 244 | return braintree.TransparentRedirect.url() 245 | 246 | class TransactionForm(BraintreeForm): 247 | """ 248 | A form to enter transaction details. 249 | """ 250 | tr_type = "Transaction" 251 | tr_fields = OrderedDict([ 252 | ("transaction", OrderedDict([ 253 | ("amount", None), 254 | ("customer", OrderedDict([ 255 | ("first_name", None), 256 | ("last_name", None), 257 | ("company", None), 258 | ("email", None), 259 | ("phone", None), 260 | ("fax", None), 261 | ("website", None)]), 262 | ), 263 | ("credit_card", OrderedDict([ 264 | ("cardholder_name", None), 265 | ("number", None), 266 | ("expiration_month", None), 267 | ("expiration_year", None), 268 | ("cvv", None)]), 269 | ), 270 | ("billing", OrderedDict([ 271 | ("first_name", None), 272 | ("last_name", None), 273 | ("company", None), 274 | ("street_address", None), 275 | ("extended_address", None), 276 | ("locality", None), 277 | ("region", None), 278 | ("postal_code", None), 279 | ("country_name", None)]), 280 | ), 281 | ("shipping", OrderedDict([ 282 | ("first_name", None), 283 | ("last_name", None), 284 | ("company", None), 285 | ("street_address", None), 286 | ("extended_address", None), 287 | ("locality", None), 288 | ("region", None), 289 | ("postal_code", None), 290 | ("country_name", None)]), 291 | ), 292 | ("options", OrderedDict([ 293 | ("store_in_vault", None), 294 | ("add_billing_address_to_payment_method", None), 295 | ("store_shipping_address_in_vault", None)]), 296 | ), 297 | ])), 298 | ]) 299 | tr_labels = { 300 | "transaction": { 301 | "credit_card": { 302 | "cvv": "CVV", 303 | "expiration_month": "Expiration Month", 304 | "expiration_year": "Expiration Year", 305 | }, 306 | "options": { 307 | "store_in_vault": "Save credit card", 308 | "add_billing_address_to_payment_method": "Save billing address", 309 | "store_shipping_address_in_vault": "Save shipping address", 310 | }, 311 | }, 312 | } 313 | tr_protected = { 314 | "transaction": { 315 | "type": None, 316 | "order_id": None, 317 | "customer_id": None, 318 | "payment_method_token": None, 319 | "customer": { 320 | "id": None, 321 | }, 322 | "credit_card": { 323 | "token": None, 324 | }, 325 | "options": { 326 | "submit_for_settlement": None, 327 | }, 328 | }, 329 | } 330 | tr_boolean_fields = [ 331 | "transaction[options][store_in_vault]", 332 | "transaction[options][add_billing_address_to_payment_method]", 333 | "transaction[options][store_shipping_address_in_vault]", 334 | ] 335 | 336 | class CustomerForm(BraintreeForm): 337 | """ 338 | A form to enter a new customer. 339 | """ 340 | tr_type = "Customer" 341 | tr_fields = OrderedDict([ 342 | ("customer", OrderedDict([ 343 | ("first_name", None), 344 | ("last_name", None), 345 | ("company", None), 346 | ("email", None), 347 | ("phone", None), 348 | ("fax", None), 349 | ("website", None), 350 | ("credit_card", OrderedDict([ 351 | ("cardholder_name", None), 352 | ("number", None), 353 | ("expiration_month", None), 354 | ("expiration_year", None), 355 | ("cvv", None), 356 | ("billing_address", OrderedDict([ 357 | ("first_name", None), 358 | ("last_name", None), 359 | ("company", None), 360 | ("street_address", None), 361 | ("extended_address", None), 362 | ("locality", None), 363 | ("region", None), 364 | ("postal_code", None), 365 | ("country_name", None)]), 366 | )]), 367 | )]), 368 | ), 369 | ]) 370 | tr_labels = { 371 | "customer": { 372 | "credit_card": { 373 | "cvv": "CVV", 374 | }, 375 | }, 376 | } 377 | tr_protected = { 378 | "customer": { 379 | "id": None, 380 | "credit_card": { 381 | "token": None, 382 | "options": { 383 | "verify_card": None, 384 | }, 385 | }, 386 | }, 387 | } 388 | 389 | class CreditCardForm(BraintreeForm): 390 | """ 391 | A form to enter a new credit card. 392 | """ 393 | tr_type = "CreditCard" 394 | tr_fields = OrderedDict([ 395 | ("credit_card", OrderedDict([ 396 | ("cardholder_name", None), 397 | ("number", None), 398 | ("expiration_month", None), 399 | ("expiration_year", None), 400 | ("cvv", None), 401 | ("billing_address", OrderedDict([ 402 | ("first_name", None), 403 | ("last_name", None), 404 | ("company", None), 405 | ("street_address", None), 406 | ("extended_address", None), 407 | ("locality", None), 408 | ("region", None), 409 | ("postal_code", None), 410 | ("country_name", None)]), 411 | )]), 412 | ), 413 | ]) 414 | tr_labels = { 415 | "credit_card": { 416 | "cvv": "CVV", 417 | }, 418 | } 419 | tr_protected = { 420 | "credit_card": { 421 | "customer_id": None, 422 | "token": None, 423 | "options": { 424 | "verify_card": None, 425 | }, 426 | }, 427 | } 428 | 429 | 430 | -------------------------------------------------------------------------------- /django_braintree/odict.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | odict 4 | ~~~~~ 5 | 6 | This module is an example implementation of an ordered dict for the 7 | collections module. It's not written for performance (it actually 8 | performs pretty bad) but to show how the API works. 9 | 10 | 11 | Questions and Answers 12 | ===================== 13 | 14 | Why would anyone need ordered dicts? 15 | 16 | Dicts in python are unordered which means that the order of items when 17 | iterating over dicts is undefined. As a matter of fact it is most of 18 | the time useless and differs from implementation to implementation. 19 | 20 | Many developers stumble upon that problem sooner or later when 21 | comparing the output of doctests which often does not match the order 22 | the developer thought it would. 23 | 24 | Also XML systems such as Genshi have their problems with unordered 25 | dicts as the input and output ordering of tag attributes is often 26 | mixed up because the ordering is lost when converting the data into 27 | a dict. Switching to lists is often not possible because the 28 | complexity of a lookup is too high. 29 | 30 | Another very common case is metaprogramming. The default namespace 31 | of a class in python is a dict. With Python 3 it becomes possible 32 | to replace it with a different object which could be an ordered dict. 33 | Django is already doing something similar with a hack that assigns 34 | numbers to some descriptors initialized in the class body of a 35 | specific subclass to restore the ordering after class creation. 36 | 37 | When porting code from programming languages such as PHP and Ruby 38 | where the item-order in a dict is guaranteed it's also a great help 39 | to have an equivalent data structure in Python to ease the transition. 40 | 41 | Where are new keys added? 42 | 43 | At the end. This behavior is consistent with Ruby 1.9 Hashmaps 44 | and PHP Arrays. It also matches what common ordered dict 45 | implementations do currently. 46 | 47 | What happens if an existing key is reassigned? 48 | 49 | The key is *not* moved. This is consitent with existing 50 | implementations and can be changed by a subclass very easily:: 51 | 52 | class movingodict(odict): 53 | def __setitem__(self, key, value): 54 | self.pop(key, None) 55 | odict.__setitem__(self, key, value) 56 | 57 | Moving keys to the end of a ordered dict on reassignment is not 58 | very useful for most applications. 59 | 60 | Does it mean the dict keys are sorted by a sort expression? 61 | 62 | That's not the case. The odict only guarantees that there is an order 63 | and that newly inserted keys are inserted at the end of the dict. If 64 | you want to sort it you can do so, but newly added keys are again added 65 | at the end of the dict. 66 | 67 | I initializes the odict with a dict literal but the keys are not 68 | ordered like they should! 69 | 70 | Dict literals in Python generate dict objects and as such the order of 71 | their items is not guaranteed. Before they are passed to the odict 72 | constructor they are already unordered. 73 | 74 | What happens if keys appear multiple times in the list passed to the 75 | constructor? 76 | 77 | The same as for the dict. The latter item overrides the former. This 78 | has the side-effect that the position of the first key is used because 79 | the key is actually overwritten: 80 | 81 | >>> odict([('a', 1), ('b', 2), ('a', 3)]) 82 | odict.odict([('a', 3), ('b', 2)]) 83 | 84 | This behavor is consistent with existing implementation in Python 85 | and the PHP array and the hashmap in Ruby 1.9. 86 | 87 | This odict doesn't scale! 88 | 89 | Yes it doesn't. The delitem operation is O(n). This is file is a 90 | mockup of a real odict that could be implemented for collections 91 | based on an linked list. 92 | 93 | Why is there no .insert()? 94 | 95 | There are few situations where you really want to insert a key at 96 | an specified index. To now make the API too complex the proposed 97 | solution for this situation is creating a list of items, manipulating 98 | that and converting it back into an odict: 99 | 100 | >>> d = odict([('a', 42), ('b', 23), ('c', 19)]) 101 | >>> l = d.items() 102 | >>> l.insert(1, ('x', 0)) 103 | >>> odict(l) 104 | odict.odict([('a', 42), ('x', 0), ('b', 23), ('c', 19)]) 105 | 106 | :copyright: (c) 2008 by Armin Ronacher and PEP 273 authors. 107 | :license: modified BSD license. 108 | """ 109 | from itertools import izip, imap 110 | from copy import deepcopy 111 | 112 | missing = object() 113 | 114 | 115 | class OrderedDict(dict): 116 | """ 117 | Ordered dict example implementation. 118 | 119 | This is the proposed interface for a an ordered dict as proposed on the 120 | Python mailinglist (proposal_). 121 | 122 | It's a dict subclass and provides some list functions. The implementation 123 | of this class is inspired by the implementation of Babel but incorporates 124 | some ideas from the `ordereddict`_ and Django's ordered dict. 125 | 126 | The constructor and `update()` both accept iterables of tuples as well as 127 | mappings: 128 | 129 | >>> d = odict([('a', 'b'), ('c', 'd')]) 130 | >>> d.update({'foo': 'bar'}) 131 | >>> d 132 | odict.odict([('a', 'b'), ('c', 'd'), ('foo', 'bar')]) 133 | 134 | Keep in mind that when updating from dict-literals the order is not 135 | preserved as these dicts are unsorted! 136 | 137 | You can copy an odict like a dict by using the constructor, `copy.copy` 138 | or the `copy` method and make deep copies with `copy.deepcopy`: 139 | 140 | >>> from copy import copy, deepcopy 141 | >>> copy(d) 142 | odict.odict([('a', 'b'), ('c', 'd'), ('foo', 'bar')]) 143 | >>> d.copy() 144 | odict.odict([('a', 'b'), ('c', 'd'), ('foo', 'bar')]) 145 | >>> odict(d) 146 | odict.odict([('a', 'b'), ('c', 'd'), ('foo', 'bar')]) 147 | >>> d['spam'] = [] 148 | >>> d2 = deepcopy(d) 149 | >>> d2['spam'].append('eggs') 150 | >>> d 151 | odict.odict([('a', 'b'), ('c', 'd'), ('foo', 'bar'), ('spam', [])]) 152 | >>> d2 153 | odict.odict([('a', 'b'), ('c', 'd'), ('foo', 'bar'), ('spam', ['eggs'])]) 154 | 155 | All iteration methods as well as `keys`, `values` and `items` return 156 | the values ordered by the the time the key-value pair is inserted: 157 | 158 | >>> d.keys() 159 | ['a', 'c', 'foo', 'spam'] 160 | >>> d.values() 161 | ['b', 'd', 'bar', []] 162 | >>> d.items() 163 | [('a', 'b'), ('c', 'd'), ('foo', 'bar'), ('spam', [])] 164 | >>> list(d.iterkeys()) 165 | ['a', 'c', 'foo', 'spam'] 166 | >>> list(d.itervalues()) 167 | ['b', 'd', 'bar', []] 168 | >>> list(d.iteritems()) 169 | [('a', 'b'), ('c', 'd'), ('foo', 'bar'), ('spam', [])] 170 | 171 | Index based lookup is supported too by `byindex` which returns the 172 | key/value pair for an index: 173 | 174 | >>> d.byindex(2) 175 | ('foo', 'bar') 176 | 177 | You can reverse the odict as well: 178 | 179 | >>> d.reverse() 180 | >>> d 181 | odict.odict([('spam', []), ('foo', 'bar'), ('c', 'd'), ('a', 'b')]) 182 | 183 | And sort it like a list: 184 | 185 | >>> d.sort(key=lambda x: x[0].lower()) 186 | >>> d 187 | odict.odict([('a', 'b'), ('c', 'd'), ('foo', 'bar'), ('spam', [])]) 188 | 189 | .. _proposal: http://thread.gmane.org/gmane.comp.python.devel/95316 190 | .. _ordereddict: http://www.xs4all.nl/~anthon/Python/ordereddict/ 191 | """ 192 | 193 | def __init__(self, *args, **kwargs): 194 | dict.__init__(self) 195 | self._keys = [] 196 | self.update(*args, **kwargs) 197 | 198 | def __delitem__(self, key): 199 | dict.__delitem__(self, key) 200 | self._keys.remove(key) 201 | 202 | def __setitem__(self, key, item): 203 | if key not in self: 204 | self._keys.append(key) 205 | dict.__setitem__(self, key, item) 206 | 207 | def __deepcopy__(self, memo=None): 208 | if memo is None: 209 | memo = {} 210 | d = memo.get(id(self), missing) 211 | if d is not missing: 212 | return d 213 | memo[id(self)] = d = self.__class__() 214 | dict.__init__(d, deepcopy(self.items(), memo)) 215 | d._keys = self._keys[:] 216 | return d 217 | 218 | def __getstate__(self): 219 | return {'items': dict(self), 'keys': self._keys} 220 | 221 | def __setstate__(self, d): 222 | self._keys = d['keys'] 223 | dict.update(d['items']) 224 | 225 | def __reversed__(self): 226 | return reversed(self._keys) 227 | 228 | def __eq__(self, other): 229 | if isinstance(other, odict): 230 | if not dict.__eq__(self, other): 231 | return False 232 | return self.items() == other.items() 233 | return dict.__eq__(self, other) 234 | 235 | def __ne__(self, other): 236 | return not self.__eq__(other) 237 | 238 | def __cmp__(self, other): 239 | if isinstance(other, odict): 240 | return cmp(self.items(), other.items()) 241 | elif isinstance(other, dict): 242 | return dict.__cmp__(self, other) 243 | return NotImplemented 244 | 245 | @classmethod 246 | def fromkeys(cls, iterable, default=None): 247 | return cls((key, default) for key in iterable) 248 | 249 | def clear(self): 250 | del self._keys[:] 251 | dict.clear(self) 252 | 253 | def copy(self): 254 | return self.__class__(self) 255 | 256 | def items(self): 257 | return zip(self._keys, self.values()) 258 | 259 | def iteritems(self): 260 | return izip(self._keys, self.itervalues()) 261 | 262 | def keys(self): 263 | return self._keys[:] 264 | 265 | def iterkeys(self): 266 | return iter(self._keys) 267 | 268 | def pop(self, key, default=missing): 269 | if default is missing: 270 | return dict.pop(self, key) 271 | elif key not in self: 272 | return default 273 | self._keys.remove(key) 274 | return dict.pop(self, key, default) 275 | 276 | def popitem(self, key): 277 | self._keys.remove(key) 278 | return dict.popitem(key) 279 | 280 | def setdefault(self, key, default=None): 281 | if key not in self: 282 | self._keys.append(key) 283 | dict.setdefault(self, key, default) 284 | 285 | def _update(self, *args, **kwargs): 286 | recursive = kwargs.get('recursive', False) 287 | kwargs = kwargs.get('kwargs', {}) 288 | 289 | sources = [] 290 | if len(args) == 1: 291 | if hasattr(args[0], 'iteritems'): 292 | sources.append(args[0].iteritems()) 293 | else: 294 | sources.append(iter(args[0])) 295 | elif args: 296 | raise TypeError('expected at most one positional argument') 297 | if kwargs: 298 | sources.append(kwargs.iteritems()) 299 | for iterable in sources: 300 | for key, val in iterable: 301 | if (self.has_key(key) and recursive 302 | and isinstance(val, dict) 303 | and isinstance(self[key], dict)): 304 | if hasattr(self[key], "recursive_update"): 305 | self[key].recursive_update(val) 306 | else: 307 | self[key].update(val) 308 | else: 309 | self[key] = val 310 | 311 | def update(self, *args, **kwargs): 312 | return self._update(kwargs=kwargs, *args) 313 | 314 | def recursive_update(self, *args, **kwargs): 315 | return self._update(kwargs=kwargs, recursive=True, *args) 316 | 317 | def values(self): 318 | return map(self.get, self._keys) 319 | 320 | def itervalues(self): 321 | return imap(self.get, self._keys) 322 | 323 | def index(self, item): 324 | return self._keys.index(item) 325 | 326 | def byindex(self, item): 327 | key = self._keys[item] 328 | return (key, dict.__getitem__(self, key)) 329 | 330 | def reverse(self): 331 | self._keys.reverse() 332 | 333 | def sort(self, *args, **kwargs): 334 | self._keys.sort(*args, **kwargs) 335 | 336 | def __repr__(self): 337 | return 'odict.odict(%r)' % self.items() 338 | 339 | __copy__ = copy 340 | __iter__ = iterkeys 341 | 342 | 343 | if __name__ == '__main__': 344 | import doctest 345 | doctest.testmod() 346 | 347 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | 3 | setup( 4 | name='django-braintree', 5 | version="1.3.4", 6 | description='Django app for interacting with the braintree API', 7 | author='Daniel Taylor', 8 | author_email='dan@programmer-art.org', 9 | url = "https://github.com/danielgtaylor/braintree_django", 10 | packages=[ 11 | 'django_braintree' 12 | ], 13 | classifiers = [ 14 | "Development Status :: 5 - Production/Stable", 15 | "Environment :: Web Environment", 16 | "Intended Audience :: Developers", 17 | "License :: OSI Approved :: BSD License", 18 | "Operating System :: OS Independent", 19 | "Programming Language :: Python", 20 | "Framework :: Django", 21 | ], 22 | install_requires = [ 23 | 'braintree>=2.8' 24 | ], 25 | ) 26 | 27 | --------------------------------------------------------------------------------