diff --git a/baph/core/cache/utils.py b/baph/core/cache/utils.py index aa514e9..a1ed7e8 100644 --- a/baph/core/cache/utils.py +++ b/baph/core/cache/utils.py @@ -1,126 +1,115 @@ from contextlib import contextmanager import time - -try: - # django 1.7 - current - from django.utils.module_loading import import_string -except: - # django 1.5 - 1.6 - from django.utils.module_loading import import_by_path as import_string +from six import string_types +from werkzeug.utils import cached_property, import_string class CacheNamespace(object): - def __init__(self, name, attr, cache_alias='default', default_value=None, - default_func=None): - self.name = name - self.attr = attr - self.cache_alias = cache_alias - self.default_value = default_value - self.default_func = default_func - self._caches = None - self._models = None - self._partitions = None - self._override = None + def __init__(self, name, attr, cache_alias='default', default_value=None, + default_func=None): + self.name = name + self.attr = attr + self.cache_alias = cache_alias + self.default_value = default_value + self.default_func = default_func + self._override = None - def __call__(self, value=None): - value = self.resolve_value(value) - return self.key_prefix(value) + def __call__(self, value=None): + value = self.resolve_value(value) + return self.key_prefix(value) - def resolve_value(self, value): - if self._override is not None: - return self._override - if value is None: - value = self.get_default() - if value is None: - raise ValueError('CacheNamespace requires a default when called with no ' - 'value') - return value + def resolve_value(self, value): + if self._override is not None: + return self._override + if value is None: + value = self.get_default() + if value is None: + raise ValueError('CacheNamespace requires a default when called ' + 'with no value') + return value - @contextmanager - def override_value(self, value): - self._override = value - yield self - self._override = None + @contextmanager + def override_value(self, value): + self._override = value + yield self + self._override = None - @classmethod - def get_cache_namespaces(cls): - """ - Returns a list of CacheNamespace instances that are currently in use - """ - from django.conf import settings - namespaces = set() - for cache, params in settings.CACHES.items(): - prefix = params.get('KEY_PREFIX', None) - if isinstance(prefix, cls): - namespaces.add(prefix) - return namespaces + @classmethod + def get_cache_namespaces(cls): + """ + Returns a list of CacheNamespace instances that are currently in use + """ + from django.conf import settings + namespaces = set() + for cache, params in settings.CACHES.items(): + prefix = params.get('KEY_PREFIX', None) + if isinstance(prefix, cls): + namespaces.add(prefix) + return namespaces - @property - def affected_caches(self): - """ - Returns a list of caches which use this namespace as KEY_PREFIX - """ - if self._caches is None: - from django.conf import settings - self._caches = [] - for alias, config in settings.CACHES.items(): - prefix = config.get('KEY_PREFIX', None) - if prefix is self: - self._caches.append(alias) - return self._caches + @cached_property + def affected_caches(self): + """ + Returns a list of caches which use this namespace as KEY_PREFIX + """ + from django.conf import settings + caches = [] + for alias, config in settings.CACHES.items(): + prefix = config.get('KEY_PREFIX', None) + if prefix is self: + caches.append(alias) + return caches - @property - def affected_models(self): - """ - Returns a list of models which are cached in self.affected_caches - """ - if self._models is None: - from baph.db.models import get_models - self._models = [] - for model in get_models(): - cache_alias = model._meta.cache_alias - if cache_alias not in self.affected_caches: - continue - self._models.append(model) - return self._models + @cached_property + def affected_models(self): + """ + Returns a list of models which are cached in self.affected_caches + """ + from baph.db.models import get_models + models = [] + for model in get_models(): + cache_alias = model._meta.cache_alias + if cache_alias not in self.affected_caches: + continue + models.append(model) + return models - @property - def partitions(self): - """ - Returns a list of available subpartitions that can be invalidated - """ - if self._partitions is None: - self._partitions = [] - for model in self.affected_models: - partitions = model._meta.cache_partitions - if partitions: - self._partitions.append( (model, partitions) ) - return self._partitions + @cached_property + def partitions(self): + """ + Returns a list of available subpartitions that can be invalidated + """ + partitions = [] + for model in self.affected_models: + _partitions = model._meta.cache_partitions + if _partitions: + partitions.append((model, _partitions)) + return partitions - @property - def cache(self): - from django.core.cache import get_cache - return get_cache(self.cache_alias) + @property + def cache(self): + from django.core.cache import get_cache + return get_cache(self.cache_alias) - def get_default(self): - if self.default_value is not None: - return self.default_value - if self.default_func is None: - return None - if isinstance(self.default_func, basestring): - self.default_func = import_string(self.default_func) - if not callable(self.default_func): - raise Exception('default_func %r is not callable') - return self.default_func() + def get_default(self): + if self.default_value is not None: + return self.default_value + if self.default_func is None: + return None + if isinstance(self.default_func, string_types): + self.default_func = import_string(self.default_func) + if not callable(self.default_func): + raise Exception('default_func %r is not callable' % self.default_func) + return self.default_func() - def version_key(self, value): - return '%s_%s' % (self.name.lower(), value) + def version_key(self, value): + return '%s_%s' % (self.name.lower(), value) - def key_prefix(self, value): - version_key = self.version_key(value) - version = self.cache.get(version_key) - if version is None: - version = int(time.time()) - self.cache.set(version_key, version) - return '%s_%s' % (version_key, version) + def key_prefix(self, value): + version_key = self.version_key(value) + version = self.cache.get(version_key) + if version is None: + version = int(time.time()) + self.cache.set(version_key, version) + return '%s_%s' % (version_key, version) diff --git a/baph/core/management/commands/nsincr.py b/baph/core/management/commands/nsincr.py index 3d55e7a..0381977 100644 --- a/baph/core/management/commands/nsincr.py +++ b/baph/core/management/commands/nsincr.py @@ -1,121 +1,206 @@ +import sys + +from six.moves import input + from baph.core.cache.utils import CacheNamespace -from baph.core.management.base import NoArgsCommand +from baph.core.management.new_base import BaseCommand, CommandError + + +def parse_args(*args): + """ converts cli args to a dict of options """ + opts = {} + for arg in args: + arg = arg.lstrip('-') + parts = arg.split('=', 1) + key = parts[0] + value = parts[1] if len(parts) > 1 else True + opts[key] = value + return opts def build_options_list(namespaces): - options = [] - for ns in namespaces: - name = ns.name - attrs = [ns.attr] - options.append((ns, name, attrs, 'ns', ns.affected_models)) - for model, _attrs in ns.partitions: - name = model.__name__ - for attr in _attrs: - options.append((ns, name, attrs + [attr], 'partition', [model])) - return options + options = [] + for ns in namespaces: + name = ns.name + attrs = [ns.attr] + options.append((ns, name, attrs, 'ns', ns.affected_models)) + for model, _attrs in ns.partitions: + name = model.__name__ + for attr in _attrs: + options.append((ns, name, attrs + [attr], 'partition', [model])) + return options -def print_options(options): - print('\n%s %s %s' % ('id', 'name'.ljust(16), 'attrs')) - for i, (ns, name, attr, type, models) in enumerate(options): - names = sorted([model.__name__ for model in models]) - print('%s %s %s' % (i, name.ljust(16), attr)) - print(' invalidates: %s' % names) def get_value_for_attr(attr): - msg = 'Enter the value for %r (ENTER to cancel): ' % attr - while True: - value = raw_input(msg).strip() - if not value: - return None - return value + """ prompt the user for an attribute value """ + msg = 'Enter the value for %r (ENTER to cancel): ' % attr + while True: + value = input(msg).strip() + if not value: + return None + return value + + +def print_options(options): + """ outputs a list of available targets for invalidation """ + print('\n%s %s %s' % ('id', 'name'.ljust(16), 'attrs')) + for i, (ns, name, attr, type, models) in enumerate(options): + names = sorted([model.__name__ for model in models]) + print('%s %s %s' % (i, name.ljust(16), attr)) + print(' invalidates: %s' % names) + def get_option(options): - name_map = {opt[1].lower(): i for i, opt in enumerate(options)} - msg = '\nIncrement which ns key? (ENTER to list, Q to quit): ' - while True: - value = raw_input(msg).strip().lower() - if not value: - print_options(options) - continue - - if value == 'q': - # quit - sys.exit() - - if value.isdigit(): - # integer index - index = int(value) - elif value in name_map: - # string reference - index = name_map[value] - else: - print('Invalid option: %r' % value) - continue - - if index >= len(options): - print('Invalid index: %r' % index) - continue - - return options[index] + """ prompt the user for which target to invalidate """ + name_map = {opt[1].lower(): i for i, opt in enumerate(options)} + msg = '\nIncrement which ns key? (ENTER to list, Q to quit): ' + while True: + value = input(msg).strip().lower() + if not value: + print_options(options) + continue + + if value == 'q': + # quit + sys.exit() + + if value.isdigit(): + # integer index + index = int(value) + elif value in name_map: + # string reference + index = name_map[value] + else: + print('Invalid option: %r' % value) + continue + + if index >= len(options): + print('Invalid index: %r' % index) + continue + + return options[index] + def increment_version_key(cache, key): - version = cache.get(key) - print(' current value of %s: %s' % (key, version)) - version = version + 1 if version else 1 - cache.set(key, version) - version = cache.get(key) - print(' new value of %s: %s' % (key, version)) - - -class Command(NoArgsCommand): - requires_model_validation = True - - def main(self): - print_options(self.options) - option = get_option(self.options) - - ns, name, attrs, type, models = option - ns_attr = attrs[0] - partition_attrs = attrs[1:] - ns_value = get_value_for_attr(ns_attr) - if ns_value is None: - return None - - if not partition_attrs: - # this is a top-level namespace increment - version_key = ns.version_key(ns_value) - increment_version_key(ns.cache, version_key) - return 1 - - # this is a partition increment - values = {} - for attr in partition_attrs: - value = get_value_for_attr(attr) - if value is None: - return 1 - values[attr] = value - - model = models[0] - cache = model.get_cache() - keys = model.get_cache_partition_version_keys(**values) - - # we need to set an override on the cachenamespace instance - # so it returns the correct value when called by cache.set - with ns.override_value(ns_value): - for version_key in keys: - increment_version_key(cache, version_key) - - return 1 - - def handle_noargs(self, **options): - self.namespaces = CacheNamespace.get_cache_namespaces() - self.options = build_options_list(self.namespaces) - - while True: - try: - result = self.main() - except KeyboardInterrupt: - print('') - break - if result is None: - break + old_version = cache.get(key) + new_version = old_version + 1 if old_version else 1 + cache.set(key, new_version) + version = cache.get(key) + return (old_version, version) + + +class Command(BaseCommand): + allow_unknown_args = True + requires_model_validation = True + + def add_arguments(self, parser): + parser.add_argument( + '--noinput', '--no-input', action='store_false', + dest='interactive', + help='Indicates the user should not be prompted for info.', + ) + parser.add_argument( + '--namespace', action='store', dest='namespace', + help='The name of the cache namespace.', + ) + + def get_cli_value(self, key): + """ get a required value from the cli args """ + if self.values.get(key): + return self.values[key] + else: + raise CommandError('missing required argument "%s"' % key) + + def get_namespace(self): + """ returns the target namespace information """ + if self.values['interactive']: + # prompt user for namespace + print_options(self.options) + return get_option(self.options) + else: + # value provided via cli + ns = self.get_cli_value('namespace').lower() + index = self.name_map[ns] + return self.options[index] + + def get_value_for_attr(self, attr): + """ get the value for a required attribute """ + if self.values['interactive']: + # prompt user for value + return get_value_for_attr(attr) + else: + # value provided via cli + return self.get_cli_value(attr) + + def purge_namespace(self, ns, ns_value): + """ invalidate everything under a namespace """ + version_key = ns.version_key(ns_value) + fullkey = ns.cache.make_key(version_key) + rv = increment_version_key(ns.cache, version_key) + return [(ns.cache_alias, fullkey, rv[0], rv[1])] + + def purge_partition(self, ns, ns_value, model): + """ invalidate a target partition within a namespace """ + values = {} + partition_attrs = model._meta.cache_partitions + for attr in partition_attrs: + value = self.get_value_for_attr(attr) + if value is None: + return 1 + values[attr] = value + + keys = model.get_cache_partition_version_keys(**values) + cache = model.get_cache() + + # we need to set an override on the cachenamespace instance + # so it returns the correct value when called by cache.set + changes = [] + cache_name = model._meta.cache_alias + with ns.override_value(ns_value): + for version_key in keys: + fullkey = cache.make_key(version_key) + rv = increment_version_key(cache, version_key) + changes.append((cache_name, fullkey, rv[0], rv[1])) + return changes + + def main(self): + option = self.get_namespace() + ns, name, attrs, type, models = option + + ns_attr = attrs[0] + ns_value = self.get_value_for_attr(ns_attr) + if ns_value is None: + return None + + if type == 'ns': + changes = self.purge_namespace(ns, ns_value) + elif type == 'partition': + changes = self.purge_partition(ns, ns_value, models[0]) + + if self.values['interactive']: + for change in changes: + print('[%s] %s incremented from %s to %s' % change) + else: + return changes + + def run_interactive(self): + """ prompt user for required invalidation params """ + while True: + try: + result = self.main() + except KeyboardInterrupt: + print('') + break + if result is None: + break + + def handle(self, *args, **options): + self.namespaces = CacheNamespace.get_cache_namespaces() + self.options = build_options_list(self.namespaces) + self.values = dict(options, **parse_args(*args)) + self.name_map = {opt[1].lower(): i for i, opt in enumerate(self.options)} + + if self.values['interactive']: + self.run_interactive() + else: + result = self.main()