Getting started¶
This walks you through generating a project from PyFr and running it. It
takes about ten minutes, most of which is the first just up building a
container image.
What you need¶
| Tool | Why |
|---|---|
| uv | The only Python tool required. uvx runs cookiecutter without installing it, and the generated project uses uv for everything else: it fetches the right Python, installs the locked dependencies and runs every command. |
| git | The generated project is a git repository. The hook runs git init and makes the first commit. |
| just | A command runner. Every task in a generated project is a just recipe: just up, just check, and the rest. |
| Docker | Only for just up, which builds the image and starts the container stack, and for the container-backed test tiers. just check needs no Docker. |
You do not need to install Python separately. A generated project pins
the interpreter in .python-version, and uv fetches it.
Generate a project¶
uvx cookiecutter gh:EmadMokhtar/pyfr
uvx downloads cookiecutter into a temporary environment and runs it;
gh:EmadMokhtar/pyfr names this repository on GitHub. cookiecutter asks the
twelve questions below, in this order. Press Enter to accept a default. For
a choice prompt, type the number of the option you want.
| Prompt | What it decides | Default |
|---|---|---|
project_name |
The human-readable name. It is the title of the README and of the documentation site. | My Service |
project_slug |
The directory name, the repository name and the container image name. Lower-case letters, digits and hyphens, starting with a letter. | Derived: the name lower-cased, spaces replaced by hyphens — my-service |
package_name |
The Python package under src/. It must be a valid Python identifier: lower-case letters, digits and underscores, at most 25 characters, and not the name of a standard-library module. |
Derived: the slug with hyphens replaced by underscores — my_service |
description |
One sentence. It opens the README and is the site's description. | A Python microservice. |
author_name |
The author recorded in pyproject.toml. The MIT and Proprietary licence texts name it too. |
Your Name |
author_email |
The author's email address in pyproject.toml. |
you@example.com |
github_org |
The GitHub user or organisation the repository will live under. It decides the repository URL, the container registry path and the documentation site's address. | your-org |
database |
postgres gives you a PostgreSQL repository, migrations, a migrations image and the schema gates. none removes every one of them. |
postgres (or none) |
cache |
redis gives you a fail-open Redis cache in front of the order repository. none removes it. |
redis (or none) |
object_storage |
s3 gives you an S3-compatible receipt store, with MinIO in the local stack. none removes it. |
s3 (or none) |
http_port |
The port the service listens on: in just dev, in the compose stack and in the container image. A plain decimal integer between 1 and 65535, and not one the project's own stack binds — 8001 (just docs), 9099 (the payment stub), 3000, 4317, 4318 and 9090 (the observability profile), nor 5432, 6379, 9000 or 9001 while PostgreSQL, Redis or MinIO is chosen; the generator refuses those and says which service has the port. |
8000 |
license |
Which licence text becomes LICENSE. |
Apache-2.0 (or MIT, MPL-2.0, Proprietary) |
Four of the free-text answers — project_name, description,
author_name and author_email — are pasted into pyproject.toml, Python
docstrings and Markdown exactly as typed. They must not contain a double
quote, a backslash or a control character such as a line break. A bad
answer to any prompt is refused before a file is written, with a message
naming the prompt and the rule; nothing is left on disk, and you run the
command again.
The first entry of each choice list is the default, so a --no-input run
produces the "everything on" project. To take the defaults but change one
answer, pass it on the command line:
uvx cookiecutter gh:EmadMokhtar/pyfr --no-input project_name="Order Service" database=none
Your answers are recorded in the generated project as .pyfr-answers.yml,
together with the template version they were rendered from. Keep it
committed and unedited: just update reads it (Updating
later).
What the hook does¶
Once the prompts are answered, cookiecutter writes the project and runs the template's post-generation hook inside it. The hook does two things.
It keeps what the answers chose. The chosen licence text is renamed to
LICENSE and the other three are deleted. For each backend answered
none, the hook deletes the files and directories that belong to that
backend alone — migrations/, the database adapter package, the cache
adapter package, and so on — after cookiecutter has already removed the
lines inside mixed files. Then it removes any directory that was left
empty. Nothing of an unchosen backend survives: no dependency, import,
setting, compose service, recipe or file.
It sets the project up, best effort. In order:
git init -quv sync— install the locked dependencies, fetching Python if needed.uv run pre-commit install— wire up the git hooks, for both thepre-commitstage (formatting, linting, secrets) and thecommit-msgstage (the Conventional Commits check).git add -A, then the first commit:git commit -q -m "chore: generate the project from pyfr".
Every step is best effort. A step that fails — no network for uv sync,
no git on the machine — prints Could not run `<command>`; run it
later. and the hook carries on with the next step, and it still exits 0.
An offline laptop must receive a fully written project, not a failed
generation. If the first commit could not be made, the hook says so:
Run `git add -A && git commit` once the steps above succeed. Each step
is independent, so a missing git does not stop uv sync, and a failed
uv sync does not stop the repository being made.
The hook ends by printing what to do next:
Next steps:
cd my-service
just up # build the image and start the stack
just check # every fast gate
see README.md for the rest
Run it¶
cd my-service
just up
just up builds the container image and starts the compose stack: the
backends you chose, a payment stub and the API, in dependency order. Once
the API reports healthy, a one-shot seed container creates five orders
through it, so there is data to read before you have typed a request. The
API serves on the port you chose — http://localhost:8000/docs with the
default — and Ctrl-C stops the stack.
In a second terminal, run every fast gate:
just check
That is lint, type-check, the import-boundary check, the unit and API test
tiers and the pre-commit hooks, followed by git diff --exit-code to catch
a hook that changed a file. It needs no Docker and finishes in a couple of
minutes. It is also what the project's own continuous integration runs on
every pull request and every push to main, from the first one.
The generated README.md lists every recipe, and just with no arguments
prints them.
Where to go next¶
The project's own documentation site. Every generated project ships a
site about itself, under its docs/, and its docs.yml workflow deploys
it to GitHub Pages on every push to main — once the Pages source setting
below is made. Until then, read the reference service's copy of that site
— the same pages, rendered for the "everything on" answers:
- Getting started places an order and explains each response. It is the same walk-through your project's site carries for your service.
- Commands
is every
justrecipe, and Configuration every environment variable. - Add an endpoint builds a feature through all four layers.
The repository settings. Six settings live in the GitHub interface,
not in the generated files, and the workflows need them: the Pages source,
a no-docs-needed label, a RELEASE_TOKEN secret when a ruleset requires
pull requests or the weekly template update should run CI, letting Actions
open pull requests when that secret is absent, squash-merge, and making the
published container packages public after the first release. The generated
README.md lists all six
under Continuous integration and releases, with what goes wrong when each
is missing; the reference service's copy is
here.
Updating later¶
A generated project is yours, and it still receives later template
versions. just update re-renders the template at the newest release with
the answers in .pyfr-answers.yml and merges the result into your branch
through an ordinary git merge: your edits survive, files you deleted stay
deleted, and paths listed in .pyfr-update-ignore are never touched. The
project's .github/workflows/template-update.yml runs the same update
every Monday and opens a pull request when there is something to merge, or
an issue naming the files when the merge conflicts. The project's own
guide, Update from the template — published for the reference service at
https://emadmokhtar.github.io/pyfr/reference-service/guides/update-from-template/
— has the details and the conflict procedure.
How PyFr itself is built — the template body, the golden diff, pruning and the regeneration loop — is in Contributing.