├── .coveragerc ├── .github └── workflows │ ├── lint.yml │ ├── publish-to-testpypi.yml │ ├── publish.yml │ └── test.yml ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── create_app ├── __init__.py ├── __main__.py ├── cli.py ├── main.py ├── project.py ├── project_configuration.py ├── project_configuration_file.py ├── settings.py ├── templates.py └── tests │ ├── __init__.py │ ├── __main__test.py │ ├── cli_test.py │ ├── main_test.py │ ├── project_configuration_file_test.py │ ├── project_configuration_test.py │ ├── project_test.py │ ├── templates_test.py │ └── utils.py ├── docs └── static │ ├── logo-cropped.png │ └── logo.png ├── pyproject.toml ├── requirements.build.frozen ├── requirements.frozen ├── requirements.test.frozen ├── scripts └── run_unit_tests.sh ├── setup.py ├── templates.json └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | relative_files = True 3 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - "LICENSE" 7 | - "*.md" 8 | 9 | pull_request: 10 | paths-ignore: 11 | - "LICENSE" 12 | - "*.md" 13 | 14 | jobs: 15 | lint: 16 | runs-on: ubuntu-latest 17 | 18 | strategy: 19 | matrix: 20 | python-version: ["3.x"] # "3.7", "3.8", "3.9", "3.10" 21 | 22 | steps: 23 | - uses: actions/checkout@v3 24 | 25 | - name: Set up Python 26 | uses: actions/setup-python@v4 27 | with: 28 | python-version: ${{ matrix.python-version }} 29 | 30 | - name: Install dependencies 31 | run: | 32 | python -m pip install --upgrade pip 33 | 34 | - name: Lint 35 | uses: pre-commit/action@v3.0.0 36 | -------------------------------------------------------------------------------- /.github/workflows/publish-to-testpypi.yml: -------------------------------------------------------------------------------- 1 | name: Publish to Test-PyPI 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | permissions: 8 | contents: read 9 | 10 | jobs: 11 | publish: 12 | if: "github.event.release.prerelease" 13 | 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | 19 | - name: Set up Python 20 | uses: actions/setup-python@v4 21 | with: 22 | python-version: "*" 23 | 24 | - name: Install latest pip, build, twine 25 | run: | 26 | python -m pip install --upgrade --disable-pip-version-check pip 27 | python -m pip install -r requirements.build.frozen 28 | 29 | - name: Build wheel and source distributions 30 | run: | 31 | python -m build 32 | 33 | - name: Upload to PyPI via Twine 34 | env: 35 | TWINE_PASSWORD: ${{ secrets.TEST_PYPI_TOKEN }} 36 | run: | 37 | twine upload --repository testpypi --verbose -u '__token__' dist/* 38 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to PyPI 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | permissions: 8 | contents: read 9 | 10 | jobs: 11 | publish: 12 | if: "!github.event.release.prerelease" 13 | 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | 19 | - name: Set up Python 20 | uses: actions/setup-python@v4 21 | with: 22 | python-version: "*" 23 | 24 | - name: Install latest pip, build, twine 25 | run: | 26 | python -m pip install --upgrade --disable-pip-version-check pip 27 | python -m pip install -r requirements.build.frozen 28 | 29 | - name: Build wheel and source distributions 30 | run: | 31 | python -m build 32 | 33 | - name: Upload to PyPI via Twine 34 | env: 35 | TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} 36 | run: | 37 | twine upload --verbose -u '__token__' dist/* 38 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - "LICENSE" 7 | - "*.md" 8 | 9 | pull_request: 10 | paths-ignore: 11 | - "LICENSE" 12 | - "*.md" 13 | 14 | jobs: 15 | test: 16 | runs-on: ${{ matrix.os }} 17 | 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | python-version: ["3.x"] # "3.7", "3.8", "3.9", "3.10" 22 | os: [ubuntu-latest] # , macOS-latest, windows-latest 23 | 24 | steps: 25 | - uses: actions/checkout@v3 26 | 27 | - name: Set up Python ${{ matrix.python-version }} 28 | uses: actions/setup-python@v4 29 | with: 30 | python-version: ${{ matrix.python-version }} 31 | 32 | - name: Install dependencies 33 | run: | 34 | python -m pip install --upgrade pip 35 | pip install -r requirements.frozen 36 | pip install -r requirements.test.frozen 37 | 38 | - name: Test with pytest 39 | run: | 40 | pytest . --junitxml=test-results.xml --cov=create_app --cov-report=xml --cov-report=html 41 | 42 | - name: Publish coverage to Coveralls 43 | 44 | uses: AndreMiras/coveralls-python-action@v20201129 45 | 46 | with: 47 | github-token: ${{ secrets.GITHUB_TOKEN }} 48 | parallel: true 49 | flag-name: py${{ matrix.python-version }}-${{ matrix.os }} 50 | debug: true 51 | 52 | coveralls-finish: 53 | needs: test 54 | 55 | runs-on: ubuntu-latest 56 | 57 | steps: 58 | - uses: actions/checkout@v3 59 | 60 | - name: Coveralls finished 61 | uses: AndreMiras/coveralls-python-action@v20201129 62 | with: 63 | parallel-finished: true 64 | debug: true 65 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | .idea/ 132 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/psf/black 3 | rev: 23.3.0 4 | hooks: 5 | - id: black 6 | - repo: https://github.com/PyCQA/isort 7 | rev: 5.12.0 8 | hooks: 9 | - id: isort 10 | args: ["--profile", "black"] 11 | - repo: https://github.com/PyCQA/flake8 12 | rev: 6.0.0 13 | hooks: 14 | - id: flake8 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 gabrielbazan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include requirements.frozen 2 | include requirements.test.frozen 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | 3 | SYSTEM_PYTHON_BIN=python3 4 | 5 | VIRTUALENV_PATH=./venv 6 | VIRTUALENV_PYTHON_BIN=${VIRTUALENV_PATH}/bin/python3 7 | VIRTUALENV_PIP_BIN=${VIRTUALENV_PATH}/bin/pip 8 | VIRTUALENV_ACTIVATE=${VIRTUALENV_PATH}/bin/activate 9 | 10 | REQUIREMENTS_FILE_PATH=./requirements.frozen 11 | TEST_REQUIREMENTS_FILE_PATH=./requirements.test.frozen 12 | BUILD_REQUIREMENTS_FILE_PATH=./requirements.build.frozen 13 | 14 | SETUP_FILENAME=setup.py 15 | 16 | 17 | install_git_hooks: 18 | pre-commit install 19 | 20 | 21 | run_git_hooks: 22 | pre-commit run --all-files 23 | 24 | 25 | cleanup: 26 | @echo "Cleaning up..." 27 | rm -fr *.egg-info 28 | rm -fr build 29 | rm -fr dist 30 | @echo "Done!" 31 | 32 | 33 | create_virtualenv: 34 | @echo "Creating virtualenv..." 35 | ${SYSTEM_PYTHON_BIN} -m venv "${VIRTUALENV_PATH}" 36 | @echo "Done!" 37 | 38 | 39 | delete_virtualenv: 40 | @echo "Deleting virtualenv..." 41 | rm -fr ${VIRTUALENV_PATH} 42 | @echo "Done!" 43 | 44 | 45 | install_requirements: 46 | @echo "Installing requirements..." 47 | ${VIRTUALENV_PIP_BIN} install -r "${REQUIREMENTS_FILE_PATH}" 48 | @echo "Done!" 49 | 50 | 51 | install_test_requirements: 52 | @echo "Installing test requirements..." 53 | ${VIRTUALENV_PIP_BIN} install -r "${TEST_REQUIREMENTS_FILE_PATH}" 54 | @echo "Done!" 55 | 56 | 57 | install_build_requirements: 58 | @echo "Installing build requirements..." 59 | ${VIRTUALENV_PIP_BIN} install -r "${BUILD_REQUIREMENTS_FILE_PATH}" 60 | @echo "Done!" 61 | 62 | 63 | install_all_requirements: install_requirements install_test_requirements install_build_requirements 64 | 65 | 66 | run_unit_tests: 67 | @echo "Running unit tests..." 68 | . ${VIRTUALENV_ACTIVATE} && ./scripts/run_unit_tests.sh && deactivate 69 | @echo "Done!" 70 | 71 | 72 | install_in_virtualenv: 73 | ${VIRTUALENV_PIP_BIN} install -e . 74 | 75 | 76 | build: 77 | ${VIRTUALENV_PYTHON_BIN} -m build 78 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Create App logo 3 |

4 | 5 |

6 | Test Workflow Status 7 | Linting Workflow Status 8 | PyPI Publication Workflow Status 9 | Coverage Status 10 | 11 | 12 | PyPI 13 | Code style: black 14 |

15 | 16 | _create_app_ is a tool for creating applications from templates. 17 | 18 | When developers start a new project, they perform some repetitive tasks to build the basic project structure before 19 | actually start coding features. This basic structure involves things like: Well, the project structure, unit testing, 20 | code coverage, containerization, code linting and formatting, GIT hooks, building code documentation, among many others. 21 | 22 | _create_app_ is a tool that allows to quickly get your basic project structure ready. It provides a set of templates 23 | from which you can get your project started, plus it's super easy to use and encourages the adoption of the best 24 | technologies, tools, and practices. 25 | 26 | At the moment, there are only Python templates available. But _create_app_ can generate projects of **any language**, 27 | as it uses [cookiecutter](https://cookiecutter.readthedocs.io/en/stable/). 28 | 29 | 30 | ## Installation 31 | 32 | Just install it with PIP: 33 | ```shell 34 | python -m pip install create_app 35 | ``` 36 | 37 | ## Usage 38 | 39 | Learn how to use the _create_app_ command: 40 | ```shell 41 | python -m create_app --help 42 | ``` 43 | 44 | You can use the _--help_ option for all subcommands too. 45 | 46 | 47 | ### List templates 48 | 49 | Use the _list_ subcommand to know which templates you can use: 50 | 51 | ```shell 52 | python -m create_app list 53 | ``` 54 | 55 | 56 | ### Create your project from a template 57 | 58 | Use the _create_ subcommand and specify the _TEMPLATE_NAME_ you wish to use: 59 | ```shell 60 | create_app create TEMPLATE_NAME 61 | ``` 62 | 63 | 64 | #### Using the template defaults 65 | 66 | If you don't want to configure your project and just want to create it from the template using all the default values, 67 | use the _--use-defaults_ flag: 68 | ```shell 69 | create_app create TEMPLATE_NAME --use-defaults 70 | ``` 71 | 72 | 73 | #### Using a configuration file 74 | 75 | When creating a project, you are asked to type your project configuration in. If you already know which the template 76 | settings are and the values you want to use, you can use the _--config-file_ option to specify these values from a JSON 77 | file. This option is specially useful for scripting: 78 | ```shell 79 | create_app create TEMPLATE_NAME --config-file=config.json 80 | ``` 81 | 82 | 83 | ### Using a custom templates index 84 | 85 | You or your organization may need to keep a separate index with your own templates. 86 | 87 | If that's the case, you can list the templates in the custom index by running: 88 | ```shell 89 | create_app list --index="https://www.somewhere.com/templates-index" 90 | ``` 91 | 92 | And create your project from a template in that index: 93 | ```shell 94 | create_app create TEMPLATE_NAME --index="https://www.somewhere.com/templates-index" 95 | ``` 96 | 97 | 98 | ### Using create_app from Python 99 | 100 | You can import create_app from Python too, which is great for scripting or creating multiple projects at once: 101 | 102 | ```python 103 | from create_app.main import create_app 104 | 105 | # Create from a template named "python_simple" 106 | create_app("python_simple") 107 | ``` 108 | 109 | 110 | ## How it works 111 | 112 | It uses a great tool named [cookiecutter](https://cookiecutter.readthedocs.io/en/stable/) to build your project from 113 | a template. The templates are cookiecutters, so you could simply use cookiecutter to build your project from the 114 | template repo URL: 115 | ```shell 116 | python -m cookiecutter https://github.com/application-creators/python_simple 117 | ``` 118 | 119 | _create_app_ is just an entry point to a set of templates, and I was inspired by 120 | [create-react-app](https://create-react-app.dev/docs/getting-started#selecting-a-template), which lets you create your 121 | project from a template by just specifying a template name. 122 | 123 | The main goal though, and what I would like to focus on, is to come up with great project templates. Then people can 124 | choose to directly use cookiecutter to build their projects, or could use this tool to discover which templates are 125 | available and to create their project. 126 | 127 | 128 | ## Index of Available Templates 129 | 130 | There's an [index of templates](/templates.json), from which you can get your project started: 131 | 132 | | **Template** | **Description** | 133 | |------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 134 | | [python_simple](https://github.com/application-creators/python_simple) | Python project with unit tests, GIT hooks ([pre-commit](https://pre-commit.com/), [black](https://github.com/psf/black), [isort](https://pycqa.github.io/isort/), and [flake8](https://flake8.pycqa.org/en/latest/)), and Docker | 135 | | [python_compose](https://github.com/application-creators/python_compose) | Python project with unit tests, GIT hooks ([pre-commit](https://pre-commit.com/), [black](https://github.com/psf/black), [isort](https://pycqa.github.io/isort/), and [flake8](https://flake8.pycqa.org/en/latest/)), and Docker Compose | 136 | | [python_fastapi](https://github.com/application-creators/python_fastapi) | FastAPI project with unit tests, GIT hooks ([pre-commit](https://pre-commit.com/), [black](https://github.com/psf/black), [isort](https://pycqa.github.io/isort/), and [flake8](https://flake8.pycqa.org/en/latest/)), and Docker | 137 | | [python_fastapi_with_database](https://github.com/application-creators/python_fastapi_with_database) | FastAPI project with unit tests, GIT hooks ([pre-commit](https://pre-commit.com/), [black](https://github.com/psf/black), [isort](https://pycqa.github.io/isort/), and [flake8](https://flake8.pycqa.org/en/latest/)), Docker Compose, a [PostgreSQL](https://www.postgresql.org/) database (which can be very easily changed for any other), [SQLAlchemy](https://www.sqlalchemy.org/), and [Alembic](https://alembic.sqlalchemy.org/) migrations | 138 | 139 | 140 | ## Contribute 141 | 142 | [Application Creators](https://github.com/application-creators) is a new GitHub organization I've created to host, 143 | debate, and maintain this tool and the project templates. Its goal is to generate state-of-the-art templates useful 144 | to everyone. Feel free to express you opinion and contribute! 145 | -------------------------------------------------------------------------------- /create_app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/application-creators/create_app/0afe77d926d523b23a7ba18c52878f78600ba142/create_app/__init__.py -------------------------------------------------------------------------------- /create_app/__main__.py: -------------------------------------------------------------------------------- 1 | from create_app.cli import main 2 | 3 | main() 4 | -------------------------------------------------------------------------------- /create_app/cli.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from create_app.main import create_app, list_templates 4 | from create_app.settings import TEMPLATES_FILE_URI 5 | 6 | 7 | class Argument: 8 | TEMPLATE_NAME = "template_name" 9 | 10 | 11 | class Option: 12 | USE_DEFAULTS = "--use-defaults" 13 | INDEX = "--index" 14 | CONFIG_FILE = "--config-file" 15 | 16 | 17 | HELP = { 18 | Option.USE_DEFAULTS: "Use default configuration values from the template", 19 | Option.INDEX: f"Templates index URL. Default: {TEMPLATES_FILE_URI}", 20 | Option.CONFIG_FILE: "Template configuration file path", 21 | } 22 | 23 | 24 | @click.group() 25 | def main(): 26 | pass 27 | 28 | 29 | @main.command() 30 | @click.option( 31 | Option.USE_DEFAULTS, 32 | is_flag=True, 33 | help=HELP[Option.USE_DEFAULTS], 34 | ) 35 | @click.option( 36 | Option.INDEX, 37 | default=TEMPLATES_FILE_URI, 38 | help=HELP[Option.INDEX], 39 | ) 40 | @click.option( 41 | Option.CONFIG_FILE, 42 | type=click.Path(), 43 | help=HELP[Option.CONFIG_FILE], 44 | ) 45 | @click.argument(Argument.TEMPLATE_NAME) 46 | def create( 47 | template_name: str, 48 | index: str, 49 | use_defaults: bool, 50 | config_file: str, 51 | ) -> None: 52 | return create_app( 53 | template_name, 54 | index=index, 55 | use_defaults=use_defaults, 56 | config_file=config_file, 57 | ) 58 | 59 | 60 | @main.command() 61 | @click.option( 62 | Option.INDEX, 63 | default=TEMPLATES_FILE_URI, 64 | help=HELP[Option.INDEX], 65 | ) 66 | def list(index: str) -> None: 67 | return list_templates(index) 68 | -------------------------------------------------------------------------------- /create_app/main.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Optional 2 | 3 | import click 4 | 5 | from create_app.project import Project 6 | from create_app.settings import TEMPLATES_FILE_URI 7 | from create_app.templates import Template, TemplatesIndex 8 | 9 | 10 | def create_app( 11 | template_name: str, 12 | index: str = TEMPLATES_FILE_URI, 13 | use_defaults: bool = False, 14 | config_file: Optional[str] = None, 15 | ) -> None: 16 | click.echo("Fetching template...") 17 | template: Template = _get_template(index, template_name) 18 | click.echo(f"Template '{template.name}' is available at {template.repo}\n") 19 | 20 | click.echo("Creating project...") 21 | _create_project(template, use_defaults, config_file) 22 | click.echo("Project created! ✨ 👏 ✨") 23 | 24 | 25 | def _get_template(index: str, template_name: str) -> Template: 26 | try: 27 | return TemplatesIndex(index).get_template(template_name) 28 | except Exception as e: 29 | raise click.ClickException(str(e)) 30 | 31 | 32 | def _create_project( 33 | template: Template, 34 | use_defaults: bool, 35 | config_file: Optional[str], 36 | ) -> None: 37 | try: 38 | return Project(template, use_defaults, config_file).create() 39 | except Exception as e: 40 | raise click.ClickException(str(e)) 41 | 42 | 43 | def list_templates(index: str) -> None: 44 | templates: Dict[str, str] = TemplatesIndex(index).get_templates() 45 | 46 | click.echo("\nTemplates in index:") 47 | 48 | for template_name, template_repo in templates.items(): 49 | click.echo(f" * {template_name} ({template_repo})") 50 | -------------------------------------------------------------------------------- /create_app/project.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Optional 2 | 3 | from cookiecutter.main import cookiecutter 4 | 5 | from create_app.project_configuration import ProjectConfiguration 6 | from create_app.project_configuration_file import ProjectConfigurationFile 7 | from create_app.templates import Template 8 | 9 | 10 | class Project: 11 | def __init__( 12 | self, 13 | template: Template, 14 | use_defaults: bool, 15 | config_file_path: Optional[str], 16 | ): 17 | self.template: Template = template 18 | self.use_defaults: bool = use_defaults 19 | self.config_file_path: Optional[str] = config_file_path 20 | 21 | def create(self) -> None: 22 | config: Dict = {} 23 | 24 | use_defaults = self.use_defaults 25 | 26 | if self.config_file_path: 27 | use_defaults = True 28 | config = ProjectConfiguration(self.config_file_path).get_config() 29 | 30 | with ProjectConfigurationFile(config) as configuration_file: 31 | cookiecutter( 32 | self.template.repo, 33 | no_input=use_defaults, 34 | config_file=configuration_file.path, 35 | ) 36 | -------------------------------------------------------------------------------- /create_app/project_configuration.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import Dict 3 | 4 | 5 | class TemplateConfigFileError(Exception): 6 | pass 7 | 8 | 9 | class TemplateConfigFileNotFound(TemplateConfigFileError): 10 | pass 11 | 12 | 13 | class ProjectConfiguration: 14 | DEFAULT_CONTEXT_KEY = "default_context" 15 | 16 | def __init__(self, template_config_file_path: str): 17 | self.template_config_file_path: str = template_config_file_path 18 | 19 | def get_config(self) -> Dict: 20 | return {ProjectConfiguration.DEFAULT_CONTEXT_KEY: self._get_template_config()} 21 | 22 | def _get_template_config(self) -> Dict: 23 | try: 24 | with open(self.template_config_file_path) as user_template_config_file: 25 | return json.load(user_template_config_file) 26 | except FileNotFoundError: 27 | raise TemplateConfigFileNotFound( 28 | f"File {self.template_config_file_path} does not exist" 29 | ) 30 | except Exception: 31 | raise TemplateConfigFileError( 32 | f"An unexpected error has occurred while attempting to read " 33 | f"the contents of file {self.template_config_file_path}" 34 | ) 35 | -------------------------------------------------------------------------------- /create_app/project_configuration_file.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from tempfile import mkstemp 4 | from typing import Dict, Optional 5 | 6 | 7 | class ProjectConfigurationFile: 8 | MODE = "w+" 9 | 10 | def __init__(self, config: Dict): 11 | self.config: Dict = config 12 | self.path: Optional[str] = None 13 | 14 | def __enter__(self): 15 | config_file_fd, config_file_path = mkstemp() 16 | self.path = config_file_path 17 | opened_config_file = os.fdopen( 18 | config_file_fd, 19 | ProjectConfigurationFile.MODE, 20 | ) 21 | json.dump(self.config, opened_config_file) 22 | opened_config_file.close() 23 | return self 24 | 25 | def __exit__(self, exc_type, exc_val, exc_tb): 26 | os.remove(self.path) 27 | -------------------------------------------------------------------------------- /create_app/settings.py: -------------------------------------------------------------------------------- 1 | GITHUB_USERNAME = "application-creators" 2 | GITHUB_PROJECT_NAME = "create_app" 3 | 4 | GIT_REPOSITORY = f"https://github.com/{GITHUB_USERNAME}/{GITHUB_PROJECT_NAME}" 5 | 6 | 7 | PYPI_PACKAGE_NAME = "create_app" 8 | 9 | 10 | PACKAGE_NAME = "create_app" 11 | 12 | REQUIREMENTS_FILE = "requirements.frozen" 13 | 14 | 15 | TEMPLATES_FILENAME = "templates.json" 16 | 17 | TEMPLATES_FILE_BRANCH = "main" 18 | 19 | TEMPLATES_FILE_URI = ( 20 | f"https://raw.githubusercontent.com/{GITHUB_USERNAME}" 21 | f"/{GITHUB_PROJECT_NAME}/{TEMPLATES_FILE_BRANCH}" 22 | f"/{TEMPLATES_FILENAME}" 23 | ) 24 | -------------------------------------------------------------------------------- /create_app/templates.py: -------------------------------------------------------------------------------- 1 | from typing import Dict 2 | 3 | from requests import get 4 | 5 | 6 | class TemplatesIndexException(Exception): 7 | pass 8 | 9 | 10 | class TemplatesIndexFetchError(TemplatesIndexException): 11 | pass 12 | 13 | 14 | class TemplateNotFound(TemplatesIndexException): 15 | pass 16 | 17 | 18 | class Template: 19 | def __init__(self, name: str, repo: str): 20 | self.name: str = name 21 | self.repo: str = repo 22 | 23 | 24 | class TemplatesIndex: 25 | TIMEOUT = 3 26 | 27 | def __init__(self, index_url: str): 28 | self.index_url: str = index_url 29 | 30 | def get_template(self, name: str) -> Template: 31 | templates: Dict[str, str] = self.get_templates() 32 | 33 | if name not in templates: 34 | raise TemplateNotFound( 35 | f"Could not find a template named '{name}' " 36 | f"in the templates index ({self.index_url})" 37 | ) 38 | 39 | return Template(name, templates[name]) 40 | 41 | def get_templates(self): 42 | try: 43 | return self._get_templates() 44 | except Exception: 45 | raise TemplatesIndexFetchError( 46 | f"Failed to fetch templates from index! ({self.index_url})" 47 | ) 48 | 49 | def _get_templates(self) -> Dict[str, str]: 50 | response = get(self.index_url, timeout=TemplatesIndex.TIMEOUT) 51 | 52 | if not response.ok: 53 | raise TemplatesIndexFetchError() 54 | 55 | return response.json() 56 | -------------------------------------------------------------------------------- /create_app/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/application-creators/create_app/0afe77d926d523b23a7ba18c52878f78600ba142/create_app/tests/__init__.py -------------------------------------------------------------------------------- /create_app/tests/__main__test.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from unittest.mock import MagicMock, patch 3 | 4 | 5 | class MainTestCase(TestCase): 6 | @patch("create_app.cli.main", MagicMock()) 7 | def test_main(self) -> None: 8 | from create_app.__main__ import main 9 | 10 | main.assert_called_once_with() 11 | -------------------------------------------------------------------------------- /create_app/tests/cli_test.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | from unittest import TestCase 3 | from unittest.mock import MagicMock, patch 4 | 5 | from create_app import cli 6 | from create_app.tests.utils import get_module 7 | 8 | MODULE = get_module(__file__) 9 | 10 | 11 | class CliTestCase(TestCase): 12 | def setUp(self) -> None: 13 | def return_decorated_function(decorated_function): 14 | return decorated_function 15 | 16 | main_mock = MagicMock() 17 | main_group_mock = MagicMock(return_value=main_mock) 18 | main_mock.command.return_value = return_decorated_function 19 | 20 | self.click_group_mock = patch( 21 | "click.group", 22 | return_value=main_group_mock, 23 | ).start() 24 | 25 | self.click_option_mock = patch( 26 | "click.option", 27 | return_value=return_decorated_function, 28 | ).start() 29 | 30 | self.click_argument_mock = patch( 31 | "click.argument", 32 | return_value=return_decorated_function, 33 | ).start() 34 | 35 | importlib.reload(cli) 36 | 37 | def tearDown(self) -> None: 38 | self.click_group_mock.stop() 39 | self.click_option_mock.stop() 40 | self.click_argument_mock.stop() 41 | 42 | @patch(f"{MODULE}.create_app") 43 | def test_create(self, create_app_mock: MagicMock): 44 | template_name_mock = MagicMock() 45 | index_mock = MagicMock() 46 | use_defaults_mock = MagicMock() 47 | config_file_mock = MagicMock() 48 | 49 | from create_app.cli import create 50 | 51 | create(template_name_mock, index_mock, use_defaults_mock, config_file_mock) 52 | 53 | create_app_mock.assert_called_once_with( 54 | template_name_mock, 55 | index=index_mock, 56 | use_defaults=use_defaults_mock, 57 | config_file=config_file_mock, 58 | ) 59 | 60 | @patch(f"{MODULE}.list_templates") 61 | def test_list(self, list_templates_mock: MagicMock): 62 | index_mock = MagicMock() 63 | 64 | from create_app.cli import list 65 | 66 | list(index_mock) 67 | 68 | list_templates_mock.assert_called_once_with(index_mock) 69 | -------------------------------------------------------------------------------- /create_app/tests/main_test.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from unittest.mock import MagicMock, patch 3 | 4 | from click import ClickException 5 | 6 | from create_app.main import create_app, list_templates 7 | from create_app.tests.utils import get_module 8 | 9 | MODULE = get_module(__file__) 10 | 11 | 12 | class MainTestCase(TestCase): 13 | @patch(f"{MODULE}.click", MagicMock()) 14 | @patch(f"{MODULE}.Project") 15 | @patch(f"{MODULE}.TemplatesIndex") 16 | def test_create_app_success( 17 | self, 18 | templates_index_class_mock: MagicMock, 19 | project_class_mock: MagicMock, 20 | ) -> None: 21 | template_name_mock = MagicMock() 22 | index_url_mock = MagicMock() 23 | use_defaults_mock = MagicMock() 24 | config_file_mock = MagicMock() 25 | 26 | templates_index_mock = MagicMock() 27 | templates_index_class_mock.return_value = templates_index_mock 28 | template_mock = MagicMock() 29 | templates_index_mock.get_template.return_value = template_mock 30 | 31 | project_mock = MagicMock() 32 | project_class_mock.return_value = project_mock 33 | 34 | create_app( 35 | template_name_mock, 36 | index=index_url_mock, 37 | use_defaults=use_defaults_mock, 38 | config_file=config_file_mock, 39 | ) 40 | 41 | templates_index_class_mock.assert_called_once_with(index_url_mock) 42 | templates_index_mock.get_template.assert_called_once_with(template_name_mock) 43 | 44 | project_class_mock.assert_called_once_with( 45 | template_mock, 46 | use_defaults_mock, 47 | config_file_mock, 48 | ) 49 | 50 | project_mock.create.assert_called_once() 51 | 52 | @patch(f"{MODULE}.click.echo", MagicMock()) 53 | @patch(f"{MODULE}.Project") 54 | @patch(f"{MODULE}.TemplatesIndex") 55 | def test_create_app_when_index_fails( 56 | self, 57 | templates_index_class_mock: MagicMock, 58 | project_class_mock: MagicMock, 59 | ) -> None: 60 | template_name_mock = MagicMock() 61 | index_url_mock = MagicMock() 62 | use_defaults_mock = MagicMock() 63 | config_file_mock = MagicMock() 64 | 65 | templates_index_mock = MagicMock() 66 | templates_index_class_mock.return_value = templates_index_mock 67 | templates_index_mock.get_template.side_effect = Exception("Failed") 68 | 69 | with self.assertRaises(ClickException): 70 | create_app( 71 | template_name_mock, 72 | index=index_url_mock, 73 | use_defaults=use_defaults_mock, 74 | config_file=config_file_mock, 75 | ) 76 | 77 | templates_index_class_mock.assert_called_once_with(index_url_mock) 78 | templates_index_mock.get_template.assert_called_once_with(template_name_mock) 79 | 80 | project_class_mock.assert_not_called() 81 | 82 | @patch(f"{MODULE}.click.echo", MagicMock()) 83 | @patch(f"{MODULE}.Project") 84 | @patch(f"{MODULE}.TemplatesIndex") 85 | def test_create_app_when_project_creation_fails( 86 | self, 87 | templates_index_class_mock: MagicMock, 88 | project_class_mock: MagicMock, 89 | ) -> None: 90 | template_name_mock = MagicMock() 91 | index_url_mock = MagicMock() 92 | use_defaults_mock = MagicMock() 93 | config_file_mock = MagicMock() 94 | 95 | templates_index_mock = MagicMock() 96 | templates_index_class_mock.return_value = templates_index_mock 97 | template_mock = MagicMock() 98 | templates_index_mock.get_template.return_value = template_mock 99 | 100 | project_mock = MagicMock() 101 | project_mock.create.side_effect = Exception("Failed") 102 | project_class_mock.return_value = project_mock 103 | 104 | with self.assertRaises(ClickException): 105 | create_app( 106 | template_name_mock, 107 | index=index_url_mock, 108 | use_defaults=use_defaults_mock, 109 | config_file=config_file_mock, 110 | ) 111 | 112 | templates_index_class_mock.assert_called_once_with(index_url_mock) 113 | templates_index_mock.get_template.assert_called_once_with(template_name_mock) 114 | 115 | project_class_mock.assert_called_once_with( 116 | template_mock, 117 | use_defaults_mock, 118 | config_file_mock, 119 | ) 120 | 121 | project_mock.create.assert_called_once() 122 | 123 | @patch(f"{MODULE}.click.echo", MagicMock()) 124 | @patch(f"{MODULE}.TemplatesIndex") 125 | def test_list_templates(self, templates_index_class_mock: MagicMock) -> None: 126 | templates_index_mock = MagicMock() 127 | templates_index_class_mock.return_value = templates_index_mock 128 | 129 | templates_mock = MagicMock() 130 | templates_index_mock.get_templates.return_value = templates_mock 131 | 132 | index_url_mock = MagicMock() 133 | 134 | list_templates(index_url_mock) 135 | 136 | templates_index_class_mock.assert_called_once_with(index_url_mock) 137 | templates_mock.items.assert_called_once_with() 138 | -------------------------------------------------------------------------------- /create_app/tests/project_configuration_file_test.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from unittest.mock import MagicMock, patch 3 | 4 | from create_app.project_configuration_file import ProjectConfigurationFile 5 | from create_app.tests.utils import get_module 6 | 7 | MODULE = get_module(__file__) 8 | 9 | 10 | class ProjectConfigurationFileTestCase(TestCase): 11 | @patch(f"{MODULE}.json") 12 | @patch(f"{MODULE}.os") 13 | @patch(f"{MODULE}.mkstemp") 14 | def test_context_manager( 15 | self, 16 | mkstemp_mock: MagicMock, 17 | os_mock: MagicMock, 18 | json_mock: MagicMock, 19 | ) -> None: 20 | config_mock = MagicMock() 21 | 22 | tmp_file_file_descriptor = MagicMock() 23 | tmp_file_path = MagicMock() 24 | 25 | mkstemp_mock.return_value = (tmp_file_file_descriptor, tmp_file_path) 26 | 27 | opened_tmp_file = MagicMock() 28 | os_mock.fdopen.return_value = opened_tmp_file 29 | 30 | with ProjectConfigurationFile(config_mock) as project_configuration_file: 31 | self.assertIs(project_configuration_file.path, tmp_file_path) 32 | 33 | mkstemp_mock.assert_called_once_with() 34 | 35 | os_mock.fdopen.assert_called_once_with( 36 | tmp_file_file_descriptor, 37 | ProjectConfigurationFile.MODE, 38 | ) 39 | 40 | json_mock.dump.assert_called_once_with(config_mock, opened_tmp_file) 41 | 42 | opened_tmp_file.close.assert_called_once() 43 | 44 | os_mock.remove.assert_called_once_with(tmp_file_path) 45 | -------------------------------------------------------------------------------- /create_app/tests/project_configuration_test.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from unittest.mock import MagicMock, patch 3 | 4 | from create_app.project_configuration import ( 5 | ProjectConfiguration, 6 | TemplateConfigFileError, 7 | TemplateConfigFileNotFound, 8 | ) 9 | from create_app.tests.utils import get_module 10 | 11 | MODULE = get_module(__file__) 12 | 13 | 14 | class ProjectConfigurationTestCase(TestCase): 15 | @patch(f"{MODULE}.ProjectConfiguration._get_template_config") 16 | def test_get_config(self, get_template_config_mock: MagicMock) -> None: 17 | template_config_file_path_mock = MagicMock() 18 | 19 | template_config_mock = MagicMock() 20 | get_template_config_mock.return_value = template_config_mock 21 | 22 | project_configuration = ProjectConfiguration(template_config_file_path_mock) 23 | returned_config = project_configuration.get_config() 24 | 25 | expected_config = { 26 | ProjectConfiguration.DEFAULT_CONTEXT_KEY: template_config_mock, 27 | } 28 | 29 | self.assertEqual(returned_config, expected_config) 30 | 31 | @patch(f"{MODULE}.json") 32 | @patch(f"{MODULE}.open") 33 | def test_get_template_config_success( 34 | self, open_mock: MagicMock, json_mock: MagicMock 35 | ) -> None: 36 | template_config_file_path_mock = MagicMock() 37 | 38 | file_mock = MagicMock() 39 | file_mock.__enter__.return_value = file_mock 40 | open_mock.return_value = file_mock 41 | 42 | json_config = MagicMock() 43 | json_mock.load.return_value = json_config 44 | 45 | project_configuration = ProjectConfiguration(template_config_file_path_mock) 46 | returned_template_config = project_configuration._get_template_config() 47 | 48 | open_mock.assert_called_once_with(template_config_file_path_mock) 49 | 50 | json_mock.load.assert_called_once_with(file_mock) 51 | 52 | self.assertIs(returned_template_config, json_config) 53 | 54 | @patch(f"{MODULE}.json") 55 | @patch(f"{MODULE}.open") 56 | def test_get_template_config_when_file_does_not_exist( 57 | self, open_mock: MagicMock, json_mock: MagicMock 58 | ) -> None: 59 | template_config_file_path_mock = MagicMock() 60 | 61 | open_mock.side_effect = FileNotFoundError("File does not exist") 62 | 63 | project_configuration = ProjectConfiguration(template_config_file_path_mock) 64 | 65 | with self.assertRaises(TemplateConfigFileNotFound): 66 | project_configuration._get_template_config() 67 | 68 | open_mock.assert_called_once_with(template_config_file_path_mock) 69 | json_mock.load.assert_not_called() 70 | 71 | @patch(f"{MODULE}.json") 72 | @patch(f"{MODULE}.open") 73 | def test_get_template_config_when_unexpected_error_is_raised( 74 | self, open_mock: MagicMock, json_mock: MagicMock 75 | ) -> None: 76 | template_config_file_path_mock = MagicMock() 77 | 78 | file_mock = MagicMock() 79 | file_mock.__enter__.return_value = file_mock 80 | open_mock.return_value = file_mock 81 | 82 | json_mock.load.side_effect = Exception("Unexpected error") 83 | 84 | project_configuration = ProjectConfiguration(template_config_file_path_mock) 85 | 86 | with self.assertRaises(TemplateConfigFileError): 87 | project_configuration._get_template_config() 88 | 89 | open_mock.assert_called_once_with(template_config_file_path_mock) 90 | json_mock.load.assert_called_once_with(file_mock) 91 | -------------------------------------------------------------------------------- /create_app/tests/project_test.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from unittest.mock import MagicMock, patch 3 | 4 | from create_app.project import Project 5 | from create_app.tests.utils import get_module 6 | 7 | MODULE = get_module(__file__) 8 | 9 | 10 | class ProjectTestCase(TestCase): 11 | @patch(f"{MODULE}.cookiecutter") 12 | @patch(f"{MODULE}.ProjectConfigurationFile") 13 | @patch(f"{MODULE}.ProjectConfiguration") 14 | def test_create_without_default_and_without_config_file_path( 15 | self, 16 | project_configuration_class_mock: MagicMock, 17 | project_configuration_file_class_mock: MagicMock, 18 | cookiecutter_mock: MagicMock, 19 | ) -> None: 20 | template_mock = MagicMock() 21 | use_defaults = False 22 | config_file_path = None 23 | 24 | project_configuration_file_mock = MagicMock() 25 | project_configuration_file_mock.__enter__.return_value = ( 26 | project_configuration_file_mock 27 | ) 28 | project_configuration_file_class_mock.return_value = ( 29 | project_configuration_file_mock 30 | ) 31 | 32 | Project(template_mock, use_defaults, config_file_path).create() 33 | 34 | project_configuration_class_mock.assert_not_called() 35 | 36 | default_config = {} 37 | project_configuration_file_class_mock.assert_called_once_with(default_config) 38 | project_configuration_file_mock.__enter__.assert_called_once() 39 | 40 | cookiecutter_mock.assert_called_once_with( 41 | template_mock.repo, 42 | no_input=False, 43 | config_file=project_configuration_file_mock.path, 44 | ) 45 | 46 | @patch(f"{MODULE}.cookiecutter") 47 | @patch(f"{MODULE}.ProjectConfigurationFile") 48 | @patch(f"{MODULE}.ProjectConfiguration") 49 | def test_create_with_default_and_without_config_file_path( 50 | self, 51 | project_configuration_class_mock: MagicMock, 52 | project_configuration_file_class_mock: MagicMock, 53 | cookiecutter_mock: MagicMock, 54 | ) -> None: 55 | template_mock = MagicMock() 56 | use_defaults = True 57 | config_file_path = None 58 | 59 | project_configuration_file_mock = MagicMock() 60 | project_configuration_file_mock.__enter__.return_value = ( 61 | project_configuration_file_mock 62 | ) 63 | project_configuration_file_class_mock.return_value = ( 64 | project_configuration_file_mock 65 | ) 66 | 67 | Project(template_mock, use_defaults, config_file_path).create() 68 | 69 | project_configuration_class_mock.assert_not_called() 70 | 71 | default_config = {} 72 | project_configuration_file_class_mock.assert_called_once_with(default_config) 73 | project_configuration_file_mock.__enter__.assert_called_once() 74 | 75 | cookiecutter_mock.assert_called_once_with( 76 | template_mock.repo, 77 | no_input=True, 78 | config_file=project_configuration_file_mock.path, 79 | ) 80 | 81 | @patch(f"{MODULE}.cookiecutter") 82 | @patch(f"{MODULE}.ProjectConfigurationFile") 83 | @patch(f"{MODULE}.ProjectConfiguration") 84 | def test_create_without_default_and_with_config_file_path( 85 | self, 86 | project_configuration_class_mock: MagicMock, 87 | project_configuration_file_class_mock: MagicMock, 88 | cookiecutter_mock: MagicMock, 89 | ) -> None: 90 | template_mock = MagicMock() 91 | use_defaults = False 92 | config_file_path = MagicMock() 93 | 94 | config_mock = MagicMock() 95 | project_configuration_mock = MagicMock() 96 | project_configuration_mock.get_config.return_value = config_mock 97 | project_configuration_class_mock.return_value = project_configuration_mock 98 | 99 | project_configuration_file_mock = MagicMock() 100 | project_configuration_file_mock.__enter__.return_value = ( 101 | project_configuration_file_mock 102 | ) 103 | project_configuration_file_class_mock.return_value = ( 104 | project_configuration_file_mock 105 | ) 106 | 107 | Project(template_mock, use_defaults, config_file_path).create() 108 | 109 | project_configuration_class_mock.assert_called_once_with(config_file_path) 110 | 111 | project_configuration_file_class_mock.assert_called_once_with(config_mock) 112 | project_configuration_file_mock.__enter__.assert_called_once() 113 | 114 | cookiecutter_mock.assert_called_once_with( 115 | template_mock.repo, 116 | no_input=True, 117 | config_file=project_configuration_file_mock.path, 118 | ) 119 | -------------------------------------------------------------------------------- /create_app/tests/templates_test.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from unittest.mock import MagicMock, patch 3 | 4 | from create_app.templates import ( 5 | TemplateNotFound, 6 | TemplatesIndex, 7 | TemplatesIndexFetchError, 8 | ) 9 | from create_app.tests.utils import get_module 10 | 11 | MODULE = get_module(__file__) 12 | 13 | 14 | class TemplatesIndexTestCase(TestCase): 15 | @patch(f"{MODULE}.get") 16 | def test_get_templates_success( 17 | self, 18 | requests_get_mock: MagicMock, 19 | ) -> None: 20 | index_uri_mock = MagicMock() 21 | 22 | response_mock = MagicMock() 23 | response_mock.ok = True 24 | requests_get_mock.return_value = response_mock 25 | 26 | templates = TemplatesIndex(index_uri_mock).get_templates() 27 | 28 | requests_get_mock.assert_called_once_with( 29 | index_uri_mock, timeout=TemplatesIndex.TIMEOUT 30 | ) 31 | 32 | self.assertIs(templates, response_mock.json()) 33 | 34 | @patch(f"{MODULE}.get") 35 | def test_get_templates_when_request_fails( 36 | self, 37 | requests_get_mock: MagicMock, 38 | ) -> None: 39 | index_uri_mock = MagicMock() 40 | 41 | requests_get_mock.side_effect = Exception("Failed") 42 | 43 | with self.assertRaises(TemplatesIndexFetchError): 44 | TemplatesIndex(index_uri_mock).get_templates() 45 | 46 | requests_get_mock.assert_called_once_with( 47 | index_uri_mock, timeout=TemplatesIndex.TIMEOUT 48 | ) 49 | 50 | @patch(f"{MODULE}.get") 51 | def test_get_templates_when_request_returns_bad_status( 52 | self, 53 | requests_get_mock: MagicMock, 54 | ) -> None: 55 | index_uri_mock = MagicMock() 56 | 57 | response_mock = MagicMock() 58 | response_mock.ok = False 59 | 60 | requests_get_mock.return_value = response_mock 61 | 62 | with self.assertRaises(TemplatesIndexFetchError): 63 | TemplatesIndex(index_uri_mock).get_templates() 64 | 65 | requests_get_mock.assert_called_once_with( 66 | index_uri_mock, timeout=TemplatesIndex.TIMEOUT 67 | ) 68 | 69 | @patch(f"{MODULE}.TemplatesIndex.get_templates") 70 | def test_get_template_success(self, get_templates_mock: MagicMock) -> None: 71 | index_url_mock = MagicMock() 72 | 73 | template_name_mock = MagicMock() 74 | template_repo_mock = MagicMock() 75 | 76 | get_templates_mock.return_value = { 77 | template_name_mock: template_repo_mock, 78 | } 79 | 80 | templates_index = TemplatesIndex(index_url_mock) 81 | 82 | template = templates_index.get_template(template_name_mock) 83 | 84 | self.assertIs(template.name, template_name_mock) 85 | self.assertIs(template.repo, template_repo_mock) 86 | 87 | @patch(f"{MODULE}.TemplatesIndex.get_templates") 88 | def test_get_template_when_template_does_not_exist_in_index( 89 | self, get_templates_mock: MagicMock 90 | ) -> None: 91 | index_url_mock = MagicMock() 92 | 93 | template_name_mock = MagicMock() 94 | 95 | templates_index = TemplatesIndex(index_url_mock) 96 | get_templates_mock.return_value = {} 97 | 98 | with self.assertRaises(TemplateNotFound): 99 | templates_index.get_template(template_name_mock) 100 | -------------------------------------------------------------------------------- /create_app/tests/utils.py: -------------------------------------------------------------------------------- 1 | from os.path import basename, dirname 2 | 3 | TEST_MODULE_SUFFIX = "_test.py" 4 | MODULES_SEPARATOR = "." 5 | 6 | 7 | def get_package_name(test_file): 8 | path = dirname(test_file) 9 | tests_path = dirname(path) 10 | package_name = basename(tests_path) 11 | return package_name 12 | 13 | 14 | def get_module_name(test_file): 15 | test_module_name = basename(test_file) 16 | return test_module_name.replace(TEST_MODULE_SUFFIX, "") 17 | 18 | 19 | def get_module(test_file): 20 | package_name = get_package_name(test_file) 21 | module_name = get_module_name(test_file) 22 | return f"{package_name}{MODULES_SEPARATOR}{module_name}" 23 | -------------------------------------------------------------------------------- /docs/static/logo-cropped.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/application-creators/create_app/0afe77d926d523b23a7ba18c52878f78600ba142/docs/static/logo-cropped.png -------------------------------------------------------------------------------- /docs/static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/application-creators/create_app/0afe77d926d523b23a7ba18c52878f78600ba142/docs/static/logo.png -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=45", "setuptools_scm[toml]>=6.2"] 3 | 4 | [tool.setuptools_scm] 5 | -------------------------------------------------------------------------------- /requirements.build.frozen: -------------------------------------------------------------------------------- 1 | build==0.8.0 2 | twine==4.0.1 3 | setuptools-scm==7.0.4 4 | -------------------------------------------------------------------------------- /requirements.frozen: -------------------------------------------------------------------------------- 1 | click==8.1.3 2 | cookiecutter==2.1.1 3 | requests==2.28.1 4 | -------------------------------------------------------------------------------- /requirements.test.frozen: -------------------------------------------------------------------------------- 1 | pytest==7.1.2 2 | pytest-cov==3.0.0 3 | -------------------------------------------------------------------------------- /scripts/run_unit_tests.sh: -------------------------------------------------------------------------------- 1 | 2 | 3 | python3 -m unittest discover -s ./../create_app -p '*_test.py' 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from setuptools import setup 4 | 5 | from create_app.settings import ( 6 | GIT_REPOSITORY, 7 | PACKAGE_NAME, 8 | PYPI_PACKAGE_NAME, 9 | REQUIREMENTS_FILE, 10 | ) 11 | 12 | ROOT_PATH = Path(__file__).parent 13 | 14 | 15 | README_FILENAME = "README.md" 16 | 17 | 18 | DESCRIPTION = ( 19 | "A tool that allows to quickly get your basic project structure ready, " 20 | "while adopting the best technologies, tools, and practices." 21 | ) 22 | 23 | 24 | ENTRY_POINTS = { 25 | "console_scripts": [f"{PACKAGE_NAME}={PACKAGE_NAME}.cli:main"], 26 | } 27 | 28 | 29 | def get_requirements(): 30 | with open(REQUIREMENTS_FILE) as file: 31 | return file.readlines() 32 | 33 | 34 | def get_long_description(): 35 | return (ROOT_PATH / README_FILENAME).read_text(encoding="utf8") 36 | 37 | 38 | setup( 39 | name=PYPI_PACKAGE_NAME, 40 | entry_points=ENTRY_POINTS, 41 | description=DESCRIPTION, 42 | long_description=get_long_description(), 43 | long_description_content_type="text/markdown", 44 | # keywords="", TODO 45 | classifiers=[ 46 | "Environment :: Console", 47 | "Intended Audience :: Developers", 48 | "Programming Language :: Python", 49 | "Programming Language :: Python :: 3.6", 50 | "Programming Language :: Python :: 3.7", 51 | "Programming Language :: Python :: 3.8", 52 | "Programming Language :: Python :: 3.9", 53 | "Programming Language :: Python :: 3.10", 54 | "Programming Language :: Python :: 3 :: Only", 55 | # "Development Status :: 5 - Production/Stable", TODO 56 | # "Operating System :: OS Independent", TODO 57 | ], 58 | url=GIT_REPOSITORY, 59 | author="Gabriel Bazan", 60 | author_email="gbazan@outlook.com", 61 | license="MIT", 62 | packages=[PACKAGE_NAME], 63 | zip_safe=False, 64 | python_requires=">=3.6.2", 65 | install_requires=get_requirements(), 66 | ) 67 | -------------------------------------------------------------------------------- /templates.json: -------------------------------------------------------------------------------- 1 | { 2 | "python_simple": "https://github.com/application-creators/python_simple", 3 | "python_compose": "https://github.com/application-creators/python_compose", 4 | "python_fastapi": "https://github.com/application-creators/python_fastapi", 5 | "python_fastapi_with_database": "https://github.com/application-creators/python_fastapi_with_database" 6 | } 7 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 88 3 | --------------------------------------------------------------------------------