This commit is contained in:
Ryan McGrath 2015-06-05 01:52:48 +09:00
commit fa22e7a03f
9 changed files with 390 additions and 0 deletions

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
*.py[co]
__pycache__
/MANIFEST
/dist

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) Ryan McGrath (2015). Copyright (c) for any originating Whitenoise
code is held by David Evans (2013).
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

4
MANIFEST.in Normal file
View file

@ -0,0 +1,4 @@
include README.rst
include LICENSE
recursive-exclude * __pycache__
recursive-exclude * *.py[co]

52
README.md Normal file
View file

@ -0,0 +1,52 @@
RedNoise
==========
Django as a framework is great, but file handling within any setup has never been particularly fun to configure. **[WhiteNoise](https://whitenoise.readthedocs.org/)** makes this, to borrow its own term, "radically simplified", and for the most part I've found it to be an ideal solution - though there are a few things I found myself wanting from time to time.
RedNoise is a different take on the DjangoWhiteNoise module from WhiteNoise. It aims to be (and as of writing, should be) completely compatible with the existing WhiteNoise API, but takes a different approach on a few things. I consider this an opinionated third-party addon to the WhiteNoise project, however I hope it goes without saying that anything here is up for grabs as a pull request or merge.
Getting Started
====================
1. `pip install django-rednoise`
2. Follow the WhiteNoise configuration guidelines - they all should work.
3. Modify your wsgi file as follows:
``` python
from django.core.wsgi import get_wsgi_application
from rednoise import DjangoRedNoise
application = get_wsgi_application()
application = DjangoRedNoise(application)
```
...and that's it. You can read on for additional configuration options if you think you need them, but the defaults are more or less sane. DjangoRedNoise is the only Class in this package; existing guides/documentation for WhiteNoise should still suffice.
Differences from WhiteNoise
-----------------------------------
- **RedNoise allows you to serve user-uploaded media**
Note that it performs no gzipping of content or anything; the use case this satisfied (for me, at least) was that users within a CMS
needed to be able to upload images as part of a site; configuring storages and some S3 setup just got annoying to deal with.
- **RedNoise respects Django's DEBUG flag**
When DEBUG is True, RedNoise will mimic the native Django static files handling routine. With this change, RedNoise can be used while
in development (provided you're developing with uwsgi) so your environment can simulate a production server. I've found this to be
faster than using Django's static serving in urls.py solution, YMMV.
- **When DEBUG is false, RedNoise mimics WhiteNoise's original behavior**
...with two exceptions. One, being that Media can also be served, and two - whereas WhiteNoise scans all static files on startup,
RedNoise will look for the file upon user request. If found, it will cache it much like WhiteNoise does - the advantage of this
approach is that one can add static file(s) as necessary after the fact without requiring a restart of the process.
- **RedNoise 404s directly at the uwsgi level, rather than through the Django application**
Personally speaking, I don't see why Django should bother processing a 404 for an image that we know doesn't exist. This is, of
course, a personal opinion of mine.
License
-------
MIT Licensed
Contact
-------
Questions, concerns? ryan [at] venodesigns dot net

52
README.rst Normal file
View file

@ -0,0 +1,52 @@
RedNoise
==========
Django as a framework is great, but file handling within any setup has never been particularly fun to configure. WhiteNoise (https://whitenoise.readthedocs.org/) makes this, to borrow its own term, "radically simplified", and for the most part I've found it to be an ideal solution - though there are a few things I found myself wanting from time to time.
RedNoise is a different take on the DjangoWhiteNoise module from WhiteNoise. It aims to be (and as of writing, should be) completely compatible with the existing WhiteNoise API, but takes a different approach on a few things. I consider this an opinionated third-party addon to the WhiteNoise project, however I hope it goes without saying that anything here is up for grabs as a pull request or merge.
Getting Started
====================
1. ``pip install django-rednoise``
2. Follow the WhiteNoise configuration guidelines - they all should work.
3. Modify your wsgi file as follows:
.. code-block:: python
from django.core.wsgi import get_wsgi_application
from rednoise import DjangoRedNoise
application = get_wsgi_application()
application = DjangoRedNoise(application)
...and that's it. You can read on for additional configuration options if you think you need them, but the defaults are more or less sane. DjangoRedNoise is the only Class in this package; existing guides/documentation for WhiteNoise should still suffice.
Differences from WhiteNoise
-----------------------------------
- **RedNoise allows you to serve user-uploaded media**
Note that it performs no gzipping of content or anything; the use case this satisfied (for me, at least) was that users within a CMS
needed to be able to upload images as part of a site; configuring storages and some S3 setup just got annoying to deal with.
- **RedNoise respects Django's DEBUG flag**
When DEBUG is True, RedNoise will mimic the native Django static files handling routine. With this change, RedNoise can be used while
in development (provided you're developing with uwsgi) so your environment can simulate a production server. I've found this to be
faster than using Django's static serving in urls.py solution, YMMV.
- **When DEBUG is false, RedNoise mimics WhiteNoise's original behavior**
...with two exceptions. One, being that Media can also be served, and two - whereas WhiteNoise scans all static files on startup,
RedNoise will look for the file upon user request. If found, it will cache it much like WhiteNoise does - the advantage of this
approach is that one can add static file(s) as necessary after the fact without requiring a restart of the process.
- **RedNoise 404s directly at the uwsgi level, rather than through the Django application**
Personally speaking, I don't see why Django should bother processing a 404 for an image that we know doesn't exist. This is, of
course, a personal opinion of mine.
License
-------
MIT Licensed
Contact
-------
Questions, concerns? ryan [at] venodesigns dot net

4
rednoise/__init__.py Normal file
View file

@ -0,0 +1,4 @@
from __future__ import absolute_import
from .base import RedNoise
__all__ = ['DjangoRedNoise']

215
rednoise/base.py Normal file
View file

@ -0,0 +1,215 @@
from __future__ import absolute_import
from os.path import isfile
try:
import urlparse
except ImportError:
import urllib.parse as urlparse
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.contrib.staticfiles import finders
from whitenoise.django import DjangoWhiteNoise
A404 = '404 NOT FOUND'
PI = 'PATH_INFO'
class DjangoRedNoise(DjangoWhiteNoise):
rednoise_config_attrs = [
'should_serve_static',
'should_serve_media'
]
debug = False
should_serve_static = True
should_serve_media = True
def __init__(self, application):
"""Basic init stuff. We allow overriding a few extra things.
"""
self.charset = settings.FILE_CHARSET
self.application = application
self.staticfiles_dirs = []
self.static_files = {}
self.media_files = {}
# Allow settings to override default attributes
# We check for existing WHITENOISE_{} stuff to be compatible, but then
# add a few RedNoise specific ones in order to not be too confusing.
self.check_and_set_settings('WHITENOISE_{}', self.config_attrs)
self.check_and_set_settings('REDNOISE_{}', self.rednoise_config_attrs)
# If DEBUG=True in settings, then we'll just default Rednoise to debug.
try:
setattr(self, 'debug', getattr(settings, 'DEBUG'))
except AttributeError:
pass
# Grab the various roots we care about.
if self.should_serve_static:
self.static_root, self.static_prefix = self.get_structure('STATIC')
try:
setattr(self, 'staticfiles_dirs', getattr(
settings, 'STATICFILES_DIRS'
))
except AttributeError:
pass
if self.should_serve_media:
self.media_root, self.media_prefix = self.get_structure('MEDIA')
def __call__(self, environ, start_response):
"""Checks to see if a request is inside our designated media or static
configurations.
"""
if self.should_serve_static and self.is_static(environ[PI]):
asset = self.load_static_file(environ[PI])
if asset is not None:
return self.serve(asset, environ, start_response)
else:
start_response(A404, [('Content-Type', 'text/plain')])
return ['Not Found']
if self.should_serve_media and self.is_media(environ[PI]):
asset = self.load_media_file(environ[PI])
if asset is not None:
return self.serve(asset, environ, start_response)
else:
start_response(A404, [('Content-Type', 'text/plain')])
return ['Not Found']
return self.application(environ, start_response)
def file_not_modified(self, static_file, environ):
"""We just hook in here to always return false (i.e, it was modified)
in DEBUG scenarios. This is optimal for development/reloading
scenarios.
In a production scenario, you want the original Whitenoise setup, so
super().
"""
if self.debug:
return False
return super(DjangoRedNoise, self).file_not_modified(
static_file,
environ
)
def add_cache_headers(self, static_file, url):
"""Again, we hook in here to blank on adding cache headers in DEBUG
scenarios. This is optimal for development/reloading
scenarios.
In a production scenario, you want the original Whitenoise setup, so
super().
"""
if self.debug:
return
super(DjangoRedNoise, self).add_cache_headers(static_file, url)
def check_and_set_settings(self, settings_key, attributes):
"""Checks settings to see if we should override something.
"""
for attr in attributes:
key = settings_key.format(attr.upper())
try:
setattr(self, attr, getattr(settings, key))
except AttributeError:
pass
def get_structure(self, key):
"""This code is almost verbatim from the Whitenoise project Django
integration. Little reason to change it, short of string substitution.
"""
url = getattr(settings, '%s_URL' % key, None)
root = getattr(settings, '%s_ROOT' % key, None)
if not url or not root:
raise ImproperlyConfigured('%s_URL and %s_ROOT \
must be configured to use RedNoise' % (key, key))
prefix = urlparse.urlparse(url).path
prefix = '/{}/'.format(prefix.strip('/'))
return root, prefix
def is_static(self, path):
"""Checks to see if a given path is trying to be all up in
our static director(y||ies).
"""
return path[:len(self.static_prefix)] == self.static_prefix
def add_static_file(self, path):
"""Custom, ish. Adopts the same approach as Whitenoise, but instead
handles creating of a File object per each valid static/media request.
This is then cached for lookup later if need-be.
If REDNOISE_DEBUG is True, then this will also scan extra
STATICFILES_DIRS.
See also: self.add_media_file()
"""
file_path = None
is_file = False
if self.debug:
result = finders.find(path.replace(self.static_prefix, ''))
if result:
is_file = True
file_path = result
if not is_file: # Account for stuff like admin, etc
file_path = ('%s/%s' % (
self.static_root, path.replace(self.static_prefix, '')
)).replace('\\', '/')
is_file = isfile(file_path)
if is_file:
files = {}
files[path] = self.get_static_file(file_path, path)
if not self.debug:
self.find_gzipped_alternatives(files)
self.static_files.update(files)
def load_static_file(self, path):
"""Retrieves a static file, optimizing along the way.
Very possible it can return None. TODO: perhaps optimize that
use case somehow.
"""
asset = self.static_files.get(path)
if asset is None or self.debug:
self.add_static_file(path)
asset = self.static_files.get(path)
return asset
def is_media(self, path):
"""Checks to see if a given path is trying to be all up in our
media director(y||ies).
"""
return path[:len(self.media_prefix)] == self.media_prefix
def add_media_file(self, path):
"""Custom, ish. Adopts the same approach as Whitenoise, but instead
handles creating of a File object per each valid static/media request.
This is then cached for lookup later if need-be.
Media and static assets have differing properties by their very
nature, so we have separate methods.
"""
file_path = ('%s/%s' % (
self.media_root, path.replace(self.media_prefix, '')
)).replace('\\', '/')
if isfile(file_path):
files = {}
files[path] = self.get_static_file(file_path, path)
self.media_files.update(files)
def load_media_file(self, path):
"""Retrieves a media file, optimizing along the way.
"""
asset = self.media_files.get(path)
if asset is None or self.debug:
self.add_media_file(path)
asset = self.media_files.get(path)
return asset

3
setup.cfg Normal file
View file

@ -0,0 +1,3 @@
[wheel]
universal = 1
description-file = README.rst

35
setup.py Normal file
View file

@ -0,0 +1,35 @@
import os
import codecs
from setuptools import setup, find_packages
PROJECT_ROOT = os.path.abspath(os.path.dirname(__file__))
def read(*path):
full_path = os.path.join(PROJECT_ROOT, *path)
with codecs.open(full_path, 'r', encoding='utf-8') as f:
return f.read()
setup(
name='django-rednoise',
version='1.0.2',
author='Ryan McGrath',
author_email='ryan@venodesigns.net',
url='https://github.com/ryanmcgrath/django-rednoise/',
packages=find_packages(exclude=['tests*']),
install_requires=['django', 'whitenoise'],
license='MIT',
description="Opinionated Django-specific addon for Whitenoise.",
long_description=read('README.rst'),
keywords=['django', 'static', 'wsgi'],
classifiers=[
'Development Status :: 5 - Production/Stable',
'Framework :: Django',
'Intended Audience :: Developers',
'License :: OSI Approved :: MIT License',
'Operating System :: OS Independent',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3.3',
'Programming Language :: Python :: 3.4',
],
)