...and also a place to demonstrate modern Python software development with Bazel.
Any client can:
- Create a new short link
- Visit an existing short link to be redirected to its target
See notes.
This app works best when running on an intranet with DNS set up so users can just type
go/ in their browsers to access this service. Short links then end up looking like:
go/my-short-link
This project demonstrates:
-
Hermetically building, running, testing, type checking, linting, and auto-formatting a Python codebase, as well as measuring code coverage, using modern, idiomatic Python and Bazel workflows.
-
Bazel managing native Python virtual environments and installing Python package dependencies (wheels or sdists from PyPI) in them automatically, in a reproducible, fast, and cache-friendly fashion.
- uv is used via rules_uv to make lockfile generation 100x faster than with pip-tools, which is what rules_python currently uses (though they are working toward uv support).
-
VSCode configuration to enable e.g. discovering the Python environment that Bazel manages (so you can "go to definition", see usage errors, etc. across third-party imports), discovering tests and running them via VSCode's "Testing" UI, and using the VSCode Python debugger, out-of-the-box.
-
Using
pytestvia Bazel, with associated VSCode integration, for an idiomatic Python testing experience. -
Using Bazel to hermetically fetch and drive ruff for linting and formatting Python code, buildifier for formatting Starlark, and mypy for type checking. (TODO: try the new rules_mypy)
-
Building an efficient container image for a Python application via rules_oci.
Ensure you have installed bazelisk,
and either create a symlink from bazel to bazelisk (recommended),
or replace bazel with bazelisk in the commands below.
Run the app:
bazel run :app
You should see something like
Running on http://0.0.0.0:8675 (CTRL + C to quit)
toward the end of the output, and you can then point your browser at a corresponding address to try the app.
bazel run :mypy
bazel test ...
bazel coverage ...
In the output, Bazel should mention the path bazel-out/_coverage/_coverage_report.dat.
You can pass this to genhtml (commonly provided by the lcov package)
to generate an html report from this coverage data.
See bazel's coverage docs for more info.
To watch the code for changes and reload changed files automatically while you're developing,
you can use bazel-watcher.
Just replace bazel with tools/ibazel.
Examples:
tools/ibazel run :app.devtools/ibazel test ...
Better yet, just open this project in VSCode. It's configured to automatically start the Flask dev server (with the debugger enabled) upon opening this project, using ibazel to watch for changes.
Search for "Demo how to use VSCode's Python debugger" in app.py,
follow the instructions in the comment, and uncomment out the associated code.
Linting and code formatting are provided via rules_lint (and made more ergonomic via aspect-cli).
Examples:
bazel run //tools/format:formataspect lint :appaspect lint :all
This can be set up as a pre-commit hook and as a PR merge check if desired.
TODO: Make the pre-commit hooks run faster.
To take additional runtime dependencies,
add them to requirements/base.in, then run:
bazel run //requirements:compile_base_deps
This re-compiles requirements/base.txt (a standard pip requirements lock file)
based on your changed requirements/base.in.
See the pip-tools docs if this is new to you.
The process of updating test- and dev-time dependencies is similar,
except using the corresponding requirements files and targets instead of base.
Run the *.update corresponding to the depset you want to upgrade, e.g.
bazel run //requirements:compile_base_deps.update
-
Build the image:
bazel build :image -
Load it into podman:
bazel run :image_load -
Run it:
docker run --rm -p 8675:8675 app:latest -
You should now be able to access the server running inside the container:
curl localhost:8675
-
The production-quality server used is hypercorn. You can access its CLI via
bazel run :hypercorn, passing any desired arguments after a--, e.g.bazel run :hypercorn -- 'app:create_app()'See the hypercorn docs for other settings you may wish to change, e.g., to configure TLS, customize logging, etc.
-
Exposing click-tracking metrics is a non-goal.
This app is deliberately very minimal (~50 lines of code).
-
Short links cannot be modified or deleted once created (unless you access the database directly).
-
No authentication is performed.
As always, it is the responsibility of the service hosting a target URL to perform any authentication and authorization required to access the requested resource.
If you need to add authentication to this service, do either of the following:
-
Run a reverse proxy that authenticates all requests before forwarding them to this service, and make that the only way to reach this service. You probably want the reverse proxy to force HTTPS while you're at it. OR:
-
Modify
app.pyto wrapapp.wsgi_appin appropriate auth middleware that performs authentication at the WSGI layer. Don't forget to take the additional dependencies.
-
-
By default, a SQLite database will be created in
/tmp/shorty.dbto persist the data. To customize this, set theSQLITE_DB_URIenvironment variable to the SQLite database URI you desire before running the app:export SQLITE_DB_URI="file:path/to/shorty.db" # or... export SQLITE_DB_URI="file:memory?mode=memory&cache=shared"