# HG changeset patch # User Peter Sanchez # Date 1558468475 25200 # Tue May 21 12:54:35 2019 -0700 # Node ID 663a3b355e29c66b6e6e41ae66311525ba360375 # Parent 0000000000000000000000000000000000000000 Initial commit diff --git a/.hgignore b/.hgignore new file mode 100644 --- /dev/null +++ b/.hgignore @@ -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 diff --git a/BSD-LICENSE b/BSD-LICENSE new file mode 100644 --- /dev/null +++ b/BSD-LICENSE @@ -0,0 +1,32 @@ +Copyright (c) 2019, Peter Sanchez +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. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,3 @@ +include BSD-LICENSE +include CHANGELOG +include README.rst diff --git a/README.rst b/README.rst new file mode 100644 --- /dev/null +++ b/README.rst @@ -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. . 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. diff --git a/json_settings2/__init__.py b/json_settings2/__init__.py new file mode 100644 --- /dev/null +++ b/json_settings2/__init__.py @@ -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) diff --git a/json_settings2/management/__init__.py b/json_settings2/management/__init__.py new file mode 100644 diff --git a/json_settings2/management/commands/__init__.py b/json_settings2/management/commands/__init__.py new file mode 100644 diff --git a/json_settings2/management/commands/write_json_settings.py b/json_settings2/management/commands/write_json_settings.py new file mode 100644 --- /dev/null +++ b/json_settings2/management/commands/write_json_settings.py @@ -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 ' + ), + ) + 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'], + ) diff --git a/setup.py b/setup.py new file mode 100644 --- /dev/null +++ b/setup.py @@ -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, +)