diff --git a/copier.yaml b/copier.yaml index 1fda0cc..3a36605 100644 --- a/copier.yaml +++ b/copier.yaml @@ -21,6 +21,23 @@ project_description: type: str help: Your project description +project_type: + type: str + choices: + - cli + - service + default: cli + help: Type of the project + +service_framework: + type: str + choices: + - fastapi + - flask + default: fastapi + when: "{{ project_type == 'service' }}" + help: Web framework to use + repository_namespace: type: str help: Your repository namespace diff --git a/template/.gitignore b/template/.gitignore index 42f20c5..8890419 100644 --- a/template/.gitignore +++ b/template/.gitignore @@ -147,8 +147,9 @@ activemq-data/ # SageMath parsed files *.sage.py -# Environments -.env +# env files (can opt-in for committing if needed) +.env* +!.env.example .envrc .venv env/ diff --git a/template/Makefile b/template/Makefile.jinja similarity index 58% rename from template/Makefile rename to template/Makefile.jinja index 382c8b8..9feef85 100644 --- a/template/Makefile +++ b/template/Makefile.jinja @@ -13,6 +13,21 @@ check: ## Run code quality tools. @echo "🚀 Static type checking: Running ty" @uv run ty check +.PHONY: release-dry +release-dry: ## Simulate the release process and show the next version + @echo "🚀 Simulating release process..." + @uv run semantic-release --noop version --print + +.PHONY: release-stable +release-stable: ## Prepare project for stable 1.0.0 release (disables 0.x.x versions) + @echo "🚀 Preparing for stable release..." + @sed -i 's/allow_zero_version = true/allow_zero_version = false/' pyproject.toml + @echo "Updated pyproject.toml: allow_zero_version = false" + @echo "Next steps:" + @echo " 1. git add pyproject.toml" + @echo " 2. git commit -m 'chore: prepare for stable 1.0.0 release'" + @echo " 3. Push to main - the next feat/fix commit will trigger 1.0.0" + .PHONY: test test: ## Test the code with pytest @echo "🚀 Testing code: Running pytest" @@ -35,3 +50,10 @@ help: [[print(f'\033[36m{m[0]:<20}\033[0m {m[1]}') for m in re.findall(r'^([a-zA-Z_-]+):.*?## (.*)$$', open(makefile).read(), re.M)] for makefile in ('$(MAKEFILE_LIST)').strip().split()]" .DEFAULT_GOAL := help + +{% if project_type == 'service' %} +.PHONY: serve +serve: ## Run the service + @echo "🚀 Running service" + @uv run python -m {{package_name}}.main +{% endif %} diff --git a/template/pyproject.toml.jinja b/template/pyproject.toml.jinja index 8ffb2cf..e28fdc7 100644 --- a/template/pyproject.toml.jinja +++ b/template/pyproject.toml.jinja @@ -1,6 +1,6 @@ [project] name = "{{ project_name }}" -version = "0.0.1" +version = "0.0.0" description = "{{ project_description }}" authors = [{name = "{{ author_username }}", email = "{{ author_email }}"}] requires-python = ">=3.10" @@ -9,14 +9,26 @@ license = "AGPL-3.0" license-files = ["LICENSE"] dependencies = [ -"typer>=0.20" + {%- if project_type == 'cli' %} + "typer>=0.20", + {%- elif project_type == 'service' %} + {%- if service_framework == 'fastapi' %} + "fastapi>=0.110.0", + {%- elif service_framework == 'flask' %} + "flask>=3.0.0", + {%- endif %} + "uvicorn>=0.27.0", + "pydantic-settings>=2.2.0", + {%- endif %} ] [project.urls] Repository = "https://github.com/trobz/{{project_name}}" +{%- if project_type == 'cli' %} [project.scripts] {{ project_name }} = "{{ package_name }}.main:app" +{%- endif %} [dependency-groups] dev = [ @@ -24,6 +36,7 @@ dev = [ "ty>=0.0.1a16", "ruff>=0.11.5", "pytest>=7.2.0", + "python-semantic-release>=10.5.3", ] [tool.pytest.ini_options] @@ -55,6 +68,18 @@ build_command = """ uv build """ +# Commit parser - this should be at the top level +commit_parser = "conventional" + +# Allow 0.x.x versions (prevents jumping straight to 1.0.0) +allow_zero_version = true + +# Don't do major version bumps when in 0.x.x +major_on_zero = false + +# Tag format +tag_format = "v{version}" + [tool.semantic_release.changelog] exclude_commit_patterns = [ '''chore(?:\([^)]*?\))?: .+''', diff --git a/template/{% if project_type == 'service' %}.env.sample{% endif %} b/template/{% if project_type == 'service' %}.env.sample{% endif %} new file mode 100644 index 0000000..c2fc6b8 --- /dev/null +++ b/template/{% if project_type == 'service' %}.env.sample{% endif %} @@ -0,0 +1,5 @@ +ENVIRONMENT=local +DEBUG=false + +SERVER_HOST=0.0.0.0 +SERVER_PORT=8000 diff --git a/template/{{package_name}}/main.py b/template/{{package_name}}/main.py deleted file mode 100644 index e5ac6c0..0000000 --- a/template/{{package_name}}/main.py +++ /dev/null @@ -1,3 +0,0 @@ -import typer - -app = typer.Typer() diff --git a/template/{{package_name}}/main.py.jinja b/template/{{package_name}}/main.py.jinja new file mode 100644 index 0000000..79e9e8d --- /dev/null +++ b/template/{{package_name}}/main.py.jinja @@ -0,0 +1,61 @@ +{% if project_type == 'cli' -%} +import typer + +app = typer.Typer() + + +@app.command() +def hello(name: str): + print(f"Hello {name}") + + +if __name__ == "__main__": + app() + +{%- elif project_type == 'service' -%} +{%- if service_framework == 'fastapi' -%} +from fastapi import FastAPI +from .settings import settings +import uvicorn + +app = FastAPI(title="{{ project_name }}") + + +@app.get("/") +def read_root(): + return {"Hello": "World", "env": settings.environment} + +@app.get("/health") +def health_check(): + return {"status": "ok"} + + +if __name__ == "__main__": + uvicorn.run( + "{{ package_name }}.main:app", + host=settings.server_host, + port=settings.server_port, + reload=settings.debug, + ) + +{%- elif service_framework == 'flask' -%} +from flask import Flask +from .settings import settings + +app = Flask(__name__) + + +@app.route("/") +def hello_world(): + return {"Hello": "World", "env": settings.environment} + +@app.route("/health") +def health_check(): + return {"status": "ok"} + + +if __name__ == "__main__": + # For development only. Use a production WSGI server for deployment. + app.run(host=settings.server_host, port=settings.server_port, debug=settings.debug) +{%- endif -%} +{%- endif -%} diff --git a/template/{{package_name}}/{% if project_type == 'service' %}settings.py{% endif %}.jinja b/template/{{package_name}}/{% if project_type == 'service' %}settings.py{% endif %}.jinja new file mode 100644 index 0000000..b71befa --- /dev/null +++ b/template/{{package_name}}/{% if project_type == 'service' %}settings.py{% endif %}.jinja @@ -0,0 +1,15 @@ +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class Settings(BaseSettings): + model_config = SettingsConfigDict( + env_file=(".env.local", ".env"), env_ignore_empty=True, extra="ignore" + ) + + environment: str = "local" + debug: bool = False + server_host: str = "0.0.0.0" + server_port: int = 8000 + + +settings = Settings()