├── .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 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
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 |
--------------------------------------------------------------------------------