Skip to content

Commit 5b51444

Browse files
authored
Use django cache with large-image (#32)
* Use django cache with large-image * Linting * Remove print statements * Remove rest caching * Add test to validate large-image uses cache * Handle improper django configuration * Update README * Bump large image
1 parent e39d46c commit 5b51444

File tree

10 files changed

+128
-33
lines changed

10 files changed

+128
-33
lines changed

README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,9 @@ Support for any storage backend:
8585

8686
Miscellaneous:
8787
- Admin interface widget for viewing image tiles.
88-
- Caching - tile sources are cached for rapid file re-opening
89-
- tiles and thumbnails are cached to prevent recreating these data on multiple requests
88+
- Caching
89+
- image tiles and thumbnails are cached to prevent recreating these data on multiple requests
90+
- utilizes the [Django cache framework](https://docs.djangoproject.com/en/4.0/topics/cache/). Specify a named cache to use with the `LARGE_IMAGE_CACHE_NAME` setting.
9091
- Easily extensible SSR templates for tile viewing with CesiumJS and GeoJS
9192
- OpenAPI specification
9293

@@ -116,7 +117,7 @@ production environments. To install our GDAL wheel, use:
116117
pip install \
117118
--find-links https://girder.github.io/large_image_wheels \
118119
django-large-image \
119-
'large-image[gdal,pil]>=1.14'
120+
'large-image[gdal,pil]>=1.15'
120121
```
121122

122123
### 🐍 Conda

django_large_image/apps.py

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import logging
22

33
from django.apps import AppConfig
4-
from django.conf import settings
54
import large_image
65

76
logger = logging.getLogger(__name__)
@@ -13,20 +12,7 @@ class DjangoLargeImageConfig(AppConfig):
1312
default_auto_field = 'django.db.models.BigAutoField'
1413

1514
def ready(self):
16-
# Set up memcached with large_image
17-
if hasattr(settings, 'MEMCACHED_URL') and settings.MEMCACHED_URL:
18-
large_image.config.setConfig('cache_memcached_url', settings.MEMCACHED_URL)
19-
if (
20-
hasattr(settings, 'MEMCACHED_USERNAME')
21-
and settings.MEMCACHED_USERNAME
22-
and hasattr(settings, 'MEMCACHED_PASSWORD')
23-
and settings.MEMCACHED_PASSWORD
24-
):
25-
large_image.config.setConfig(
26-
'cache_memcached_username', settings.MEMCACHED_USERNAME
27-
)
28-
large_image.config.setConfig(
29-
'cache_memcached_password', settings.MEMCACHED_PASSWORD
30-
)
31-
large_image.config.setConfig('cache_backend', 'memcached')
32-
logger.info('large_image is configured for memcached.')
15+
# Set up cache with large_image
16+
# This isn't necessary but it makes sure we always default
17+
# to the django cache if others are available
18+
large_image.config.setConfig('cache_backend', 'django')

django_large_image/cache.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import threading
2+
3+
from django.conf import settings
4+
from django.core.cache import caches
5+
from django.core.exceptions import ImproperlyConfigured
6+
from large_image.cache_util.base import BaseCache
7+
from large_image.exceptions import TileCacheConfigurationError
8+
9+
10+
class DjangoCache(BaseCache):
11+
"""Use Django cache as the backing cache for large-image."""
12+
13+
def __init__(self, cache, getsizeof=None):
14+
super().__init__(0, getsizeof=getsizeof)
15+
self._django_cache = cache
16+
17+
def __repr__(self): # pragma: no cover
18+
return f'DjangoCache<{repr(self._django_cache._alias)}>'
19+
20+
def __iter__(self): # pragma: no cover
21+
# return invalid iter
22+
return None
23+
24+
def __len__(self): # pragma: no cover
25+
# return invalid length
26+
return -1
27+
28+
def __contains__(self, key):
29+
hashed_key = self._hashKey(key)
30+
return self._django_cache.__contains__(hashed_key)
31+
32+
def __delitem__(self, key):
33+
hashed_key = self._hashKey(key)
34+
return self._django_cache.delete(hashed_key)
35+
36+
def __getitem__(self, key):
37+
hashed_key = self._hashKey(key)
38+
value = self._django_cache.get(hashed_key)
39+
if value is None:
40+
return self.__missing__(key)
41+
return value
42+
43+
def __setitem__(self, key, value):
44+
hashed_key = self._hashKey(key)
45+
# TODO: do we want to use `add` instead to add a key only if it doesn’t already exist
46+
return self._django_cache.set(hashed_key, value)
47+
48+
@property
49+
def curritems(self): # pragma: no cover
50+
raise NotImplementedError
51+
52+
@property
53+
def currsize(self): # pragma: no cover
54+
raise NotImplementedError
55+
56+
@property
57+
def maxsize(self): # pragma: no cover
58+
raise NotImplementedError
59+
60+
def clear(self):
61+
self._django_cache.clear()
62+
63+
@staticmethod
64+
def getCache(): # noqa: N802
65+
try:
66+
name = getattr(settings, 'LARGE_IMAGE_CACHE_NAME', 'default')
67+
dajngo_cache = caches[name]
68+
except ImproperlyConfigured:
69+
raise TileCacheConfigurationError
70+
cache_lock = threading.Lock()
71+
cache = DjangoCache(dajngo_cache)
72+
return cache, cache_lock

django_large_image/rest/base.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@
99

1010
from django_large_image import tilesource, utilities
1111

12-
CACHE_TIMEOUT = 60 * 60 * 2
13-
1412

1513
class LargeImageMixinBase:
1614
def get_path(self, request: Request, pk: int = None) -> Union[str, pathlib.Path]:

django_large_image/rest/data.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
from django.http import HttpResponse
2-
from django.utils.decorators import method_decorator
3-
from django.views.decorators.cache import cache_page
42
from drf_yasg.utils import swagger_auto_schema
53
from rest_framework.decorators import action
64
from rest_framework.exceptions import ValidationError
@@ -9,7 +7,7 @@
97

108
from django_large_image import tilesource, utilities
119
from django_large_image.rest import params
12-
from django_large_image.rest.base import CACHE_TIMEOUT, LargeImageMixinBase
10+
from django_large_image.rest.base import LargeImageMixinBase
1311
from django_large_image.rest.renderers import image_data_renderers, image_renderers
1412

1513
thumbnail_summary = 'Returns thumbnail of full image.'
@@ -23,7 +21,6 @@
2321

2422

2523
class DataMixin(LargeImageMixinBase):
26-
@method_decorator(cache_page(CACHE_TIMEOUT))
2724
@swagger_auto_schema(
2825
method='GET',
2926
operation_summary=thumbnail_summary,

django_large_image/rest/tiles.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
from django.http import HttpResponse
2-
from django.utils.decorators import method_decorator
3-
from django.views.decorators.cache import cache_page
42
from drf_yasg.utils import swagger_auto_schema
53
from large_image.exceptions import TileSourceXYZRangeError
64
from rest_framework.decorators import action
@@ -10,7 +8,7 @@
108

119
from django_large_image import tilesource
1210
from django_large_image.rest import params
13-
from django_large_image.rest.base import CACHE_TIMEOUT, LargeImageMixinBase
11+
from django_large_image.rest.base import LargeImageMixinBase
1412
from django_large_image.rest.renderers import image_renderers
1513
from django_large_image.rest.serializers import TileMetadataSerializer
1614

@@ -35,7 +33,6 @@ def tiles_metadata(self, request: Request, pk: int = None) -> Response:
3533
serializer = TileMetadataSerializer(source)
3634
return Response(serializer.data)
3735

38-
@method_decorator(cache_page(CACHE_TIMEOUT))
3936
@swagger_auto_schema(
4037
method='GET',
4138
operation_summary=tile_summary,

project/example/core/tests/conftest.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,8 @@ def lonely_header_file() -> models.ImageFile:
6262
file__filename='envi_rgbsmall_bip.hdr',
6363
file__from_path=datastore.fetch('envi_rgbsmall_bip.hdr'),
6464
)
65+
66+
67+
@pytest.fixture
68+
def geotiff_path():
69+
return datastore.fetch('rgb_geotiff.tiff')
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
from large_image.cache_util.base import BaseCache
2+
import pytest
3+
4+
from django_large_image import tilesource
5+
from django_large_image.cache import DjangoCache
6+
7+
8+
@pytest.fixture
9+
def cache_miss_counter():
10+
class Counter:
11+
def __init__(self):
12+
self.count = 0
13+
14+
def reset(self):
15+
self.count = 0
16+
17+
counter = Counter()
18+
19+
def missing(*args, **kwargs):
20+
counter.count += 1
21+
BaseCache.__missing__(*args, **kwargs)
22+
23+
original = DjangoCache.__missing__
24+
DjangoCache.__missing__ = missing
25+
yield counter
26+
DjangoCache.__missing__ = original
27+
28+
29+
def test_tile(geotiff_path, cache_miss_counter):
30+
source = tilesource.get_tilesource_from_path(geotiff_path)
31+
cache_miss_counter.reset()
32+
# Check size of cache
33+
_ = source.getTile(0, 0, 0, encoding='PNG')
34+
assert cache_miss_counter.count == 1
35+
_ = source.getTile(0, 0, 0, encoding='PNG')
36+
assert cache_miss_counter.count == 1

project/setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
'django-s3-file-field[boto3]',
5151
'gunicorn',
5252
'django-large-image',
53-
'large-image[gdal,pil,ometiff,converter,vips,openslide,openjpeg]>=1.14',
53+
'large-image[gdal,pil,ometiff,converter,vips,openslide,openjpeg]>=1.15',
5454
'pooch',
5555
],
5656
extras_require={

setup.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,15 @@
4242
'djangorestframework',
4343
'drf-yasg',
4444
'filelock',
45-
'large-image>=1.14',
45+
'large-image>=1.15',
4646
],
4747
extras_require={
4848
'colormaps': [
4949
'matplotlib',
5050
'cmocean',
5151
],
5252
},
53+
entry_points={
54+
'large_image.cache': ['django = django_large_image.cache:DjangoCache'],
55+
},
5356
)

0 commit comments

Comments
 (0)