diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs
index 03b9eafb7d1ac8151ee55f2849eb2ed7265b237c..a02c88ca8b9c9746ebc81c2e5c8f059abe18b3a9 100644
--- a/.git-blame-ignore-revs
+++ b/.git-blame-ignore-revs
@@ -8,3 +8,6 @@
 
 #  fix/pre-commit-whitespace - Whitespace fixes
 e7dfadaf4e189ef0e0f67798e8984695111257e3
+
+#  fix/requirements-into-setuppy - Reformat setup.py without content changes
+31b402966a4a741d08cb1e89db96f252fbb41c54
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index c55f4dfec1e2e1ecb2c8f127edebecbc7550b049..a1a86985769caa5e56f22521d980db6765b42a33 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -2,27 +2,45 @@ stages:
   - check
   - test
 
+.before_script: &before_script
+  before_script:
+    - eval $(ssh-agent -s)
+    - echo "$SSH_PRIVATE_KEY_GITLAB" | tr -d '\r' | ssh-add -
+    #  Our self-hosted runners have persistent home directories, so here we store
+    #  the known_hosts file in the temporary project dir and tell git to use a ssh
+    #  command that uses this hosts file during operations
+    - export GIT_SSH_COMMAND="ssh -o UserKnownHostsFile=$CI_PROJECT_DIR/.ssh/known_hosts"
+    - mkdir -p $CI_PROJECT_DIR/.ssh
+    - chmod 700 $CI_PROJECT_DIR/.ssh
+    - ssh-keyscan -p 10022 git.xfel.eu > $CI_PROJECT_DIR/.ssh/known_hosts
+    - ls  $CI_PROJECT_DIR/.ssh
+    - echo $GIT_SSH_COMMAND
+    - python3 -m venv .venv
+    - source .venv/bin/activate
+    - python3 -m pip install --upgrade pip
+
 checks:
   stage: check
   only: [merge_requests]
   allow_failure: true
+  <<: *before_script
   script:
     - export PATH=/home/gitlab-runner/.local/bin:$PATH
-    # We'd like to run the pre-commit hooks only on files that are being
-    # modified by this merge request, however
-    # `CI_MERGE_REQUEST_TARGET_BRANCH_SHA` is a 'premium' feature according to
-    # GitLab... so this is a workaround for extracting the hash
+    #  We'd like to run the pre-commit hooks only on files that are being
+    #  modified by this merge request, however
+    #  `CI_MERGE_REQUEST_TARGET_BRANCH_SHA` is a 'premium' feature according to
+    #  GitLab... so this is a workaround for extracting the hash
     - export CI_MERGE_REQUEST_TARGET_BRANCH_SHA=$(git ls-remote origin $CI_MERGE_REQUEST_TARGET_BRANCH_NAME | cut -d$'\t' -f1)
-    - export FILES=$(git diff $CI_COMMIT_SHA  $CI_MERGE_REQUEST_TARGET_BRANCH_SHA --name-only | tr '\n' ' ')
-    - python3 -m pip install --user -r requirements.txt
-    - echo "Running pre-commit on diff from  $CI_COMMIT_SHA to $CI_MERGE_REQUEST_TARGET_BRANCH_SHA ($CI_MERGE_REQUEST_TARGET_BRANCH_NAME)"
+    - export FILES=$(git diff $CI_COMMIT_SHA $CI_MERGE_REQUEST_TARGET_BRANCH_SHA --name-only | tr '\n' ' ')
+    - python3 -m pip install ".[test,dev]"
+    - echo "Running pre-commit on diff from $CI_COMMIT_SHA to $CI_MERGE_REQUEST_TARGET_BRANCH_SHA ($CI_MERGE_REQUEST_TARGET_BRANCH_NAME)"
     #  Pass list of modified files to pre-commit so that it only checks them
     - echo $FILES | xargs pre-commit run --color=always --files
 
 pytest:
   stage: test
   only: [merge_requests]
+  <<: *before_script
   script:
-    - python3 -m pip install --user -r requirements.txt
-    - python3 -m pip install --user 'pytest>=5.4.0' pytest-asyncio testpath
-    - pytest -vv tests/test_*
+    - python3 -m pip install ".[test]"
+    - python3 -m pytest --cov=cal_tools --cov=xfel_calibrate  --ignore=tests/legacy
diff --git a/README.rst b/README.rst
index 4a9cbb379f4f12463b5170ce5061f9a1a420bd8b..167a2ece7c628d8b783f7bd04065851ea7b26bf6 100644
--- a/README.rst
+++ b/README.rst
@@ -24,9 +24,7 @@ Installation using python virtual environment - recommended
 3. ``python3 -m venv .venv`` - create the virtual environment
 4. ``source .venv/bin/activate`` - activate the virtual environment
 5. ``python3 -m pip install --upgrade pip`` - upgrade version of pip
-6. ``python3 -m pip install -r requirements.txt`` - install dependencies
-7. ``python3 -m pip install .`` - install the pycalibration package (add ``-e`` flag for editable development installation)
-8. ``pip install "git+ssh://git@git.xfel.eu:10022/karaboDevices/pyDetLib.git#egg=XFELDetectorAnalysis&subdirectory=lib"``
+6. ``python3 -m pip install .`` - install the pycalibration package (add ``-e`` flag for editable development installation)
 
 Copy/paste script:
 
@@ -38,19 +36,22 @@ Copy/paste script:
   python3 -m venv .venv
   source .venv/bin/activate
   python3 -m pip install --upgrade pip
-  python3 -m pip install -r requirements.txt
-  python3 -m pip install .  # `-e` flag for editable install
-  python3 -m pip install "git+ssh://git@git.xfel.eu:10022/karaboDevices/pyDetLib.git#egg=XFELDetectorAnalysis&subdirectory=lib/"
+  python3 -m pip install .  # `-e` flag for editable install, e.g. `pip install -e .`
 
 
 Installation into user home directory
 =====================================
 
+This is not recommended as `pycalibration` has pinned dependencies for
+stability, if you install it directly into you users home environment then it
+will down/upgrade your local packages, which may cause major issues and may
+**break your local environment**, it is highly recommended to use the venv
+installation method instead.
+
 1. ``git clone ssh://git@git.xfel.eu:10022/detectors/pycalibration.git && cd pycalibration`` - clone the offline calibration package from EuXFEL GitLab
 2. ``module load anaconda/3`` - load the anaconda/3 environment. If installing into other python environments, this step can be skipped
-3. ``pip install -r requirements.txt`` - install all requirements of this tool chain in your home directory
-4. ``pip install .`` - install the pycalibration package (add ``-e`` flag for editable development installation)
-5. ``export PATH=$HOME/.local/bin:$PATH`` - make sure that the home directory is in the PATH environment variable
+3. ``pip install .`` - install the pycalibration package (add ``-e`` flag for editable development installation)
+4. ``export PATH=$HOME/.local/bin:$PATH`` - make sure that the home directory is in the PATH environment variable
 
 Copy/paste script:
 
@@ -59,8 +60,7 @@ Copy/paste script:
   git clone ssh://git@git.xfel.eu:10022/detectors/pycalibration.git
   cd pycalibration
   module load anaconda/3
-  pip install -r requirements.txt --user
-  pip install .  # `-e` flag for editable install, e.g. `pip install -e .`
+  pip install --user .  # `-e` flag for editable install, e.g. `pip install -e .`
   export PATH=$HOME/.local/bin:$PATH
 
 
@@ -89,6 +89,11 @@ Development guidelines can be found on the GitLab Wiki page here: https://git.xf
 Basics
 ======
 
+If you are installing the package for development purposes then you should
+install the optional dependencies as well. Follow the instructions as above, but
+instead of ``pip install .`` use ``pip install ".[test,dev]"`` to install both
+the extras.
+
 The installation instructions above assume that you have set up SSH keys for use
 with GitLab to allow for passwordless clones from GitLab, this way it's possible
 to run ``pip install git+ssh...`` commands and install packages directly from
@@ -191,8 +196,8 @@ yourself first:
 
   salloc -p exfel/upex -t 01:00:00
 
-where `-p` gives the partition to use: exfel or upex and `-t` the duration the
-node should be allocated. Then `ssh` onto that node.
+where `-p` gives the partition to use: exfel **or** upex and `-t` the duration
+the node should be allocated. Then ``ssh`` onto that node.
 
 Then activate your environment as described above (or just continue if you are
 not using a venv).
@@ -216,19 +221,79 @@ Finally run the script:
 
 .. code::
 
-    python3 calibrate.py --input /gpfs/exfel/exp/SPB/201701/p002012/raw/r0100 \
-      --output ../../test_out --mem-cells 30 --detector AGIPD --sequences 0,1
+  python3 calibrate.py --input /gpfs/exfel/exp/SPB/201701/p002012/raw/r0100 \
+    --output ../../test_out --mem-cells 30 --detector AGIPD --sequences 0,1
 
-Here `--input` should point to a directory of `RAW` files for the detector you
-are calibrating. They will be output into the folder specified by `--output`,
-which will have the run number or the last folder in the hierarchy of the input
-appended. Additionally, you need to specify the number of `--mem-cells` used for
-the run, as well as the `--detector`. Finally, you can optionally specify to
-only process certain `--sequences` of files, matching the sequence numbers of
-the `RAW` input. These should be given as a comma-separated list.
+Here ``--input`` should point to a directory of ``RAW`` files for the detector
+you are calibrating. They will be output into the folder specified by
+``--output``, which will have the run number or the last folder in the hierarchy
+of the input appended. Additionally, you need to specify the number of
+``--mem-cells`` used for the run, as well as the ``--detector``. Finally, you
+can optionally specify to only process certain ``--sequences`` of files,
+matching the sequence numbers of the `RAW` input. These should be given as a
+comma-separated list.
 
-Finally, there is a `--no-relgain` option, which disables relative gain
+Finally, there is a ``--no-relgain`` option, which disables relative gain
 correction. This can be useful while we still further characterize the detectors
 to provide accurate relative gain correction constants.
 
 You'll get a series of plots in the output directory as well.
+
+Appendix
+********
+
+Important information that doesn't really fit in as part of the readme.
+
+TODO: Place this into the docs? Also, improve docs (out of scope for PR !437)
+
+GitLab Access for ``xcaltst`` and ``xcal``
+==========================================
+
+To make it easier to work with and deploy software via ``xcaltst``/``xcal``, we
+have created an xcal account for gitlab with the following details:
+
+- Full Name: ReadOnly Gitlab Calibration External
+- User ID: 423
+- Username: ``xcalgitlab``
+- Password: ask Robert Rosca
+
+This account is intended to be used as a read only account which can be given
+access to certain repos to make it easier to clone them when using our
+functional accounts on Maxwell.
+
+The ``xcaltst`` account has an ed25519 keypair under ``~/.ssh/gitlab/``, the
+public key has been added to the ``xcalgitlab``'s approved SSH keys.
+
+Additionally this block has been added to ``~/.ssh/config``:
+
+.. code::
+
+  # Special flags for gitlab over SSH
+  Host git.xfel.eu
+      User git
+      Port 10022
+      ForwardX11 no
+      IdentityFile ~/.ssh/gitlab/id_ed25519
+
+Now any repository that ``xcalgitlab`` has read access to, e.g. if it is added as
+a reporter, can be cloned on Maxwell without having to enter a password.
+
+For example, ``xcalgitlab`` is a reporter on the pycalibration
+https://git.xfel.eu/gitlab/detectors/pycalibration repository, so now
+``xcalgitlab`` can do passwordless clones with SSH:
+
+.. code::
+
+  [xcaltst@max-exfl017 tmp]$ git clone ssh://git@git.xfel.eu:10022/detectors/pycalibration.git
+  Cloning into 'pycalibration'...
+  remote: Enumerating objects: 9414, done.
+  remote: Counting objects: 100% (9414/9414), done.
+  remote: Compressing objects: 100% (2858/2858), done.
+  remote: Total 9414 (delta 6510), reused 9408 (delta 6504)
+  Receiving objects: 100% (9414/9414), 611.81 MiB | 54.87 MiB/s, done.
+  Resolving deltas: 100% (6510/6510), done.
+
+References:
+
+- Redmine ticket: https://in.xfel.eu/redmine/issues/83954
+- Original issue: https://git.xfel.eu/gitlab/detectors/calibration_workshop/issues/121
diff --git a/requirements.txt b/requirements.txt
deleted file mode 100644
index d66d4c5d2dbe422bc19b65d443ea9ebe450681b5..0000000000000000000000000000000000000000
--- a/requirements.txt
+++ /dev/null
@@ -1,44 +0,0 @@
-git+file:///gpfs/exfel/sw/calsoft/git/cal_db_interactive@2.0.1
-git+file:///gpfs/exfel/sw/calsoft/git/nbparameterise@0.3
-git+file:///gpfs/exfel/sw/calsoft/git/pyDetLib@2.5.6-2.10.0#subdirectory=lib
-Cython == 0.29.21
-Jinja2 == 2.11.2
-astcheck == 0.2.5
-astsearch == 0.1.3
-dill == 0.3.0
-extra_data == 1.2.0
-extra_geom == 1.1.1
-fabio == 0.9.0
-gitpython == 3.1.0
-h5py == 2.10.0
-iminuit == 1.3.8
-ipykernel == 5.1.4
-ipyparallel == 6.2.4
-ipython == 7.12.0
-ipython_genutils == 0.2.0
-jupyter-core == 4.6.1
-jupyter_client == 6.1.7
-jupyter_console == 6.1.0
-karabo_data == 0.7.0
-lxml == 4.5.0
-metadata_client == 3.0.8
-nbclient == 0.5.1
-nbconvert == 5.6.1
-nbformat == 5.0.7
-notebook == 6.1.5
-numpy == 1.19.1
-pre-commit == 2.10.0
-prettytable == 0.7.2
-princess == 0.2
-pypandoc == 1.4
-python-dateutil == 2.8.1
-pyyaml == 5.3
-pyzmq == 19.0.0
-requests==2.22.0
-scikit-learn == 0.22.2.post1
-sharedmem == 0.3.8
-sphinx == 1.8.5
-tabulate == 0.8.6
-traitlets == 4.3.3
-unittest-xml-reporting == 3.0.2
-.
diff --git a/setup.py b/setup.py
index fc9ead08dfc7c9a90354c5bcebc689759ebd36ec..e12fac28af282b16625e6ad92c6bb7ddc1968697 100644
--- a/setup.py
+++ b/setup.py
@@ -1,46 +1,29 @@
-import sys
 from distutils.command.build import build
 from distutils.extension import Extension
-from subprocess import check_call, check_output
+from subprocess import check_output
 
 import numpy
 from Cython.Distutils import build_ext
 from setuptools import setup
-from setuptools.command.install import install
 
-extensions = [Extension("cal_tools.cython.agipdalgs",
-                        ['cal_tools/cython/agipdalgs.pyx'],
-                        include_dirs=[numpy.get_include()],
-                        extra_compile_args=['-fopenmp', '-march=native'],
-                        extra_link_args=['-fopenmp'], ),
-              ]
-
-class PostInstallCommand(install):
-    """Post-installation for installation mode."""
-    def run(self):
-        install.run(self)
-        # check if this is a karabo installation
-        python_path = sys.executable
-        if "karabo" in python_path:
-            print("Karabo installation detected, checking for PyDetLib installation")
-            try:
-                import XFELDetAna
-                print("...found!")
-                return
-            except:
-                "No PyDetLib installation found, attempting to install"
-                check_call("karabo -g https://in.xfel.eu/gitlab install pyDetLib master".split())
-        else:
-            print("Python environment seems to not be a Karabo environment. "+
-                  "Please install PyDetLib manually.")
+extensions = [
+    Extension(
+        "cal_tools.cython.agipdalgs",
+        ["cal_tools/cython/agipdalgs.pyx"],
+        include_dirs=[numpy.get_include()],
+        extra_compile_args=["-fopenmp", "-march=native"],
+        extra_link_args=["-fopenmp"],
+    ),
+]
 
 
 class PreInstallCommand(build):
     """Pre-installation for installation mode."""
+
     def run(self):
-        version = check_output(['git', 'describe', '--tag']).decode('utf8')
+        version = check_output(["git", "describe", "--tag"]).decode("utf8")
         version = version.replace("\n", "")
-        file = open('xfel_calibrate/VERSION.py', 'w')
+        file = open("xfel_calibrate/VERSION.py", "w")
         file.write('__version__="{}"'.format(version))
         file.close()
 
@@ -57,33 +40,115 @@ for ctypes in notebooks.values():
         data_files += nb.get("pre_notebooks", [])
 
 setup(
-    name='European XFEL Offline Calibration',
+    name="European XFEL Offline Calibration",
     version="1.0",
-    packages=['cal_tools', 'xfel_calibrate'],
-    package_dir={'cal_tools': 'cal_tools/cal_tools',
-                 'xfel_calibrate': 'xfel_calibrate',
-                 'xfel_calibrate.notebooks': 'xfel_calibrate/notebooks',
-                 },
+    author="Steffen Hauf",
+    author_email="steffen.hauf@xfel.eu",
+    maintainer="EuXFEL Calibration Team",
+    url="",
+    description="",
+    long_description="",
+    long_description_content_type="text/markdown",
+    # TODO: find licence, assuming this will be open sourced eventually
+    license="(c) European XFEL GmbH 2018",
+    packages=[
+        "cal_tools",
+        "xfel_calibrate",
+    ],  # TODO: use setuptools.find_packages(), need to resolve issue
+    package_dir={
+        "cal_tools": "cal_tools/cal_tools",
+        "xfel_calibrate": "xfel_calibrate",
+        "xfel_calibrate.notebooks": "xfel_calibrate/notebooks",
+    },
     package_data={
-        'xfel_calibrate': ['bin/*.sh'] + data_files + ['titlepage.tmpl',
-                                                       'xfel.pdf']
+        "xfel_calibrate": [
+            "bin/*.sh",
+            "titlepage.tmpl",
+            "xfel.pdf",
+        ]
+        + data_files
+    },
+    entry_points={
+        "console_scripts": [
+            "xfel-calibrate = xfel_calibrate.calibrate:run",
+        ],
     },
-
     cmdclass={
-        'build' : PreInstallCommand,
-        'install': PostInstallCommand,
-        'build_ext': build_ext
+        "build": PreInstallCommand,
+        "build_ext": build_ext,
     },
-    url='',
-    license='(c) European XFEL GmbH 2018',
-    author='Steffen Hauf',
-    author_email='steffen.hauf@xfel.eu',
-    description='',
-    entry_points = {
-              'console_scripts': [
-                  'xfel-calibrate = xfel_calibrate.calibrate:run',
-              ],
-          },
-    ext_modules=extensions
-
+    ext_modules=extensions,
+    install_requires=[
+        "iCalibrationDB @ git+ssh://git@git.xfel.eu:10022/detectors/cal_db_interactive.git@2.0.1", # noqa
+        "nbparameterise @ git+ssh://git@git.xfel.eu:10022/detectors/nbparameterise.git@0.3", # noqa
+        "XFELDetectorAnalysis @ git+ssh://git@git.xfel.eu:10022/karaboDevices/pyDetLib.git@2.5.6-2.10.0#subdirectory=lib", # noqa
+        "Cython==0.29.21",
+        "Jinja2==2.11.2",
+        "astcheck==0.2.5",
+        "astsearch==0.1.3",
+        "dill==0.3.0",
+        "extra_data==1.2.0",
+        "extra_geom==1.1.1",
+        "fabio==0.9.0",
+        "gitpython==3.1.0",
+        "h5py==2.10.0",
+        "iminuit==1.3.8",
+        "ipykernel==5.1.4",
+        "ipyparallel==6.2.4",
+        "ipython==7.12.0",
+        "ipython_genutils==0.2.0",
+        "jupyter-core==4.6.1",
+        "jupyter_client==6.1.7",
+        "jupyter_console==6.1.0",
+        "karabo_data==0.7.0",
+        "lxml==4.5.0",
+        "metadata_client==3.0.8",
+        "nbclient==0.5.1",
+        "nbconvert==5.6.1",
+        "nbformat==5.0.7",
+        "notebook==6.1.5",
+        "numpy==1.19.1",
+        "prettytable==0.7.2",
+        "princess==0.2",
+        "pypandoc==1.4",
+        "python-dateutil==2.8.1",
+        "pyyaml==5.3",
+        "pyzmq==19.0.0",
+        "requests==2.22.0",
+        "scikit-learn==0.22.2.post1",
+        "sharedmem==0.3.8",
+        "tabulate==0.8.6",
+        "traitlets==4.3.3",
+    ],
+    extras_require={
+        "docs": [
+            "nbsphinx",
+            "sphinx==1.8.5",
+        ],
+        "test": [
+            "coverage",
+            "nbval",
+            "pytest-asyncio",
+            "pytest-cov",
+            "pytest>=5.4.0",
+            "testpath",
+            "unittest-xml-reporting==3.0.2",
+        ],
+        "dev": [
+            "nbqa[toolchain]",
+            "pre-commit",
+        ],
+    },
+    python_requires=">=3.6",
+    classifiers=[
+        "Development Status :: 5 - Production/Stable",
+        "Environment :: Console",
+        "Intended Audience :: Developers",
+        "Intended Audience :: Science/Research",
+        # "License :: OSI Approved :: BSD License",  # TODO: put license here
+        "Operating System :: POSIX :: Linux",
+        "Programming Language :: Python :: 3",
+        "Topic :: Scientific/Engineering :: Information Analysis",
+        "Topic :: Scientific/Engineering :: Physics",
+    ],
 )