django-ddp/dddp/__init__.py
2016-01-28 15:34:43 +11:00

185 lines
5.5 KiB
Python

"""Django/PostgreSQL implementation of the Meteor server."""
from __future__ import unicode_literals
import sys
from gevent.local import local
from dddp import alea
__version__ = '0.19.1'
__url__ = 'https://github.com/django-ddp/django-ddp'
default_app_config = 'dddp.apps.DjangoDDPConfig'
ADDED = 'added'
CHANGED = 'changed'
REMOVED = 'removed'
_GREEN = {}
def greenify():
"""Patch threading and psycopg2 modules for green threads."""
# don't greenify twice.
if _GREEN:
return
_GREEN[True] = True
from gevent.monkey import patch_all, saved
if ('threading' in sys.modules) and ('threading' not in saved):
import warnings
warnings.warn('threading module loaded before patching!')
patch_all()
try:
# Use psycopg2 by default
import psycopg2
del psycopg2
except ImportError:
# Fallback to psycopg2cffi if required (eg: pypy)
from psycopg2cffi import compat
compat.register()
from psycogreen.gevent import patch_psycopg
patch_psycopg()
class MeteorError(Exception):
"""
MeteorError.
This exception can be thrown by DDP API endpoints (methods) and publication
methods. MeteorError is not expected to be logged or shown by the server,
leaving all handling of the error condition for the client.
Args:
error (str): A string code uniquely identifying this kind of error.
This string should be used by clients to determine the appropriate
action to take, instead of attempting to parse the reason or detail
fields.
reason (Optional[str]): A short human-readable summary of the error.
detail (Optional[str]): Additional information about the error.
When returning errors to clients, Django DDP will default this to a
textual stack trace if `django.conf.settings.DEBUG` is `True`.
"""
def __init__(self, error, reason=None, details=None, **kwargs):
"""MeteorError constructor."""
super(MeteorError, self).__init__(error, reason, details, kwargs)
def as_dict(self, **kwargs):
"""Return an error dict for self.args and kwargs."""
error, reason, details, err_kwargs = self.args
result = {
key: val
for key, val in {
'error': error, 'reason': reason, 'details': details,
}.items()
if val is not None
}
result.update(err_kwargs)
result.update(kwargs)
return result
class AlreadyRegistered(Exception):
"""Raised when registering over the top of an existing registration."""
pass
class ThreadLocal(local):
"""Thread local storage for greenlet state."""
_init_done = False
def __init__(self):
"""Create new thread storage instance."""
if self._init_done:
raise SystemError('__init__ called too many times')
self._init_done = True
def __getattr__(self, name):
"""Create missing attributes using default factories."""
try:
factory = THREAD_LOCAL_FACTORIES[name]
except KeyError:
raise AttributeError(name)
return self.get(name, factory)
def get(self, name, factory, *factory_args, **factory_kwargs):
"""Get attribute, creating if required using specified factory."""
update_thread_local = getattr(factory, 'update_thread_local', True)
if (not update_thread_local) or (name not in self.__dict__):
obj = factory(*factory_args, **factory_kwargs)
if update_thread_local:
setattr(self, name, obj)
return obj
return getattr(self, name)
@staticmethod
def get_factory(name):
"""Get factory for given name."""
return THREAD_LOCAL_FACTORIES[name]
class RandomStreams(object):
"""Namespaced PRNG generator."""
def __init__(self):
"""Initialize with random PRNG state."""
self._streams = {}
self._seed = THREAD_LOCAL.alea_random.hex_string(20)
def get_seed(self):
"""Retrieve current PRNG seed."""
return self._seed
def set_seed(self, val):
"""Set current PRNG seed."""
self._streams = {}
self._seed = val
random_seed = property(get_seed, set_seed)
def __getitem__(self, key):
"""Get namespaced PRNG stream."""
if key not in self._streams:
return self._streams.setdefault(key, alea.Alea(self._seed, key))
return self._streams[key]
def serializer_factory():
"""Make a new DDP serializer."""
from django.core.serializers import get_serializer
return get_serializer('python')()
THREAD_LOCAL_FACTORIES = {
'alea_random': alea.Alea,
'random_streams': RandomStreams,
'serializer': serializer_factory,
'user_id': lambda: None,
'user_ddp_id': lambda: None,
'user': lambda: None,
}
THREAD_LOCAL = this = ThreadLocal() # pylint: disable=invalid-name
METEOR_ID_CHARS = u'23456789ABCDEFGHJKLMNPQRSTWXYZabcdefghijkmnopqrstuvwxyz'
def meteor_random_id(name=None, length=17):
"""Generate a new ID, optionally using namespace of given `name`."""
if name is None:
stream = THREAD_LOCAL.alea_random
else:
stream = THREAD_LOCAL.random_streams[name]
return stream.random_string(length, METEOR_ID_CHARS)
def autodiscover():
"""Import all `ddp` submodules from `settings.INSTALLED_APPS`."""
from django.utils.module_loading import autodiscover_modules
from dddp.api import API
autodiscover_modules('ddp', register_to=API)
return API