diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index 86f5c7e..98c02c9 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -39,7 +39,7 @@ jobs:
working-directory: server
run: |
python -m pip install --upgrade pip
- python -m pip install -r requirements.txt
+ python -m pip install -r requirements_dev.txt
- name: Test Server
working-directory: server
run: |
diff --git a/README.md b/README.md
index 17813a7..c54680b 100644
--- a/README.md
+++ b/README.md
@@ -24,7 +24,8 @@ then point to it in the environment and install dependencies:
docker-compose up -d database
export DATABASE_HOST=localhost
- pip install -r server/requirements.txt
+ cd server
+ make install
For the first run, set up a user account and,
then issue the following commands to install and start the service:
@@ -53,6 +54,29 @@ output through a management command:
apikeyserv can manage multiple signing keys to allow for key rotation.
Keys can be retired by unchecking their "active" flag.
+Managing requirements
+=========================
+
+This project uses `pip-tools `__
+and `pur `__ to manage the
+``requirements.txt`` file.
+
+To add a Python dependency to the project:
+
+- Add the dependency to ``requirements.in`` or ``requirements_dev.in``
+- Run ``make requirements``
+
+To upgrade the project, run::
+
+ make upgrade
+ make install
+ make test
+
+Or in a single call: ``make upgrade install test``
+
+Django will only be upgraded with patch-level versions.
+To change from e.g. Django 3.0 to 3.1, update the version in ``requirements.in`` yourself.
+
Deployment
==========
diff --git a/server/Makefile b/server/Makefile
new file mode 100644
index 0000000..4203c06
--- /dev/null
+++ b/server/Makefile
@@ -0,0 +1,27 @@
+.PHONY: install
+install:
+ pip install wheel
+ pip install -r requirements_dev.txt
+
+.PHONY: sync
+sync:
+ pip-sync requirements_dev.txt
+
+.PHONY: requirements
+requirements: requirements.in requirements_dev.in
+ pip install pip-tools
+ pip-compile -v --output-file requirements.txt requirements.in
+ pip-compile -v --allow-unsafe --output-file requirements_dev.txt requirements_dev.in
+
+.PHONY: upgrade
+upgrade:
+ pur --patch=Django -r requirements_dev.in
+ make requirements
+
+.PHONY: test
+test:
+ pytest --reuse-db --nomigrations -vs .
+
+.PHONY: test
+retest:
+ pytest --reuse-db --nomigrations -vvs --lf .
diff --git a/server/requirements.in b/server/requirements.in
new file mode 100644
index 0000000..eceb22f
--- /dev/null
+++ b/server/requirements.in
@@ -0,0 +1,11 @@
+asgiref==3.7.2
+cffi==1.16.0
+Django==4.2.10
+django-cors-headers==4.3.1
+django-healthchecks==1.5.0
+psycopg2-binary==2.9.9
+pycparser==2.21
+PyJWT[crypto]>=2.8.*
+six==1.16.0
+sqlparse==0.4.4
+uwsgi==2.0.24
diff --git a/server/requirements.txt b/server/requirements.txt
index cfce10d..a66dc64 100644
--- a/server/requirements.txt
+++ b/server/requirements.txt
@@ -1,15 +1,56 @@
-asgiref==3.6.0
-cffi==1.15.0
-Django==4.2.10
-django-cors-headers==4.2.0
+#
+# This file is autogenerated by pip-compile with Python 3.11
+# by the following command:
+#
+# pip-compile --output-file=requirements.txt requirements.in
+#
+asgiref==3.7.2
+ # via
+ # -r requirements.in
+ # django
+ # django-cors-headers
+certifi==2024.2.2
+ # via
+ # django-healthchecks
+ # requests
+cffi==1.16.0
+ # via
+ # -r requirements.in
+ # cryptography
+charset-normalizer==3.3.2
+ # via requests
+cryptography==42.0.4
+ # via pyjwt
+django==4.2.10
+ # via
+ # -r requirements.in
+ # django-cors-headers
+ # django-healthchecks
+django-cors-headers==4.3.1
+ # via -r requirements.in
django-healthchecks==1.5.0
-psycopg2-binary==2.9.6
+ # via -r requirements.in
+idna==3.6
+ # via requests
+psycopg2-binary==2.9.9
+ # via -r requirements.in
pycparser==2.21
-pytest==7.1.2
-pytest-django==4.5.2
-PyJWT[crypto]>=2.8.*
+ # via
+ # -r requirements.in
+ # cffi
+pyjwt[crypto]==2.8.0
+ # via
+ # -r requirements.in
+ # pyjwt
+requests==2.31.0
+ # via django-healthchecks
six==1.16.0
+ # via -r requirements.in
sqlparse==0.4.4
-uwsgi==2.0.22
-
-setuptools>=65.5.1 # CVE-2022-40897
+ # via
+ # -r requirements.in
+ # django
+urllib3==2.2.1
+ # via requests
+uwsgi==2.0.24
+ # via -r requirements.in
diff --git a/server/requirements_dev.in b/server/requirements_dev.in
new file mode 100644
index 0000000..42b138a
--- /dev/null
+++ b/server/requirements_dev.in
@@ -0,0 +1,7 @@
+-r ./requirements.in
+
+setuptools>=69.1.0 # CVE-2022-40897
+pip-tools==7.4.0
+pur == 7.3.1
+pytest==8.0.1
+pytest-django==4.8.0
diff --git a/server/requirements_dev.txt b/server/requirements_dev.txt
new file mode 100644
index 0000000..8439dad
--- /dev/null
+++ b/server/requirements_dev.txt
@@ -0,0 +1,94 @@
+#
+# This file is autogenerated by pip-compile with Python 3.11
+# by the following command:
+#
+# pip-compile --allow-unsafe --output-file=requirements_dev.txt requirements_dev.in
+#
+asgiref==3.7.2
+ # via
+ # -r ./requirements.in
+ # django
+ # django-cors-headers
+build==1.0.3
+ # via pip-tools
+certifi==2024.2.2
+ # via
+ # django-healthchecks
+ # requests
+cffi==1.16.0
+ # via
+ # -r ./requirements.in
+ # cryptography
+charset-normalizer==3.3.2
+ # via requests
+click==8.1.7
+ # via
+ # pip-tools
+ # pur
+cryptography==42.0.4
+ # via pyjwt
+django==4.2.10
+ # via
+ # -r ./requirements.in
+ # django-cors-headers
+ # django-healthchecks
+django-cors-headers==4.3.1
+ # via -r ./requirements.in
+django-healthchecks==1.5.0
+ # via -r ./requirements.in
+idna==3.6
+ # via requests
+iniconfig==2.0.0
+ # via pytest
+packaging==23.2
+ # via
+ # build
+ # pytest
+pip-tools==7.4.0
+ # via -r requirements_dev.in
+pluggy==1.4.0
+ # via pytest
+psycopg2-binary==2.9.9
+ # via -r ./requirements.in
+pur==7.3.1
+ # via -r requirements_dev.in
+pycparser==2.21
+ # via
+ # -r ./requirements.in
+ # cffi
+pyjwt[crypto]==2.8.0
+ # via
+ # -r ./requirements.in
+ # pyjwt
+pyproject-hooks==1.0.0
+ # via
+ # build
+ # pip-tools
+pytest==8.0.1
+ # via
+ # -r requirements_dev.in
+ # pytest-django
+pytest-django==4.8.0
+ # via -r requirements_dev.in
+requests==2.31.0
+ # via django-healthchecks
+six==1.16.0
+ # via -r ./requirements.in
+sqlparse==0.4.4
+ # via
+ # -r ./requirements.in
+ # django
+urllib3==2.2.1
+ # via requests
+uwsgi==2.0.24
+ # via -r ./requirements.in
+wheel==0.42.0
+ # via pip-tools
+
+# The following packages are considered to be unsafe in a requirements file:
+pip==24.0
+ # via pip-tools
+setuptools==69.1.0
+ # via
+ # -r requirements_dev.in
+ # pip-tools