Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
branch: master
Fetching contributors…

Octocat-spinner-32-eaf2f5

Cannot retrieve contributors at this time

file 163 lines (123 sloc) 5.675 kb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162
from zlib import crc32

from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.core.mail import EmailMessage
from django.template import Context, loader
from django.utils.importlib import import_module


class peekable(object):
    """Wrapper for an iterator to allow 1-item lookahead"""
    # Lowercase to blend in with itertools. The fact that it's a class is an
    # implementation detail.

    # TODO: Liberate into itertools.

    def __init__(self, iterable):
        self._it = iter(iterable)

    def __iter__(self):
        return self

    def __nonzero__(self):
        try:
            self.peek()
        except StopIteration:
            return False
        return True

    def peek(self):
        """Return the item that will be next returned from ``next()``.

Raise ``StopIteration`` if there are no items left.

"""
        # TODO: Give peek a default arg. Raise StopIteration only when it isn't
        # provided. If it is, return the arg. Just like get('key', object())
        if not hasattr(self, '_peek'):
            self._peek = self._it.next()
        return self._peek

    def next(self):
        ret = self.peek()
        del self._peek
        return ret


def collate(*iterables, **kwargs):
    """Return an iterable ordered collation of the already-sorted items
from each of ``iterables``, compared by kwarg ``key``.

If ``reverse=True`` is passed, iterables must return their results in
descending order rather than ascending.

"""
    # TODO: Liberate into the stdlib.
    key = kwargs.pop('key', lambda a: a)
    reverse = kwargs.pop('reverse', False)

    min_or_max = max if reverse else min
    peekables = [peekable(it) for it in iterables]
    peekables = [p for p in peekables if p] # Kill empties.
    while peekables:
        _, p = min_or_max((key(p.peek()), p) for p in peekables)
        yield p.next()
        peekables = [p for p in peekables if p]


def hash_to_unsigned(data):
    """If ``data`` is a string or unicode string, return an unsigned 4-byte int
hash of it. If ``data`` is already an int that fits those parameters,
return it verbatim.

If ``data`` is an int outside that range, behavior is undefined at the
moment. We rely on the ``PositiveIntegerField`` on
:class:`~tidings.models.WatchFilter` to scream if the int is too long for
the field.

We use CRC32 to do the hashing. Though CRC32 is not a good general-purpose
hash function, it has no collisions on a dictionary of 38,470 English
words, which should be fine for the small sets that :class:`WatchFilters
<tidings.models.WatchFilter>` are designed to enumerate. As a bonus, it is
fast and available as a built-in function in some DBs. If your set of
filter values is very large or has different CRC32 distribution properties
than English words, you might want to do your own hashing in your
:class:`~tidings.events.Event` subclass and pass ints when specifying
filter values.

"""
    if isinstance(data, basestring):
        # Return a CRC32 value identical across Python versions and platforms
        # by stripping the sign bit as on
        # http://docs.python.org/library/zlib.html.
        return crc32(data.encode('utf-8')) & 0xffffffff
    else:
        return int(data)


def emails_with_users_and_watches(subject, template_path, vars,
    users_and_watches, from_email=settings.TIDINGS_FROM_ADDRESS,
    **extra_kwargs):
    """Return iterable of EmailMessages with user and watch values substituted.

A convenience function for generating emails by repeatedly rendering a
Django template with the given ``vars`` plus a ``user`` and ``watches`` key
for each pair in ``users_and_watches``

:arg template_path: path to template file
:arg vars: a map which becomes the Context passed in to the template
:arg extra_kwargs: additional kwargs to pass into EmailMessage constructor

"""
    template = loader.get_template(template_path)
    context = Context(vars)
    for u, w in users_and_watches:
        context['user'] = u
        context['watch'] = w[0] # Arbitrary single watch for compatibility
                                 # with 0.1. TODO: remove.
        context['watches'] = w
        yield EmailMessage(subject,
                           template.render(context),
                           from_email,
                           [u.email],
                           **extra_kwargs)


def _imported_symbol(import_path):
    """Resolve a dotted path into a symbol, and return that.

For example...

>>> _imported_symbol('django.db.models.Model')
<class 'django.db.models.base.Model'>

Raise ImportError is there's no such module, AttributeError if no such
symbol.

"""
    module_name, symbol_name = import_path.rsplit('.', 1)
    module = import_module(module_name)
    return getattr(module, symbol_name)


def import_from_setting(setting_name, fallback):
    """Return the resolution of an import path stored in a Django setting.

:arg setting_name: The name of the setting holding the import path
:arg fallback: An import path to use if the given setting doesn't exist

Raise ImproperlyConfigured if a path is given that can't be resolved.

"""
    path = getattr(settings, setting_name, fallback)
    try:
        return _imported_symbol(path)
    except (ImportError, AttributeError):
        raise ImproperlyConfigured('No such module or attribute: %s' % path)


# Here to be imported by others:
reverse = import_from_setting('TIDINGS_REVERSE',
                              'django.core.urlresolvers.reverse')
Something went wrong with that request. Please try again.