Skip to content

Commit

Permalink
Merge pull request #12 from gadventures/dot-accessible-utility
Browse files Browse the repository at this point in the history
Utility function to wrap Python dict in dot-accessible model.
  • Loading branch information
bartek committed Jul 6, 2015
2 parents fbde8e7 + b356cb9 commit f61be2b
Show file tree
Hide file tree
Showing 3 changed files with 101 additions and 3 deletions.
47 changes: 46 additions & 1 deletion gapipy/models/base.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import datetime
from decimal import Decimal
from itertools import ifilter, ifilterfalse
import datetime

from gapipy import client as client_module
from gapipy.query import Query
Expand Down Expand Up @@ -194,3 +195,47 @@ def __init__(self, data):
getattr(self, self._related_resource_lookup))
resource = resource_cls({'id': self.id, 'href': self.href}, stub=True)
setattr(self, 'resource', resource)

class DictToModel(object):
"""
A simple container that is used by `utils.dict_to_model` to help create
dot-accessible models without being explicit about field definitions.
This class simply iterates over any complex values and recursively creates
new objects for those their respective keys, allowing the entire path to be
dot-accessible.
"""
def __init__(self, data, class_name=None):
self._class_name = self._humanize(class_name) if class_name else ''

# Add any shallow data to this object as primitive types.
shallow = self._shallow(data)
self.__dict__.update(shallow)

# Anything that will contain nested values that need to be dot
# accessible receive treatment of having its own class representation.
for k, v in self._deep(data).items():
if isinstance(v, (list, tuple)):
value = [DictToModel(i, class_name=k) for i in v]
else:
value = DictToModel(v, class_name=k)
setattr(self, k, value)

def __str__(self):
return self._class_name

def __repr__(self):
return '<{}>'.format(self.__str__())

def _humanize(self, class_name):
return class_name.replace("_", " ").title()

def _get_data(self, data):
""" Use as predicate for _shallow, _deep """
return isinstance(data[1], (dict, list))

def _shallow(self, data):
return {k: v for k, v in ifilterfalse(self._get_data, data.items())}

def _deep(self, data):
return {k: v for k, v in ifilter(self._get_data, data.items())}
19 changes: 18 additions & 1 deletion gapipy/utils.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from functools import partial
from importlib import import_module


def get_resource_class_from_class_name(name):
resource_module = import_module('gapipy.resources')
return getattr(resource_module, name)
Expand All @@ -19,6 +19,23 @@ def get_available_resource_classes():
resource_module = import_module('gapipy.resources')
return [getattr(resource_module, r) for r in available_resources]

def dict_to_model(class_name=None):
"""
Wrapper to be used within resource definitions for objects you'd like to be
dot-accessible without explicitly defining model fields for any nested data.
You should use this with `_model_fields`, or `_model_collection_fields`,
like so:
_model_collection_fields = [
('phone_numbers', dict_to_model('Phone Numbers')),
]
The optional`class_name` simply improves readability of the object
within a debug environment.
"""
from .models.base import DictToModel
return partial(DictToModel, class_name=class_name)

def is_free(amount):
"""
Expand Down
38 changes: 37 additions & 1 deletion tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
from unittest import TestCase

from gapipy.utils import (
dict_to_model,
duration_label,
humanize_amount,
humanize_price,
humanize_time,
duration_label,
location_label,
)

Expand Down Expand Up @@ -60,3 +61,38 @@ def test_location_label(self):
place_2 = Place(name='Montreal')
self.assertEqual(location_label(place_1, place_2), 'Toronto – Montreal')
self.assertEqual(location_label(place_1, place_1), 'Toronto')

def test_dict_to_model(self):
data = {
'id': '123',
'name': {
'first': 'Foo',
'last': 'Baz',
'reverse': {
'first': 'Oof',
'last': 'Zab',
},
},
'phone_numbers': [
{
'number': '555-555-5555',
}

],
}

wrapper = dict_to_model('Profile')
model = wrapper(data)

self.assertEqual(str(model), 'Profile')
self.assertEqual(repr(model), '<Profile>')
self.assertEqual(model.id, '123')
self.assertEqual(model.name.first, 'Foo')
self.assertEqual(model.name.last, 'Baz')
self.assertEqual(str(model.name), 'Name')

self.assertEqual(model.name.reverse.first, 'Oof')
self.assertEqual(model.name.reverse.last, 'Zab')

self.assertEqual(str(model.phone_numbers[0]), 'Phone Numbers')
self.assertEqual(model.phone_numbers[0].number, '555-555-5555')

0 comments on commit f61be2b

Please sign in to comment.