Commits

Kevin Chan committed acafd4f

Initial commit -- django-garage utility functions for Django development.

Comments (0)

Files changed (19)

+syntax: glob
+.DS_Store
+*~
+*.pyc
+*.bak
+*_BAK
+*_BACKUP
+*.bbprojectd
+*.sw[nop]
+\#*\#
+.\#*
+.~*\#
+*_UNUSED
+UNUSED/
+*_OLD
+build
+dist
+*.egg-info/
+.git
+.gitignore
+
+CHANGELOG
+
+2013-01-12 Kevin Chan <kefin@makedostudio.com>
+
+	* packaged garage repository into preliminary app distribution.
+Copyright (c) 2012-2013 Kevin Chan <kefin@makedostudio.com>
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+  notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+  notice, this list of conditions and the following disclaimer in the
+  documentation and/or other materials provided with the distribution.
+
+* Neither the name of the Makedostudio nor the names of its
+  contributors may be used to endorse or promote products derived from
+  this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+include LICENSE
+include README.md
+
+recursive-include docs *
+recursive-include garage/templates *
+recursive-include garage/*/templates *
+recursive-include garage/static *
+
+global-exclude docs/build
+global-exclude .DS_Store *.hgignore *.hgtags *_hgignore.txt *_gitignore.txt *.gitignore *.git *.hg
+
+django-garage
+=============
+
+Django-garage is a collection of useful functions and modules for
+building Django applications and projects.
+
+
+## Contact Info
+
+For questions about this application, please email Kevin Chan at
+kefin@makedostudio.com.
+syntax: glob
+.DS_Store
+*~
+*.pyc
+*.bak
+*_BAK
+*_BACKUP
+*.bbprojectd
+*.sw[nop]
+\#*\#
+.\#*
+.~*\#
+*_UNUSED
+UNUSED/
+*_OLD
+build
+dist
+*.egg-info/
+.git
+.gitignore
+

docs/coming-soon.txt

+Coming soon!

garage/__init__.py

+# -*- coding: utf-8 -*-
+"""
+garage
+
+Utilities and helpers functions.
+
+* created: 2011-02-15 Kevin Chan <kefin@makedostudio.com>
+* updated: 2013-01-12 kchan
+"""
+
+from django.conf import settings
+from django.template import RequestContext
+
+
+# helper functions and shortcuts
+
+# get setting
+
+def get_setting(name, default=None):
+    """Retrieve attribute from settings."""
+    return getattr(settings, name, default)
+
+
+# shortcuts
+
+def resp(request, template, context):
+    """Shortcut for render_to_response()."""
+    return render_to_response(template, context,
+                              context_instance=RequestContext(request))
+
+
+# get/set session vars
+
+def set_session_var(request, skey, sval):
+    """Set key-value in session cookie."""
+    try:
+        request.session[skey] = sval
+    except (TypeError, AttributeError):
+        pass
+
+
+def get_session_var(request, skey, default=None):
+    """Get value from session cookie."""
+    try:
+        return request.session.get(skey)
+    except (TypeError, AttributeError):
+        return None
+# -*- coding: utf-8 -*-
+"""
+garage.cache
+
+Cache helpers.
+
+* created: 2011-03-14 Kevin Chan <kefin@makedostudio.com>
+* updated: 2012-07-17 kchan
+"""
+
+import hashlib
+try:
+    import cPickle as pickle
+except ImportError:
+    import pickle
+
+from django.core.cache import cache
+
+from garage import get_setting, get_site_id, logger
+from garage.html_utils import safe_str
+
+
+
+# cache helpers
+# * simple caching for generic functions and objects
+# * uses django caching backend
+
+def s2hex(s):
+    """Convert any string to hex digits (for use as cache key)."""
+    try:
+        return hashlib.md5(s).hexdigest()
+    except UnicodeEncodeError:
+        return hashlib.md5(safe_str(s)).hexdigest()
+    except:
+        return hashlib.md5(repr(s)).hexdigest()
+
+
+# utility function to create cache key
+# * accepts prefix string
+
+def create_cache_key(s, prefix=None):
+    """Generate cache key based on input data"""
+    if prefix is None:
+        prefix = ''
+    return '%s%s' % (prefix, s2hex(s))
+
+
+# helper function to calculate cache key with site id prefix
+
+def cache_key(s, *args):
+    s = [s]
+    s.extend([a for a in args])
+    return create_cache_key('_'.join(s), prefix='%s_' % str(get_site_id()))
+
+
+# see:
+# http://djangosnippets.org/snippets/492/
+
+def cache_data(cache_key='', timeout=get_setting('OBJECT_CACHE_TIMEOUT')):
+    """
+    Decorator to cache objects.
+    """
+    def decorator(f):
+        def _cache_controller(*args, **kwargs):
+            if not get_setting('USE_MINI_CACHE'):
+                return f(*args, **kwargs)
+            if isinstance(cache_key, basestring):
+                k = cache_key % locals()
+            elif callable(cache_key):
+                k = cache_key(*args, **kwargs)
+            result = cache.get(k)
+            if result is None:
+                result = f(*args, **kwargs)
+                cache.set(k, result, timeout)
+                if get_setting('CACHE_DEBUG'):
+                    logger().debug('Cached data: %s' % k)
+                    # logger().debug('Cached data: %s | data: %s' \
+                    #               % (k, repr(result)))
+            else:
+                if get_setting('CACHE_DEBUG'):
+                    # logger().debug('Return cached data: %s | data: %s' \
+                    #               % (k, repr(result)))
+                    logger().debug('Return cached data: %s' % k)
+            return result
+        return _cache_controller
+    return decorator
+
+
+def delete_cache(cache_key):
+    """
+    Delete cached object.
+    """
+    if not get_setting('USE_MINI_CACHE'):
+        return False
+    if cache.get(cache_key):
+        cache.set(cache_key, None, 0)
+        result = True
+    else:
+        result = False
+    if get_setting('CACHE_DEBUG'):
+        if result is True:
+            logger().debug('delete_cache: Deleting cached data: %s' % cache_key)
+        else:
+            logger().debug('delete_cache: Unable to get cached data: %s' % cache_key)
+    return result
+
+
+#######################################################################
+# cache function
+# from: http://djangosnippets.org/snippets/202/
+
+# def cache_function(length):
+#   """
+#   A variant of the snippet posted by Jeff Wheeler at
+#   http://www.djangosnippets.org/snippets/109/
+#
+#   Caches a function, using the function and its arguments as the key, and the return
+#   value as the value saved. It passes all arguments on to the function, as
+#   it should.
+#
+#   The decorator itself takes a length argument, which is the number of
+#   seconds the cache will keep the result around.
+#
+#   It will put in a MethodNotFinishedError in the cache while the function is
+#   processing. This should not matter in most cases, but if the app is using
+#   threads, you won't be able to get the previous value, and will need to
+#   wait until the function finishes. If this is not desired behavior, you can
+#   remove the first two lines after the ``else``.
+#   """
+#   def decorator(func):
+#       def inner_func(*args, **kwargs):
+#           raw = [func.__name__, func.__module__, args, kwargs]
+#           pickled = pickle.dumps(raw, protocol=pickle.HIGHEST_PROTOCOL)
+#           key = hashlib.md5.new(pickled).hexdigest()
+#           value = cache.get(key)
+#           if cache.has_key(key):
+#               return value
+#           else:
+#               # This will set a temporary value while ``func`` is being
+#               # processed. When using threads, this is vital, as otherwise
+#               # the function can be called several times before it finishes
+#               # and is put into the cache.
+#               class MethodNotFinishedError(Exception): pass
+#               cache.set(key, MethodNotFinishedError(
+#                   'The function %s has not finished processing yet. This \
+# value will be replaced when it finishes.' % (func.__name__)
+#               ), length)
+#               result = func(*args, **kwargs)
+#               cache.set(key, result, length)
+#               return result
+#       return inner_func
+#   return decorator
+# -*- coding: utf-8 -*-
+"""
+garage.db
+
+Database and queryset helper functions
+
+* created: 2011-02-15 Kevin Chan <kefin@makedostudio.com>
+* updated: 2012-07-17 kchan
+"""
+
+import copy
+
+from garage import get_setting
+
+
+
+# batch queryset iterator
+#
+# from:
+# http://djangosnippets.org/snippets/1170/
+#
+# Most of the time when I need to iterate over Whatever.objects.all()
+# in a shell script, my machine promptly reminds me that sometimes even
+# 4GB isn't enough memory to prevent swapping like a mad man, and
+# bringing my laptop to a crawl. I've written 10 bazillion versions of
+# this code. Never again.
+#
+# Caveats
+#
+# Note that you'll want to order the queryset, as ordering is not
+# guaranteed by the database and you might end up iterating over some
+# items twice, and some not at all. Also, if your database is being
+# written to in between the time you start and finish your script,
+# you might miss some items or process them twice.
+#
+
+# batch size for queryset iterator
+# * number of entries to retrieve and iterate in batches
+QS_BATCH_SIZE = 50
+
+def batch_qs(qs, batch_size=get_setting('QS_BATCH_SIZE', QS_BATCH_SIZE)):
+    """
+    Returns a (start, end, total, queryset) tuple for each batch in the given
+    queryset.
+
+    Usage:
+    # Make sure to order your querset
+    article_qs = Article.objects.order_by('id')
+    for start, end, total, qs in batch_qs(article_qs):
+    print "Now processing %s - %s of %s" % (start + 1, end, total)
+    for article in qs:
+    print article.body
+    """
+    total = qs.count()
+    for start in range(0, total, batch_size):
+        end = min(start + batch_size, total)
+        yield (start, end, total, qs[start:end])
+
+
+# clone objects
+#
+# from:
+# http://djangosnippets.org/snippets/1271/
+
+class ClonableMixin(object):
+    def clone(self):
+        """Return an identical copy of the instance with a new ID."""
+        if not self.pk:
+            raise ValueError('Instance must be saved before it can be cloned.')
+        duplicate = copy.copy(self)
+        # Setting pk to None tricks Django into thinking this is a new object.
+        duplicate.pk = None
+        duplicate.save()
+        # ... but the trick loses all ManyToMany relations.
+        for field in self._meta.many_to_many:
+            source = getattr(self, field.attname)
+            destination = getattr(duplicate, field.attname)
+            for item in source.all():
+                destination.add(item)
+        return duplicate
+
+
+# See:
+# http://www.bromer.eu/2009/05/23/a-generic-copyclone-action-for-django-11/
+# http://djangosnippets.org/snippets/1271/
+# * The function below combines code from the above reference articles.
+
+def clone_objects(objects):
+    """
+    Generic model object cloner function.
+    """
+    def clone(obj):
+        """Return an identical copy of the instance with a new ID."""
+        if not obj.pk:
+            raise ValueError('Instance must be saved before it can be cloned.')
+        duplicate = copy.copy(obj)
+        # Setting pk to None tricks Django into thinking this is a new object.
+        duplicate.pk = None
+        duplicate.save()
+        # ... but the trick loses all ManyToMany relations.
+        for field in obj._meta.many_to_many:
+            source = getattr(obj, field.attname)
+            destination = getattr(duplicate, field.attname)
+            for item in source.all():
+                destination.add(item)
+        return duplicate
+
+    if not hasattr(objects,'__iter__'):
+        objects = [ objects ]
+
+    objs = []
+    for obj in objects:
+        new_obj = clone(obj)
+        new_obj.save()
+        objs.append(new_obj)
+
+    return objs
+# -*- coding: utf-8 -*-
+"""
+garage.debug
+
+Debugging utilities.
+
+* created: 2011-02-15 Kevin Chan <kefin@makedostudio.com>
+* updated: 2011-10-24 kchan
+"""
+
+from garage.logger import logger
+
+
+
+# decorator to time execution and dump to log file
+# * see: http://stackoverflow.com/questions/4170992/
+
+def print_latency(f):
+    def wrapped(*args, **kwargs):
+        try:
+            start = time.time()
+            r = f(*args, **kwargs)
+        finally:
+            logger().debug('Latency (%s): %.4fs' \
+                               % (f.__name__, time.time() - start))
+        return r
+    wrapped.__name__ = f.__name__
+    return wrapped

garage/help_text.py

+# -*- coding: utf-8 -*-
+"""
+garage.help_text
+
+Helper function to retrieve help text for backend admin form views.
+
+* created: 2011-03-18 Kevin Chan <kefin@makedostudio.com>
+* updated: 2012-07-17 kchan
+"""
+
+
+# maintain a help text registry for django models
+
+HELP_TEXT_REGISTRY = {}
+
+def register_help_text_dictionary(module, dictionary):
+    HELP_TEXT_REGISTRY[module] = dictionary
+
+
+def unregister_help_text_dictionary(module):
+    try:
+        d = HELP_TEXT_REGISTRY.get(module)
+        del HELP_TEXT_REGISTRY[module]
+        return d
+    except (AttributeError, KeyError):
+        return None
+
+
+def get_help_text_registry(module=None):
+    if module:
+        return HELP_TEXT_REGISTRY.get(module)
+    return HELP_TEXT_REGISTRY
+
+
+def get_help_text(module, model, field, default_dict={}):
+    """
+    Get help text for model and field in module help registry.
+
+    # TODO: write example instructions on how to use.
+    """
+    for d in [get_help_text_registry(module), default_dict]:
+        try:
+            txt = d[model].get(field)
+            if txt:
+                return txt
+        except (TypeError, KeyError):
+            pass
+    return ''

garage/html_utils.py

+# -*- coding: utf-8 -*-
+"""
+garage.html_utils
+
+HTML utility functions.
+
+* created: 2008-06-22 kevin chan <kefin@makedostudio.com>
+* updated: 2012-07-14 kchan
+"""
+
+import re
+import string
+from unicodedata import normalize
+from htmlentitydefs import codepoint2name, name2codepoint
+from markdown import markdown
+from textile import textile
+
+from garage.utils import trim, safe_unicode, safe_str
+
+
+
+### functions to escape html special characters
+
+# escape basic html special characters (quotes, brackets, ampersands, etc.)
+
+def html_escape(text):
+    """
+    Escape html entities within text.
+    """
+    htmlchars = {
+        "&": "&amp;",
+        '"': "&quot;",
+        #"'": "&apos;",
+        ">": "&gt;",
+        "<": "&lt;",
+        }
+    if not isinstance(text, basestring):
+        text = str(text)
+    return ''.join([htmlchars.get(c, c) for c in text])
+
+
+# convert non-ascii characters to html entities
+
+def html_entities(u):
+    result = []
+    for c in u:
+        if ord(c) < 128:
+            result.append(c)
+        else:
+            try:
+                result.append('&%s;' % codepoint2name[ord(c)])
+            except KeyError:
+                result.append("&#%s;" % ord(c))
+    return ''.join(result)
+
+
+# convert string into "safe" html
+
+def safe_html(data):
+    """
+    Convert string into "safe" html
+
+    * escapes entities
+    * converts unicode
+    """
+    return html_entities(safe_unicode(html_escape(data)))
+
+
+def escape(txt):
+    """Same as html_escape but accepts all kinds of input."""
+    if isinstance(txt, basestring):
+        return html_escape(txt)
+    if isinstance(txt, (list, tuple)):
+        return [html_escape(s) for s in txt]
+    try:
+        return dict([html_escape(k), html_escape(v)] for k, v in txt.items())
+    except (AttributeError, TypeError, KeyError):
+        return html_escape(txt)
+
+
+def strip_tags(html_txt):
+    """*Very simple* strip html tags function"""
+    return re.sub(r'<[^>]*?>', '', html_txt)
+
+
+def strip_html(text):
+    """
+    Removes HTML markup from a text string.
+
+    :Info: http://effbot.org/zone/re-sub.htm#unescape-html
+
+    :param text: The HTML source.
+    :return: The plain text.  If the HTML source contains non-ASCII
+    entities or character references, this is a Unicode string.
+    """
+    def fixup(m):
+        text = m.group(0)
+        if text[:1] == "<":
+            return "" # ignore tags
+        if text[:2] == "&#":
+            try:
+                if text[:3] == "&#x":
+                    return unichr(int(text[3:-1], 16))
+                else:
+                    return unichr(int(text[2:-1]))
+            except ValueError:
+                pass
+        elif text[:1] == "&":
+            import htmlentitydefs
+            entity = htmlentitydefs.entitydefs.get(text[1:-1])
+            if entity:
+                if entity[:2] == "&#":
+                    try:
+                        return unichr(int(entity[2:-1]))
+                    except ValueError:
+                        pass
+                else:
+                    return unicode(entity, "iso-8859-1")
+        return text # leave as is
+    return re.sub("(?s)<[^>]*>|&#?\w+;", fixup, text)
+
+
+def unescape(text):
+    """
+    Removes HTML or XML character references and entities from a text string.
+
+    :Info: http://effbot.org/zone/re-sub.htm#unescape-html
+
+    :param text: The HTML (or XML) source text.
+    :return: The plain text, as a Unicode string, if necessary.
+    """
+    def fixup(m):
+        text = m.group(0)
+        if text[:2] == "&#":
+            # character reference
+            try:
+                if text[:3] == "&#x":
+                    return unichr(int(text[3:-1], 16))
+                else:
+                    return unichr(int(text[2:-1]))
+            except ValueError:
+                pass
+        else:
+            # named entity
+            try:
+                text = unichr(name2codepoint[text[1:-1]])
+            except KeyError:
+                pass
+        return text # leave as is
+    return re.sub("&#?\w+;", fixup, text)
+
+
+# ents.pty
+# Convert SGML character entities into Unicode
+#
+# Function taken from:
+# http://stackoverflow.com/questions/1197981/convert-html-entities-to-ascii-in-python/1582036#1582036
+#
+# Thanks agazso!
+
+# def html2unicode(s):
+#     """
+#     Take an input string s, find all things that look like SGML character
+#     entities, and replace them with the Unicode equivalent.
+#
+#     :Info:
+#
+#     http://stackoverflow.com/questions/1197981/convert-html-entities-to-ascii-in-python/1582036#1582036
+#
+#     """
+#     matches = re.findall("&#\d+;", s)
+#     if len(matches) > 0:
+#         hits = set(matches)
+#         for hit in hits:
+#             name = hit[2:-1]
+#             try:
+#                 entnum = int(name)
+#                 s = s.replace(hit, unichr(entnum))
+#             except ValueError:
+#                 pass
+#     matches = re.findall("&\w+;", s)
+#     hits = set(matches)
+#     amp = "&"
+#     if amp in hits:
+#         hits.remove(amp)
+#     for hit in hits:
+#         name = hit[1:-1]
+#         if name in name2codepoint:
+#             s = s.replace(hit, unichr(name2codepoint[name]))
+#     s = s.replace(amp, "&")
+#     return s
+
+
+### slugify functions
+
+SlugDeleteChars = """'"‘’“”:;,~!@#$%^*()_+`=<>./?\\|—–"""
+
+def strip_accents(s):
+    """Strip accents from string and return ascii version."""
+    return normalize('NFKD', unicode(s)).encode('ASCII', 'ignore')
+
+def slugify(s, delete_chars=SlugDeleteChars):
+    """Slugify string."""
+    s = s.strip("\r\n")
+    s = s.replace("\n", " ")
+    s = trim(s)
+    s = strip_html(strip_accents(unescape(s)))
+    s = s.replace("–", "-")
+    s = s.replace("—", "-")
+    s = s.replace("&amp;", " and ")
+    s = s.replace("&", " and ")
+    s = re.sub(r'([0-9]+)%', '\\1-percent', s)
+    s = s.translate(string.maketrans(' _','--'), delete_chars).lower()
+    s = re.sub(r'--+', '-', s)
+    s = s.strip('-')
+    return s
+
+
+
+########################################################################
+# functions for converting plain text content to html
+# * available conversion methods:
+#   * no conversion
+#   * markdown
+#   * textile
+#   * simple conversion of line breaks
+#   * visual editor (using wysiwyg editor like TinyMCE)
+
+NO_CONVERSION = 1
+MARKDOWN_CONVERSION = 2
+TEXTILE_CONVERSION = 3
+SIMPLE_CONVERSION = 4
+VISUAL_EDITOR = 5
+
+CONVERSION_CHOICES = (
+    (NO_CONVERSION, 'None'),
+    (MARKDOWN_CONVERSION, 'Markdown'),
+    (TEXTILE_CONVERSION, 'Textile'),
+    (SIMPLE_CONVERSION, 'Simple (Convert Line Breaks)'),
+    (VISUAL_EDITOR, 'Visual (WYSIWYG) Editor'),
+)
+
+CONVERSION_METHODS = (
+    (NO_CONVERSION, 'none'),
+    (MARKDOWN_CONVERSION, 'markdown'),
+    (TEXTILE_CONVERSION, 'textile'),
+    (SIMPLE_CONVERSION, 'markdown'),
+    (VISUAL_EDITOR, 'visual')
+)
+
+def txt2html(txt, method):
+    try:
+        assert txt is not None and len(txt) > 0
+        if method == MARKDOWN_CONVERSION:
+            txt = markdown(txt)
+        elif method == TEXTILE_CONVERSION:
+            txt = textile(txt)
+        elif method == SIMPLE_CONVERSION:
+            txt = markdown(txt)
+        else:
+            # NO_CONVERSION
+            pass
+    except (TypeError, AssertionError):
+        pass
+    return txt
+
+
+def get_cvt_method(name):
+    """
+    Get conversion method "code" corresponding to name
+    """
+    c = {
+        'none': NO_CONVERSION,
+        'markdown': MARKDOWN_CONVERSION,
+        'textile': TEXTILE_CONVERSION
+    }
+    try:
+        method = c.get(name.lower(), 'none')
+    except (TypeError, AttributeError):
+        method = NO_CONVERSION
+    return method
+
+
+def get_cvt_method_name(code):
+    """
+    Get conversion method name corresponding to "code"
+    """
+    if code > 0:
+        code -= 1
+    try:
+        codenum, name = CONVERSION_METHODS[code]
+    except:
+        codenum, name = CONVERSION_METHODS[NO_CONVERSION]
+    return name
+
+
+def to_html(txt, cvt_method='markdown'):
+    """
+    Convert text block to html
+    * cvt_method is name of method (markdown, textile, or none)
+    * cf. txt2html where method is the conversion "code" (number)
+    """
+    return txt2html(txt, get_cvt_method(cvt_method))

garage/image_utils.py

+# -*- coding: utf-8 -*-
+"""
+garage.image_utils
+
+Image-processing utility functions.
+
+* created: 2012-08-10 Kevin Chan <kefin@makedostudio.com>
+* updated: 2012-09-08 kchan
+"""
+
+import os.path
+import re
+import imghdr
+import Image
+
+
+
+# image utilities
+
+DEFAULT_IMG_QUALITY = 75
+RE_PATS = [
+	r'^(.*)([_\-][0-9]+x[0-9]+)(\.[^\.]+)$',
+	r'^(.*)(\.[^\.]+)$'
+    ]
+Regexp = None
+DEFAULT_FNAME = 'image'
+
+
+def resize_image(img, box, fit):
+    """
+    Downsample image and resize to 'box' dimensions.
+    @param img: Image - an Image-object
+    @param box: tuple(x, y) - the bounding box of the result image
+    @param fit: boolean - crop the image to fill the box
+    """
+    w, h = box
+
+    def f2i(n):
+        return int(round(n))
+
+    if fit:
+        x, y = img.size
+        src_ratio = 1.0 * x/y
+        dst_ratio = 1.0 * w/h
+        if src_ratio > dst_ratio:
+            dy = 0.0
+            dx = dst_ratio * y - x
+        else:
+            dx = 0.0
+            dy = x / dst_ratio - y
+        x += dx
+        y += dy
+        a0 = 1.0 * abs(dx/2)
+        a1 = a0 + x
+        b0 = 1.0 * abs(dy/2)
+        b1 = b0 + y
+        a0 = f2i(a0)
+        a1 = f2i(a1)
+        b0 = f2i(b0)
+        b1 = f2i(b1)
+        crop_coordinates = (a0, b0, a1, b1)
+        img = img.crop(crop_coordinates)
+
+    img = img.resize(box, Image.ANTIALIAS)
+    return img
+
+
+def get_image_size(image_file):
+    img = Image.open(image_file)
+    imgw, imgh = img.size
+    return imgw, imgh
+
+
+def create_thumb(image_file, w, h, quality, dst, fbase, fext):
+    """
+    Uses resize function above.
+    """
+    quality = int(quality)
+
+    img = Image.open(image_file)
+
+    imgw, imgh = img.size
+    r = (1.0 * imgw) / imgh
+    if not w:
+        w = round(r * float(h))
+    if not h:
+        h = round(float(w) / r)
+
+    w = int(w)
+    h = int(h)
+
+    img = resize_image(img, (w, h,), True)
+    if img.mode != 'RGB':
+        img = img.convert('RGB')
+
+    filename = '%s-%dx%d.%s' % (fbase, w, h, fext)
+    output = os.path.join(dst, filename)
+    img.save(output, quality=quality)
+    return output
+
+
+def get_file_basename(f, default=DEFAULT_FNAME):
+    global Regexp
+    if Regexp is None:
+        Regexp = [re.compile(r, re.I) for r in RE_PATS]
+    fname = os.path.basename(f)
+    for r in Regexp:
+        m = r.match(fname)
+        if m:
+            return m.group(1)
+    return default
+
+
+def get_img_ext(path, default_ext='unknown'):
+    """
+    Detect image type from file path and return file extension.
+    """
+    imgtype = imghdr.what(path)
+    suffix = {
+        'rgb': 'rgb',
+        'gif': 'gif',
+        'pbm': 'pbm',
+        'pgm': 'pgm',
+        'ppm': 'ppm',
+        'tiff': 'tif',
+        'rast': 'rast',
+        'xbm': 'xbm',
+        'jpeg': 'jpg',
+        'bmp': 'bmp',
+        'png': 'png'
+        }
+    return suffix.get(imgtype, default_ext)
+
+
+def generate_thumb(img_file, width, height, quality, dest_dir):
+    """
+    Process image file and create thumbnail according to parameters.
+    """
+    fbase = get_file_basename(img_file)
+    fext = get_img_ext(img_file)
+    return create_thumb(img_file, width, height, quality, dest_dir, fbase, fext)
+# -*- coding: utf-8 -*-
+"""
+garage.logger
+
+Logging for debug purposes.
+
+* created: 2011-03-13 Kevin Chan <kefin@makedostudio.com>
+* updated: 2012-07-17 kchan
+"""
+
+import logging
+
+from garage import get_setting
+
+
+
+# settings for debug log - add to project settings:
+
+# LOG_DIR = os.path.join(SITE_ROOT, 'log')
+# LOG_FILE = os.path.join(LOG_DIR, '%s.log' % server_acct)
+# LOG_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
+# LOG_PROJECT_CODE = server_acct
+
+
+# simple logger using Python logging module
+
+log_levels = {
+    'notset': logging.NOTSET,
+    'debug': logging.DEBUG,
+    'info': logging.INFO,
+    'warning': logging.WARNING,
+    'error': logging.ERROR,
+    'critical': logging.CRITICAL
+    }
+
+log_fmt = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
+
+def create_log(logname, logfile=None, level='debug', format=log_fmt):
+    """
+    Create and return simple file logger.
+
+    * level is keyword in `log_levels` (`notset`, `debug`, `info`, etc.)
+    * format is format of log entry to output
+
+    :More info: `<http://docs.python.org/library/logging.html>`_
+
+    :param logname: name of log.
+    :param logfile: path of log file.
+    :param level: log level (see ``log_levels``).
+    :param format: log entry format (default is ``log_fmt``).
+    :returns: logger object.
+    """
+    log_level = log_levels.get(level)
+    logger = logging.getLogger(logname)
+    logger.setLevel(log_level)
+    if logfile:
+        handler = logging.FileHandler(logfile)
+    else:
+        handler = logging.StreamHandler()
+    handler.setLevel(log_level)
+    handler.setFormatter(logging.Formatter(format))
+    logger.addHandler(handler)
+    return logger
+
+
+# function to return a logging object for debug and diagnostic use
+
+DebugLogger = None
+
+def logger():
+    """
+    :return: simple logger object.
+    """
+    global DebugLogger
+    if not DebugLogger:
+        proj = get_setting('LOG_PROJECT_CODE')
+        logfile = get_setting('LOG_FILE')
+        logfmt = get_setting('LOG_FORMAT')
+        DebugLogger = create_log(logname=proj, logfile=logfile, format=logfmt)
+    return DebugLogger

garage/slugify.py

+# -*- coding: utf-8 -*-
+"""
+garage.slugify
+
+Functions to create slugs.
+
+* created: 2011-02-15 Kevin Chan <kefin@makedostudio.com>
+* updated: 2013-01-05 kchan
+"""
+
+import re
+import string
+from unicodedata import normalize
+
+from django.core.exceptions import ValidationError
+
+from garage import get_setting
+
+
+
+# general slugify function
+
+SlugDeleteChars = """'"‘’“”:;,~!@#$%^*()_+`=<>./?\\|—–"""
+SubstChar = u"-"
+
+def strip_accents(s):
+    """Strip accents from string and return ascii version."""
+    return normalize('NFKD', unicode(s)).encode('ASCII', 'ignore')
+
+# def slugify(s, delete_chars=SlugDeleteChars):
+#     """Slugify string."""
+#     from garage.utils import trim
+#     from garage.html_utils import strip_html, unescape
+#     s = s.strip("\r\n")
+#     s = s.replace("\n", " ")
+#     s = trim(s)
+#     s = strip_html(strip_accents(unescape(s)))
+#     s = s.replace("–", "-")
+#     s = s.replace("—", "-")
+#     s = s.replace("&amp;", " and ")
+#     s = s.replace("&", " and ")
+#     s = re.sub(r'([0-9]+)%', '\\1-percent', s)
+#     s = s.translate(string.maketrans(' _','--'), delete_chars).lower()
+#     s = re.sub(r'--+', '-', s)
+#     s = s.strip('-')
+#     return s
+
+
+def slugify(s, delete_chars=SlugDeleteChars, subst_char=SubstChar):
+    """
+    Convert (unicode) string to slug.
+    """
+    def convert_unwanted_chars(txt):
+        converted = []
+        for ch in txt:
+            if ch in delete_chars:
+                ch = subst_char
+            converted.append(ch)
+        return u''.join(converted)
+
+    s = s.decode("utf-8")
+    s = s.strip(u"\r\n")
+    s = s.replace(u"\n", u" ")
+    s = re.sub(r'[¡]', u'', s)
+    s = s.replace(u"’", u"'")
+    s = strip_tags(unescape(s))
+    s = re.sub(r"['’]s", u's', s)
+    s = re.sub(r'([0-9\.]+)%', u'\\1-percent', s)
+    s = s.replace(u"&amp;", u" and ")
+    s = s.replace(u"&", u" and ")
+    s = s.replace(u" ", u"-")
+    s = s.replace(u"_", u"-")
+    s = convert_unwanted_chars(s)
+    s = re.sub(r'\.\.+', u'.', s)
+    s = re.sub(r'--+', u'-', s)
+    s = s.strip(u'.')
+    s = s.strip(u'-')
+    s = s.lower()
+    return s
+
+
+# function to generate unique slugs for django model entries
+
+# default slug spearators
+SLUG_SEPARATOR = '-'
+SLUG_ITERATION_SEPARATOR = '--'
+
+def get_slug_separator():
+    return get_setting('SLUG_SEPARATOR', SLUG_SEPARATOR)
+
+def get_slug_iteration_separator():
+    """
+    The slug iteration separator is used to mark entries with the same slug base.
+
+    e.g. article (first entry)
+         article--2 (second entry)
+    """
+    return get_setting('SLUG_ITERATION_SEPARATOR', SLUG_ITERATION_SEPARATOR)
+
+
+slug_pat = r'^(.+)%s(\d+)$'
+slug_regex = None
+
+def get_slug_base(s, slug_iteration_separator=None):
+    """
+    Return slug minus the slug_iteration_separator + 'n' sufix.
+
+    * Example: 'article--2' will return 'article' if
+    slug_iteration_separator is '--'.
+    """
+    global slug_regex
+    if not slug_iteration_separator:
+        slug_iteration_separator = get_slug_iteration_separator()
+    if slug_regex is None:
+        slug_regex = re.compile(slug_pat % slug_iteration_separator, re.I)
+    m = slug_regex.match(s)
+    if m:
+        return m.group(1)
+    return s
+
+
+def slug_creation_error(msg=None):
+    if msg is None:
+        msg = 'Unable to create slug.'
+    raise ValidationError(msg)
+
+
+def get_unique_slug(instance, slug_field, queryset=None, slug_base=None,
+                    prefix=None, suffix=None, slug_separator=None):
+    """
+    Helper function to generate unique slug for model entries.
+
+    * object instance must already exist.
+    * unqiue slug has the format: prefix-slug-n
+    * examples:
+
+    if prefix is '20110311-', generated unique slugs for identically
+    slugged articles will produce:
+
+    article -> 20110311-article
+    article -> 20110211-article--2
+    article -> 20110211-article--3
+    etc.
+
+    :param instance: object instance
+    :param slug_base: slug to use as base for unique slug
+    :param slug_field: unique slug field name for queries to test uniqueness
+    :param queryset: queryset to use for db access (optional)
+    :param prefix: prefix to prepend to unique slug before doing query
+    :param suffix: suffix to prepend to unique slug before doing query
+    :param slug_separator: string to separate last part of the slug
+           from the base (default: SLUG_ITERATION_SEPARATOR)
+    :returns: (unque_slug, unique_slug_without_prefix)
+    """
+    if not prefix:
+        prefix = ''
+    if not suffix:
+        suffix = ''
+    if not slug_separator:
+        slug_separator = get_slug_iteration_separator()
+    try:
+        if queryset is None:
+            queryset = instance.__class__._default_manager.all()
+        if instance.pk:
+            queryset = queryset.exclude(pk=instance.pk)
+        if slug_base is None:
+            try:
+                slug_base = getattr(instance, slug_field)
+            except AttributeError:
+                slug_base = 'entry'
+        slug = None
+        num = ''
+        next = 1
+        while 1:
+            slug = '%s%s%s' % (slug_base, num, suffix)
+            unique_slug = '%s%s' % (prefix, slug)
+            if not queryset.filter(**{slug_field: unique_slug}):
+                return (unique_slug, slug)
+            next += 1
+            num = '%s%d' % (slug_separator, next)
+    except (AttributeError, TypeError):
+        slug_creation_error()
+
+
+def create_unique_slug(obj, slug_field=None):
+    """
+    Create simple unique slug for object instance.
+
+    * if slug_field is not supplied, assume it's called 'slug'.
+
+    :param instance: model object instance
+    :returns: unique slug
+    """
+    if not slug_field:
+        slug_field = 'slug'
+    try:
+        sbase = get_slug_base(getattr(obj, slug_field))
+    except (AttributeError, TypeError):
+        sbase = None
+    slugs = get_unique_slug(obj, slug_field=slug_field, slug_base=sbase)
+    return slugs[0]
+
+
+
+# unique slug function
+# from: http://djangosnippets.org/snippets/690/
+#
+# def unique_slugify(instance, value, slug_field_name='slug', queryset=None,
+#                  slug_separator='-'):
+#   """
+#   Calculates and stores a unique slug of ``value`` for an instance.
+#
+#   ``slug_field_name`` should be a string matching the name of the field to
+#   store the slug in (and the field to check against for uniqueness).
+#
+#   ``queryset`` usually doesn't need to be explicitly provided - it'll default
+#   to using the ``.all()`` queryset from the model's default manager.
+#   """
+#   slug_field = instance._meta.get_field(slug_field_name)
+#
+#   slug = getattr(instance, slug_field.attname)
+#   slug_len = slug_field.max_length
+#
+#   # Sort out the initial slug, limiting its length if necessary.
+#   slug = slugify(value)
+#   if slug_len:
+#       slug = slug[:slug_len]
+#   slug = _slug_strip(slug, slug_separator)
+#   original_slug = slug
+#
+#   # Create the queryset if one wasn't explicitly provided and exclude the
+#   # current instance from the queryset.
+#   if queryset is None:
+#       queryset = instance.__class__._default_manager.all()
+#   if instance.pk:
+#       queryset = queryset.exclude(pk=instance.pk)
+#
+#   # Find a unique slug. If one matches, at '-2' to the end and try again
+#   # (then '-3', etc).
+#   next = 2
+#   while not slug or queryset.filter(**{slug_field_name: slug}):
+#       slug = original_slug
+#       end = '%s%s' % (slug_separator, next)
+#       if slug_len and len(slug) + len(end) > slug_len:
+#           slug = slug[:slug_len-len(end)]
+#           slug = _slug_strip(slug, slug_separator)
+#       slug = '%s%s' % (slug, end)
+#       next += 1
+#
+#   setattr(instance, slug_field.attname, slug)
+#
+#
+# def _slug_strip(value, separator='-'):
+#   """
+#   Cleans up a slug by removing slug separator characters that occur at the
+#   beginning or end of a slug.
+#
+#   If an alternate separator is used, it will also replace any instances of
+#   the default '-' separator with the new separator.
+#   """
+#   separator = separator or ''
+#   if separator == '-' or not separator:
+#       re_sep = '-'
+#   else:
+#       re_sep = '(?:-|%s)' % re.escape(separator)
+#   # Remove multiple instances and if an alternate separator is provided,
+#   # replace the default '-' separator.
+#   if separator != re_sep:
+#       value = re.sub('%s+' % re_sep, separator, value)
+#   # Remove separator from the beginning and end of the slug.
+#   if separator:
+#       if separator != '-':
+#           re_sep = re.escape(separator)
+#       value = re.sub(r'^%s+|%s+$' % (re_sep, re_sep), '', value)
+#   return value
+
+#######################################################################
+# -*- coding: utf-8 -*-
+"""
+urlgen.py
+
+Django Uri Param Generator
+
+from:
+http://djangosnippets.org/snippets/1734/
+
+
+USAGE EXAMPLE
+
+[ urlGen Location ]
+    |-- __init__.py
+    |-- urlgen.py
+[ Sample File views.py ]
+
+from urlgen.urlgen import urlGen
+uri = urlGen()
+orderURI = uri.generate('order', request.GET)
+return render_to_response('templates.html', {'orderURI': orderURI,}, context_instance=RequestContext(request))
+
+[ template.html ]
+Order By : <a href="{{ pageURI }}name">Name</a> | <a href="{{ orderURI }}latest">Latest</a>
+
+[ HTML OUTPUT ]
+<a href="?order=name">Name</a> | <a href="?order=latest">Latest</a>
+
+uri = urlGen()
+
+order = uri.generate('order', request.GET)
+# OUTPUT
+# ?order=
+page = uri.generate('page', {'search': 'unus', 'order': 'name', 'page': 15})
+# OUTPUT
+# ?search=unus&order=name&page=
+
+
+
+* created: 2011-01-07 Kevin Chan <kefin@makedostudio.com>
+* updated: 2011-01-07 kchan
+"""
+
+
+class urlGen:
+
+    """
+        REVISION: 1.0
+        AUTHOR: NICKOLAS WHITING
+
+        Build a URL QuerString based on given uri, param
+        to add current querystring for a URL
+
+        param: Parameter to add to querystring
+
+        uri: Dictionary of current querystring Params | Accepts django's request.GET
+
+        Usage Example
+
+        Add a current URI param and remove if it exists
+
+        uri = urlGen()
+
+        Current URI: ?page=1&search=nick
+        uri.urlgen('page', request.GET)
+        Outputs: ?search=nick&page=
+
+        Add a uri Param and exclude a current
+
+        Current URI: ?search=nick&page=2
+        urlgen('order', request.GET, ['page',])
+        Outputs: ?search=nick&order=
+
+
+    """
+
+    def generate(self, param, uri = {}, exclude = []):
+        self.param = param
+        self.uri = uri
+        self.exclude = exclude
+
+        self.querystring = False
+
+        """
+        BUG FIX:
+
+        Append param to exclude to ensure the param
+        Doesnt get added twice to URI
+
+        """
+        exclude.append(param)
+
+        # Add the URI Param if it is the only one given
+
+        if len(self.uri) == 0:
+            try:
+                self.appendQuerystring(self.param)
+            except ExceptionError:
+                raise ExceptionError (
+                    'urlgen recieved an unexpected error adding %s param failed' % (params)
+                    )
+        else:
+            for k,v in self.uri.iteritems():
+                if self.param is not str(k) and k not in self.exclude:
+                    self.appendQuerystring(k, v)
+
+            # Append the param to end of URL
+
+            self.appendQuerystring(self.param)
+
+        return self.querystring
+
+    # def appendQuerystring(self, param, value = False):
+
+    #     """
+    #     Appends a param to the current querystring
+    #     """
+    #     if self.querystring is False:
+    #         if value is False:
+    #             self.querystring = '?%s=' % (str(param))
+    #         else:
+    #             self.querystring = '?%s=%s' % (str(param), str(value))
+    #     else:
+    #         if value is False:
+    #             self.querystring  = '%s&%s=' % (self.querystring, str(param))
+    #         else:
+    #             self.querystring  = '%s&%s=%s' % (self.querystring, str(param), str(value))
+
+
+    def appendQuerystring(self, param, value=False):
+
+        """
+        Appends a param to the current querystring
+        """
+        if self.querystring is False:
+            if value is False:
+                self.querystring = '?%s=' % (param)
+            else:
+                self.querystring = '?%s=%s' % (param, value)
+        else:
+            if value is False:
+                self.querystring  = '%s&%s=' % (self.querystring, param)
+            else:
+                self.querystring  = '%s&%s=%s' % (self.querystring, param, value)
+# -*- coding: utf-8 -*-
+"""
+garage.utils
+
+Utility functions
+
+* created: 2008-08-11 kevin chan <kefin@makedostudio.com>
+* updated: 2012-09-08 kchan
+"""
+
+import os
+import sys
+import re
+import hashlib
+import string
+import codecs
+import yaml
+
+try:
+    from yaml import CLoader as Loader
+    from yaml import CDumper as Dumper
+except ImportError:
+    from yaml import Loader, Dumper
+
+from django.conf import settings
+
+from minipylib.crypto import encode_data, decode_data
+
+
+
+# add directory to module search path (sys.path)
+
+def add_to_sys_path(app_dir):
+    """
+    Add directory to sys.path
+    """
+    if os.path.isdir(app_dir):
+        sys.path.append(app_dir)
+
+
+# import module given path
+
+def import_module(path):
+    """
+    Dynamically import module from path and return a module object
+    """
+    try:
+        assert path is not None and os.path.isfile(path)
+        src = open(path, 'rb')
+        m = hashlib.md5()
+        m.update(path)
+        module = imp.load_source(m.hexdigest(), path, src)
+        src.close()
+    except (TypeError, AssertionError, IOError):
+        module = None
+    return module
+
+
+# import vars from module
+
+def import_module_vars(module, *args):
+    """
+    Import vars from module
+    * module is module name
+    * args are variables to import
+    * returns None on error, otherwise dict of name/values
+    * if no args, return module __dict__
+
+    Example:
+    data = import_module_vars('webapp.urls', 'URLS')
+
+    * see: http://stackoverflow.com/questions/2259427/load-python-code-at-runtime
+
+    """
+    try:
+        m = __import__(module, globals(), locals(), args, -1)
+    except ImportError:
+        return None
+
+    if module.find('.') != -1:
+        # submodule
+        m = sys.modules[module]
+
+    if len(args) == 0:
+        result = m.__dict__
+    else:
+        result = {}
+        for name in args:
+            try:
+                result[name] = getattr(m, name)
+            except AttributeError:
+                result[name] = None
+    return result
+
+
+def import_module_settings(module):
+    """
+    Import settings from module
+    * only global vars in ALL CAPS are imported
+    * return None on error
+    """
+    data = import_module_vars(module)
+    try:
+        return dict([(k, v) for k, v in data.items() if k == k.upper()])
+    except AttributeError:
+        pass
+    return None
+
+
+# create class instance based on module and class name
+
+def get_instance(module, class_name, *args, **kwargs):
+    """
+    Return an instance of the object based on
+    module name and class name
+    """
+    __import__(module)
+    f = getattr(sys.modules[module], class_name)
+    obj = f(*args, **kwargs)
+    return obj
+
+
+# get text file content
+
+default_encoding = "utf-8"
+
+def get_file_contents(path, encoding=default_encoding):
+    """
+    Load text file from file system and return content as string.
+    * default encoding is utf-8
+    * return None is file cannot be read
+    """
+    try:
+        assert path is not None and os.path.isfile(path)
+        file_obj = codecs.open(path, "r", encoding)
+        data = file_obj.read()
+        file_obj.close()
+    except (TypeError, AssertionError, IOError):
+        data = None
+    return data
+
+
+# write data to file
+
+def write_file(path, data, encoding=default_encoding):
+    """
+    Write text file to file system.
+    """
+    try:
+        the_file = open(path, 'wb')
+        the_file.write(data.encode(encoding))
+        the_file.close()
+        return True
+    except IOError:
+        return False
+
+
+# make directories
+
+def make_dir(path):
+    """
+    Make sure path exists by create directories
+    * path should be directory path
+      (example: /home/veryloopy/www/app/content/articles/archives/)
+    """
+    if not os.path.exists(path):
+        try:
+            os.makedirs(path)
+        except OSError:
+            pass
+    if os.path.exists(path):
+        return True
+    else:
+        return False
+
+
+# YAML utilities
+
+# yaml usage:
+# data = load(stream, Loader=Loader)
+# output = dump(data, Dumper=Dumper)
+
+def load_yaml(data):
+    """
+    Parse yaml data.
+
+    :param data: YAML-formatted data
+    :returns: loaded data structure
+    """
+    return yaml.load(data, Loader=Loader)
+
+
+def load_yaml_docs(data):
+    """
+    Parse a series of documents embedded in a YAML file.
+
+    * documents are delimited by '---' in the file
+
+    :param data: YAML-formatted data
+    :returns: loaded data structure
+    """
+    return yaml.load_all(data, Loader=Loader)
+
+
+def dump_yaml(data, **opts):
+    """
+    Dump data structure in yaml format.
+
+    example usage:
+    print dump_yaml(y, explicit_start=True, default_flow_style=False)
+
+    :param data: data structure
+    :param opts: optional parameters for yaml engine
+    :returns: YAML-formatted `basestring` for output
+    """
+    return yaml.dump(data, Dumper=Dumper, **opts)
+
+
+# encryption/decryption, encode/decode functions
+
+def sha1hash(s):
+    """
+    Calculate sha1 hash in hex for string.
+    """
+    try:
+        return hashlib.sha1(s).hexdigest()
+    except UnicodeEncodeError:
+        return hashlib.sha1(safe_str(s)).hexdigest()
+    except:
+        return hashlib.sha1(repr(s)).hexdigest()
+
+
+def ezencode(data, secret_key=None, encoding='base16'):
+    """
+    Encrypt data and encode in base16 or some other format.
+    """
+    if not secret_key:
+        secret_key = getattr(settings, 'SECRET_KEY')
+    return encode_data(data, secret_key, pickle_data=True, encoding=encoding)
+
+def ezdecode(encrypted, secret_key=None, encoding='base16'):
+    """
+    Decode and decrypt data previously encoded using my_encode_data.
+    """
+    if not secret_key:
+        secret_key = getattr(settings, 'SECRET_KEY')
+    return decode_data(encrypted, secret_key, pickle_data=True, encoding=encoding)
+
+
+# encode/decode functions
+# * note: encode_sdata and decode_sdata do not perform any sort of
+#   encryption
+
+def encode_sdata(data):
+    """
+    Encode data (dict) using pickle, b16encode and base64
+
+    :param data: any Python data object
+    :returns: pickled string of data
+    """
+    try:
+        return base64.b16encode(pickle.dumps(data))
+    except:
+        return ''
+
+def decode_sdata(encoded_string):
+    """
+    Decode data pickled and encoded using encode_sdata
+
+    :param encoded_string: pickled string of data
+    :returns: unpickled data
+    """
+    try:
+        return pickle.loads(base64.b16decode(encoded_string))
+    except:
+        return None
+
+
+# utility functions for packing/unpacking cookies
+
+def encode_cookie(data, max_age, magic=None, secret_key=None):
+    """
+    Prep and encode session cookie
+
+    * this is a utility function to prepare a cookie
+
+    Cookie contains the following keys:
+
+    * max_age - max age of cookie before expiration
+    * expiration - expiration time
+    * expiration_timestamp - expiration timestamp in iso8601 format
+    * cookie_timestamp - timestamp of cookie in iso-8601 format
+    * magic - magic code for authenticaton when decoding
+
+    :param secret_key: secret key used for encryption/decryption
+    :returns: encrypted string containing cookie data
+    """
+    if magic is None:
+        magic = ''
+    expire_cookie = datetime.datetime.utcnow() + datetime.timedelta(seconds=max_age)
+    #expiration_timestamp = expire_cookie.strftime("%a, %d %b %Y %H:%M:%S GMT")
+    expiration_timestamp = expire_cookie.strftime("%Y-%m-%dT%H:%M:%S")
+    cookie_timestamp = time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime())
+    cookie_data = {
+        'max_age': max_age,
+        'expiration': expire_cookie.isoformat(),
+        'expiration_timestamp': expiration_timestamp,
+        'cookie_timestamp': cookie_timestamp,
+        'magic': magic,
+        'data': data
+    }
+    return ezencode(cookie_data, secret_key=secret_key)
+
+
+def decode_cookie(cookie, secret_key=None):
+    """
+    Decode cookie encoded using above function
+
+    :param cookie: encoded cookie data string
+    :param secret_key: secret key used for encryption/decryption
+    :returns: decoded cookie data
+    """
+    return ezdecode(cookie, secret_key=secret_key)
+
+
+# data object class for storing generic dict key/value pairs
+#
+# from web.py
+#
+# class Storage(dict):
+#   """
+#   A Storage object is like a dictionary except `obj.foo` can be used
+#   in addition to `obj['foo']`.
+#
+#       >>> o = storage(a=1)
+#       >>> o.a
+#       1
+#       >>> o['a']
+#       1
+#       >>> o.a = 2
+#       >>> o['a']
+#       2
+#       >>> del o.a
+#       >>> o.a
+#       Traceback (most recent call last):
+#           ...
+#       AttributeError: 'a'
+#
+#   """
+#   def __getattr__(self, key):
+#       try:
+#           return self[key]
+#       except KeyError, k:
+#           raise AttributeError, k
+#
+#   def __setattr__(self, key, value):
+#       self[key] = value
+#
+#   def __delattr__(self, key):
+#       try:
+#           del self[key]
+#       except KeyError, k:
+#           raise AttributeError, k
+#
+#   def __repr__(self):
+#       return '<Storage ' + dict.__repr__(self) + '>'
+#
+
+class DataObject(dict):
+    """
+    Data object class
+
+    * based on webpy dict-like Storage object
+    """
+    def __init__(self, *args, **kwargs):
+        self.add(*args, **kwargs)
+
+    def __getattr__(self, key):
+        try:
+            return self[key]
+        except KeyError, k:
+            raise AttributeError, k
+
+    def __setattr__(self, key, value):
+        self[key] = value
+
+    def __delattr__(self, key):
+        try:
+            del self[key]
+        except KeyError, k:
+            raise AttributeError, k
+
+    def __repr__(self):
+        return '<DataObject ' + dict.__repr__(self) + '>'
+
+    def add(self, *args, **kwargs):
+        """
+        add({
+            'a': 1,
+            'b': 3.14
+            'c': 'foo'
+        })
+        """
+        for d in args:
+            if isinstance(d, basestring):
+                self[d] = True
+            elif isinstance(d, dict):
+                for name, value in d.items():
+                    self[name] = value
+            else:
+                try:
+                    for name in d:
+                        self[name] = True
+                except TypeError:
+                    pass
+        for name, value in kwargs.items():
+            try:
+                self[name] = value
+            except TypeError:
+                pass
+
+
+# enum type
+#
+# from:
+# http://stackoverflow.com/questions/36932/whats-the-best-way-to-implement-an-enum-in-python
+#
+# def enum(**enums):
+#     return type('Enum', (), enums)
+# Used like so:
+#
+# >>> Numbers = enum(ONE=1, TWO=2, THREE='three')
+# >>> Numbers.ONE
+# 1
+# >>> Numbers.TWO
+# 2
+# >>> Numbers.THREE
+# 'three'
+# You can also easily support automatic enumeration with something like this:
+#
+# def enum(*sequential, **named):
+#     enums = dict(zip(sequential, range(len(sequential))), **named)
+#     return type('Enum', (), enums)
+# Used like so:
+#
+# >>> Numbers = enum('ZERO', 'ONE', 'TWO')
+# >>> Numbers.ZERO
+# 0
+# >>> Numbers.ONE
+# 1
+
+def enum(**enums):
+    return type('Enum', (), enums)
+
+
+# utility string functions
+
+def trim(s):
+    """Trim white space from beginning and end of string."""
+    return s.lstrip().rstrip()
+
+
+def cvt2list(s):
+    """Convert object to list"""
+    if isinstance(s, (list, tuple)):
+        return s
+    return [s]
+
+
+def check_eos(s):
+    """
+    Check end of string and make sure there's a return.
+    """
+    cr = '\n'
+    try:
+        if not s.endswith(cr):
+            s += cr
+    except (TypeError, AttributeError):
+        pass
+    return s
+
+
+# string test functions
+
+def has_digits(s):
+    """
+    Test if string has digits.
+
+    :param s: string
+    :returns: number of digits in string
+    """
+    return len(set(s) & set(string.digits))
+
+def has_alpha(s):
+    """
+    Test if string has alphabets.
+
+    :param s: string
+    :returns: number of letters in string
+    """
+    return len(set(s) & set(string.letters))
+
+def has_alphanum(s):
+    """
+    Test if string has alphabets and digits.
+
+    :param s: string
+    :returns: number of letters and digits in string
+    """
+    alphanum = set(string.letters + string.digits)
+    return len(set(s) & alphanum)
+
+
+# convert uri request string to list
+
+def uri_to_list(path):
+    """
+    Parse request path and split uri into list
+    * /action/param1/param2 will be parsed as:
+      ['action', 'param1', 'param2']
+    """
+    if path[0] == '/':
+        path = path[1:]
+    if path[-1:] == '/':
+        path = path[:-1]
+    return path.split('/')
+
+
+# utility functions to convert names to/from CamelCase
+
+def to_camel_case(name):
+    """
+    Convert name to CamelCase.
+    * does not do any sanity checking (assumes name
+      is somewhat already alphanumeric).
+    """
+    delete_chars = """'":;,~!@#$%^&*()_+-`=<>./?\\|"""
+    result = []
+    prev = ' '
+    for c in name:
+        if not prev.isalnum():
+            c = c.upper()
+        else:
+            c = c.lower()
+        result.append(c)
+        prev = c
+    return "".join(result).translate(string.maketrans(' -','__'), delete_chars)
+
+
+# perform substitution on a chunk of text
+
+# default id pattern: ${VARIABLE}
+IdPattern = r'\$\{([a-z_][a-z0-9_]*)\}'
+IdRegexp = re.compile(IdPattern, re.I)
+
+def substitute(txt, context, pattern=None):
+    """
+    Perform variable substitution on a chunk of text.
+    * returns None if input text is None.
+    * default var pattern is ${var}
+
+    Parameters:
+    * txt is text or template to perform substitution on
+    * context is dict of key/value pairs or callable to retrieve values
+    * pattern is regexp pattern or compiled regexp object to perform match
+    """
+    if txt is None:
+        return None
+    if pattern is None:
+        regexp = IdRegexp
+    elif isinstance(pattern, basestring):
+        regexp = re.compile(pattern, re.I)
+    else:
+        regexp = pattern
+    if callable(context):
+        getval = context
+    else:
+        if context is None:
+            context = {}
+        getval = lambda kw: context.get(kw, '')
+    done = False
+    while not done:
+        matches = regexp.findall(txt)
+        if len(matches) > 0:
+            txt = regexp.sub(lambda m: getval(m.group(1)), txt)
+        else:
+            done = True
+    return txt
+
+
+# simple string substitution function
+
+def subs(template, context):
+    """
+    Perform simple string substitutions using string template
+
+    Example:
+    Caption = '<div class="%(caption_css_class)s">%(caption)s</div>'
+    output = subs(Caption, {
+                    'caption_css_class': 'image_caption',
+                    'caption': 'test'})
+
+    """
+    result = ''
+    try:
+        assert template is not None
+        result = template % context
+    except (AssertionError, TypeError):
+        pass
+    return result
+
+
+# format timestamp/date string
+
+def fmt_date(date_string, fmt='%Y-%m-%dT%H:%M:%S'):
+    """
+    Return formatted timestamp from iso 8601 date/time string
+    * input should be YYYY-MM-DDTHH:MM:SS
+    * default output is same (iso 8601)
+    * bad input will return '1900-01-01T00:00:00'
+    * TODO: timezone
+    """
+    def chkval(t):
+        if t is None or len(t) == 0: return '0'
+        else: return t
+    default_timestamp = (1900, 1, 1, 0, 0, 0)
+    pat = re.compile(r'^(\d+)-(\d+)-(\d+)(T(\d+):(\d+):?(\d+)?(\.\d+)?)?$')
+    m = pat.match(date_string)
+    if m:
+        timestamp = m.group(1, 2, 3, 5, 6, 7)
+    else:
+        timestamp = default_timestamp
+    dt = [int(chkval(t)) for t in timestamp]
+    try:
+        return datetime(*dt).strftime(fmt)
+    except ValueError:
+        return datetime(*default_timestamp).strftime(fmt)
+
+
+
+# from: http://code.activestate.com/recipes/466341-guaranteed-conversion-to-unicode-or-byte-string/
+# Recipe 466341 (r1): Guaranteed conversion to unicode or byte string
+
+def safe_unicode(obj, *args):
+    """ return the unicode representation of obj """
+    try:
+        return unicode(obj, *args)
+    except UnicodeDecodeError:
+        # obj is byte string
+        ascii_text = str(obj).encode('string_escape')
+        return unicode(ascii_text)
+
+def safe_str(obj):
+    """ return the byte string representation of obj """
+    try:
+        return str(obj)
+    except UnicodeEncodeError:
+        # obj is unicode
+        return unicode(obj).encode('unicode_escape')
+
+
+### get file basename and extensions
+
+PATH_REGEXP_PAT = r'^(.*)(\.[^\.]+)$'
+PathRegexp = None
+
+def get_file_ext(filename):
+    """
+    Extract extension for file.
+    """
+    global PathRegexp
+    if PathRegexp is None:
+        PathRegexp = re.compile(PATH_REGEXP_PAT, re.I)
+    m = PathRegexp.match(filename)
+    if m:
+        fbase, fext = m.groups()
+    else:
+        fbase, fext = (filename, '')
+    return fbase, fext
+"""
+setup.py
+
+Setup.py for django-garage.
+
+Copyright (c) 2012-2013 kevin chan <kefin@makedostudio.com>
+
+* created: 2013-01-12 Kevin Chan <kefin@makedostudio.com>
+* updated: 2013-01-12 kchan
+"""
+
+import os
+from setuptools import setup
+
+README = open(os.path.join(os.path.dirname(__file__), 'README.md')).read()
+
+# allow setup.py to be run from any path
+os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir)))
+
+
+setup(
+    name = "django-garage",
+    version = "0.1.0",
+    packages = ['garage'],
+    include_package_data = True,
+    license = "BSD",
+    description = "A collection of useful functions and modules for Django development",
+    long_description = README,
+    url = "https://bitbucket.org/kchan",
+    author = "Kevin Chan",
+    author_email = "kefin@makedostudio.com",
+    classifiers = [
+        'Development Status :: 3 - Alpha',
+        'Environment :: Web Environment',
+        'Framework :: Django',
+        'Intended Audience :: Developers',
+        'License :: OSI Approved :: BSD License',
+        'Operating System :: OS Independent',
+        'Programming Language :: Python',
+        'Programming Language :: Python :: 2.6',
+        'Programming Language :: Python :: 2.7',
+        'Topic :: Internet :: WWW/HTTP',
+        'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
+        ],
+    install_requires = [
+        'django',
+        'markdown',
+        'textile',
+        'PIL',
+        'pyyaml',
+        ]
+)