Setting up an application

  • We want:
    • clear structure
    • predictable imports
    • CLI commands
    • uv sync as the go-to command for installation

Structure

  • The basic structure relies on a src folder, and then the package. Why?
    • source: https://hynek.me/articles/testing-packaging/
    • If you use the ad hoc layout without an src directory, your tests do not run against the package as it will be installed by its users. They run against whatever the situation in your project directory is.
    • But this is not just about tests: the same is true for your application. The behavior can change completely once you package and install it somewhere else.
β”œβ”€β”€ pyproject.toml
β”œβ”€β”€ src
    └── hello_svc ## package name
        β”œβ”€β”€ __init__.py  ## indicate the folder is a package
        └── views.py ## main logic 
└── tests
    └── test_e2e.py

py-project.toml

[project]
name = "hello-svc" ## dashes and underscore are normalized to dashes
version = "0"
requires-python = "==3.13.*"
dependencies = ["fastapi", "granian", "stamina"]

[dependency-groups] ## allows to separate local development and production
dev = ["fastapi[standard]", "pytest"] # dev is a dependency group automatically installed by uv

[build-system] ## specifying uv build, no need for setup.py
requires = ["uv_build"] 
build-backend = "uv_build"

Entrypoints

  • Entrypoints are clean places for your app to interface with the real world, they’re the bridge.
  • They should contain code that will only run once e.g. setup a config, run logic, and cleanup
  • You don’t want outside code to need to know what file to import within your package

It may look like this

β”œβ”€β”€ src
β”‚Β Β  └── hello_svc
β”‚Β Β      β”œβ”€β”€ entrypoints
β”‚Β Β      β”‚Β Β  β”œβ”€β”€ asgi.py
β”‚Β Β      β”‚Β Β  β”œβ”€β”€ cli.py
β”‚Β Β      β”‚Β Β  β”œβ”€β”€ __init__.py
β”‚Β Β      β”‚Β Β  └── queue_worker.py
β”‚Β Β      β”œβ”€β”€ __init__.py
β”‚Β Β      └── views.py
└── tests
    └── test_e2e.py

Lifecycle or updating

  • uv lock --upgrade

Local workflows

  • Adding a new dependency to your local dev env
    • uv add --dev <package>
  • Ephemeral dependencies
    • uv run --with <package> not add to the .lock file
  • Using just & justfile
    • For testing, linting, typing
test *args:
	uv run -m pytest {{ args }}

_pre-commit *args:
	uvx --with pre-commit-uv pre-commit {{ args }}

lint:
	- uv run -m ruff check --fix --unsafe-fixes .
	- uv run -m ruff format .
	@just _pre-commit run --all-files #run _every hook_ in your `.pre-commit-config.yaml` on every file in the repository,
	
	
typing:
	uv run -m mypy src
	
check-all: lint typing