Repository: FlexMeasures/flexmeasures (language: python)

Issue:
Separate requirements files by supported python version
The .txt files (at the least, .in files can probably stay generic) should be available for each Python version we support, as the set of available packages per Python version differs.

[This section](https://github.com/jazzband/pip-tools#cross-environment-usage-of-requirementsinrequirementstxt-and-pip-compile) of the pip-tools Readme states this as good practice, as well.

This has some consequences for Github actions and the Makefile, e.g. for the latter: can we pick the right requirements folder automatically by knowing the current environment's Python version? (on Github Actions, we set the Python version explicitly)



Discussion / hints:

Fix patch:
diff --git a/Dockerfile b/Dockerfile
index b3a86d787..416936049 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -12,7 +12,10 @@ WORKDIR /app
 COPY requirements /app/requirements
 
 # py dev tooling
-RUN python3 -m pip install --no-cache-dir  --upgrade pip && python3 --version && pip3 install --no-cache-dir --upgrade setuptools && pip3 install highspy && pip3 install --no-cache-dir -r requirements/app.txt -r requirements/dev.txt -r requirements/test.txt
+RUN python3 -m pip install --no-cache-dir --upgrade pip && python3 --version && \
+    pip3 install --no-cache-dir --upgrade setuptools && pip3 install highspy && \
+    PYV=$(python3 -c "import sys;t='{v[0]}.{v[1]}'.format(v=list(sys.version_info[:2]));sys.stdout.write(t)") && \
+    pip3 install --no-cache-dir -r requirements/$PYV/app.txt -r requirements/$PYV/dev.txt -r requirements/$PYV/test.txt
 
 # Copy code and meta/config data
 COPY setup.* .flaskenv wsgi.py /app/
diff --git a/Makefile b/Makefile
index ab1b2a901..ea2789d90 100644
--- a/Makefile
+++ b/Makefile
@@ -1,3 +1,7 @@
+# Check Python major and minor version
+# For more information, see https://stackoverflow.com/a/22105036
+PYV = $(shell python -c "import sys;t='{v[0]}.{v[1]}'.format(v=list(sys.version_info[:2]));sys.stdout.write(t)")
+
 # Note: use tabs
 # actions which are virtual, i.e. not a script
 .PHONY: install install-for-dev install-for-test install-deps install-flexmeasures run-local test freeze-deps upgrade-deps update-docs update-docs-pdf show-file-space show-data-model clean-db
@@ -35,14 +39,15 @@ install: install-deps install-flexmeasures
 
 install-for-dev:
 	make freeze-deps
-	pip-sync requirements/app.txt requirements/dev.txt requirements/test.txt
+	make ensure-deps-folder
+	pip-sync requirements/${PYV}/app.txt requirements/${PYV}/dev.txt requirements/${PYV}/test.txt
 	make install-flexmeasures
 
 install-for-test:
 	make install-pip-tools
 # Pass pinned=no if you want to test against latest stable packages, default is our pinned dependency set
 ifneq ($(pinned), no)
-	pip-sync requirements/app.txt requirements/test.txt
+	pip-sync requirements/${PYV}/app.txt requirements/${PYV}/test.txt
 else
 	# cutting off the -c inter-layer dependency (that's pip-tools specific)
 	tail -n +3 requirements/test.in >> temp-test.in
@@ -56,7 +61,7 @@ install-deps:
 	make freeze-deps
 # Pass pinned=no if you want to test against latest stable packages, default is our pinned dependency set
 ifneq ($(pinned), no)
-	pip-sync requirements/app.txt
+	pip-sync requirements/${PYV}/app.txt
 else
 	pip install --upgrade -r requirements/app.in
 endif
@@ -68,23 +73,35 @@ install-pip-tools:
 	pip3 install -q "pip-tools>=7.0"
 
 install-docs-dependencies:
-	pip install -r requirements/docs.txt
+	pip install -r requirements/${PYV}/docs.txt
 
 freeze-deps:
+	make ensure-deps-folder
 	make install-pip-tools
-	pip-compile -o requirements/app.txt requirements/app.in
-	pip-compile -o requirements/test.txt requirements/test.in
-	pip-compile -o requirements/dev.txt requirements/dev.in
-	pip-compile -o requirements/docs.txt requirements/docs.in
+	pip-compile -o requirements/${PYV}/app.txt requirements/app.in
+	# Create app.txt to create constraints for test.txt and dev.txt
+	cat requirements/${PYV}/app.txt > requirements/app.txt
+	pip-compile -o requirements/${PYV}/test.txt requirements/test.in
+	cat requirements/${PYV}/test.txt > requirements/test.txt
+	pip-compile -o requirements/${PYV}/dev.txt requirements/dev.in
+	pip-compile -o requirements/${PYV}/docs.txt requirements/docs.in
+	rm requirements/app.txt
+	rm requirements/test.txt
 
 upgrade-deps:
+	make ensure-deps-folder
 	make install-pip-tools
-	pip-compile --upgrade -o requirements/app.txt requirements/app.in
-	pip-compile --upgrade -o requirements/test.txt requirements/test.in
-	pip-compile --upgrade -o requirements/dev.txt requirements/dev.in
-	pip-compile --upgrade -o requirements/docs.txt requirements/docs.in
-	make test
+	pip-compile --upgrade -o requirements/${PYV}/app.txt requirements/app.in
+	# Create app.txt to create constraints for test.txt and dev.txt
+	cat requirements/${PYV}/app.txt > requirements/app.txt
+	pip-compile --upgrade -o requirements/${PYV}/test.txt requirements/test.in
+	cat requirements/${PYV}/test.txt > requirements/test.txt
+	pip-compile --upgrade -o requirements/${PYV}/dev.txt requirements/dev.in
+	pip-compile --upgrade -o requirements/${PYV}/docs.txt requirements/docs.in
+	rm requirements/app.txt
+	rm requirements/test.txt
 
+	make test
 
 # ---- Data ----
 
@@ -104,5 +121,8 @@ show-data-model:
 	# Use --help to learn more. 
 	./flexmeasures/data/scripts/visualize_data_model.py --uml
 
+ensure-deps-folder:
+	mkdir -p requirements/${PYV}
+
 clean-db:
-	./flexmeasures/data/scripts/clean_database.sh ${db_name} ${db_user}
+	./flexmeasures/data/scripts/clean_database.sh ${db_name} ${db_user}
\ No newline at end of file
diff --git a/documentation/changelog.rst b/documentation/changelog.rst
index 581a84610..5ffe5f595 100644
--- a/documentation/changelog.rst
+++ b/documentation/changelog.rst
@@ -41,6 +41,7 @@ Infrastructure / Support
 * Document the `device_scheduler` linear program [see `PR #764 <https://www.github.com/FlexMeasures/flexmeasures/pull/764>`_].
 * Add support for `HiGHS <https://highs.dev/>`_ solver [see `PR #766 <https://www.github.com/FlexMeasures/flexmeasures/pull/766>`_].
 * Add support for installing FlexMeasures under Python 3.11 [see `PR #771 <https://www.github.com/FlexMeasures/flexmeasures/pull/771>`_].
+* Start keeping sets of pinned requirements per supported Python version. Also fixes recent Docker build problem. [see `PR #776 <https://www.github.com/FlexMeasures/flexmeasures/pull/776>`_]
 * Removed obsolete code dealing with deprecated data models (e.g. assets, markets and weather sensors), and sunset the fm0 scheme for entity addresses [see `PR #695 <https://www.github.com/FlexMeasures/flexmeasures/pull/695>`_ and `project 11 <https://www.github.com/FlexMeasures/flexmeasures/projects/11>`_]
 
 v0.14.2 | July 25, 2023
diff --git a/requirements/app.txt b/requirements/3.10/app.txt
similarity index 98%
rename from requirements/app.txt
rename to requirements/3.10/app.txt
index 53917b19d..d595aa916 100644
--- a/requirements/app.txt
+++ b/requirements/3.10/app.txt
@@ -2,7 +2,7 @@
 # This file is autogenerated by pip-compile with Python 3.10
 # by the following command:
 #
-#    pip-compile --output-file=requirements/app.txt requirements/app.in
+#    pip-compile --output-file=requirements/3.10/app.txt requirements/app.in
 #
 alembic==1.11.1
     # via flask-migrate
@@ -208,7 +208,7 @@ patsy==0.5.3
     # via statsmodels
 pillow==9.5.0
     # via matplotlib
-pint==0.22
+pint==0.21.1
     # via -r requirements/app.in
 ply==3.11
     # via pyomo
@@ -278,7 +278,7 @@ scikit-learn==1.2.2
     # via
     #   sktime
     #   timetomodel
-scipy==1.11.1
+scipy==1.10.1
     # via
     #   properscoring
     #   scikit-learn
@@ -323,7 +323,6 @@ typing-extensions==4.7.0
     # via
     #   alembic
     #   altair
-    #   pint
     #   py-moneyed
     #   pydantic
 tzdata==2023.3
diff --git a/requirements/dev.txt b/requirements/3.10/dev.txt
similarity index 91%
rename from requirements/dev.txt
rename to requirements/3.10/dev.txt
index 332320639..0957405a4 100644
--- a/requirements/dev.txt
+++ b/requirements/3.10/dev.txt
@@ -2,7 +2,7 @@
 # This file is autogenerated by pip-compile with Python 3.10
 # by the following command:
 #
-#    pip-compile --output-file=requirements/dev.txt requirements/dev.in
+#    pip-compile --output-file=requirements/3.10/dev.txt requirements/dev.in
 #
 black==22.3.0
     # via -r requirements/dev.in
@@ -56,8 +56,10 @@ pyinstrument==4.5.0
     # via -r requirements/dev.in
 pytest-runner==6.0.0
     # via -r requirements/dev.in
-pyyaml==6.0
-    # via pre-commit
+pyyaml==6.0.1
+    # via
+    #   -c requirements/app.txt
+    #   pre-commit
 setuptools-scm==7.1.0
     # via -r requirements/dev.in
 tomli==2.0.1
diff --git a/requirements/docs.txt b/requirements/3.10/docs.txt
similarity index 96%
rename from requirements/docs.txt
rename to requirements/3.10/docs.txt
index 722bf528c..45c2c9560 100644
--- a/requirements/docs.txt
+++ b/requirements/3.10/docs.txt
@@ -2,7 +2,7 @@
 # This file is autogenerated by pip-compile with Python 3.10
 # by the following command:
 #
-#    pip-compile --output-file=requirements/docs.txt requirements/docs.in
+#    pip-compile --output-file=requirements/3.10/docs.txt requirements/docs.in
 #
 alabaster==0.7.13
     # via sphinx
diff --git a/requirements/3.11/app.txt b/requirements/3.11/app.txt
new file mode 100644
index 000000000..8d0bad887
--- /dev/null
+++ b/requirements/3.11/app.txt
@@ -0,0 +1,354 @@
+#
+# This file is autogenerated by pip-compile with Python 3.11
+# by the following command:
+#
+#    pip-compile --output-file=requirements/3.11/app.txt requirements/app.in
+#
+alembic==1.11.1
+    # via flask-migrate
+altair==5.0.1
+    # via -r requirements/app.in
+arrow==1.2.3
+    # via rq-dashboard
+attrs==23.1.0
+    # via jsonschema
+babel==2.12.1
+    # via py-moneyed
+bcrypt==4.0.1
+    # via -r requirements/app.in
+blinker==1.6.2
+    # via
+    #   flask-mail
+    #   flask-principal
+    #   flask-security-too
+    #   sentry-sdk
+certifi==2023.5.7
+    # via
+    #   requests
+    #   sentry-sdk
+charset-normalizer==3.1.0
+    # via requests
+click==8.1.3
+    # via
+    #   -r requirements/app.in
+    #   click-default-group
+    #   flask
+    #   rq
+click-default-group==1.2.2
+    # via -r requirements/app.in
+colour==0.1.5
+    # via -r requirements/app.in
+contourpy==1.1.0
+    # via matplotlib
+convertdate==2.4.0
+    # via workalendar
+cycler==0.11.0
+    # via matplotlib
+deprecated==1.2.14
+    # via sktime
+dill==0.3.6
+    # via openturns
+dnspython==2.3.0
+    # via email-validator
+email-validator==2.0.0.post2
+    # via
+    #   -r requirements/app.in
+    #   flask-security-too
+filelock==3.12.2
+    # via tldextract
+flask==2.1.2
+    # via
+    #   -r requirements/app.in
+    #   flask-classful
+    #   flask-cors
+    #   flask-json
+    #   flask-login
+    #   flask-mail
+    #   flask-marshmallow
+    #   flask-migrate
+    #   flask-principal
+    #   flask-security-too
+    #   flask-sqlalchemy
+    #   flask-sslify
+    #   flask-wtf
+    #   rq-dashboard
+    #   sentry-sdk
+flask-classful @ git+https://github.com/superseed/flask-classful@werkzeug-2.2
+    # via -r requirements/app.in
+flask-cors==4.0.0
+    # via -r requirements/app.in
+flask-json==0.3.5
+    # via -r requirements/app.in
+flask-login==0.6.1
+    # via
+    #   -r requirements/app.in
+    #   flask-security-too
+flask-mail==0.9.1
+    # via -r requirements/app.in
+flask-marshmallow==0.14.0
+    # via -r requirements/app.in
+flask-migrate==4.0.4
+    # via -r requirements/app.in
+flask-principal==0.4.0
+    # via flask-security-too
+flask-security-too==5.1.2
+    # via -r requirements/app.in
+flask-sqlalchemy==2.5.1
+    # via
+    #   -r requirements/app.in
+    #   flask-migrate
+flask-sslify==0.1.5
+    # via -r requirements/app.in
+flask-wtf==1.1.1
+    # via
+    #   -r requirements/app.in
+    #   flask-security-too
+fonttools==4.40.0
+    # via matplotlib
+greenlet==2.0.2
+    # via sqlalchemy
+humanize==4.7.0
+    # via -r requirements/app.in
+idna==3.4
+    # via
+    #   email-validator
+    #   requests
+    #   tldextract
+importlib-metadata==6.7.0
+    # via
+    #   -r requirements/app.in
+    #   timely-beliefs
+inflect==6.0.2
+    # via -r requirements/app.in
+inflection==0.5.1
+    # via -r requirements/app.in
+iso8601==2.0.0
+    # via -r requirements/app.in
+isodate==0.6.1
+    # via
+    #   -r requirements/app.in
+    #   timely-beliefs
+itsdangerous==2.1.2
+    # via
+    #   flask
+    #   flask-security-too
+    #   flask-wtf
+jinja2==3.1.2
+    # via
+    #   altair
+    #   flask
+joblib==1.3.1
+    # via scikit-learn
+jsonschema==4.17.3
+    # via altair
+kiwisolver==1.4.4
+    # via matplotlib
+lunardate==0.2.0
+    # via workalendar
+mako==1.2.4
+    # via alembic
+markupsafe==2.1.3
+    # via
+    #   jinja2
+    #   mako
+    #   sentry-sdk
+    #   wtforms
+marshmallow==3.19.0
+    # via
+    #   -r requirements/app.in
+    #   flask-marshmallow
+    #   marshmallow-polyfield
+    #   marshmallow-sqlalchemy
+    #   webargs
+marshmallow-polyfield==5.11
+    # via -r requirements/app.in
+marshmallow-sqlalchemy==0.29.0
+    # via -r requirements/app.in
+matplotlib==3.7.1
+    # via timetomodel
+numpy==1.23.5
+    # via
+    #   -r requirements/app.in
+    #   altair
+    #   contourpy
+    #   matplotlib
+    #   pandas
+    #   patsy
+    #   properscoring
+    #   scikit-learn
+    #   scipy
+    #   sktime
+    #   statsmodels
+    #   timely-beliefs
+    #   timetomodel
+    #   uniplot
+openturns==1.21
+    # via timely-beliefs
+packaging==23.1
+    # via
+    #   marshmallow
+    #   marshmallow-sqlalchemy
+    #   matplotlib
+    #   sktime
+    #   statsmodels
+    #   webargs
+pandas==2.0.3
+    # via
+    #   -r requirements/app.in
+    #   altair
+    #   sktime
+    #   statsmodels
+    #   timely-beliefs
+    #   timetomodel
+passlib==1.7.4
+    # via flask-security-too
+patsy==0.5.3
+    # via statsmodels
+pillow==9.5.0
+    # via matplotlib
+pint==0.21.1
+    # via -r requirements/app.in
+ply==3.11
+    # via pyomo
+properscoring==0.1
+    # via timely-beliefs
+pscript==0.7.7
+    # via -r requirements/app.in
+psutil==5.9.5
+    # via openturns
+psycopg2-binary==2.9.6
+    # via
+    #   -r requirements/app.in
+    #   timely-beliefs
+py-moneyed==3.0
+    # via -r requirements/app.in
+pydantic==1.10.10
+    # via
+    #   -r requirements/app.in
+    #   inflect
+pyluach==2.2.0
+    # via workalendar
+pymeeus==0.5.12
+    # via convertdate
+pyomo==6.6.1
+    # via -r requirements/app.in
+pyparsing==3.1.0
+    # via matplotlib
+pyrsistent==0.19.3
+    # via jsonschema
+python-dateutil==2.8.2
+    # via
+    #   arrow
+    #   matplotlib
+    #   pandas
+    #   timetomodel
+    #   workalendar
+python-dotenv==1.0.0
+    # via -r requirements/app.in
+pytz==2023.3
+    # via
+    #   -r requirements/app.in
+    #   pandas
+    #   timely-beliefs
+    #   timetomodel
+pyyaml==6.0.1
+    # via -r requirements/app.in
+redis==4.6.0
+    # via
+    #   -r requirements/app.in
+    #   rq
+    #   rq-dashboard
+requests==2.31.0
+    # via
+    #   requests-file
+    #   tldextract
+requests-file==1.5.1
+    # via tldextract
+rq==1.15.1
+    # via
+    #   -r requirements/app.in
+    #   rq-dashboard
+rq-dashboard==0.6.1
+    # via -r requirements/app.in
+scikit-base==0.5.0
+    # via sktime
+scikit-learn==1.2.2
+    # via
+    #   sktime
+    #   timetomodel
+scipy==1.10.1
+    # via
+    #   properscoring
+    #   scikit-learn
+    #   sktime
+    #   statsmodels
+    #   timely-beliefs
+    #   timetomodel
+sentry-sdk[flask]==1.26.0
+    # via -r requirements/app.in
+six==1.16.0
+    # via
+    #   flask-marshmallow
+    #   isodate
+    #   patsy
+    #   python-dateutil
+    #   requests-file
+sktime==0.20.0
+    # via timely-beliefs
+sqlalchemy==1.4.48
+    # via
+    #   -r requirements/app.in
+    #   alembic
+    #   flask-sqlalchemy
+    #   marshmallow-sqlalchemy
+    #   timely-beliefs
+    #   timetomodel
+statsmodels==0.14.0
+    # via timetomodel
+tabulate==0.9.0
+    # via -r requirements/app.in
+threadpoolctl==3.1.0
+    # via scikit-learn
+timely-beliefs[forecast]==1.21.0
+    # via -r requirements/app.in
+timetomodel==0.7.3
+    # via -r requirements/app.in
+tldextract==3.4.4
+    # via -r requirements/app.in
+toolz==0.12.0
+    # via altair
+typing-extensions==4.7.0
+    # via
+    #   alembic
+    #   py-moneyed
+    #   pydantic
+tzdata==2023.3
+    # via pandas
+uniplot==0.10.0
+    # via -r requirements/app.in
+urllib3==2.0.3
+    # via
+    #   requests
+    #   sentry-sdk
+webargs==8.2.0
+    # via -r requirements/app.in
+werkzeug==2.1.0
+    # via
+    #   -r requirements/app.in
+    #   flask
+    #   flask-login
+workalendar==17.0.0
+    # via -r requirements/app.in
+wrapt==1.15.0
+    # via deprecated
+wtforms==3.0.1
+    # via
+    #   flask-security-too
+    #   flask-wtf
+xlrd==2.0.1
+    # via -r requirements/app.in
+zipp==3.15.0
+    # via importlib-metadata
+
+# The following packages are considered to be unsafe in a requirements file:
+# setuptools
diff --git a/requirements/3.11/dev.txt b/requirements/3.11/dev.txt
new file mode 100644
index 000000000..2befb9f67
--- /dev/null
+++ b/requirements/3.11/dev.txt
@@ -0,0 +1,76 @@
+#
+# This file is autogenerated by pip-compile with Python 3.11
+# by the following command:
+#
+#    pip-compile --output-file=requirements/3.11/dev.txt requirements/dev.in
+#
+black==22.3.0
+    # via -r requirements/dev.in
+cfgv==3.3.1
+    # via pre-commit
+click==8.1.3
+    # via
+    #   -c requirements/app.txt
+    #   -c requirements/test.txt
+    #   black
+distlib==0.3.6
+    # via virtualenv
+filelock==3.12.2
+    # via
+    #   -c requirements/app.txt
+    #   virtualenv
+flake8==4.0.1
+    # via -r requirements/dev.in
+flake8-blind-except==0.2.1
+    # via -r requirements/dev.in
+identify==2.5.24
+    # via pre-commit
+mccabe==0.6.1
+    # via flake8
+mypy==1.4.1
+    # via -r requirements/dev.in
+mypy-extensions==1.0.0
+    # via
+    #   black
+    #   mypy
+nodeenv==1.8.0
+    # via pre-commit
+packaging==23.1
+    # via
+    #   -c requirements/app.txt
+    #   -c requirements/test.txt
+    #   setuptools-scm
+pathspec==0.11.1
+    # via black
+platformdirs==3.8.0
+    # via
+    #   black
+    #   virtualenv
+pre-commit==3.3.3
+    # via -r requirements/dev.in
+pycodestyle==2.8.0
+    # via flake8
+pyflakes==2.4.0
+    # via flake8
+pyinstrument==4.5.0
+    # via -r requirements/dev.in
+pytest-runner==6.0.0
+    # via -r requirements/dev.in
+pyyaml==6.0.1
+    # via
+    #   -c requirements/app.txt
+    #   pre-commit
+setuptools-scm==7.1.0
+    # via -r requirements/dev.in
+typing-extensions==4.7.0
+    # via
+    #   -c requirements/app.txt
+    #   mypy
+    #   setuptools-scm
+virtualenv==20.23.1
+    # via pre-commit
+watchdog==3.0.0
+    # via -r requirements/dev.in
+
+# The following packages are considered to be unsafe in a requirements file:
+# setuptools
diff --git a/requirements/3.11/docs.txt b/requirements/3.11/docs.txt
new file mode 100644
index 000000000..0536ff4e5
--- /dev/null
+++ b/requirements/3.11/docs.txt
@@ -0,0 +1,94 @@
+#
+# This file is autogenerated by pip-compile with Python 3.11
+# by the following command:
+#
+#    pip-compile --output-file=requirements/3.11/docs.txt requirements/docs.in
+#
+alabaster==0.7.13
+    # via sphinx
+babel==2.12.1
+    # via
+    #   -c requirements/app.txt
+    #   sphinx
+certifi==2023.5.7
+    # via
+    #   -c requirements/app.txt
+    #   requests
+charset-normalizer==3.1.0
+    # via
+    #   -c requirements/app.txt
+    #   requests
+docutils==0.18.1
+    # via
+    #   sphinx
+    #   sphinx-rtd-theme
+    #   sphinx-tabs
+idna==3.4
+    # via
+    #   -c requirements/app.txt
+    #   requests
+imagesize==1.4.1
+    # via sphinx
+jinja2==3.1.2
+    # via
+    #   -c requirements/app.txt
+    #   sphinx
+markupsafe==2.1.3
+    # via
+    #   -c requirements/app.txt
+    #   jinja2
+packaging==23.1
+    # via
+    #   -c requirements/app.txt
+    #   sphinx
+pygments==2.15.1
+    # via
+    #   sphinx
+    #   sphinx-tabs
+requests==2.31.0
+    # via
+    #   -c requirements/app.txt
+    #   sphinx
+six==1.16.0
+    # via
+    #   -c requirements/app.txt
+    #   sphinxcontrib-httpdomain
+snowballstemmer==2.2.0
+    # via sphinx
+sphinx==6.2.1
+    # via
+    #   -r requirements/docs.in
+    #   sphinx-copybutton
+    #   sphinx-fontawesome
+    #   sphinx-rtd-theme
+    #   sphinx-tabs
+    #   sphinxcontrib-httpdomain
+    #   sphinxcontrib-jquery
+sphinx-copybutton==0.5.2
+    # via -r requirements/docs.in
+sphinx-fontawesome==0.0.6
+    # via -r requirements/docs.in
+sphinx-rtd-theme==1.2.2
+    # via -r requirements/docs.in
+sphinx-tabs==3.4.1
+    # via -r requirements/docs.in
+sphinxcontrib-applehelp==1.0.4
+    # via sphinx
+sphinxcontrib-devhelp==1.0.2
+    # via sphinx
+sphinxcontrib-htmlhelp==2.0.1
+    # via sphinx
+sphinxcontrib-httpdomain==1.8.1
+    # via -r requirements/docs.in
+sphinxcontrib-jquery==4.1
+    # via sphinx-rtd-theme
+sphinxcontrib-jsmath==1.0.1
+    # via sphinx
+sphinxcontrib-qthelp==1.0.3
+    # via sphinx
+sphinxcontrib-serializinghtml==1.1.5
+    # via sphinx
+urllib3==2.0.3
+    # via
+    #   -c requirements/app.txt
+    #   requests
diff --git a/requirements/3.8/app.txt b/requirements/3.8/app.txt
new file mode 100644
index 000000000..31e114a73
--- /dev/null
+++ b/requirements/3.8/app.txt
@@ -0,0 +1,371 @@
+#
+# This file is autogenerated by pip-compile with Python 3.8
+# by the following command:
+#
+#    pip-compile --output-file=requirements/3.8/app.txt requirements/app.in
+#
+alembic==1.11.1
+    # via flask-migrate
+altair==5.0.1
+    # via -r requirements/app.in
+arrow==1.2.3
+    # via rq-dashboard
+async-timeout==4.0.2
+    # via redis
+attrs==23.1.0
+    # via jsonschema
+babel==2.12.1
+    # via py-moneyed
+backports-zoneinfo==0.2.1
+    # via workalendar
+bcrypt==4.0.1
+    # via -r requirements/app.in
+blinker==1.6.2
+    # via
+    #   flask-mail
+    #   flask-principal
+    #   flask-security-too
+    #   sentry-sdk
+certifi==2023.5.7
+    # via
+    #   requests
+    #   sentry-sdk
+charset-normalizer==3.1.0
+    # via requests
+click==8.1.3
+    # via
+    #   -r requirements/app.in
+    #   click-default-group
+    #   flask
+    #   rq
+click-default-group==1.2.2
+    # via -r requirements/app.in
+colour==0.1.5
+    # via -r requirements/app.in
+contourpy==1.1.0
+    # via matplotlib
+convertdate==2.4.0
+    # via workalendar
+cycler==0.11.0
+    # via matplotlib
+deprecated==1.2.14
+    # via sktime
+dill==0.3.6
+    # via openturns
+dnspython==2.3.0
+    # via email-validator
+email-validator==2.0.0.post2
+    # via
+    #   -r requirements/app.in
+    #   flask-security-too
+filelock==3.12.2
+    # via tldextract
+flask==2.1.2
+    # via
+    #   -r requirements/app.in
+    #   flask-classful
+    #   flask-cors
+    #   flask-json
+    #   flask-login
+    #   flask-mail
+    #   flask-marshmallow
+    #   flask-migrate
+    #   flask-principal
+    #   flask-security-too
+    #   flask-sqlalchemy
+    #   flask-sslify
+    #   flask-wtf
+    #   rq-dashboard
+    #   sentry-sdk
+flask-classful @ git+https://github.com/superseed/flask-classful@werkzeug-2.2
+    # via -r requirements/app.in
+flask-cors==4.0.0
+    # via -r requirements/app.in
+flask-json==0.3.5
+    # via -r requirements/app.in
+flask-login==0.6.1
+    # via
+    #   -r requirements/app.in
+    #   flask-security-too
+flask-mail==0.9.1
+    # via -r requirements/app.in
+flask-marshmallow==0.14.0
+    # via -r requirements/app.in
+flask-migrate==4.0.4
+    # via -r requirements/app.in
+flask-principal==0.4.0
+    # via flask-security-too
+flask-security-too==5.1.2
+    # via -r requirements/app.in
+flask-sqlalchemy==2.5.1
+    # via
+    #   -r requirements/app.in
+    #   flask-migrate
+flask-sslify==0.1.5
+    # via -r requirements/app.in
+flask-wtf==1.1.1
+    # via
+    #   -r requirements/app.in
+    #   flask-security-too
+fonttools==4.40.0
+    # via matplotlib
+greenlet==2.0.2
+    # via sqlalchemy
+humanize==4.7.0
+    # via -r requirements/app.in
+idna==3.4
+    # via
+    #   email-validator
+    #   requests
+    #   tldextract
+importlib-metadata==6.7.0
+    # via
+    #   -r requirements/app.in
+    #   alembic
+    #   flask
+    #   timely-beliefs
+importlib-resources==6.0.0
+    # via
+    #   alembic
+    #   jsonschema
+    #   matplotlib
+inflect==6.0.2
+    # via -r requirements/app.in
+inflection==0.5.1
+    # via -r requirements/app.in
+iso8601==2.0.0
+    # via -r requirements/app.in
+isodate==0.6.1
+    # via
+    #   -r requirements/app.in
+    #   timely-beliefs
+itsdangerous==2.1.2
+    # via
+    #   flask
+    #   flask-security-too
+    #   flask-wtf
+jinja2==3.1.2
+    # via
+    #   altair
+    #   flask
+joblib==1.3.1
+    # via scikit-learn
+jsonschema==4.17.3
+    # via altair
+kiwisolver==1.4.4
+    # via matplotlib
+lunardate==0.2.0
+    # via workalendar
+mako==1.2.4
+    # via alembic
+markupsafe==2.1.3
+    # via
+    #   jinja2
+    #   mako
+    #   sentry-sdk
+    #   wtforms
+marshmallow==3.19.0
+    # via
+    #   -r requirements/app.in
+    #   flask-marshmallow
+    #   marshmallow-polyfield
+    #   marshmallow-sqlalchemy
+    #   webargs
+marshmallow-polyfield==5.11
+    # via -r requirements/app.in
+marshmallow-sqlalchemy==0.29.0
+    # via -r requirements/app.in
+matplotlib==3.7.1
+    # via timetomodel
+numpy==1.23.5
+    # via
+    #   -r requirements/app.in
+    #   altair
+    #   contourpy
+    #   matplotlib
+    #   pandas
+    #   patsy
+    #   properscoring
+    #   scikit-learn
+    #   scipy
+    #   sktime
+    #   statsmodels
+    #   timely-beliefs
+    #   timetomodel
+    #   uniplot
+openturns==1.21
+    # via timely-beliefs
+packaging==23.1
+    # via
+    #   marshmallow
+    #   marshmallow-sqlalchemy
+    #   matplotlib
+    #   sktime
+    #   statsmodels
+    #   webargs
+pandas==2.0.3
+    # via
+    #   -r requirements/app.in
+    #   altair
+    #   sktime
+    #   statsmodels
+    #   timely-beliefs
+    #   timetomodel
+passlib==1.7.4
+    # via flask-security-too
+patsy==0.5.3
+    # via statsmodels
+pillow==9.5.0
+    # via matplotlib
+pint==0.21.1
+    # via -r requirements/app.in
+pkgutil-resolve-name==1.3.10
+    # via jsonschema
+ply==3.11
+    # via pyomo
+properscoring==0.1
+    # via timely-beliefs
+pscript==0.7.7
+    # via -r requirements/app.in
+psutil==5.9.5
+    # via openturns
+psycopg2-binary==2.9.6
+    # via
+    #   -r requirements/app.in
+    #   timely-beliefs
+py-moneyed==3.0
+    # via -r requirements/app.in
+pydantic==1.10.10
+    # via
+    #   -r requirements/app.in
+    #   inflect
+pyluach==2.2.0
+    # via workalendar
+pymeeus==0.5.12
+    # via convertdate
+pyomo==6.6.1
+    # via -r requirements/app.in
+pyparsing==3.1.0
+    # via matplotlib
+pyrsistent==0.19.3
+    # via jsonschema
+python-dateutil==2.8.2
+    # via
+    #   arrow
+    #   matplotlib
+    #   pandas
+    #   timetomodel
+    #   workalendar
+python-dotenv==1.0.0
+    # via -r requirements/app.in
+pytz==2023.3
+    # via
+    #   -r requirements/app.in
+    #   babel
+    #   pandas
+    #   timely-beliefs
+    #   timetomodel
+pyyaml==6.0.1
+    # via -r requirements/app.in
+redis==4.6.0
+    # via
+    #   -r requirements/app.in
+    #   rq
+    #   rq-dashboard
+requests==2.31.0
+    # via
+    #   requests-file
+    #   tldextract
+requests-file==1.5.1
+    # via tldextract
+rq==1.15.1
+    # via
+    #   -r requirements/app.in
+    #   rq-dashboard
+rq-dashboard==0.6.1
+    # via -r requirements/app.in
+scikit-base==0.5.0
+    # via sktime
+scikit-learn==1.2.2
+    # via
+    #   sktime
+    #   timetomodel
+scipy==1.10.1
+    # via
+    #   properscoring
+    #   scikit-learn
+    #   sktime
+    #   statsmodels
+    #   timely-beliefs
+    #   timetomodel
+sentry-sdk[flask]==1.26.0
+    # via -r requirements/app.in
+six==1.16.0
+    # via
+    #   flask-marshmallow
+    #   isodate
+    #   patsy
+    #   python-dateutil
+    #   requests-file
+sktime==0.20.0
+    # via timely-beliefs
+sqlalchemy==1.4.48
+    # via
+    #   -r requirements/app.in
+    #   alembic
+    #   flask-sqlalchemy
+    #   marshmallow-sqlalchemy
+    #   timely-beliefs
+    #   timetomodel
+statsmodels==0.14.0
+    # via timetomodel
+tabulate==0.9.0
+    # via -r requirements/app.in
+threadpoolctl==3.1.0
+    # via scikit-learn
+timely-beliefs[forecast]==1.21.0
+    # via -r requirements/app.in
+timetomodel==0.7.3
+    # via -r requirements/app.in
+tldextract==3.4.4
+    # via -r requirements/app.in
+toolz==0.12.0
+    # via altair
+typing-extensions==4.7.0
+    # via
+    #   alembic
+    #   altair
+    #   py-moneyed
+    #   pydantic
+tzdata==2023.3
+    # via pandas
+uniplot==0.10.0
+    # via -r requirements/app.in
+urllib3==2.0.3
+    # via
+    #   requests
+    #   sentry-sdk
+webargs==8.2.0
+    # via -r requirements/app.in
+werkzeug==2.1.0
+    # via
+    #   -r requirements/app.in
+    #   flask
+    #   flask-login
+workalendar==17.0.0
+    # via -r requirements/app.in
+wrapt==1.15.0
+    # via deprecated
+wtforms==3.0.1
+    # via
+    #   flask-security-too
+    #   flask-wtf
+xlrd==2.0.1
+    # via -r requirements/app.in
+zipp==3.15.0
+    # via
+    #   importlib-metadata
+    #   importlib-resources
+
+# The following packages are considered to be unsafe in a requirements file:
+# setuptools
diff --git a/requirements/3.8/dev.txt b/requirements/3.8/dev.txt
new file mode 100644
index 000000000..16cd06098
--- /dev/null
+++ b/requirements/3.8/dev.txt
@@ -0,0 +1,83 @@
+#
+# This file is autogenerated by pip-compile with Python 3.8
+# by the following command:
+#
+#    pip-compile --output-file=requirements/3.8/dev.txt requirements/dev.in
+#
+black==22.3.0
+    # via -r requirements/dev.in
+cfgv==3.3.1
+    # via pre-commit
+click==8.1.3
+    # via
+    #   -c requirements/app.txt
+    #   -c requirements/test.txt
+    #   black
+distlib==0.3.6
+    # via virtualenv
+filelock==3.12.2
+    # via
+    #   -c requirements/app.txt
+    #   virtualenv
+flake8==4.0.1
+    # via -r requirements/dev.in
+flake8-blind-except==0.2.1
+    # via -r requirements/dev.in
+identify==2.5.24
+    # via pre-commit
+mccabe==0.6.1
+    # via flake8
+mypy==1.4.1
+    # via -r requirements/dev.in
+mypy-extensions==1.0.0
+    # via
+    #   black
+    #   mypy
+nodeenv==1.8.0
+    # via pre-commit
+packaging==23.1
+    # via
+    #   -c requirements/app.txt
+    #   -c requirements/test.txt
+    #   setuptools-scm
+pathspec==0.11.1
+    # via black
+platformdirs==3.8.0
+    # via
+    #   black
+    #   virtualenv
+pre-commit==3.3.3
+    # via -r requirements/dev.in
+pycodestyle==2.8.0
+    # via flake8
+pyflakes==2.4.0
+    # via flake8
+pyinstrument==4.5.0
+    # via -r requirements/dev.in
+pytest-runner==6.0.0
+    # via -r requirements/dev.in
+pyyaml==6.0.1
+    # via
+    #   -c requirements/app.txt
+    #   pre-commit
+setuptools-scm==7.1.0
+    # via -r requirements/dev.in
+tomli==2.0.1
+    # via
+    #   -c requirements/test.txt
+    #   black
+    #   mypy
+    #   setuptools-scm
+typing-extensions==4.7.0
+    # via
+    #   -c requirements/app.txt
+    #   black
+    #   mypy
+    #   setuptools-scm
+virtualenv==20.23.1
+    # via pre-commit
+watchdog==3.0.0
+    # via -r requirements/dev.in
+
+# The following packages are considered to be unsafe in a requirements file:
+# setuptools
diff --git a/requirements/3.8/docs.txt b/requirements/3.8/docs.txt
new file mode 100644
index 000000000..1b6b64506
--- /dev/null
+++ b/requirements/3.8/docs.txt
@@ -0,0 +1,106 @@
+#
+# This file is autogenerated by pip-compile with Python 3.8
+# by the following command:
+#
+#    pip-compile --output-file=requirements/3.8/docs.txt requirements/docs.in
+#
+alabaster==0.7.13
+    # via sphinx
+babel==2.12.1
+    # via
+    #   -c requirements/app.txt
+    #   sphinx
+certifi==2023.5.7
+    # via
+    #   -c requirements/app.txt
+    #   requests
+charset-normalizer==3.1.0
+    # via
+    #   -c requirements/app.txt
+    #   requests
+docutils==0.18.1
+    # via
+    #   sphinx
+    #   sphinx-rtd-theme
+    #   sphinx-tabs
+idna==3.4
+    # via
+    #   -c requirements/app.txt
+    #   requests
+imagesize==1.4.1
+    # via sphinx
+importlib-metadata==6.7.0
+    # via
+    #   -c requirements/app.txt
+    #   sphinx
+jinja2==3.1.2
+    # via
+    #   -c requirements/app.txt
+    #   sphinx
+markupsafe==2.1.3
+    # via
+    #   -c requirements/app.txt
+    #   jinja2
+packaging==23.1
+    # via
+    #   -c requirements/app.txt
+    #   sphinx
+pygments==2.15.1
+    # via
+    #   sphinx
+    #   sphinx-tabs
+pytz==2023.3
+    # via
+    #   -c requirements/app.txt
+    #   babel
+requests==2.31.0
+    # via
+    #   -c requirements/app.txt
+    #   sphinx
+six==1.16.0
+    # via
+    #   -c requirements/app.txt
+    #   sphinxcontrib-httpdomain
+snowballstemmer==2.2.0
+    # via sphinx
+sphinx==6.2.1
+    # via
+    #   -r requirements/docs.in
+    #   sphinx-copybutton
+    #   sphinx-fontawesome
+    #   sphinx-rtd-theme
+    #   sphinx-tabs
+    #   sphinxcontrib-httpdomain
+    #   sphinxcontrib-jquery
+sphinx-copybutton==0.5.2
+    # via -r requirements/docs.in
+sphinx-fontawesome==0.0.6
+    # via -r requirements/docs.in
+sphinx-rtd-theme==1.2.2
+    # via -r requirements/docs.in
+sphinx-tabs==3.4.1
+    # via -r requirements/docs.in
+sphinxcontrib-applehelp==1.0.4
+    # via sphinx
+sphinxcontrib-devhelp==1.0.2
+    # via sphinx
+sphinxcontrib-htmlhelp==2.0.1
+    # via sphinx
+sphinxcontrib-httpdomain==1.8.1
+    # via -r requirements/docs.in
+sphinxcontrib-jquery==4.1
+    # via sphinx-rtd-theme
+sphinxcontrib-jsmath==1.0.1
+    # via sphinx
+sphinxcontrib-qthelp==1.0.3
+    # via sphinx
+sphinxcontrib-serializinghtml==1.1.5
+    # via sphinx
+urllib3==2.0.3
+    # via
+    #   -c requirements/app.txt
+    #   requests
+zipp==3.15.0
+    # via
+    #   -c requirements/app.txt
+    #   importlib-metadata
diff --git a/requirements/3.9/app.txt b/requirements/3.9/app.txt
new file mode 100644
index 000000000..1b4d79a38
--- /dev/null
+++ b/requirements/3.9/app.txt
@@ -0,0 +1,362 @@
+#
+# This file is autogenerated by pip-compile with Python 3.9
+# by the following command:
+#
+#    pip-compile --output-file=requirements/3.9/app.txt requirements/app.in
+#
+alembic==1.11.1
+    # via flask-migrate
+altair==5.0.1
+    # via -r requirements/app.in
+arrow==1.2.3
+    # via rq-dashboard
+async-timeout==4.0.2
+    # via redis
+attrs==23.1.0
+    # via jsonschema
+babel==2.12.1
+    # via py-moneyed
+bcrypt==4.0.1
+    # via -r requirements/app.in
+blinker==1.6.2
+    # via
+    #   flask-mail
+    #   flask-principal
+    #   flask-security-too
+    #   sentry-sdk
+certifi==2023.5.7
+    # via
+    #   requests
+    #   sentry-sdk
+charset-normalizer==3.1.0
+    # via requests
+click==8.1.3
+    # via
+    #   -r requirements/app.in
+    #   click-default-group
+    #   flask
+    #   rq
+click-default-group==1.2.2
+    # via -r requirements/app.in
+colour==0.1.5
+    # via -r requirements/app.in
+contourpy==1.1.0
+    # via matplotlib
+convertdate==2.4.0
+    # via workalendar
+cycler==0.11.0
+    # via matplotlib
+deprecated==1.2.14
+    # via sktime
+dill==0.3.6
+    # via openturns
+dnspython==2.3.0
+    # via email-validator
+email-validator==2.0.0.post2
+    # via
+    #   -r requirements/app.in
+    #   flask-security-too
+filelock==3.12.2
+    # via tldextract
+flask==2.1.2
+    # via
+    #   -r requirements/app.in
+    #   flask-classful
+    #   flask-cors
+    #   flask-json
+    #   flask-login
+    #   flask-mail
+    #   flask-marshmallow
+    #   flask-migrate
+    #   flask-principal
+    #   flask-security-too
+    #   flask-sqlalchemy
+    #   flask-sslify
+    #   flask-wtf
+    #   rq-dashboard
+    #   sentry-sdk
+flask-classful @ git+https://github.com/superseed/flask-classful@werkzeug-2.2
+    # via -r requirements/app.in
+flask-cors==4.0.0
+    # via -r requirements/app.in
+flask-json==0.3.5
+    # via -r requirements/app.in
+flask-login==0.6.1
+    # via
+    #   -r requirements/app.in
+    #   flask-security-too
+flask-mail==0.9.1
+    # via -r requirements/app.in
+flask-marshmallow==0.14.0
+    # via -r requirements/app.in
+flask-migrate==4.0.4
+    # via -r requirements/app.in
+flask-principal==0.4.0
+    # via flask-security-too
+flask-security-too==5.1.2
+    # via -r requirements/app.in
+flask-sqlalchemy==2.5.1
+    # via
+    #   -r requirements/app.in
+    #   flask-migrate
+flask-sslify==0.1.5
+    # via -r requirements/app.in
+flask-wtf==1.1.1
+    # via
+    #   -r requirements/app.in
+    #   flask-security-too
+fonttools==4.40.0
+    # via matplotlib
+greenlet==2.0.2
+    # via sqlalchemy
+humanize==4.7.0
+    # via -r requirements/app.in
+idna==3.4
+    # via
+    #   email-validator
+    #   requests
+    #   tldextract
+importlib-metadata==6.7.0
+    # via
+    #   -r requirements/app.in
+    #   flask
+    #   timely-beliefs
+importlib-resources==6.0.0
+    # via matplotlib
+inflect==6.0.2
+    # via -r requirements/app.in
+inflection==0.5.1
+    # via -r requirements/app.in
+iso8601==2.0.0
+    # via -r requirements/app.in
+isodate==0.6.1
+    # via
+    #   -r requirements/app.in
+    #   timely-beliefs
+itsdangerous==2.1.2
+    # via
+    #   flask
+    #   flask-security-too
+    #   flask-wtf
+jinja2==3.1.2
+    # via
+    #   altair
+    #   flask
+joblib==1.3.1
+    # via scikit-learn
+jsonschema==4.17.3
+    # via altair
+kiwisolver==1.4.4
+    # via matplotlib
+lunardate==0.2.0
+    # via workalendar
+mako==1.2.4
+    # via alembic
+markupsafe==2.1.3
+    # via
+    #   jinja2
+    #   mako
+    #   sentry-sdk
+    #   wtforms
+marshmallow==3.19.0
+    # via
+    #   -r requirements/app.in
+    #   flask-marshmallow
+    #   marshmallow-polyfield
+    #   marshmallow-sqlalchemy
+    #   webargs
+marshmallow-polyfield==5.11
+    # via -r requirements/app.in
+marshmallow-sqlalchemy==0.29.0
+    # via -r requirements/app.in
+matplotlib==3.7.1
+    # via timetomodel
+numpy==1.23.5
+    # via
+    #   -r requirements/app.in
+    #   altair
+    #   contourpy
+    #   matplotlib
+    #   pandas
+    #   patsy
+    #   properscoring
+    #   scikit-learn
+    #   scipy
+    #   sktime
+    #   statsmodels
+    #   timely-beliefs
+    #   timetomodel
+    #   uniplot
+openturns==1.21
+    # via timely-beliefs
+packaging==23.1
+    # via
+    #   marshmallow
+    #   marshmallow-sqlalchemy
+    #   matplotlib
+    #   sktime
+    #   statsmodels
+    #   webargs
+pandas==2.0.3
+    # via
+    #   -r requirements/app.in
+    #   altair
+    #   sktime
+    #   statsmodels
+    #   timely-beliefs
+    #   timetomodel
+passlib==1.7.4
+    # via flask-security-too
+patsy==0.5.3
+    # via statsmodels
+pillow==9.5.0
+    # via matplotlib
+pint==0.21.1
+    # via -r requirements/app.in
+ply==3.11
+    # via pyomo
+properscoring==0.1
+    # via timely-beliefs
+pscript==0.7.7
+    # via -r requirements/app.in
+psutil==5.9.5
+    # via openturns
+psycopg2-binary==2.9.6
+    # via
+    #   -r requirements/app.in
+    #   timely-beliefs
+py-moneyed==3.0
+    # via -r requirements/app.in
+pydantic==1.10.10
+    # via
+    #   -r requirements/app.in
+    #   inflect
+pyluach==2.2.0
+    # via workalendar
+pymeeus==0.5.12
+    # via convertdate
+pyomo==6.6.1
+    # via -r requirements/app.in
+pyparsing==3.1.0
+    # via matplotlib
+pyrsistent==0.19.3
+    # via jsonschema
+python-dateutil==2.8.2
+    # via
+    #   arrow
+    #   matplotlib
+    #   pandas
+    #   timetomodel
+    #   workalendar
+python-dotenv==1.0.0
+    # via -r requirements/app.in
+pytz==2023.3
+    # via
+    #   -r requirements/app.in
+    #   pandas
+    #   timely-beliefs
+    #   timetomodel
+pyyaml==6.0.1
+    # via -r requirements/app.in
+redis==4.6.0
+    # via
+    #   -r requirements/app.in
+    #   rq
+    #   rq-dashboard
+requests==2.31.0
+    # via
+    #   requests-file
+    #   tldextract
+requests-file==1.5.1
+    # via tldextract
+rq==1.15.1
+    # via
+    #   -r requirements/app.in
+    #   rq-dashboard
+rq-dashboard==0.6.1
+    # via -r requirements/app.in
+scikit-base==0.5.0
+    # via sktime
+scikit-learn==1.2.2
+    # via
+    #   sktime
+    #   timetomodel
+scipy==1.10.1
+    # via
+    #   properscoring
+    #   scikit-learn
+    #   sktime
+    #   statsmodels
+    #   timely-beliefs
+    #   timetomodel
+sentry-sdk[flask]==1.26.0
+    # via -r requirements/app.in
+six==1.16.0
+    # via
+    #   flask-marshmallow
+    #   isodate
+    #   patsy
+    #   python-dateutil
+    #   requests-file
+sktime==0.20.0
+    # via timely-beliefs
+sqlalchemy==1.4.48
+    # via
+    #   -r requirements/app.in
+    #   alembic
+    #   flask-sqlalchemy
+    #   marshmallow-sqlalchemy
+    #   timely-beliefs
+    #   timetomodel
+statsmodels==0.14.0
+    # via timetomodel
+tabulate==0.9.0
+    # via -r requirements/app.in
+threadpoolctl==3.1.0
+    # via scikit-learn
+timely-beliefs[forecast]==1.21.0
+    # via -r requirements/app.in
+timetomodel==0.7.3
+    # via -r requirements/app.in
+tldextract==3.4.4
+    # via -r requirements/app.in
+toolz==0.12.0
+    # via altair
+typing-extensions==4.7.0
+    # via
+    #   alembic
+    #   altair
+    #   py-moneyed
+    #   pydantic
+tzdata==2023.3
+    # via pandas
+uniplot==0.10.0
+    # via -r requirements/app.in
+urllib3==2.0.3
+    # via
+    #   requests
+    #   sentry-sdk
+webargs==8.2.0
+    # via -r requirements/app.in
+werkzeug==2.1.0
+    # via
+    #   -r requirements/app.in
+    #   flask
+    #   flask-login
+workalendar==17.0.0
+    # via -r requirements/app.in
+wrapt==1.15.0
+    # via deprecated
+wtforms==3.0.1
+    # via
+    #   flask-security-too
+    #   flask-wtf
+xlrd==2.0.1
+    # via -r requirements/app.in
+zipp==3.15.0
+    # via
+    #   importlib-metadata
+    #   importlib-resources
+
+# The following packages are considered to be unsafe in a requirements file:
+# setuptools
diff --git a/requirements/3.9/dev.txt b/requirements/3.9/dev.txt
new file mode 100644
index 000000000..40dfe7227
--- /dev/null
+++ b/requirements/3.9/dev.txt
@@ -0,0 +1,83 @@
+#
+# This file is autogenerated by pip-compile with Python 3.9
+# by the following command:
+#
+#    pip-compile --output-file=requirements/3.9/dev.txt requirements/dev.in
+#
+black==22.3.0
+    # via -r requirements/dev.in
+cfgv==3.3.1
+    # via pre-commit
+click==8.1.3
+    # via
+    #   -c requirements/app.txt
+    #   -c requirements/test.txt
+    #   black
+distlib==0.3.6
+    # via virtualenv
+filelock==3.12.2
+    # via
+    #   -c requirements/app.txt
+    #   virtualenv
+flake8==4.0.1
+    # via -r requirements/dev.in
+flake8-blind-except==0.2.1
+    # via -r requirements/dev.in
+identify==2.5.24
+    # via pre-commit
+mccabe==0.6.1
+    # via flake8
+mypy==1.4.1
+    # via -r requirements/dev.in
+mypy-extensions==1.0.0
+    # via
+    #   black
+    #   mypy
+nodeenv==1.8.0
+    # via pre-commit
+packaging==23.1
+    # via
+    #   -c requirements/app.txt
+    #   -c requirements/test.txt
+    #   setuptools-scm
+pathspec==0.11.1
+    # via black
+platformdirs==3.8.0
+    # via
+    #   black
+    #   virtualenv
+pre-commit==3.3.3
+    # via -r requirements/dev.in
+pycodestyle==2.8.0
+    # via flake8
+pyflakes==2.4.0
+    # via flake8
+pyinstrument==4.5.0
+    # via -r requirements/dev.in
+pytest-runner==6.0.0
+    # via -r requirements/dev.in
+pyyaml==6.0.1
+    # via
+    #   -c requirements/app.txt
+    #   pre-commit
+setuptools-scm==7.1.0
+    # via -r requirements/dev.in
+tomli==2.0.1
+    # via
+    #   -c requirements/test.txt
+    #   black
+    #   mypy
+    #   setuptools-scm
+typing-extensions==4.7.0
+    # via
+    #   -c requirements/app.txt
+    #   black
+    #   mypy
+    #   setuptools-scm
+virtualenv==20.23.1
+    # via pre-commit
+watchdog==3.0.0
+    # via -r requirements/dev.in
+
+# The following packages are considered to be unsafe in a requirements file:
+# setuptools
diff --git a/requirements/3.9/docs.txt b/requirements/3.9/docs.txt
new file mode 100644
index 000000000..a144fea98
--- /dev/null
+++ b/requirements/3.9/docs.txt
@@ -0,0 +1,102 @@
+#
+# This file is autogenerated by pip-compile with Python 3.9
+# by the following command:
+#
+#    pip-compile --output-file=requirements/3.9/docs.txt requirements/docs.in
+#
+alabaster==0.7.13
+    # via sphinx
+babel==2.12.1
+    # via
+    #   -c requirements/app.txt
+    #   sphinx
+certifi==2023.5.7
+    # via
+    #   -c requirements/app.txt
+    #   requests
+charset-normalizer==3.1.0
+    # via
+    #   -c requirements/app.txt
+    #   requests
+docutils==0.18.1
+    # via
+    #   sphinx
+    #   sphinx-rtd-theme
+    #   sphinx-tabs
+idna==3.4
+    # via
+    #   -c requirements/app.txt
+    #   requests
+imagesize==1.4.1
+    # via sphinx
+importlib-metadata==6.7.0
+    # via
+    #   -c requirements/app.txt
+    #   sphinx
+jinja2==3.1.2
+    # via
+    #   -c requirements/app.txt
+    #   sphinx
+markupsafe==2.1.3
+    # via
+    #   -c requirements/app.txt
+    #   jinja2
+packaging==23.1
+    # via
+    #   -c requirements/app.txt
+    #   sphinx
+pygments==2.15.1
+    # via
+    #   sphinx
+    #   sphinx-tabs
+requests==2.31.0
+    # via
+    #   -c requirements/app.txt
+    #   sphinx
+six==1.16.0
+    # via
+    #   -c requirements/app.txt
+    #   sphinxcontrib-httpdomain
+snowballstemmer==2.2.0
+    # via sphinx
+sphinx==6.2.1
+    # via
+    #   -r requirements/docs.in
+    #   sphinx-copybutton
+    #   sphinx-fontawesome
+    #   sphinx-rtd-theme
+    #   sphinx-tabs
+    #   sphinxcontrib-httpdomain
+    #   sphinxcontrib-jquery
+sphinx-copybutton==0.5.2
+    # via -r requirements/docs.in
+sphinx-fontawesome==0.0.6
+    # via -r requirements/docs.in
+sphinx-rtd-theme==1.2.2
+    # via -r requirements/docs.in
+sphinx-tabs==3.4.1
+    # via -r requirements/docs.in
+sphinxcontrib-applehelp==1.0.4
+    # via sphinx
+sphinxcontrib-devhelp==1.0.2
+    # via sphinx
+sphinxcontrib-htmlhelp==2.0.1
+    # via sphinx
+sphinxcontrib-httpdomain==1.8.1
+    # via -r requirements/docs.in
+sphinxcontrib-jquery==4.1
+    # via sphinx-rtd-theme
+sphinxcontrib-jsmath==1.0.1
+    # via sphinx
+sphinxcontrib-qthelp==1.0.3
+    # via sphinx
+sphinxcontrib-serializinghtml==1.1.5
+    # via sphinx
+urllib3==2.0.3
+    # via
+    #   -c requirements/app.txt
+    #   requests
+zipp==3.15.0
+    # via
+    #   -c requirements/app.txt
+    #   importlib-metadata
diff --git a/requirements/app.in b/requirements/app.in
index 0babfb74b..ea7e81a39 100644
--- a/requirements/app.in
+++ b/requirements/app.in
@@ -68,4 +68,4 @@ Flask-SQLAlchemy>=2.4.3,<3
 # flask should be after all the flask plugins, because setup might find they ARE flask
 # <2.3: https://github.com/Parallels/rq-dashboard/issues/417 and https://github.com/FlexMeasures/flexmeasures/issues/754 and flask-login 0.6.1 not compatible
 flask>=1.0, <=2.1.2
-werkzeug<=2.1
\ No newline at end of file
+werkzeug<=2.1


Test patch:
diff --git a/.github/workflows/lint-and-test.yml b/.github/workflows/lint-and-test.yml
index 10a6c7cc4..d04c9a6d4 100644
--- a/.github/workflows/lint-and-test.yml
+++ b/.github/workflows/lint-and-test.yml
@@ -38,13 +38,13 @@ jobs:
         run: |
           git fetch --prune --unshallow
           git fetch --depth=1 origin +refs/tags/*:refs/tags/*
-      - name: "Caching dependencies (txt)"
+      - name: "Caching for dependencies (.txt) - restore existing or ensure new cache will be made"
         uses: actions/cache@v2
         id: cache
         with:
           path: ${{ env.pythonLocation }}
           # manually disable a cache if needed by (re)setting CACHE_DATE
-          key: ${{ runner.os }}-pip-${{ env.pythonLocation }}-${{ SECRETS.CACHE_DATE }}-${{ hashFiles('**/requirements/**.txt') }}
+          key: ${{ runner.os }}-pip-${{ env.pythonLocation }}-${{ SECRETS.CACHE_DATE }}-${{ hashFiles('**/requirements/**/*.txt') }}
           restore-keys: |
             ${{ runner.os }}-pip-
       - run: |
diff --git a/flexmeasures/api/v3_0/tests/conftest.py b/flexmeasures/api/v3_0/tests/conftest.py
index 59b32532a..683874769 100644
--- a/flexmeasures/api/v3_0/tests/conftest.py
+++ b/flexmeasures/api/v3_0/tests/conftest.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
 from datetime import timedelta
 
 import pandas as pd
diff --git a/requirements/test.txt b/requirements/3.10/test.txt
similarity index 96%
rename from requirements/test.txt
rename to requirements/3.10/test.txt
index 06d3ea4f2..d78bf5b67 100644
--- a/requirements/test.txt
+++ b/requirements/3.10/test.txt
@@ -2,7 +2,7 @@
 # This file is autogenerated by pip-compile with Python 3.10
 # by the following command:
 #
-#    pip-compile --output-file=requirements/test.txt requirements/test.in
+#    pip-compile --output-file=requirements/3.10/test.txt requirements/test.in
 #
 async-timeout==4.0.2
     # via
diff --git a/requirements/3.11/test.txt b/requirements/3.11/test.txt
new file mode 100644
index 000000000..251e9f3e3
--- /dev/null
+++ b/requirements/3.11/test.txt
@@ -0,0 +1,95 @@
+#
+# This file is autogenerated by pip-compile with Python 3.11
+# by the following command:
+#
+#    pip-compile --output-file=requirements/3.11/test.txt requirements/test.in
+#
+certifi==2023.5.7
+    # via
+    #   -c requirements/app.txt
+    #   requests
+charset-normalizer==3.1.0
+    # via
+    #   -c requirements/app.txt
+    #   requests
+click==8.1.3
+    # via
+    #   -c requirements/app.txt
+    #   flask
+coverage[toml]==7.2.7
+    # via pytest-cov
+fakeredis==2.15.0
+    # via -r requirements/test.in
+flask==2.1.2
+    # via
+    #   -c requirements/app.txt
+    #   pytest-flask
+highspy==1.5.3
+    # via -r requirements/test.in
+idna==3.4
+    # via
+    #   -c requirements/app.txt
+    #   requests
+iniconfig==2.0.0
+    # via pytest
+itsdangerous==2.1.2
+    # via
+    #   -c requirements/app.txt
+    #   flask
+jinja2==3.1.2
+    # via
+    #   -c requirements/app.txt
+    #   flask
+lupa==2.0
+    # via -r requirements/test.in
+markupsafe==2.1.3
+    # via
+    #   -c requirements/app.txt
+    #   jinja2
+packaging==23.1
+    # via
+    #   -c requirements/app.txt
+    #   pytest
+    #   pytest-sugar
+pluggy==1.2.0
+    # via pytest
+pytest==7.4.0
+    # via
+    #   -r requirements/test.in
+    #   pytest-cov
+    #   pytest-flask
+    #   pytest-sugar
+pytest-cov==4.1.0
+    # via -r requirements/test.in
+pytest-flask==1.2.0
+    # via -r requirements/test.in
+pytest-sugar==0.9.7
+    # via -r requirements/test.in
+redis==4.6.0
+    # via
+    #   -c requirements/app.txt
+    #   fakeredis
+requests==2.31.0
+    # via
+    #   -c requirements/app.txt
+    #   -r requirements/test.in
+    #   requests-mock
+requests-mock==1.11.0
+    # via -r requirements/test.in
+six==1.16.0
+    # via
+    #   -c requirements/app.txt
+    #   requests-mock
+sortedcontainers==2.4.0
+    # via fakeredis
+termcolor==2.3.0
+    # via pytest-sugar
+urllib3==2.0.3
+    # via
+    #   -c requirements/app.txt
+    #   requests
+werkzeug==2.1.0
+    # via
+    #   -c requirements/app.txt
+    #   flask
+    #   pytest-flask
diff --git a/requirements/3.8/test.txt b/requirements/3.8/test.txt
new file mode 100644
index 000000000..3e37981cf
--- /dev/null
+++ b/requirements/3.8/test.txt
@@ -0,0 +1,113 @@
+#
+# This file is autogenerated by pip-compile with Python 3.8
+# by the following command:
+#
+#    pip-compile --output-file=requirements/3.8/test.txt requirements/test.in
+#
+async-timeout==4.0.2
+    # via
+    #   -c requirements/app.txt
+    #   redis
+certifi==2023.5.7
+    # via
+    #   -c requirements/app.txt
+    #   requests
+charset-normalizer==3.1.0
+    # via
+    #   -c requirements/app.txt
+    #   requests
+click==8.1.3
+    # via
+    #   -c requirements/app.txt
+    #   flask
+coverage[toml]==7.2.7
+    # via pytest-cov
+exceptiongroup==1.1.1
+    # via pytest
+fakeredis==2.15.0
+    # via -r requirements/test.in
+flask==2.1.2
+    # via
+    #   -c requirements/app.txt
+    #   pytest-flask
+highspy==1.5.3
+    # via -r requirements/test.in
+idna==3.4
+    # via
+    #   -c requirements/app.txt
+    #   requests
+importlib-metadata==6.7.0
+    # via
+    #   -c requirements/app.txt
+    #   flask
+iniconfig==2.0.0
+    # via pytest
+itsdangerous==2.1.2
+    # via
+    #   -c requirements/app.txt
+    #   flask
+jinja2==3.1.2
+    # via
+    #   -c requirements/app.txt
+    #   flask
+lupa==2.0
+    # via -r requirements/test.in
+markupsafe==2.1.3
+    # via
+    #   -c requirements/app.txt
+    #   jinja2
+packaging==23.1
+    # via
+    #   -c requirements/app.txt
+    #   pytest
+    #   pytest-sugar
+pluggy==1.2.0
+    # via pytest
+pytest==7.4.0
+    # via
+    #   -r requirements/test.in
+    #   pytest-cov
+    #   pytest-flask
+    #   pytest-sugar
+pytest-cov==4.1.0
+    # via -r requirements/test.in
+pytest-flask==1.2.0
+    # via -r requirements/test.in
+pytest-sugar==0.9.7
+    # via -r requirements/test.in
+redis==4.6.0
+    # via
+    #   -c requirements/app.txt
+    #   fakeredis
+requests==2.31.0
+    # via
+    #   -c requirements/app.txt
+    #   -r requirements/test.in
+    #   requests-mock
+requests-mock==1.11.0
+    # via -r requirements/test.in
+six==1.16.0
+    # via
+    #   -c requirements/app.txt
+    #   requests-mock
+sortedcontainers==2.4.0
+    # via fakeredis
+termcolor==2.3.0
+    # via pytest-sugar
+tomli==2.0.1
+    # via
+    #   coverage
+    #   pytest
+urllib3==2.0.3
+    # via
+    #   -c requirements/app.txt
+    #   requests
+werkzeug==2.1.0
+    # via
+    #   -c requirements/app.txt
+    #   flask
+    #   pytest-flask
+zipp==3.15.0
+    # via
+    #   -c requirements/app.txt
+    #   importlib-metadata
diff --git a/requirements/3.9/test.txt b/requirements/3.9/test.txt
new file mode 100644
index 000000000..64f814c36
--- /dev/null
+++ b/requirements/3.9/test.txt
@@ -0,0 +1,113 @@
+#
+# This file is autogenerated by pip-compile with Python 3.9
+# by the following command:
+#
+#    pip-compile --output-file=requirements/3.9/test.txt requirements/test.in
+#
+async-timeout==4.0.2
+    # via
+    #   -c requirements/app.txt
+    #   redis
+certifi==2023.5.7
+    # via
+    #   -c requirements/app.txt
+    #   requests
+charset-normalizer==3.1.0
+    # via
+    #   -c requirements/app.txt
+    #   requests
+click==8.1.3
+    # via
+    #   -c requirements/app.txt
+    #   flask
+coverage[toml]==7.2.7
+    # via pytest-cov
+exceptiongroup==1.1.1
+    # via pytest
+fakeredis==2.15.0
+    # via -r requirements/test.in
+flask==2.1.2
+    # via
+    #   -c requirements/app.txt
+    #   pytest-flask
+highspy==1.5.3
+    # via -r requirements/test.in
+idna==3.4
+    # via
+    #   -c requirements/app.txt
+    #   requests
+importlib-metadata==6.7.0
+    # via
+    #   -c requirements/app.txt
+    #   flask
+iniconfig==2.0.0
+    # via pytest
+itsdangerous==2.1.2
+    # via
+    #   -c requirements/app.txt
+    #   flask
+jinja2==3.1.2
+    # via
+    #   -c requirements/app.txt
+    #   flask
+lupa==2.0
+    # via -r requirements/test.in
+markupsafe==2.1.3
+    # via
+    #   -c requirements/app.txt
+    #   jinja2
+packaging==23.1
+    # via
+    #   -c requirements/app.txt
+    #   pytest
+    #   pytest-sugar
+pluggy==1.2.0
+    # via pytest
+pytest==7.4.0
+    # via
+    #   -r requirements/test.in
+    #   pytest-cov
+    #   pytest-flask
+    #   pytest-sugar
+pytest-cov==4.1.0
+    # via -r requirements/test.in
+pytest-flask==1.2.0
+    # via -r requirements/test.in
+pytest-sugar==0.9.7
+    # via -r requirements/test.in
+redis==4.6.0
+    # via
+    #   -c requirements/app.txt
+    #   fakeredis
+requests==2.31.0
+    # via
+    #   -c requirements/app.txt
+    #   -r requirements/test.in
+    #   requests-mock
+requests-mock==1.11.0
+    # via -r requirements/test.in
+six==1.16.0
+    # via
+    #   -c requirements/app.txt
+    #   requests-mock
+sortedcontainers==2.4.0
+    # via fakeredis
+termcolor==2.3.0
+    # via pytest-sugar
+tomli==2.0.1
+    # via
+    #   coverage
+    #   pytest
+urllib3==2.0.3
+    # via
+    #   -c requirements/app.txt
+    #   requests
+werkzeug==2.1.0
+    # via
+    #   -c requirements/app.txt
+    #   flask
+    #   pytest-flask
+zipp==3.15.0
+    # via
+    #   -c requirements/app.txt
+    #   importlib-metadata
diff --git a/requirements/test.in b/requirements/test.in
index d657cd3f1..a04503a46 100644
--- a/requirements/test.in
+++ b/requirements/test.in
@@ -1,5 +1,5 @@
 -c app.txt
-# we are not using app.in, because we want to test exactly what is being packaged
+# we are not using app.in and test.in, because we want to develop against exactly what is being packaged and tested
 
 pytest
 pytest-flask