diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 9df0060..4733868 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -24,11 +24,15 @@ jobs: python-version: 3.8.5 - name: Generate coverage report run: | - pip install -r requirements.txt - pytest --cov cold_silence --cov-report=xml + make setup + make install + make test - name: Upload coverage to Codecov uses: codecov/codecov-action@v1 with: token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: true verbose: true + - name: Clean up + run: | + make clean diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..da37f4c --- /dev/null +++ b/COPYING @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2021 + +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. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..7a37e22 --- /dev/null +++ b/Makefile @@ -0,0 +1,24 @@ +clean: + rm -rf project_output + rm -f .coverage + rm -f coverage.xml + rm -rf .pytest_cache + +test: + pytest --cov cold_silence --cov-report=xml + +test_clean: + make test + make clean + +build_default_project: + python3 src/cold_silence/main.py + +setup: + pip install -r requirements.txt + +format: + black ./ + +install: + pip install . \ No newline at end of file diff --git a/README.md b/README.md index 93122ab..bc4631c 100644 --- a/README.md +++ b/README.md @@ -6,15 +6,35 @@ Start a new Django project sans boilerplate code. +# Setup + +```bash +cd [project_dir] +pip install . +cd [project_dir]/src/cold_silence +pip install -r requirements.txt + +OR + +cd [project_dir] +make install +make setup +``` + # Quickstart ```bash -cd src +cd [project_dir]/src/cold_silence python3 main.py + +OR + +cd [project_dir] +make build_default_project ``` ```bash -cd src +cd [project_dir]/src/cold_silence python3 main.py --server_port 8080 --project_name my_project --service_name my_service --project_directory my_project --verbose ``` @@ -42,6 +62,23 @@ python3 main.py --server_port 8080 --project_name my_project --service_name my_s │ └── staging.env ``` +# Testing + +To run tests, run the following + +```bash +cd [project_dir] +pytest --cov cold_silence --cov-report=xml + +OR + +make test # This will generate several artifacts, along with a project, which can be removed with make clean + +OR + +make test_clean # This will automatically remove the generated artifacts and generated project +``` + # Info Python Version: 3.8.5 diff --git a/src/cold_silence/main.py b/src/cold_silence/main.py index 8ce1595..907f55b 100644 --- a/src/cold_silence/main.py +++ b/src/cold_silence/main.py @@ -4,6 +4,7 @@ from cold_silence.nginx_gen import NginxGen from pathlib import Path from cold_silence.settings_gen import SettingsGen +from cold_silence.requirements_gen import RequirementsGen from subprocess import Popen as pop from cold_silence.utils import ( DEFAULT_PATH, @@ -11,19 +12,16 @@ DEFAULT_SERVER_PORT, DEFAULT_PROJECT_NAME, DEFAULT_PROJECT_DIRECTORY, + DEFAULT_CONFIG, + ENGINE_SQLITE3, + verify_or_install_django, + verify_or_install_gunicorn, + verify_or_install_rest_framework, + verify_or_install_black, ) import argparse -import pip import sys - -def __verify_django_version(): - import django - - version = django.__version__ - print("Proceeding using Django version: " + version) - - def __print_message(message, verbose): if verbose: print(message) @@ -38,7 +36,7 @@ def __end_gen_message(message, verbose): def parse_args(args): - print(args) + parser = argparse.ArgumentParser() parser.add_argument( @@ -65,6 +63,18 @@ def parse_args(args): type=str, ) + parser.add_argument( + "--engine", + help="Database to use for the project, the default is sqlite.", + type=str, + ) + + parser.add_argument( + "--rest_api", + help="Generate a REST API starter, by default this is disabled", + action="store_true", + ) + parser.add_argument("--verbose", help="Display verbose output", action="store_true") args = parser.parse_args(args) @@ -85,40 +95,38 @@ def parse_args(args): else DEFAULT_PROJECT_DIRECTORY ) + rest_api = args.rest_api if args.rest_api is not None else False + + engine = args.engine if args.engine is not None else ENGINE_SQLITE3 + verbose = args.verbose return { + "engine": engine, "path": path, - "service_name": service_name, - "server_port": server_port, - "project_name": project_name, "project_directory": project_directory, + "project_name": project_name, + "rest_api": rest_api, + "server_port": server_port, + "service_name": service_name, "verbose": verbose, } -def generate_project( - path=DEFAULT_PATH, - service_name=DEFAULT_SERVICE_NAME, - server_port=DEFAULT_SERVER_PORT, - project_name=DEFAULT_PROJECT_NAME, - project_directory=DEFAULT_PROJECT_DIRECTORY, - verbose=False, -): - try: - __verify_django_version() - except Exception as e: - try: - print( - "Can't find Django, attempting to install using `python3 -m pip install django`..." - ) - - pip.main(["install", "django"]) - __verify_django_version() - except Exception as e: - print("Unable to install Django, install Django before proceeding.") - exit(1) - out_path = path +def generate_project(config=DEFAULT_CONFIG): + verify_or_install_django() + verify_or_install_gunicorn() + verify_or_install_rest_framework() + verify_or_install_black() + + engine = config.get("engine", ENGINE_SQLITE3) + out_path = config.get("path", DEFAULT_PATH) + project_directory = config.get("project_directory", DEFAULT_PROJECT_DIRECTORY) + project_name = config.get("project_name", DEFAULT_PROJECT_NAME) + rest_api = config.get("rest_api", False) + server_port = config.get("server_port", DEFAULT_SERVER_PORT) + service_name = config.get("service_name", DEFAULT_SERVICE_NAME) + verbose = config.get("verbose", False) __print_message("Creating directory " + out_path + " ...", verbose=verbose) @@ -131,7 +139,7 @@ def generate_project( __print_message(out_path + " created!", verbose=verbose) __begin_gen_message( - "Django project " + project_name + " in " + out_path, verbose=verbose + "django project " + project_name + " in " + out_path, verbose=verbose ) try: @@ -143,36 +151,45 @@ def generate_project( # https://stackoverflow.com/a/2837319 op.communicate() except Exception as e: - print(e) - exit(1) + raise e - __end_gen_message("Django project " + project_name, verbose=verbose) + __end_gen_message("django project " + project_name, verbose=verbose) - # Get inside the Django project's main directory + # Get inside the django project's main directory settings_path = out_path + "/" + project_name + "/" + project_name __begin_gen_message("Settings file", verbose=verbose) - SettingsGen().generate_settings_file(path=settings_path) + SettingsGen().generate_settings_file( + path=settings_path, project_name=project_name, engine=engine, rest_api=rest_api + ) __end_gen_message("Settings file", verbose=verbose) + __begin_gen_message("Requirements file", verbose=verbose) + + RequirementsGen().generate_requirements_file( + path=out_path, project_directory=project_directory + ) + + __end_gen_message("Requirements file", verbose=verbose) + __begin_gen_message("Environment variable files", verbose=verbose) - EnvGen().generate_all_env_files(path=path) + EnvGen().generate_all_env_files(path=out_path) __end_gen_message("Environment variable files", verbose=verbose) __begin_gen_message("Git ignore file", verbose=verbose) - GitIgnoreGen().generate_gitignore_file(path=path) + GitIgnoreGen().generate_gitignore_file(path=out_path) __end_gen_message("Git ignore file", verbose=verbose) __begin_gen_message("Nginx files", verbose=verbose) NginxGen().generate_nginx_files( - path=path, server_port=server_port, service_name=service_name + path=out_path, server_port=server_port, service_name=service_name ) __end_gen_message("Nginx files", verbose=verbose) @@ -180,7 +197,7 @@ def generate_project( __begin_gen_message("Dockerfiles and docker-compose files", verbose=verbose) DockerGen().generate_docker_files( - path=path, + path=out_path, project_directory=project_directory, project_name=project_name, server_port=server_port, @@ -189,21 +206,25 @@ def generate_project( __end_gen_message("Dockerfiles and docker-compose files", verbose=verbose) + try: + op = pop( + ["/usr/bin/python3", "-m", "black", "."], + shell=False, + cwd=out_path, + ) + # https://stackoverflow.com/a/2837319 + op.communicate() + except Exception as e: + raise e + print("Project generation complete, you're ready to get started!") print("Happy programming!") def main(): - args = parse_args(sys.argv[1:]) + config = parse_args(sys.argv[1:]) - generate_project( - path=args["path"], - service_name=args["service_name"], - server_port=args["server_port"], - project_name=args["project_name"], - project_directory=args["project_directory"], - verbose=args["verbose"], - ) + generate_project(config=config) if __name__ == "__main__": diff --git a/src/cold_silence/requirements_gen.py b/src/cold_silence/requirements_gen.py new file mode 100644 index 0000000..7820143 --- /dev/null +++ b/src/cold_silence/requirements_gen.py @@ -0,0 +1,46 @@ +from cold_silence.utils import ( + DEFAULT_PROJECT_DIRECTORY, + write_to_file, + DEFAULT_PATH, +) + + +class RequirementsGen: + # Methods + def __generate_rest_api_requirements_file_content(self): + try: + import rest_framework + except Exception as e: + raise e + + return "djangorestframework=={0}".format(rest_framework.__version__) + + def __generate_standard_requirements_file_content(self): + try: + import django + import gunicorn + except Exception as e: + raise e + + return """ +Django=={0} +gunicorn=={1} +""".format( + django.__version__, gunicorn.__version__ + ) + + def generate_requirements_file( + self, + path=DEFAULT_PATH, + project_directory=DEFAULT_PROJECT_DIRECTORY, + rest_api=False, + ): + content = self.__generate_standard_requirements_file_content() + + if rest_api: + rest_content = self.__generate_rest_api_requirements_file_content() + content += "\n{0}".format(rest_content) + + write_to_file( + "{0}/{1}/requirements.txt".format(path, project_directory), contents=content + ) diff --git a/src/cold_silence/settings_gen.py b/src/cold_silence/settings_gen.py index 4c3c5f2..2e1a4e6 100644 --- a/src/cold_silence/settings_gen.py +++ b/src/cold_silence/settings_gen.py @@ -4,7 +4,6 @@ DEFAULT_PROJECT_NAME, ENGINE_SQLITE3, ) -import os class SettingsGen: @@ -32,8 +31,25 @@ def __generate_database_config(self, engine): engine, db_config ) + def __generate_installed_apps_content(self, rest_api=False): + installed_apps = [ + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + ] + + if rest_api: + installed_apps.append("rest_framework") + + content = "".join("\n\"{0}\",".format(app) for app in installed_apps) + + return "[{0}]".format(content) + def __generate_settings_file_content( - self, project_name=DEFAULT_PROJECT_NAME, engine=ENGINE_SQLITE3 + self, project_name=DEFAULT_PROJECT_NAME, engine=ENGINE_SQLITE3, rest_api=False ): try: import django @@ -46,6 +62,7 @@ def __generate_settings_file_content( db_config = self.__generate_database_config(engine=engine) + installed_apps = self.__generate_installed_apps_content(rest_api=rest_api) content = """ \"\"\" Django settings for {0} project. @@ -84,14 +101,7 @@ def get_env_value(env_variable): # Application definition -INSTALLED_APPS = [ - "django.contrib.admin", - "django.contrib.auth", - "django.contrib.contenttypes", - "django.contrib.sessions", - "django.contrib.messages", - "django.contrib.staticfiles", -] +INSTALLED_APPS = {4} MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", @@ -151,7 +161,11 @@ def get_env_value(env_variable): STATIC_URL = "/static/" """.format( - project_name, django.__version__, django_major_version, db_config + project_name, + django.__version__, + django_major_version, + db_config, + installed_apps, ) except Exception as e: @@ -164,11 +178,12 @@ def generate_settings_file( path=DEFAULT_PATH, project_name=DEFAULT_PROJECT_NAME, engine=ENGINE_SQLITE3, + rest_api=False, ): settings_file_path = "{0}/settings.py".format(path) content = self.__generate_settings_file_content( - project_name=project_name, engine=engine + project_name=project_name, engine=engine, rest_api=rest_api ) write_to_file(settings_file_path, contents=content) diff --git a/src/cold_silence/utils.py b/src/cold_silence/utils.py index 00bd361..5a1bd88 100644 --- a/src/cold_silence/utils.py +++ b/src/cold_silence/utils.py @@ -2,6 +2,7 @@ import errno import os import random +import pip DEFAULT_PATH = "project_output" @@ -15,6 +16,8 @@ DEFAULT_DEBUG = True +DEFAULT_CONFIG = {} + ENGINE_SQLITE3 = "sqlite3" ENGINE_POSTGRESQL = "postgresql" @@ -36,6 +39,91 @@ def generate_random_string(length=30): return "".join(random.choices(ascii_letters + digits + punctuation, k=length)) +def __verify_django_version(): + import django + + version = django.__version__ + print("Proceeding using django version: " + version) + + +def __verify_rest_framework_version(): + import rest_framework + + version = rest_framework.__version__ + print("Proceeding using rest_framework version: " + version) + + +def __verify_gunicorn_version(): + import gunicorn + + version = gunicorn.__version__ + print("Proceeding using gunicorn version: " + version) + +def __verify_black_version(): + import black + + version = black.__version__ + print("Proceeding using black version: " + version) + + +def verify_or_install_django(): + try: + __verify_django_version() + except Exception as e: + try: + print( + "Can't find django, attempting to install using `python3 -m pip install django`..." + ) + + pip.main(["install", "django"]) + __verify_django_version() + except Exception as e: + raise e + + +def verify_or_install_rest_framework(): + try: + __verify_rest_framework_version() + except Exception as e: + try: + print( + "Can't find rest_framework, attempting to install using `python3 -m pip install djangorestframework`..." + ) + + pip.main(["install", "djangorestframework"]) + __verify_rest_framework_version() + except Exception as e: + raise e + + +def verify_or_install_gunicorn(): + try: + __verify_gunicorn_version() + except Exception as e: + try: + print( + "Can't find gunicorn, attempting to install using `python3 -m pip install gunicorn`..." + ) + + pip.main(["install", "gunicorn"]) + __verify_gunicorn_version() + except Exception as e: + raise e + +def verify_or_install_black(): + try: + __verify_black_version() + except Exception as e: + try: + print( + "Can't find black, attempting to install using `python3 -m pip install black`..." + ) + + pip.main(["install", "black"]) + __verify_black_version() + except Exception as e: + raise e + def write_to_file(file_path, contents): if not os.path.exists(os.path.dirname(file_path)): try: diff --git a/tests/test_main.py b/tests/test_main.py index 0305983..658f2a2 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -3,6 +3,7 @@ import os import unittest + class MainTestSuite(unittest.TestCase): def test_create_project(self): @@ -57,62 +58,9 @@ def test_create_project(self): True, os.path.exists(os.path.dirname(DEFAULT_PATH + "/prod.env")) ) - def test_create_verbose_project(self): - - generate_project(verbose=True) - - self.assertEqual( - True, - os.path.exists( - os.path.dirname( - DEFAULT_PATH + "/" + DEFAULT_PROJECT_DIRECTORY + "/" + "Dockerfile" - ) - ), - ) - self.assertEqual( - True, - os.path.exists( - os.path.dirname( - DEFAULT_PATH - + "/" - + DEFAULT_PROJECT_DIRECTORY - + "/" - + "Dockerfile.prod" - ) - ), - ) - self.assertEqual( - True, - os.path.exists(os.path.dirname(DEFAULT_PATH + "/" + "docker-compose.yaml")), - ) - self.assertEqual( - True, os.path.exists(os.path.dirname(DEFAULT_PATH + "/nginx/Dockerfile")) - ) - self.assertEqual( - True, os.path.exists(os.path.dirname(DEFAULT_PATH + "/nginx/nginx.conf")) - ) - self.assertEqual( - True, os.path.exists(os.path.dirname(DEFAULT_PATH + "/settings.py")) - ) - self.assertEqual( - True, os.path.exists(os.path.dirname(DEFAULT_PATH + "/.gitignore")) - ) - self.assertEqual( - True, os.path.exists(os.path.dirname(DEFAULT_PATH + "/local.env")) - ) - self.assertEqual( - True, os.path.exists(os.path.dirname(DEFAULT_PATH + "/dev.env")) - ) - self.assertEqual( - True, os.path.exists(os.path.dirname(DEFAULT_PATH + "/staging.env")) - ) - self.assertEqual( - True, os.path.exists(os.path.dirname(DEFAULT_PATH + "/prod.env")) - ) - def test_create_project_from_main(self): - args = parse_args( + config = parse_args( [ "--server_port", "8080", @@ -126,14 +74,7 @@ def test_create_project_from_main(self): ] ) - generate_project( - path=args["path"], - service_name=args["service_name"], - server_port=args["server_port"], - project_name=args["project_name"], - project_directory=args["project_directory"], - verbose=args["verbose"], - ) + generate_project(config=config) self.assertEqual( True, diff --git a/tests/test_utils.py b/tests/test_utils.py index 0e7554a..01af036 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,7 +1,7 @@ import unittest import os from cold_silence.utils import DEFAULT_PATH, write_to_file -import pytest + class UtilsTestSuite(unittest.TestCase): def test_create_file(self):