"""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