diff --git a/cookiecutter.json b/cookiecutter.json index fe99160..903a6e4 100644 --- a/cookiecutter.json +++ b/cookiecutter.json @@ -22,6 +22,10 @@ "3.12", "3.13" ], + "package_manager": [ + "uv", + "pip" + ], "use_requests": [ "n", "y" @@ -43,7 +47,7 @@ "https" ], "__template_repo": "https://github.com/btr1975/cookiecutter-python-fastapi-openapi", - "__template_version": "1.0.13", + "__template_version": "2.0.0", "_new_lines": "\n", "_copy_without_render": [ "{{cookiecutter.__app_name}}/templates", @@ -75,6 +79,11 @@ "3.12": "3.12", "3.13": "3.13" }, + "package_manager": { + "__prompt__": "Which pacakge manager for Python will be supported", + "uv": "UV By Astral", + "pip": "PIP (The built in Python Package Installer)" + }, "use_requests": { "__prompt__": "Will you use the requests library", "n": "No", diff --git a/hooks/post_gen_project.py b/hooks/post_gen_project.py index b579ba6..1085703 100644 --- a/hooks/post_gen_project.py +++ b/hooks/post_gen_project.py @@ -20,6 +20,11 @@ '{% if cookiecutter.container_runtime == "docker" %}containers/Containerfile{% endif %}', ] +REMOVE_PATHS_UV = [ + '{% if cookiecutter.package_manager == "uv" %}requirements.txt{% endif %}', + '{% if cookiecutter.package_manager == "uv" %}requirements-dev.txt{% endif %}', +] + def remove_paths(paths_to_remove: List[str]) -> None: """Remove files and directories @@ -41,3 +46,4 @@ def remove_paths(paths_to_remove: List[str]) -> None: remove_paths(REMOVE_PATHS_NO_WEBPAGES) remove_paths(REMOVE_PATHS_PODMAN) remove_paths(REMOVE_PATHS_DOCKER) + remove_paths(REMOVE_PATHS_UV) diff --git a/tests/conftest.py b/tests/conftest.py index 9dcd5aa..6711159 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,6 +14,21 @@ def bake_project_api_only_podman() -> dict: "email": "name@example.com", "git_username": "some-username", "git_url": "https://github.com/some-username/python-with-cli", + "package_manager": "pip", + } + + return options + + +@pytest.fixture +def bake_project_uv_api_only_podman() -> dict: + options = { + "git_repo_name": "api-only", + "include_webpages": "n", + "email": "name@example.com", + "git_username": "some-username", + "git_url": "https://github.com/some-username/python-with-cli", + "package_manager": "uv", } return options @@ -28,6 +43,22 @@ def bake_project_api_only_docker() -> dict: "git_username": "some-username", "git_url": "https://github.com/some-username/python-with-cli", "container_runtime": "docker", + "package_manager": "pip", + } + + return options + + +@pytest.fixture +def bake_project_uv_api_only_docker() -> dict: + options = { + "git_repo_name": "api-only", + "include_webpages": "n", + "email": "name@example.com", + "git_username": "some-username", + "git_url": "https://github.com/some-username/python-with-cli", + "container_runtime": "docker", + "package_manager": "uv", } return options @@ -41,6 +72,21 @@ def bake_project_api_with_webpages_podman() -> dict: "email": "name@example.com", "git_username": "some-username", "git_url": "https://github.com/some-username/python-with-cli", + "package_manager": "pip", + } + + return options + + +@pytest.fixture +def bake_project_uv_api_with_webpages_podman() -> dict: + options = { + "git_repo_name": "api-with-webpages", + "include_webpages": "y", + "email": "name@example.com", + "git_username": "some-username", + "git_url": "https://github.com/some-username/python-with-cli", + "package_manager": "uv", } return options @@ -55,6 +101,22 @@ def bake_project_api_with_webpages_docker() -> dict: "git_username": "some-username", "git_url": "https://github.com/some-username/python-with-cli", "container_runtime": "docker", + "package_manager": "pip", + } + + return options + + +@pytest.fixture +def bake_project_uv_api_with_webpages_docker() -> dict: + options = { + "git_repo_name": "api-with-webpages", + "include_webpages": "y", + "email": "name@example.com", + "git_username": "some-username", + "git_url": "https://github.com/some-username/python-with-cli", + "container_runtime": "docker", + "package_manager": "uv", } return options diff --git a/tests/test_cookies.py b/tests/test_cookies.py index fb3b9fe..a8d0806 100644 --- a/tests/test_cookies.py +++ b/tests/test_cookies.py @@ -6,6 +6,27 @@ def test_bake_project_api_only_podman(cookies, bake_project_api_only_podman): assert result.project_path.name == "api-only" assert result.project_path.is_dir() assert result.project_path.joinpath("README.md").is_file() + assert result.project_path.joinpath("requirements.txt").is_file() + assert result.project_path.joinpath("requirements-dev.txt").is_file() + assert result.project_path.joinpath("api_only").is_dir() + assert result.project_path.joinpath("containers").is_dir() + assert result.project_path.joinpath("containers", "Containerfile").is_file() + assert not result.project_path.joinpath("containers", "Dockerfile").is_file() + assert not result.project_path.joinpath("api_only", "static").is_dir() + assert not result.project_path.joinpath("api_only", "templates").is_dir() + assert not result.project_path.joinpath("api_only", "routers", "hello_world.py").is_file() + + +def test_bake_project_uv_api_only_podman(cookies, bake_project_uv_api_only_podman): + result = cookies.bake(extra_context=bake_project_uv_api_only_podman) + + assert result.exit_code == 0 + assert result.exception is None + assert result.project_path.name == "api-only" + assert result.project_path.is_dir() + assert result.project_path.joinpath("README.md").is_file() + assert not result.project_path.joinpath("requirements.txt").is_file() + assert not result.project_path.joinpath("requirements-dev.txt").is_file() assert result.project_path.joinpath("api_only").is_dir() assert result.project_path.joinpath("containers").is_dir() assert result.project_path.joinpath("containers", "Containerfile").is_file() @@ -23,6 +44,27 @@ def test_bake_project_api_only_docker(cookies, bake_project_api_only_docker): assert result.project_path.name == "api-only" assert result.project_path.is_dir() assert result.project_path.joinpath("README.md").is_file() + assert result.project_path.joinpath("requirements.txt").is_file() + assert result.project_path.joinpath("requirements-dev.txt").is_file() + assert result.project_path.joinpath("api_only").is_dir() + assert result.project_path.joinpath("containers").is_dir() + assert not result.project_path.joinpath("containers", "Containerfile").is_file() + assert result.project_path.joinpath("containers", "Dockerfile").is_file() + assert not result.project_path.joinpath("api_only", "static").is_dir() + assert not result.project_path.joinpath("api_only", "templates").is_dir() + assert not result.project_path.joinpath("api_only", "routers", "hello_world.py").is_file() + + +def test_bake_project_uv_api_only_docker(cookies, bake_project_uv_api_only_docker): + result = cookies.bake(extra_context=bake_project_uv_api_only_docker) + + assert result.exit_code == 0 + assert result.exception is None + assert result.project_path.name == "api-only" + assert result.project_path.is_dir() + assert result.project_path.joinpath("README.md").is_file() + assert not result.project_path.joinpath("requirements.txt").is_file() + assert not result.project_path.joinpath("requirements-dev.txt").is_file() assert result.project_path.joinpath("api_only").is_dir() assert result.project_path.joinpath("containers").is_dir() assert not result.project_path.joinpath("containers", "Containerfile").is_file() @@ -40,6 +82,27 @@ def test_bake_project_api_with_webpages_podman(cookies, bake_project_api_with_we assert result.project_path.name == "api-with-webpages" assert result.project_path.is_dir() assert result.project_path.joinpath("README.md").is_file() + assert result.project_path.joinpath("requirements.txt").is_file() + assert result.project_path.joinpath("requirements-dev.txt").is_file() + assert result.project_path.joinpath("api_with_webpages").is_dir() + assert result.project_path.joinpath("containers").is_dir() + assert result.project_path.joinpath("containers", "Containerfile").is_file() + assert not result.project_path.joinpath("containers", "Dockerfile").is_file() + assert result.project_path.joinpath("api_with_webpages", "static").is_dir() + assert result.project_path.joinpath("api_with_webpages", "templates").is_dir() + assert result.project_path.joinpath("api_with_webpages", "routers", "hello_world.py").is_file() + + +def test_bake_project_uv_api_with_webpages_podman(cookies, bake_project_uv_api_with_webpages_podman): + result = cookies.bake(extra_context=bake_project_uv_api_with_webpages_podman) + + assert result.exit_code == 0 + assert result.exception is None + assert result.project_path.name == "api-with-webpages" + assert result.project_path.is_dir() + assert result.project_path.joinpath("README.md").is_file() + assert not result.project_path.joinpath("requirements.txt").is_file() + assert not result.project_path.joinpath("requirements-dev.txt").is_file() assert result.project_path.joinpath("api_with_webpages").is_dir() assert result.project_path.joinpath("containers").is_dir() assert result.project_path.joinpath("containers", "Containerfile").is_file() @@ -57,6 +120,27 @@ def test_bake_project_api_with_webpages_docker(cookies, bake_project_api_with_we assert result.project_path.name == "api-with-webpages" assert result.project_path.is_dir() assert result.project_path.joinpath("README.md").is_file() + assert result.project_path.joinpath("requirements.txt").is_file() + assert result.project_path.joinpath("requirements-dev.txt").is_file() + assert result.project_path.joinpath("api_with_webpages").is_dir() + assert result.project_path.joinpath("containers").is_dir() + assert not result.project_path.joinpath("containers", "Containerfile").is_file() + assert result.project_path.joinpath("containers", "Dockerfile").is_file() + assert result.project_path.joinpath("api_with_webpages", "static").is_dir() + assert result.project_path.joinpath("api_with_webpages", "templates").is_dir() + assert result.project_path.joinpath("api_with_webpages", "routers", "hello_world.py").is_file() + + +def test_bake_project_uv_api_with_webpages_docker(cookies, bake_project_uv_api_with_webpages_docker): + result = cookies.bake(extra_context=bake_project_uv_api_with_webpages_docker) + + assert result.exit_code == 0 + assert result.exception is None + assert result.project_path.name == "api-with-webpages" + assert result.project_path.is_dir() + assert result.project_path.joinpath("README.md").is_file() + assert not result.project_path.joinpath("requirements.txt").is_file() + assert not result.project_path.joinpath("requirements-dev.txt").is_file() assert result.project_path.joinpath("api_with_webpages").is_dir() assert result.project_path.joinpath("containers").is_dir() assert not result.project_path.joinpath("containers", "Containerfile").is_file() diff --git a/{{cookiecutter.git_repo_name}}/Makefile b/{{cookiecutter.git_repo_name}}/Makefile index 016a4d7..d7ff444 100644 --- a/{{cookiecutter.git_repo_name}}/Makefile +++ b/{{cookiecutter.git_repo_name}}/Makefile @@ -1,10 +1,10 @@ # Makefile for project needs # Author: Ben Trachtenberg -# Version: 1.0.8 +# Version: 2.0.0 # .PHONY: all info build build-container coverage format pylint pytest gh-pages build dev-run start-container \ - stop-container remove-container check-vuln check-security + stop-container remove-container check-vuln check-security pip-export info: @echo "make options" @@ -21,23 +21,16 @@ info: @echo " start-container To start the container" @echo " stop-container To stop the container" @echo " remove-container To remove the container" +{% if cookiecutter.package_manager == 'uv' %} @echo " pip-export To export the requirements to requirements.txt and requirements-dev.txt"{% endif %} {% if cookiecutter.app_documents_location == 'github-pages' %} @echo " gh-pages To create the GitHub pages"{% endif %} +{% if cookiecutter.package_manager == 'pip' %} + all: format pylint coverage check-security check-vuln build: @python -m build -{% if cookiecutter.container_runtime == "podman" %} -build-container: - @cd containers && podman build --ssh=default --build-arg=build_branch=main -t {{ cookiecutter.git_repo_name }}:latest -f Containerfile -{% endif %} - -{% if cookiecutter.container_runtime == "docker" %} -build-container: - @cd containers && docker build --ssh=default --build-arg=build_branch=main -t {{ cookiecutter.git_repo_name }}:latest -f Dockerfile -{% endif %} - coverage: @pytest --cov --cov-report=html -vvv @@ -54,6 +47,12 @@ pytest: dev-run: @python -c "from {{cookiecutter.__app_name}} import cli;cli()" start -p 8080 -r +check-vuln: + @pip-audit -r requirements.txt + +check-security: + @bandit -c pyproject.toml -r . + {% if cookiecutter.app_documents_location == 'github-pages' %} gh-pages: @rm -rf ./docs/source/code @@ -61,7 +60,49 @@ gh-pages: @sphinx-build ./docs ./docs/gh-pages {% endif %} +{% elif cookiecutter.package_manager == 'uv' %} + +all: format pylint coverage check-security pip-export + +build: + @uv build --wheel --sdist + +coverage: + @uv run pytest --cov --cov-report=html -vvv + +format: + @uv run black {{cookiecutter.__app_name}}/ + @uv run black tests/ + +pylint: + @uv run pylint {{cookiecutter.__app_name}}/ + +pytest: + @uv run pytest --cov -vvv + +dev-run: + @uv run python -c "from {{cookiecutter.__app_name}} import cli;cli()" start -p 8080 -r + +check-security: + @uv run bandit -c pyproject.toml -r . + +pip-export: + @uv export --no-dev --no-emit-project --no-editable > requirements.txt + @uv export --no-emit-project --no-editable > requirements-dev.txt + +{% if cookiecutter.app_documents_location == 'github-pages' %} +gh-pages: + @rm -rf ./docs/source/code + @uv run sphinx-apidoc -o ./docs/source/code ./{{cookiecutter.__app_name}} + @uv run sphinx-build ./docs ./docs/gh-pages +{% endif %} + +{% endif %} + {% if cookiecutter.container_runtime == "podman" %} +build-container: + @cd containers && podman build --ssh=default --build-arg=build_branch=main -t {{ cookiecutter.git_repo_name }}:latest -f Containerfile + start-container: @podman run -itd --name {{ cookiecutter.git_repo_name }} -p 8080:8080 localhost/{{ cookiecutter.git_repo_name }}:latest @@ -73,6 +114,9 @@ remove-container: {% endif %} {% if cookiecutter.container_runtime == "docker" %} +build-container: + @cd containers && docker build --ssh=default --build-arg=build_branch=main -t {{ cookiecutter.git_repo_name }}:latest -f Dockerfile + start-container: @docker run -itd --name {{ cookiecutter.git_repo_name }} -p 8080:8080 localhost/{{ cookiecutter.git_repo_name }}:latest @@ -82,9 +126,3 @@ stop-container: remove-container: @docker rm {{ cookiecutter.git_repo_name }} {% endif %} - -check-vuln: - @pip-audit -r requirements.txt - -check-security: - @bandit -c pyproject.toml -r . diff --git a/{{cookiecutter.git_repo_name}}/make.bat b/{{cookiecutter.git_repo_name}}/make.bat index a1727e2..23ff15d 100644 --- a/{{cookiecutter.git_repo_name}}/make.bat +++ b/{{cookiecutter.git_repo_name}}/make.bat @@ -1,7 +1,7 @@ @ECHO OFF REM Makefile for project needs REM Author: Ben Trachtenberg -REM Version: 1.0.7 +REM Version: 2.0.0 REM SET option=%1 @@ -10,6 +10,8 @@ IF "%option%" == "" ( GOTO BAD_OPTIONS ) +{% if cookiecutter.package_manager == 'pip' %} + IF "%option%" == "all" ( black {{cookiecutter.__app_name}}/ black tests/ @@ -70,6 +72,72 @@ IF "%option%" == "gh-pages" ( ) {% endif %} +{% elif cookiecutter.package_manager == 'uv' %} + +IF "%option%" == "all" ( + uv run black {{cookiecutter.__app_name}}/ + uv run black tests/ + uv run pylint {{cookiecutter.__app_name}}\ + uv run pytest --cov --cov-report=html -vvv + uv run bandit -c pyproject.toml -r . + uv export --no-dev --no-emit-project --no-editable > requirements.txt + uv export --no-emit-project --no-editable > requirements-dev.txt + GOTO END +) + +IF "%option%" == "build" ( + uv build --wheel --sdist + GOTO END +) + +IF "%option%" == "coverage" ( + uv run pytest --cov --cov-report=html -vvv + GOTO END +) + +IF "%option%" == "pylint" ( + uv run pylint {{cookiecutter.__app_name}}\ + GOTO END +) + +IF "%option%" == "pytest" ( + uv run pytest --cov -vvv + GOTO END +) + +IF "%option%" == "dev-run" ( + uv run python -c "from {{cookiecutter.__app_name}} import cli;cli()" start -p 8080 -r + GOTO END +) + +IF "%option%" == "format" ( + uv run black {{cookiecutter.__app_name}}/ + uv run black tests/ + GOTO END +) + +IF "%option%" == "check-security" ( + uv run bandit -c pyproject.toml -r . + GOTO END +) + +IF "%option%" == "pip-export" ( + uv export --no-dev --no-emit-project --no-editable > requirements.txt + uv export --no-emit-project --no-editable > requirements-dev.txt + GOTO END +) + +{% if cookiecutter.app_documents_location == 'github-pages' %} +IF "%option%" == "gh-pages" ( + rmdir /s /q docs\source\code + uv run sphinx-apidoc -o ./docs/source/code ./{{cookiecutter.__app_name}} + uv run sphinx-build ./docs ./docs/gh-pages + GOTO END +) +{% endif %} + +{% endif %} + :OPTIONS @ECHO make options @ECHO all To run coverage, format, pylint, and check-vuln @@ -81,6 +149,7 @@ IF "%option%" == "gh-pages" ( @ECHO format To format the code with black @ECHO pylint To run pylint @ECHO pytest To run pytest with verbose option +{% if cookiecutter.package_manager == 'uv' %}@ECHO pip-export To export the requirements.txt and requirements-dev.txt{% endif %} {% if cookiecutter.app_documents_location == 'github-pages' %}@ECHO gh-pages To create the GitHub pages{% endif %} GOTO END diff --git a/{{cookiecutter.git_repo_name}}/pyproject.toml b/{{cookiecutter.git_repo_name}}/pyproject.toml index 11f9e43..9c8da70 100644 --- a/{{cookiecutter.git_repo_name}}/pyproject.toml +++ b/{{cookiecutter.git_repo_name}}/pyproject.toml @@ -7,7 +7,11 @@ build-backend = "setuptools.build_meta" [project] name = "{{ cookiecutter.git_repo_name }}" +{% if cookiecutter.package_manager == 'pip' %} dynamic = ["version", "readme", "dependencies"] +{% elif cookiecutter.package_manager == 'uv' %} +dynamic = ["version", "readme"] +{% endif %} requires-python = ">={{ cookiecutter.minimum_python_version }}" description = "{{ cookiecutter.app_description }}" keywords = [ @@ -49,6 +53,31 @@ classifiers = [ ] +{% if cookiecutter.package_manager == 'uv' %} +dependencies = [ + "fastapi", + "pydantic", + "uvicorn", + {% if cookiecutter.use_requests == 'y' %}"requests",{% endif %} + {% if cookiecutter.use_cryptography == 'y' %}"cryptography",{% endif %} +] + +[dependency-groups] +dev = [ + "black", + "tomli", + "pytest-cov", + "sphinx", + "pylint", + "myst-parser", + "sphinx_rtd_theme", + "sphinxcontrib-mermaid", + "httpx", + "bandit", + {% if cookiecutter.use_requests == 'y' %}"requests-mock",{% endif %} +] +{% endif %} + [project.urls] Documentation = "https://{{ cookiecutter.git_repo_name }}.readthedocs.io/en/latest/" Source = "{{ cookiecutter.git_url }}" @@ -72,7 +101,9 @@ zip-safe = false [tool.setuptools.dynamic] version = {attr = "{{cookiecutter.__app_name}}.version.__version__"} readme = {file = "README.md", content-type = "text/markdown"} +{% if cookiecutter.package_manager == 'pip' %} dependencies = {file = "requirements.txt"} +{% endif %} [tool.pytest.ini_options] addopts = "--strict-markers" @@ -107,3 +138,12 @@ exclude_dirs = [ "venv", "docs", ] + +{% if cookiecutter.package_manager == 'uv' %} +# UV settings reference https://docs.astral.sh/uv/reference/settings/ + +[tool.uv] +keyring-provider = "subprocess" +native-tls = true + +{% endif %}