7 files changed, 480 insertions(+), 0 deletions(-)

A => .hgignore
A => BSD-LICENSE
A => MANIFEST.in
A => README.rst
A => json_settings2/__init__.py
A => json_settings2/management/commands/write_json_settings.py
A => setup.py
A => .hgignore +15 -0
@@ 0,0 1,15 @@ 
+syntax:glob
+.coverage
+.tox
+settings_local.py
+.*.swp
+**.pyc
+MANIFEST
+.idea
+
+syntax:regexp
+^htmlcov$
+^env$
+syntax: glob
+*.komodoproject
+.DS_Store
  No newline at end of file

          
A => BSD-LICENSE +32 -0
@@ 0,0 1,32 @@ 
+Copyright (c) 2019, Peter Sanchez <petersanchez@gmail.com>
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or 
+without modification, are permitted provided that the 
+following conditions are met:
+
+ * Redistributions of source code must retain the above 
+   copyright notice, this list of conditions and the 
+   following disclaimer.
+
+ * Redistributions in binary form must reproduce the above 
+   copyright notice, this list of conditions and the following 
+   disclaimer in the documentation and/or other materials 
+   provided with the distribution.
+
+ * Neither the name of Peter Sanchez nor the names of its 
+   contributors may be used to endorse or promote products 
+   derived from this software without specific prior written 
+   permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 
+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 
+TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

          
A => MANIFEST.in +3 -0
@@ 0,0 1,3 @@ 
+include BSD-LICENSE
+include CHANGELOG
+include README.rst

          
A => README.rst +220 -0
@@ 0,0 1,220 @@ 
+.. |nlshield| image:: https://img.shields.io/badge/100%25-Netlandish-blue.svg?style=square-flat
+              :target: https://www.netlandish.com
+
+================================
+django-json-settings2 |nlshield|
+================================
+:Info: Simple application to store Django settings in a json file.
+:Version: 0.1.0
+:Author: Team Netlandish (https://www.netlandish.com)
+
+Python / Django Support
+=======================
+
+* Python 3.6+ for Django versions 1.11+
+
+Truthfully, this app is so simple it will probably work with previous 
+version of Python and Django but we can't promise that.
+
+Why?
+====
+
+I know, right? Who needs another way to store settings outside of the 
+standard Django ``settings.py`` setup.
+
+None of the existing ways actually fit our typical Django deployment 
+setup in a way that was satisfactory. This method allows us to store 
+settings externally and in a way that fits our needs. Maybe it'll 
+fit yours too.
+
+Also, there is already a ``django-json-settings`` app and while that app 
+very well may be perfect for your project, it isn't a good fit for ours.
+
+We created this app years ago and simply tuned it slowly as needed. It's 
+very simple yet flexible enough to work within virtually any workflow.
+
+This app is really nothing more than helper functins wrapped on top 
+of standard ``json`` module ops.
+
+Others
+======
+
+There are several other options to store settings outside of the typical
+Django ``settings.py`` file. Here are a few:
+
+* https://github.com/isotoma/django-json-settings
+* https://github.com/theskumar/python-dotenv
+
+There's dozens of others. Pick the one that best suits your needs.
+
+Usage
+=====
+
+Saving Settings
+---------------
+
+You're going to need to save your desired settings to a json file 
+first. There's a simple helper function, and management command, 
+included to help.
+
+For instance, say you want to create a simple setting for your ``SECRET_KEY`` 
+variable::
+
+    $ python
+    >>> settings_to_save = ['SECRET_KEY']
+    >>> from json_settings2 import write_settings_from_django
+    >>> write_settings_from_django(*settings_to_save)
+    >>> exit()
+    $ cat .settings.json
+    {
+        "SECRET_KEY": "SUPER SECRET KEY IS HERE! COOOOOOOLLLLL!"
+    }
+    $
+
+The ``write_settings_from_django`` function takes a few optional variables:
+
+* settings_vars = Positional args giving every Django setting to save
+* filename = Filename of the json settings file. Defaults to ``.settings.json``
+* directory = Directory in which to save ``filename``. Defaults to ``.``.
+* indent = Indentation level for the json output. Set to ``None`` for the most 
+           compact file. Defaults to ``4``.
+* force = If ``directory``/``filename`` exists, overwrite it. 
+          Defaults to ``False``
+
+You can also just use the management command. This requires that you place
+``json_settings2`` in your ``INSTALLED_APPS`` setting::
+
+    $ python manage.py write_json_settings SECRET_KEY
+
+You can add as many settings as you'd like too::
+
+    $ python manage.py write_json_settings SECRET_KEY DATABASES STATIC_URL
+
+To see the options, simply::
+
+    $ python manage.py help write_json_settings
+
+Loading Settings
+----------------
+
+The easiest way is to store all your default and local settings in 
+``settings.py`` and load the json settings at the end. It's pretty 
+straight forward. Let's see an example::
+
+    $ cat settings.py
+    import os
+    from json_settings2 import load_settings
+
+    DEBUG = True
+    STATIC_URL = '/static/'
+    ... LOTS OF OTHERS SETTINGS HERE ...
+
+    SETTINGS_DIR = os.path.dirname(os.path.abspath(__file__))
+    load_settings(globals(), directory=SETTINGS_DIR, depth=3)
+
+Essentially this will tell the function to look for the settings file 
+starting in the same directory as ``settings.py`` and if not found, 
+look up to 3 levels higher in the directory tree. So let's say the 
+path if your ``settings.py`` file is ``/home/user/app/current/code/settings.py``
+
+The ``load_settings`` function will search the following paths for 
+``.settings.json``:
+
+* ``/home/user/app/current/code/``
+* ``/home/user/app/current/``
+* ``/home/user/app/``
+* ``/home/user/``
+
+Useful if you want to store your settings outside of the code deployment 
+directories, which is often the case.
+
+The ``load_settings`` function takes the following variables:
+
+* current_settings - Dictionary that will be updated with found settings. 
+                     Generally you'd pass in ``globals()``.
+* filename - Name of json file with settings. Defaults to ``.settings.json``
+* directory - Path of the directory where ``filename`` lives. Defaults to ``.``.
+* depth - Number of parent directories to scan for ``filename``. Defaults to ``0``.
+* store = Store settings into the ``current_settings`` dict. Defaults to ``True``.
+
+If ``store`` is set to ``False`` then the ``current_settings`` dict will not 
+be altered.
+
+The function will always return the pythonic representation of what was found 
+in the json settings file.
+
+**Note on directory** - By default, the ``directory`` variable above is 
+set to ``.`` - meaning current directory. This usually means the directory
+where you started the Python interpreter or are running ``manage.py`` from.
+This is usually NOT what you want. It's best practice to always set the 
+expected directory to avoid troubleshooting headaches.
+
+What Is Used As a Setting?
+--------------------------
+
+When calling ``load_settings`` you can include extra data in your json 
+settings file that is useful for other puposes in your code but is not 
+something you want cluttering your ``django.conf.settings`` object.
+
+Only keys that are stored in all capital letters will be stored
+to the ``current_settings`` dict. So if your json settings has options 
+that are not all caps, they will only be returned as part of the loaded 
+json data.
+
+In other words, say you ``load_settings`` on the following data::
+
+    {
+        "SeCreT_Key": "This will not be saved in Django settings.",
+        "SECRET_KEY": "This WILL be saved in Django settings.",
+        "secret_key": :This will not be saved in Django settings."
+    }
+
+Your ``SECRET_KEY`` setting will be set to ``This WILL be saved in Django settings.``
+
+Where To Load Settings?
+-----------------------
+
+Normally you can place it at the bottom of the ``settings.py`` file. 
+However, there are often times that you need those settings to guide 
+the values of other settings.
+
+There is nothing stopping you from loading your json settings from 
+anywhere in the process. It's up to you. Just remember that if you 
+load your settings and then set a duplicate variable AFTER loading 
+the json settings, the duplicate variable will have the final say.
+
+For example::
+
+    $ cat .setting.json
+    {
+        "STATIC_URL": "/my/cool/static/url/"
+    }
+    $ cat settings.py
+    import os
+    from json_settings2 import load_settings
+
+    SETTINGS_DIR = os.path.dirname(os.path.abspath(__file__))
+    load_settings(globals(), directory=SETTINGS_DIR, depth=3)
+
+    DEBUG = True
+    STATIC_URL = '/static/'
+    ... LOTS OF OTHERS SETTINGS HERE ...
+
+The value of your ``STATIC_URL`` setting will be set to ``/static/`` when 
+you might be expecting it to be ``/my/cool/static/url/``. Just a heads up.
+
+Copyright & Warranty
+====================
+All documentation, libraries, and sample code are
+Copyright 2019 Netlandish Inc. <hello@netlandish.com>. The library and
+sample code are made available to you under the terms of the BSD license
+which is contained in the included file, BSD-LICENSE.
+
+
+==================
+Commercial Support
+==================
+
+This software, and lots of other software like it, has been built in support of many of
+Netlandish's own projects, and the projects of our clients. We would love to help you
+on your next project so get in touch by dropping us a note at hello@netlandish.com.

          
A => json_settings2/__init__.py +112 -0
@@ 0,0 1,112 @@ 
+import os
+import json
+import stat
+from pathlib import Path
+
+
+VERSION = (0, 1, 0, 'final', 0)
+
+
+def get_version():
+    "Returns a PEP 386-compliant version number from VERSION."
+    assert len(VERSION) == 5
+    assert VERSION[3] in ('alpha', 'beta', 'rc', 'final')
+
+    # Now build the two parts of the version number:
+    # main = X.Y[.Z]
+    # sub = .devN - for pre-alpha releases
+    #     | {a|b|c}N - for alpha, beta and rc releases
+
+    parts = 2 if VERSION[2] == 0 else 3
+    main = '.'.join(str(x) for x in VERSION[:parts])
+
+    sub = ''
+    if VERSION[3] != 'final':
+        mapping = {'alpha': 'a', 'beta': 'b', 'rc': 'c'}
+        sub = mapping[VERSION[3]] + str(VERSION[4])
+
+    return str(main + sub)
+
+
+def load_settings(
+    current_settings,
+    filename='.settings.json',
+    directory='.',
+    depth=0,
+    store=True,
+):
+    """ Load settings...
+
+        filename - Name of json file with settings
+        directory - Path of the directory where `filename` lives
+        depth - Number of parent directories to scan for `filename`
+        store = Store settings into the globals variable.
+
+        Should be called in your django settings.py like so:
+
+            from json_settings2 import load_settings
+            load_settings(globals())
+
+        Will return the dictionary with all loaded variables added to it.
+    """
+    assert isinstance(current_settings, dict)
+    path = Path(os.path.expanduser(directory)).absolute()
+    file_path = path / filename
+    depth_counter = 0
+
+    while not file_path.exists():
+        if depth_counter > depth:
+            raise FileNotFoundError(
+                f'File {filename} not found using {path} with depth of {depth}'
+            )
+        file_path = path.parents[depth_counter] / filename
+        depth_counter += 1
+
+    with file_path.open() as fd:
+        json_data = json.load(fd)
+
+    for k, v in json_data.items():
+        if k == k.upper() and store:
+            # Django setting, set it
+            current_settings[k] = v
+
+    return json_data
+
+
+def write_settings_from_django(
+    *settings_vars,
+    filename='.settings.json',
+    directory='.',
+    indent=4,
+    force=False,
+):
+    """ Helper to write settings from current django settings
+        to json settings.
+
+        settings_vars = List of django settings to include in json settings
+        filename = Filename of the json settings file
+        directory = Directory in which to save `filename`
+        indent = Indentation level for the json output. Set to "None" for
+                 the most compact file.
+        force = If `directory`/`filename` exists, overwrite it
+
+        Example:
+
+            write_settings_from_django(DATABASES, DEBUG, ADMINS, SECRET_KEY)
+
+    """
+    from django.conf import settings
+
+    path = Path(os.path.expanduser(directory)).absolute() / filename
+    if path.exists() and not force:
+        raise FileExistsError(f'File {path} already exists. Not overwriting.')
+
+    _vars = {}
+    for setting in settings_vars:
+        _vars[setting] = getattr(settings, setting)
+
+    with path.open(mode='w') as fd:
+        json.dump(_vars, fd, indent=indent)
+
+    # Equivilent of 0600
+    os.chmod(path, stat.S_IRUSR | stat.S_IWUSR)

          
A => json_settings2/management/commands/write_json_settings.py +62 -0
@@ 0,0 1,62 @@ 
+from django.conf import settings
+from django.core.management.base import BaseCommand, CommandError
+
+from json_settings2 import write_settings_from_django
+
+
+class Command(BaseCommand):
+    help = 'Write specified django settings to file.'
+
+    def add_arguments(self, parser):
+        parser.add_argument(
+            'django_settings',
+            nargs='+',
+            help='Django settings variables that should be written to file',
+        )
+        parser.add_argument(
+            '-f',
+            '--filename',
+            action='store',
+            default='.settings.json',
+            dest='filename',
+            help='Filename to save settings as. Defaults to .settings.json',
+        )
+        parser.add_argument(
+            '-d',
+            '--directory',
+            action='store',
+            default='.',
+            dest='directory',
+            help=(
+                'Directory to save settings file in. '
+                'Defaults to <current directory>'
+            ),
+        )
+        parser.add_argument(
+            '-i',
+            '--indent',
+            action='store',
+            type=int,
+            default=4,
+            dest='indent',
+            help='Indentation level for json output. Defaults to 4.',
+        )
+        parser.add_argument(
+            '--force',
+            action='store_true',
+            default=False,
+            dest='force',
+            help=(
+                'If settings file already exists, overwrite it. '
+                'Defaults to False'
+            ),
+        )
+
+    def handle(self, *args, **options):
+        write_settings_from_django(
+            *options['django_settings'],
+            filename=options['filename'],
+            directory=options['directory'],
+            indent=options['indent'],
+            force=options['force'],
+        )

          
A => setup.py +36 -0
@@ 0,0 1,36 @@ 
+import os
+from setuptools import setup, find_packages
+
+
+project_name = 'json_settings2'
+
+if os.path.exists('README.rst'):
+    long_description = open('README.rst', 'r').read()
+else:
+    long_description = \
+        'See https://bitbucket.org/netlandish/django-json-settings2/'
+
+
+setup(
+    name=project_name,
+    version=__import__(project_name).get_version(),
+    packages=find_packages(),
+    description='Set, and save, Django settings to json formatted files.',
+    author='Netlandish Inc.',
+    author_email='hello@netlandish.com',
+    url='https://bitbucket.org/netlandish/django-json-settings2/',
+    long_description=long_description,
+    platforms=['any'],
+    install_requires=['Django>=1.11'],
+    classifiers=[
+        'Development Status :: 4 - Beta',
+        'Intended Audience :: Developers',
+        'Natural Language :: English',
+        'Operating System :: OS Independent',
+        'Programming Language :: Python',
+        'Programming Language :: Python :: 3.6',
+        'Programming Language :: Python :: 3.7',
+        'Environment :: Web Environment',
+    ],
+    include_package_data=True,
+)