Change spec/source registry to generator/cacheable

This commit is contained in:
Eric Eldredge 2013-01-23 22:46:57 -05:00
parent 4ecfa5d35e
commit a8855d4c27
9 changed files with 104 additions and 94 deletions

View file

@ -10,4 +10,4 @@ class Thumbnail(ImageSpec):
super(Thumbnail, self).__init__(**kwargs)
register.spec('ik:thumbnail', Thumbnail)
register.generator('ik:thumbnail', Thumbnail)

View file

@ -14,19 +14,19 @@ class JustInTime(object):
class Optimistic(object):
"""
A caching strategy that acts immediately when the source file chages and
assumes that the cache files will not be removed (i.e. doesn't revalidate
on access).
A caching strategy that acts immediately when the cacheable file changes
and assumes that the cache files will not be removed (i.e. doesn't
revalidate on access).
"""
def on_source_created(self, file):
def on_cacheable_created(self, file):
validate_now(file)
def on_source_deleted(self, file):
def on_cacheable_deleted(self, file):
clear_now(file)
def on_source_changed(self, file):
def on_cacheable_changed(self, file):
validate_now(file)

View file

@ -1,35 +1,35 @@
from django.core.management.base import BaseCommand
import re
from ...files import GeneratedImageCacheFile
from ...registry import generator_registry, source_group_registry
from ...registry import generator_registry, cacheable_registry
class Command(BaseCommand):
help = ('Warm the image cache for the specified specs (or all specs if none'
' was provided). Simple wildcard matching (using asterisks) is'
' supported.')
args = '[spec_ids]'
help = ('Warm the image cache for the specified generators'
' (or all generators if none was provided).'
' Simple wildcard matching (using asterisks) is supported.')
args = '[generator_ids]'
def handle(self, *args, **options):
specs = generator_registry.get_ids()
generators = generator_registry.get_ids()
if args:
patterns = self.compile_patterns(args)
specs = (id for id in specs if any(p.match(id) for p in patterns))
generators = (id for id in generators if any(p.match(id) for p in patterns))
for spec_id in specs:
self.stdout.write('Validating spec: %s\n' % spec_id)
for source_group in source_group_registry.get(spec_id):
for source in source_group.files():
if source:
spec = generator_registry.get(spec_id, source=source) # TODO: HINTS! (Probably based on source, so this will need to be moved into loop below.)
self.stdout.write(' %s\n' % source)
for generator_id in generators:
self.stdout.write('Validating generator: %s\n' % generator_id)
for cacheables in cacheable_registry.get(generator_id):
for cacheable in cacheables.files():
if cacheable:
generator = generator_registry.get(generator_id, cacheable=cacheable) # TODO: HINTS! (Probably based on cacheable, so this will need to be moved into loop below.)
self.stdout.write(' %s\n' % cacheable)
try:
# TODO: Allow other validation actions through command option
GeneratedImageCacheFile(spec).validate()
GeneratedImageCacheFile(generator).validate()
except Exception, err:
# TODO: How should we handle failures? Don't want to error, but should call it out more than this.
self.stdout.write(' FAILED: %s\n' % err)
def compile_patterns(self, spec_ids):
return [re.compile('%s$' % '.*'.join(re.escape(part) for part in id.split('*'))) for id in spec_ids]
def compile_patterns(self, generator_ids):
return [re.compile('%s$' % '.*'.join(re.escape(part) for part in id.split('*'))) for id in generator_ids]

View file

@ -45,8 +45,8 @@ class ImageSpecField(SpecHostField):
self.set_spec_id(cls, name)
# Add the model and field as a source for this spec id
register.sources(self.spec_id,
[ImageFieldSourceGroup(cls, self.source)])
register.cacheables(self.spec_id,
ImageFieldSourceGroup(cls, self.source))
class ProcessedImageField(models.ImageField, SpecHostField):

View file

@ -1,11 +1,11 @@
from .exceptions import AlreadyRegistered, NotRegistered
from .signals import (before_access, source_created, source_changed,
source_deleted)
from .signals import (before_access, cacheable_created, cacheable_changed,
cacheable_deleted)
class GeneratorRegistry(object):
"""
An object for registering generators (specs). This registry provides
An object for registering generators. This registry provides
a convenient way for a distributable app to define default generators
without locking the users of the app into it.
@ -15,7 +15,7 @@ class GeneratorRegistry(object):
def register(self, id, generator):
if id in self._generators:
raise AlreadyRegistered('The spec or generator with id %s is'
raise AlreadyRegistered('The generator with id %s is'
' already registered' % id)
self._generators[id] = generator
@ -24,14 +24,14 @@ class GeneratorRegistry(object):
try:
del self._generators[id]
except KeyError:
raise NotRegistered('The spec or generator with id %s is not'
raise NotRegistered('The generator with id %s is not'
' registered' % id)
def get(self, id, **kwargs):
try:
generator = self._generators[id]
except KeyError:
raise NotRegistered('The spec or generator with id %s is not'
raise NotRegistered('The generator with id %s is not'
' registered' % id)
if callable(generator):
return generator(**kwargs)
@ -42,108 +42,114 @@ class GeneratorRegistry(object):
return self._generators.keys()
class SourceGroupRegistry(object):
class CacheableRegistry(object):
"""
An object for registering source groups with specs. The two are
An object for registering cacheables with generators. The two are
associated with each other via a string id. We do this (as opposed to
associating them directly by, for example, putting a ``source_groups``
attribute on specs) so that specs can be overridden without losing the
associated sources. That way, a distributable app can define its own
specs without locking the users of the app into it.
associating them directly by, for example, putting a ``cacheables``
attribute on generators) so that generators can be overridden without
losing the associated cacheables. That way, a distributable app can define
its own generators without locking the users of the app into it.
"""
_signals = [
source_created,
source_changed,
source_deleted,
cacheable_created,
cacheable_changed,
cacheable_deleted,
]
def __init__(self):
self._source_groups = {}
self._cacheables = {}
for signal in self._signals:
signal.connect(self.source_group_receiver)
signal.connect(self.cacheable_receiver)
before_access.connect(self.before_access_receiver)
def register(self, spec_id, source_groups):
def register(self, generator_id, cacheables):
"""
Associates source groups with a spec id
Associates cacheables with a generator id
"""
for source_group in source_groups:
if source_group not in self._source_groups:
self._source_groups[source_group] = set()
self._source_groups[source_group].add(spec_id)
for cacheable in cacheables:
if cacheable not in self._cacheables:
self._cacheables[cacheable] = set()
self._cacheables[cacheable].add(generator_id)
def unregister(self, spec_id, source_groups):
def unregister(self, generator_id, cacheables):
"""
Disassociates sources with a spec id
Disassociates cacheables with a generator id
"""
for source_group in source_groups:
for cacheable in cacheables:
try:
self._source_groups[source_group].remove(spec_id)
self._cacheables[cacheable].remove(generator_id)
except KeyError:
continue
def get(self, spec_id):
return [source_group for source_group in self._source_groups
if spec_id in self._source_groups[source_group]]
def get(self, generator_id):
return [cacheable for cacheable in self._cacheables
if generator_id in self._cacheables[cacheable]]
def before_access_receiver(self, sender, generator, file, **kwargs):
generator.image_cache_strategy.invoke_callback('before_access', file)
def before_access_receiver(self, sender, generator, cacheable, **kwargs):
generator.image_cache_strategy.invoke_callback('before_access', cacheable)
def source_group_receiver(self, sender, source, signal, info, **kwargs):
def cacheable_receiver(self, sender, cacheable, signal, info, **kwargs):
"""
Redirects signals dispatched on sources to the appropriate specs.
Redirects signals dispatched on cacheables
to the appropriate generators.
"""
source_group = sender
if source_group not in self._source_groups:
cacheable = sender
if cacheable not in self._cacheables:
return
for spec in (generator_registry.get(id, source=source, **info)
for id in self._source_groups[source_group]):
for generator in (generator_registry.get(id, cacheable=cacheable, **info)
for id in self._cacheables[cacheable]):
event_name = {
source_created: 'source_created',
source_changed: 'source_changed',
source_deleted: 'source_deleted',
cacheable_created: 'cacheable_created',
cacheable_changed: 'cacheable_changed',
cacheable_deleted: 'cacheable_deleted',
}
spec._handle_source_event(event_name, source)
generator._handle_cacheable_event(event_name, cacheable)
class Register(object):
"""
Register specs and sources.
Register generators and cacheables.
"""
def spec(self, id, spec=None):
if spec is None:
def generator(self, id, generator=None):
if generator is None:
# Return a decorator
def decorator(cls):
self.spec(id, cls)
self.generator(id, cls)
return cls
return decorator
generator_registry.register(id, spec)
generator_registry.register(id, generator)
def sources(self, spec_id, sources):
source_group_registry.register(spec_id, sources)
# iterable that returns kwargs or callable that returns iterable of kwargs
def cacheables(self, generator_id, cacheables):
if callable(cacheables):
cacheables = cacheables()
cacheable_registry.register(generator_id, cacheables)
class Unregister(object):
"""
Unregister specs and sources.
Unregister generators and cacheables.
"""
def spec(self, id, spec):
generator_registry.unregister(id, spec)
def generator(self, id, generator):
generator_registry.unregister(id, generator)
def sources(self, spec_id, sources):
source_group_registry.unregister(spec_id, sources)
def cacheables(self, generator_id, cacheables):
if callable(cacheables):
cacheables = cacheables()
cacheable_registry.unregister(generator_id, cacheables)
generator_registry = GeneratorRegistry()
source_group_registry = SourceGroupRegistry()
cacheable_registry = CacheableRegistry()
register = Register()
unregister = Unregister()

View file

@ -1,6 +1,6 @@
from django.dispatch import Signal
before_access = Signal()
source_created = Signal(providing_args=[])
source_changed = Signal()
source_deleted = Signal()
cacheable_created = Signal(providing_args=[])
cacheable_changed = Signal()
cacheable_deleted = Signal()

View file

@ -228,7 +228,7 @@ class SpecHost(object):
"""
self.spec_id = id
register.spec(id, self._original_spec)
register.generator(id, self._original_spec)
def get_spec(self, **kwargs):
"""

View file

@ -1,6 +1,6 @@
from django.db.models.signals import post_init, post_save, post_delete
from django.utils.functional import wraps
from ..signals import source_created, source_changed, source_deleted
from ..signals import cacheable_created, cacheable_changed, cacheable_deleted
def ik_model_receiver(fn):
@ -58,23 +58,25 @@ class ModelSignalRouter(object):
src in self._source_groups if src.model_class is instance.__class__)
@ik_model_receiver
def post_save_receiver(self, sender, instance=None, created=False, raw=False, **kwargs):
def post_save_receiver(self, sender, instance=None, created=False,
raw=False, **kwargs):
if not raw:
self.init_instance(instance)
old_hashes = instance._ik.get('source_hashes', {}).copy()
new_hashes = self.update_source_hashes(instance)
for attname, file in self.get_field_dict(instance).items():
if created:
self.dispatch_signal(source_created, file, sender, instance,
attname)
self.dispatch_signal(cacheable_created, file, sender,
instance, attname)
elif old_hashes[attname] != new_hashes[attname]:
self.dispatch_signal(source_changed, file, sender, instance,
attname)
self.dispatch_signal(cacheable_changed, file, sender,
instance, attname)
@ik_model_receiver
def post_delete_receiver(self, sender, instance=None, **kwargs):
for attname, file in self.get_field_dict(instance).items():
self.dispatch_signal(source_deleted, file, sender, instance, attname)
self.dispatch_signal(cacheable_deleted, file, sender, instance,
attname)
@ik_model_receiver
def post_init_receiver(self, sender, instance=None, **kwargs):
@ -119,5 +121,7 @@ class ImageFieldSourceGroup(object):
for instance in self.model_class.objects.all():
yield getattr(instance, self.image_field)
def __call__(self):
return self.files()
signal_router = ModelSignalRouter()

View file

@ -12,5 +12,5 @@ class ResizeTo1PixelSquare(ImageSpec):
super(ResizeTo1PixelSquare, self).__init__(**kwargs)
register.spec('testspec', TestSpec)
register.spec('1pxsq', ResizeTo1PixelSquare)
register.generator('testspec', TestSpec)
register.generator('1pxsq', ResizeTo1PixelSquare)