From 1b727c5b7792da1d95ab7cf5edc238f1aed77e65 Mon Sep 17 00:00:00 2001 From: Jens Kleineheismann Date: Tue, 29 Oct 2019 18:34:25 +0100 Subject: [PATCH] INIT --- .coveragerc | 2 + .gitignore | 10 ++ .pylintrc | 27 +++++ README.rst | 19 ++++ bin/coverage-html.py | 91 ++++++++++++++++ bin/django-deploy.py | 10 ++ setup.py | 83 +++++++++++++++ src/django_deploy/__init__.py | 1 + src/django_deploy/config.py | 1 + src/django_deploy/django_settings.py | 1 + src/django_deploy/main.py | 9 ++ src/django_deploy/program.py | 114 +++++++++++++++++++++ src/django_deploy/tests/__init__.py | 0 src/django_deploy/tests/django_settings.py | 1 + src/django_deploy/tests/test_main.py | 37 +++++++ src/django_deploy/tests/test_program.py | 69 +++++++++++++ src/django_deploy/version.py | 1 + tox.ini | 10 ++ 18 files changed, 486 insertions(+) create mode 100644 .coveragerc create mode 100644 .gitignore create mode 100644 .pylintrc create mode 100644 README.rst create mode 100755 bin/coverage-html.py create mode 100755 bin/django-deploy.py create mode 100644 setup.py create mode 100644 src/django_deploy/__init__.py create mode 100644 src/django_deploy/config.py create mode 100644 src/django_deploy/django_settings.py create mode 100644 src/django_deploy/main.py create mode 100644 src/django_deploy/program.py create mode 100644 src/django_deploy/tests/__init__.py create mode 100644 src/django_deploy/tests/django_settings.py create mode 100644 src/django_deploy/tests/test_main.py create mode 100644 src/django_deploy/tests/test_program.py create mode 100644 src/django_deploy/version.py create mode 100644 tox.ini diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..cd79f6a --- /dev/null +++ b/.coveragerc @@ -0,0 +1,2 @@ +[run] +source = django_deploy diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5517dde --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +/.idea/ + +/env/ + +*.pyc +/.coverage +/.tox/ +/geckodriver.log + +/src/django_deploy.egg-info/ diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..bfd1a4a --- /dev/null +++ b/.pylintrc @@ -0,0 +1,27 @@ +[MASTER] + +persistent=no +#load-plugins=pylint_django + + +[MESSAGES CONTROL] + +disable=missing-docstring, + missing-module-docstring, + missing-class-docstring, + missing-function-docstring, + useless-object-inheritance, + +[BASIC] + +good-names=_, + c, + d, + e, + i, + j, + k, + +[FORMAT] + +max-line-length=120 diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..889d05d --- /dev/null +++ b/README.rst @@ -0,0 +1,19 @@ +ABOUT +===== +This is a helper to deploy django apps. + + +REQUIREMENTS +============ +See INSTALL.rst + + +INSTALLATION +============ +See INSTALL.rst + + +LICENCE +======= +Permission to use, copy, modify, and/or distribute this software +for any purpose with or without fee is hereby granted. diff --git a/bin/coverage-html.py b/bin/coverage-html.py new file mode 100755 index 0000000..e72fb19 --- /dev/null +++ b/bin/coverage-html.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from __future__ import unicode_literals +import argparse +import datetime +import os +import shutil +import sys +import time +import coverage +from selenium import webdriver +from selenium.common.exceptions import WebDriverException + + +class Command(object): # pylint: disable=too-few-public-methods + default_browser = 'firefox' + + @staticmethod + def _setup_argparser(): + kwargs = { + 'description': 'Create a coverage report from a previous coverage run and show it within a browser window.', + } + parser = argparse.ArgumentParser(**kwargs) + parser.add_argument('-k', '--keep', action='store_true', dest='keep_report', + help='keep the report after closing the browser') + return parser + + def _parse_args(self, argv=None): + if argv is None: + argv = sys.argv[1:] + + return self._argparser.parse_args(argv) + + @staticmethod + def _create_report_directory(path=None): + if path is None: + timestamp = datetime.datetime.now().strftime('%Y%m%d-%H%M') + dirname = 'coverage-report-{}'.format(timestamp) + path = os.path.join('tmp', dirname) + os.makedirs(path) + return path + + @staticmethod + def _remove_report_directory(path): + if os.path.isdir(path): + sys.stdout.write('Removing report directory {}\n'.format(path)) + shutil.rmtree(path) + + def _create_report(self, path): + return self._coverage.html_report(directory=path, skip_covered=True) + + @staticmethod + def _show_report(path): + browser = webdriver.Firefox() + start_file = os.path.abspath(os.path.join(path, 'index.html')) + browser.get('file://{}'.format(start_file)) + while True: + time.sleep(1) + try: + _ = browser.window_handles + except WebDriverException: + break + return True + + def __init__(self): + self._argparser = self._setup_argparser() + self._coverage = coverage.Coverage() + self._coverage.load() + + def __call__(self, argv=None): + cmd_args = self._parse_args(argv) + report_dir = self._create_report_directory() + sys.stdout.write('Report directory: {}\n'.format(report_dir)) + try: + self._create_report(report_dir) + self._show_report(report_dir) + finally: + if not cmd_args.keep_report: + self._remove_report_directory(report_dir) + + return os.EX_OK + + +def main(): + cmd = Command() + exitval = cmd() + sys.exit(exitval) + + +if __name__ == '__main__': + main() diff --git a/bin/django-deploy.py b/bin/django-deploy.py new file mode 100755 index 0000000..9b92286 --- /dev/null +++ b/bin/django-deploy.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python +import os +import sys + +sys.path.append(os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'src')) + +from django_deploy import main + +if __name__ == '__main__': + main() diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..dadfe32 --- /dev/null +++ b/setup.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from __future__ import unicode_literals +import os +import sys +from setuptools import setup, find_packages +from setuptools import Command + + +class MyCommand(Command): + user_options = [] + + def initialize_options(self): + pass + + def finalize_options(self): + pass + + +class CreatePythonEnvironment(MyCommand): + description = 'create a (virtual) python environment' + + @staticmethod + def run(): + python_bin = sys.executable if sys.executable else 'python' + python_ver = sys.version_info.major + if python_ver == 2: + path = os.path.join('env', 'python2') + symlink_path = os.path.join('env', 'python') + venv_module = 'virtualenv' + prompt = '(py2) ' + elif python_ver == 3: + path = os.path.join('env', 'python3') + symlink_path = os.path.join('env', 'python') + venv_module = 'venv' + prompt = 'py3' + else: + sys.stderr.write('Python {} is not supported.\n'.format(python_ver)) + sys.exit(os.EX_USAGE) + + sys.stdout.write('Creating new python environment in {path}\n'.format(path=path)) + cmd = ('{bin} -m {venv_module}' + ' --prompt="{prompt}"' + ' {path}'.format(bin=python_bin, path=path, + venv_module=venv_module, prompt=prompt)) + os.system(cmd) + + if symlink_path and symlink_path != path and not os.path.exists(symlink_path): + symlink_dir = os.path.dirname(symlink_path) + relpath = os.path.relpath(path, symlink_dir) + os.symlink(relpath, symlink_path) + + print('') + print('Depending on your operating system or command shell,') + print('you should activate the new environment for this shell session') + print('by running ONE of the following commands:') + print('- Windows: %s' % os.path.join(path, 'Scripts', 'activate')) + print('- C Shell: source %s/bin/activate.csh' % path) + print('- All others: source %s/bin/activate' % path) + + +setup( + name='django-deploy', + version='0.1.dev0', + description='Helper to deploy django apps.', + url='https://dev.heinzelwerk.de/git/python/django-deploy', + maintainer='Jens Kleineheismann', + maintainer_email='heinzel@farbemachtstark.de', + cmdclass={ + 'python': CreatePythonEnvironment, + }, + packages=find_packages('src'), + package_dir={'': 'src'}, + #include_package_data=True, + entry_points={ + 'console_scripts': [ + 'django-deploy.py = django_deploy:main', + ], + }, + install_requires=[ + 'django', + ], +) diff --git a/src/django_deploy/__init__.py b/src/django_deploy/__init__.py new file mode 100644 index 0000000..c28a133 --- /dev/null +++ b/src/django_deploy/__init__.py @@ -0,0 +1 @@ +from .main import main diff --git a/src/django_deploy/config.py b/src/django_deploy/config.py new file mode 100644 index 0000000..55970af --- /dev/null +++ b/src/django_deploy/config.py @@ -0,0 +1 @@ +DJANGO_SETTINGS_MODULE_NAME = 'main' diff --git a/src/django_deploy/django_settings.py b/src/django_deploy/django_settings.py new file mode 100644 index 0000000..81fcbb1 --- /dev/null +++ b/src/django_deploy/django_settings.py @@ -0,0 +1 @@ +# ADD_INSTALLED_APPS = ['django_deploy'] diff --git a/src/django_deploy/main.py b/src/django_deploy/main.py new file mode 100644 index 0000000..a2ce4fd --- /dev/null +++ b/src/django_deploy/main.py @@ -0,0 +1,9 @@ +import sys + +from .program import Program + + +def main(*args, **kwargs): + program = Program() + exitval = program(*args, **kwargs) + sys.exit(exitval) diff --git a/src/django_deploy/program.py b/src/django_deploy/program.py new file mode 100644 index 0000000..13151a1 --- /dev/null +++ b/src/django_deploy/program.py @@ -0,0 +1,114 @@ +import argparse +import datetime +import importlib +import os +import sys + +from .config import DJANGO_SETTINGS_MODULE_NAME +from .version import VERSION + + +class Program(object): # pylint: disable=too-few-public-methods + @staticmethod + def _setup_argparser(parser): + parser.add_argument('-V', '--version', action='version', + version='%(prog)s ' + VERSION) + + parser.add_argument('-a', '--app', + action='append', dest='merge_apps', metavar='MODULE', + help='Merge settings from the django app MODULE\n' + 'Can be used multiple times') + + parser.add_argument('-c', '--create', + action='store_true', dest='create', + help='Create the django project directory') + + parser.add_argument('project_dir', metavar='PATH', + help='The directory, where the django project is or will be installed.') + + return parser + + def _parse_args(self, argv=None): + if argv is None: + argv = sys.argv[1:] + if not argv: + argv = () + + return self._argparser.parse_args(argv) + + @staticmethod + def _install_django_files(project_dir, overwrite=False): + if os.path.exists(project_dir): + if not overwrite: + sys.stderr.write('directory already exists: {}\n'.format(project_dir)) + return os.EX_NOPERM + else: + os.makedirs(project_dir) + + settings_module = DJANGO_SETTINGS_MODULE_NAME + cmd = 'django-admin startproject {name} {path}'.format(name=settings_module, + path=project_dir) + sys.stdout.write('Installing django files to {path}\n'.format(path=project_dir)) + exitval = os.system(cmd) + return exitval + + @staticmethod + def _add_installed_apps_from_app(project_dir, app): + settings_dir = os.path.join(project_dir, DJANGO_SETTINGS_MODULE_NAME) + + settings_from_app = importlib.import_module('{}.django_settings'.format(app)) + if hasattr(settings_from_app, 'ADD_INSTALLED_APPS'): + add_apps = settings_from_app.ADD_INSTALLED_APPS + else: + add_apps = [] + + if not add_apps: + sys.stdout.write('{}: do not care about INSTALLED_APPS\n'.format(app)) + return os.EX_OK + + sys.path.insert(0, settings_dir) + settings = importlib.import_module('settings') + sys.path.pop(0) + + missing_apps = [] + for app in add_apps: + if app not in settings.INSTALLED_APPS: + missing_apps.append(app) + + if missing_apps: + sys.stdout.write('{app}: adding {apps} to INSTALLED_APPS\n'.format(app=app, apps=missing_apps)) + comment = '### {app}: added apps with django-deploy at {timestamp}' + code = 'INSTALLED_APPS += [\n' + for app in missing_apps: + code += ' \'{}\',\n'.format(app) + code += ']\n' + + append_text = '\n' + comment + '\n' + code + '\n' + settings_file = os.path.join(settings_dir, 'settings.py') + with open(settings_file, 'a') as f: + now = datetime.datetime.now() + timestamp = now.strftime('%Y-%m-%d %H:%M:%S') + f.write(append_text.format(app=app, timestamp=timestamp)) + else: + sys.stdout.write('{app}: INSTALLED_APPS is fine\n'.format(app=app)) + + return os.EX_OK + + def __init__(self): + self._argparser = argparse.ArgumentParser() + self._setup_argparser(self._argparser) + + def __call__(self, *args, **kwargs): + argv = kwargs.get('argv', None) + cmd_args = self._parse_args(argv) + exitval = os.EX_OK + if cmd_args.create: + exitval = self._install_django_files(cmd_args.project_dir) + if exitval != os.EX_OK: + return exitval + if cmd_args.merge_apps: + for app in cmd_args.merge_apps: + exitval = self._add_installed_apps_from_app(cmd_args.project_dir, app) + if exitval != os.EX_OK: + return exitval + return exitval diff --git a/src/django_deploy/tests/__init__.py b/src/django_deploy/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/django_deploy/tests/django_settings.py b/src/django_deploy/tests/django_settings.py new file mode 100644 index 0000000..d4f2f21 --- /dev/null +++ b/src/django_deploy/tests/django_settings.py @@ -0,0 +1 @@ +ADD_INSTALLED_APPS = ['fake_app1', 'fake_app2'] diff --git a/src/django_deploy/tests/test_main.py b/src/django_deploy/tests/test_main.py new file mode 100644 index 0000000..cf56c77 --- /dev/null +++ b/src/django_deploy/tests/test_main.py @@ -0,0 +1,37 @@ +import os +import unittest +import pytest + +from .. import main + + +class MainTestCase(unittest.TestCase): + @pytest.fixture(autouse=True) + def capsys(self, capsys): # pylint: disable=method-hidden + self.capsys = capsys + + def test_main_without_arguments(self): + exitval = os.EX_SOFTWARE + try: + main() + except SystemExit as e: + exitval = e.code + finally: + captured = self.capsys.readouterr() + + is_usage = captured.err.startswith('usage:') + self.assertTrue(is_usage, 'main() does not produce usage text') + self.assertNotEqual(os.EX_OK, exitval) + + def test_main_help(self): + exitval = os.EX_SOFTWARE + try: + main(argv=['-h']) + except SystemExit as e: + exitval = e.code + finally: + captured = self.capsys.readouterr() + + is_usage = captured.out.startswith('usage:') + self.assertTrue(is_usage, 'main() does not produce usage text') + self.assertEqual(os.EX_OK, exitval) diff --git a/src/django_deploy/tests/test_program.py b/src/django_deploy/tests/test_program.py new file mode 100644 index 0000000..c71dbe3 --- /dev/null +++ b/src/django_deploy/tests/test_program.py @@ -0,0 +1,69 @@ +import importlib +import os +import sys +import unittest +import pytest + +from ..config import DJANGO_SETTINGS_MODULE_NAME +from ..program import Program + + +class MainTestCase(unittest.TestCase): + @pytest.fixture(autouse=True) + def tmpdir(self, tmpdir): # pylint: disable=method-hidden + self.tmpdir = tmpdir + + def setUp(self): + self._program = Program() + + def test_create(self): + tmpdir = self.tmpdir + project_dir = os.path.join(str(tmpdir), 'env', 'django') + + exitval = self._program(argv=['-c', project_dir]) + + self.assertEqual(os.EX_OK, exitval, 'program() does not return os.EX_OK') + self.assertTrue(os.path.isdir(project_dir), 'project directory was not created') + settings_dir = os.path.join(project_dir, DJANGO_SETTINGS_MODULE_NAME) + self.assertTrue(os.path.isdir(settings_dir), 'settings directory was not created') + settings_file = os.path.join(settings_dir, 'settings.py') + self.assertTrue(os.path.isfile(settings_file), 'settings.py was not created') + manage_script = os.path.join(project_dir, 'manage.py') + self.assertTrue(os.path.isfile(manage_script), 'manage.py was not created') + + def test_create_dont_overwrite(self): + tmpdir = self.tmpdir + project_dir = os.path.join(str(tmpdir), 'env', 'django') + + self._program(argv=['--create', project_dir]) + exitval = self._program(argv=['--create', project_dir]) + + self.assertEqual(os.EX_NOPERM, exitval, 'second call to program() does not exit with os.EX_NOPERM') + + def test_merge_installed_apps(self): + tmpdir = self.tmpdir + project_dir = os.path.join(str(tmpdir), 'env', 'django') + + expected_installed_apps = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'fake_app1', + 'fake_app2', + ] + + argv = ['-c', project_dir] + self._program(argv=argv) + + argv = ['-a', 'django_deploy', '-a', 'django_deploy.tests', project_dir] + self._program(argv=argv) + + settings_dir = os.path.join(project_dir, DJANGO_SETTINGS_MODULE_NAME) + sys.path.insert(0, settings_dir) + settings = importlib.import_module('settings') + importlib.reload(settings) + sys.path.pop(0) + self.assertListEqual(expected_installed_apps, settings.INSTALLED_APPS) diff --git a/src/django_deploy/version.py b/src/django_deploy/version.py new file mode 100644 index 0000000..8084d01 --- /dev/null +++ b/src/django_deploy/version.py @@ -0,0 +1 @@ +VERSION = '0.1.dev0' diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..7e6a0a3 --- /dev/null +++ b/tox.ini @@ -0,0 +1,10 @@ +[tox] +envlist = py3,py2 + +[testenv] +deps = coverage + pylint + pytest +commands = coverage run -m pytest + coverage report --skip-covered + pylint django_deploy