From dca3109b1cf29467ba051023e5fe28b01f002aa2 Mon Sep 17 00:00:00 2001 From: Iain-S <25081046+Iain-S@users.noreply.github.com> Date: Mon, 18 Sep 2023 13:09:38 +0100 Subject: [PATCH 01/11] Add docs/ dir --- .flake8 | 1 + .readthedocs.yaml | 34 +++++ docs/Makefile | 20 +++ docs/_static/.gitignore | 0 docs/_templates/.gitignore | 0 docs/conf.py | 49 +++++++ docs/index.rst | 18 +++ docs/make.bat | 35 +++++ docs/sometext.rst | 4 + poetry.lock | 294 ++++++++++++++++++++++++++++++++++++- pyproject.toml | 7 + 11 files changed, 461 insertions(+), 1 deletion(-) create mode 100644 .readthedocs.yaml create mode 100644 docs/Makefile create mode 100644 docs/_static/.gitignore create mode 100644 docs/_templates/.gitignore create mode 100644 docs/conf.py create mode 100644 docs/index.rst create mode 100644 docs/make.bat create mode 100644 docs/sometext.rst diff --git a/.flake8 b/.flake8 index 963dde0..d9a4763 100644 --- a/.flake8 +++ b/.flake8 @@ -2,6 +2,7 @@ max-line-length = 160 per-file-ignores = __init__.py:F401 + docs/conf.py:E402 rctab/routers/accounting/routes.py:E711,E712 rctab/routers/accounting/desired_states.py:E711,E712 rctab/routers/accounting/send_emails.py:E712,E203 diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..eb78644 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,34 @@ +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Set the OS, Python version and other tools you might need +build: + os: ubuntu-22.04 + tools: + python: "3.11" + # You can also specify other tool versions: + # nodejs: "19" + # rust: "1.64" + # golang: "1.19" + +# Build documentation in the "docs/" directory with Sphinx +sphinx: + configuration: docs/conf.py + +# Optionally build your docs in additional formats such as PDF and ePub +# formats: +# - pdf +# - epub + +# Optional but recommended, declare the Python requirements required +# to build your documentation +# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html +python: + install: + - method: pip + path: . + extra_requirements: + - docs diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..d4bb2cb --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/_static/.gitignore b/docs/_static/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/docs/_templates/.gitignore b/docs/_templates/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..0d93304 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,49 @@ +"""Configuration file for the Sphinx documentation builder.""" +from importlib import metadata +from unittest.mock import MagicMock + +import databases +import pydantic + +# pylint: disable=invalid-name + +# Patch settings base class to avoid having to set env vars + +pydantic.BaseSettings = MagicMock() # type: ignore +databases.Database = MagicMock() # type: ignore +# pylint: disable=wrong-import-position +import rctab + +# pylint: enable=wrong-import-position + +# General configuration + +project = "rctab-infrastructure" +author = "The Alan Turing Institute's Research Computing Team" +# pylint: disable=redefined-builtin +copyright = f"2023, {author}" +# pylint: enable=redefined-builtin + +version = metadata.version(rctab.__package__) +release = version + +templates_path = ["_templates"] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] + +# -- Options for HTML output ------------------------------------------------- + +html_theme = "alabaster" +html_static_path = ["_static"] + +# -- General configuration + +extensions = [ + "sphinx.ext.duration", + "sphinx.ext.doctest", + "sphinx.ext.autodoc", + "sphinx.ext.autosummary", +] + +# -- Options for HTML output + +html_theme = "sphinx_rtd_theme" diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..a1a71dd --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,18 @@ +Welcome to rctab-infrastructure's documentation! +================================================ + +.. automodule:: rctab.main + :members: + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + sometext + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..32bb245 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/sometext.rst b/docs/sometext.rst new file mode 100644 index 0000000..4d116f0 --- /dev/null +++ b/docs/sometext.rst @@ -0,0 +1,4 @@ +Some Text +--------- + +Lorem ipsum... diff --git a/poetry.lock b/poetry.lock index 798ab40..aad397e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,5 +1,16 @@ # This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. +[[package]] +name = "alabaster" +version = "0.7.13" +description = "A configurable sidebar-enabled Sphinx theme" +optional = true +python-versions = ">=3.6" +files = [ + {file = "alabaster-0.7.13-py3-none-any.whl", hash = "sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3"}, + {file = "alabaster-0.7.13.tar.gz", hash = "sha256:a27a4a084d5e690e16e01e03ad2b2e552c61a65469419b907243193de1a84ae2"}, +] + [[package]] name = "alembic" version = "1.11.3" @@ -235,6 +246,17 @@ cryptography = ">=2.5" msal = ">=1.20.0,<2.0.0" msal-extensions = ">=0.3.0,<2.0.0" +[[package]] +name = "babel" +version = "2.12.1" +description = "Internationalization utilities" +optional = true +python-versions = ">=3.7" +files = [ + {file = "Babel-2.12.1-py3-none-any.whl", hash = "sha256:b4246fb7677d3b98f501a39d43396d3cafdc8eadb045f4a31be01863f655c610"}, + {file = "Babel-2.12.1.tar.gz", hash = "sha256:cc2d99999cd01d44420ae725a21c9e3711b3aadc7976d6147f622d8581963455"}, +] + [[package]] name = "backcall" version = "0.2.0" @@ -778,6 +800,17 @@ idna = ["idna (>=2.1,<4.0)"] trio = ["trio (>=0.14,<0.23)"] wmi = ["wmi (>=1.5.1,<2.0.0)"] +[[package]] +name = "docutils" +version = "0.18.1" +description = "Docutils -- Python Documentation Utilities" +optional = true +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "docutils-0.18.1-py2.py3-none-any.whl", hash = "sha256:23010f129180089fbcd3bc08cfefccb3b890b0050e1ca00c867036e9d161b98c"}, + {file = "docutils-0.18.1.tar.gz", hash = "sha256:679987caf361a7539d76e584cbeddc311e3aee937877c87346f31debc63e9d06"}, +] + [[package]] name = "dparse" version = "0.6.3" @@ -1290,6 +1323,17 @@ files = [ {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, ] +[[package]] +name = "imagesize" +version = "1.4.1" +description = "Getting image size from png/jpeg/jpeg2000/gif file" +optional = true +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, + {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, +] + [[package]] name = "iniconfig" version = "2.0.0" @@ -1650,6 +1694,25 @@ files = [ {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, ] +[[package]] +name = "mdit-py-plugins" +version = "0.4.0" +description = "Collection of plugins for markdown-it-py" +optional = true +python-versions = ">=3.8" +files = [ + {file = "mdit_py_plugins-0.4.0-py3-none-any.whl", hash = "sha256:b51b3bb70691f57f974e257e367107857a93b36f322a9e6d44ca5bf28ec2def9"}, + {file = "mdit_py_plugins-0.4.0.tar.gz", hash = "sha256:d8ab27e9aed6c38aa716819fedfde15ca275715955f8a185a8e1cf90fb1d2c1b"}, +] + +[package.dependencies] +markdown-it-py = ">=1.0.0,<4.0.0" + +[package.extras] +code-style = ["pre-commit"] +rtd = ["myst-parser", "sphinx-book-theme"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] + [[package]] name = "mdurl" version = "0.1.2" @@ -1842,6 +1905,32 @@ files = [ {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, ] +[[package]] +name = "myst-parser" +version = "2.0.0" +description = "An extended [CommonMark](https://spec.commonmark.org/) compliant parser," +optional = true +python-versions = ">=3.8" +files = [ + {file = "myst_parser-2.0.0-py3-none-any.whl", hash = "sha256:7c36344ae39c8e740dad7fdabf5aa6fc4897a813083c6cc9990044eb93656b14"}, + {file = "myst_parser-2.0.0.tar.gz", hash = "sha256:ea929a67a6a0b1683cdbe19b8d2e724cd7643f8aa3e7bb18dd65beac3483bead"}, +] + +[package.dependencies] +docutils = ">=0.16,<0.21" +jinja2 = "*" +markdown-it-py = ">=3.0,<4.0" +mdit-py-plugins = ">=0.4,<1.0" +pyyaml = "*" +sphinx = ">=6,<8" + +[package.extras] +code-style = ["pre-commit (>=3.0,<4.0)"] +linkify = ["linkify-it-py (>=2.0,<3.0)"] +rtd = ["ipython", "pydata-sphinx-theme (==v0.13.0rc4)", "sphinx-autodoc2 (>=0.4.2,<0.5.0)", "sphinx-book-theme (==1.0.0rc2)", "sphinx-copybutton", "sphinx-design2", "sphinx-pyscript", "sphinx-tippy (>=0.3.1)", "sphinx-togglebutton", "sphinxext-opengraph (>=0.8.2,<0.9.0)", "sphinxext-rediraffe (>=0.2.7,<0.3.0)"] +testing = ["beautifulsoup4", "coverage[toml]", "pytest (>=7,<8)", "pytest-cov", "pytest-param-files (>=0.3.4,<0.4.0)", "pytest-regressions", "sphinx-pytest"] +testing-docutils = ["pygments", "pytest (>=7,<8)", "pytest-param-files (>=0.3.4,<0.4.0)"] + [[package]] name = "nest-asyncio" version = "1.5.7" @@ -2179,6 +2268,20 @@ files = [ dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] +[[package]] +name = "pockets" +version = "0.9.1" +description = "A collection of helpful Python tools!" +optional = true +python-versions = "*" +files = [ + {file = "pockets-0.9.1-py2.py3-none-any.whl", hash = "sha256:68597934193c08a08eb2bf6a1d85593f627c22f9b065cc727a4f03f669d96d86"}, + {file = "pockets-0.9.1.tar.gz", hash = "sha256:9320f1a3c6f7a9133fe3b571f283bcf3353cd70249025ae8d618e40e9f7e92b3"}, +] + +[package.dependencies] +six = ">=1.5.2" + [[package]] name = "portalocker" version = "2.7.0" @@ -3173,6 +3276,192 @@ files = [ {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, ] +[[package]] +name = "sphinx" +version = "7.2.6" +description = "Python documentation generator" +optional = true +python-versions = ">=3.9" +files = [ + {file = "sphinx-7.2.6-py3-none-any.whl", hash = "sha256:1e09160a40b956dc623c910118fa636da93bd3ca0b9876a7b3df90f07d691560"}, + {file = "sphinx-7.2.6.tar.gz", hash = "sha256:9a5160e1ea90688d5963ba09a2dcd8bdd526620edbb65c328728f1b2228d5ab5"}, +] + +[package.dependencies] +alabaster = ">=0.7,<0.8" +babel = ">=2.9" +colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} +docutils = ">=0.18.1,<0.21" +imagesize = ">=1.3" +Jinja2 = ">=3.0" +packaging = ">=21.0" +Pygments = ">=2.14" +requests = ">=2.25.0" +snowballstemmer = ">=2.0" +sphinxcontrib-applehelp = "*" +sphinxcontrib-devhelp = "*" +sphinxcontrib-htmlhelp = ">=2.0.0" +sphinxcontrib-jsmath = "*" +sphinxcontrib-qthelp = "*" +sphinxcontrib-serializinghtml = ">=1.1.9" + +[package.extras] +docs = ["sphinxcontrib-websupport"] +lint = ["docutils-stubs", "flake8 (>=3.5.0)", "flake8-simplify", "isort", "mypy (>=0.990)", "ruff", "sphinx-lint", "types-requests"] +test = ["cython (>=3.0)", "filelock", "html5lib", "pytest (>=4.6)", "setuptools (>=67.0)"] + +[[package]] +name = "sphinx-rtd-theme" +version = "1.3.0" +description = "Read the Docs theme for Sphinx" +optional = true +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +files = [ + {file = "sphinx_rtd_theme-1.3.0-py2.py3-none-any.whl", hash = "sha256:46ddef89cc2416a81ecfbeaceab1881948c014b1b6e4450b815311a89fb977b0"}, + {file = "sphinx_rtd_theme-1.3.0.tar.gz", hash = "sha256:590b030c7abb9cf038ec053b95e5380b5c70d61591eb0b552063fbe7c41f0931"}, +] + +[package.dependencies] +docutils = "<0.19" +sphinx = ">=1.6,<8" +sphinxcontrib-jquery = ">=4,<5" + +[package.extras] +dev = ["bump2version", "sphinxcontrib-httpdomain", "transifex-client", "wheel"] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "1.0.7" +description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" +optional = true +python-versions = ">=3.9" +files = [ + {file = "sphinxcontrib_applehelp-1.0.7-py3-none-any.whl", hash = "sha256:094c4d56209d1734e7d252f6e0b3ccc090bd52ee56807a5d9315b19c122ab15d"}, + {file = "sphinxcontrib_applehelp-1.0.7.tar.gz", hash = "sha256:39fdc8d762d33b01a7d8f026a3b7d71563ea3b72787d5f00ad8465bd9d6dfbfa"}, +] + +[package.dependencies] +Sphinx = ">=5" + +[package.extras] +lint = ["docutils-stubs", "flake8", "mypy"] +test = ["pytest"] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "1.0.5" +description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp documents" +optional = true +python-versions = ">=3.9" +files = [ + {file = "sphinxcontrib_devhelp-1.0.5-py3-none-any.whl", hash = "sha256:fe8009aed765188f08fcaadbb3ea0d90ce8ae2d76710b7e29ea7d047177dae2f"}, + {file = "sphinxcontrib_devhelp-1.0.5.tar.gz", hash = "sha256:63b41e0d38207ca40ebbeabcf4d8e51f76c03e78cd61abe118cf4435c73d4212"}, +] + +[package.dependencies] +Sphinx = ">=5" + +[package.extras] +lint = ["docutils-stubs", "flake8", "mypy"] +test = ["pytest"] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.0.4" +description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" +optional = true +python-versions = ">=3.9" +files = [ + {file = "sphinxcontrib_htmlhelp-2.0.4-py3-none-any.whl", hash = "sha256:8001661c077a73c29beaf4a79968d0726103c5605e27db92b9ebed8bab1359e9"}, + {file = "sphinxcontrib_htmlhelp-2.0.4.tar.gz", hash = "sha256:6c26a118a05b76000738429b724a0568dbde5b72391a688577da08f11891092a"}, +] + +[package.dependencies] +Sphinx = ">=5" + +[package.extras] +lint = ["docutils-stubs", "flake8", "mypy"] +test = ["html5lib", "pytest"] + +[[package]] +name = "sphinxcontrib-jquery" +version = "4.1" +description = "Extension to include jQuery on newer Sphinx releases" +optional = true +python-versions = ">=2.7" +files = [ + {file = "sphinxcontrib-jquery-4.1.tar.gz", hash = "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a"}, + {file = "sphinxcontrib_jquery-4.1-py2.py3-none-any.whl", hash = "sha256:f936030d7d0147dd026a4f2b5a57343d233f1fc7b363f68b3d4f1cb0993878ae"}, +] + +[package.dependencies] +Sphinx = ">=1.8" + +[[package]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +description = "A sphinx extension which renders display math in HTML via JavaScript" +optional = true +python-versions = ">=3.5" +files = [ + {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, + {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, +] + +[package.extras] +test = ["flake8", "mypy", "pytest"] + +[[package]] +name = "sphinxcontrib-napoleon" +version = "0.7" +description = "Sphinx \"napoleon\" extension." +optional = true +python-versions = "*" +files = [ + {file = "sphinxcontrib-napoleon-0.7.tar.gz", hash = "sha256:407382beed396e9f2d7f3043fad6afda95719204a1e1a231ac865f40abcbfcf8"}, + {file = "sphinxcontrib_napoleon-0.7-py2.py3-none-any.whl", hash = "sha256:711e41a3974bdf110a484aec4c1a556799eb0b3f3b897521a018ad7e2db13fef"}, +] + +[package.dependencies] +pockets = ">=0.3" +six = ">=1.5.2" + +[[package]] +name = "sphinxcontrib-qthelp" +version = "1.0.6" +description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp documents" +optional = true +python-versions = ">=3.9" +files = [ + {file = "sphinxcontrib_qthelp-1.0.6-py3-none-any.whl", hash = "sha256:bf76886ee7470b934e363da7a954ea2825650013d367728588732c7350f49ea4"}, + {file = "sphinxcontrib_qthelp-1.0.6.tar.gz", hash = "sha256:62b9d1a186ab7f5ee3356d906f648cacb7a6bdb94d201ee7adf26db55092982d"}, +] + +[package.dependencies] +Sphinx = ">=5" + +[package.extras] +lint = ["docutils-stubs", "flake8", "mypy"] +test = ["pytest"] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "1.1.9" +description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)" +optional = true +python-versions = ">=3.9" +files = [ + {file = "sphinxcontrib_serializinghtml-1.1.9-py3-none-any.whl", hash = "sha256:9b36e503703ff04f20e9675771df105e58aa029cfcbc23b8ed716019b7416ae1"}, + {file = "sphinxcontrib_serializinghtml-1.1.9.tar.gz", hash = "sha256:0c64ff898339e1fac29abd2bf5f11078f3ec413cfe9c046d3120d7ca65530b54"}, +] + +[package.dependencies] +Sphinx = ">=5" + +[package.extras] +lint = ["docutils-stubs", "flake8", "mypy"] +test = ["pytest"] + [[package]] name = "sqlalchemy" version = "1.4.41" @@ -3707,7 +3996,10 @@ files = [ {file = "wrapt-1.15.0.tar.gz", hash = "sha256:d06730c6aed78cee4126234cf2d071e01b44b915e725a6cb439a879ec9754a3a"}, ] +[extras] +docs = ["myst-parser", "sphinx-rtd-theme", "sphinxcontrib-napoleon"] + [metadata] lock-version = "2.0" python-versions = ">=3.10 <3.12" -content-hash = "734b46c0dc41f2dae88e79c03b2c0a92d27e399af8010eb163656f1dcc260c8e" +content-hash = "5125da5d64a2d6172dc041e8da2dab86898946768e4d9f38d582d4eaa032b04a" diff --git a/pyproject.toml b/pyproject.toml index 6e5fb34..460ad9a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,10 @@ requests = "^2.25.1" sendgrid = "^6.9.1" sqlalchemy = "~1.4.32" uvicorn = { version = "^0.17.4", extras = ["standard"] } +sphinx = {version = "^7.2.6", optional = true} +sphinx-rtd-theme = {version = "^1.3.0", optional = true} +sphinxcontrib-napoleon = {version = "^0.7", optional = true} +myst-parser = {version = "^2.0.0", optional = true} [tool.poetry.group.dev.dependencies] black = "^22.12.0" @@ -51,6 +55,9 @@ sqlalchemy-stubs = "^0.3" pydocstyle = "^6.3.0" hypothesis = "^6.82.6" +[tool.poetry.extras] +docs = ["sphinx-rtd-theme", "sphinxcontrib-napoleon", "myst-parser", "sphinx-subprojecttoctree"] + [tool.isort] profile = "black" From 4c0d8c41acc284a6edddba91d46e902f850469d2 Mon Sep 17 00:00:00 2001 From: Iain-S <25081046+Iain-S@users.noreply.github.com> Date: Tue, 19 Sep 2023 10:58:45 +0100 Subject: [PATCH 02/11] Add theme and logo --- docs/RCTab-hex.png | Bin 0 -> 50653 bytes docs/conf.py | 17 +++++++++++------ docs/index.rst | 2 +- 3 files changed, 12 insertions(+), 7 deletions(-) create mode 100644 docs/RCTab-hex.png diff --git a/docs/RCTab-hex.png b/docs/RCTab-hex.png new file mode 100644 index 0000000000000000000000000000000000000000..025e0ea48221f47de3eff1b39534700f618ed960 GIT binary patch literal 50653 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

(yvGFMfF8jHGO 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

+kNU(OQYglY?V+O~><|GuhvlUD4!y^bG-gq!hO= zJUlwU{+JEv{VYMiqbq9V)pK=quuz}&x4eocN)fGY)(TiOM&D)f2ZFtq;e?<|5u%7H zuM?KJviNw*XM*9`HWb;;k&_D2XhUFeeh$&>HwpXN-7`Dbc^4H78FCPbz5iTez43sG zYgOjwqX9EYN&`&#SjdE1+o&?=FM!j`5=1?_YV_~Qo9Jl+;?@eN4=+;i`Lxd3t;oO>3g&TmKJ+WhrH2YtVv zO^y0euu2HofABHFPJQcL7g6<3F&K)9zWh897gf%e+fGe(7qxAFwF~~UvajfPX=F+$ zFx^Mrb5>^Hc_uvw1|G5T>KG;m0% zo74i0)oygf8Qh)gP3O1li zN2?TX3^g$usN_eicQ*dx=T#g014`9dZB$%?)2NL6;jT>fcH>B9%Md9bX0y~vp{n+r zLD;`7V<;GzS;LSh(a~nAC^QbSt`(*&=P>_Rhh2RS2YAHK=7p52+jBR_y?Jz;vJaiC zx_YA%&^fH#zCnawS#(ROT<^KIXzR(fRUszt+AgbDE+;w4dc_c63J(;7vO-Av>*HC1 z7#5pmS`%EhuKd8D`~o)jBFp?N-^RVo`L1Vd5oq#2HQcA$zm0RK7D3`f(;un`AH2K2 za4_s6mFg6iNNryphpx_SI5qJTV*xW%gS5SeOZJukpD9Tx4DvaUPaa-`9R87D{!+;RM(fqd-4qJo>I1Zl~Crslv#Qo5kZsf#BF-RzvGkQ35n~bsecJS?X9y6j)6*wa z%yU2OdmH{E3~Eu^bl{)T8~D&&5c7*h%!023ql2cs{KROVa@_hx^C4wFZMjB}yJs_0!3FSz@$nbJ|X=~4tdWep@suggL3J+V4+xi1hUrEJ#YuF+LS-!dTPkd%6e@pb7Ki7Iwn z=Ztp`4t0`RW7W;$xmKbbAl+|JcyJLWzZC7FU-?&WSAcz=r^-)jy^w zPId-GH%-7hT}+nkb60p&`v`Imt&;N^`F2pwG7ZpbgNCt4XL|nJ(yanGhiRC$>>Yq2 zlh_8GNfTYB;;wpBhMwr2NVgDn{BGBIFPUElsqf?l^<{vuJal26K8ZGd_Y8^M3&)ZK zDq#rEnGky_Lnk+Fc}Utc3Cp{!B6aI~KW1^?<}dYjiyzRHoTkW}L+cH??`XKp3tGA7 zJ@z$@sUBc>(AyrHFOL1R`7c9nuCS1n!*X5Zd4BUzxwugB0{+vjW9^R&Hv3)#JZ3vq@zMJJDh}6_W36TD3$|X*#HU?B$5BXl1$| zY7+7+Q@y*RIwGw%AJ(e8n=ls9{)C^OfCcln1~U7AgVGo^LrOR={8oBbyS%=8}Ljwmbx{x=VX} zK5&u~5rm-I7e7JbcZde}`~Ix=p*GqFH`Om6M1{=!gH5#D&5X!^W$hE1;OQP&h1jfWK$pa-GTt|Rc%aqZ_)^hC{{L&!wdtlU-~!^92gOp7tSpO&N^x25Wy z-nXxxWwn^}->vzMl#{D6L84KCw2D-%f-6fC=XYqaI#zYlk5XP~H}xx<#Y}g^;rWCO z(|)F>A4!~Jq!#n!igTQcHJ`l+F)r}|IJ8#0aTGELfV2hxBpd(fLG&Yfn}cq8A_5wl zeUJ|NFE#1%+vVH=e*pou%Bg{R2NgB9Ek65jjT3k|%`j13fj!57IX^`7*?ubg3M8A! zLTk<13TxbMuhZUWNP}0@j7M1JRwYPLbae4dIwzw+qEpvEH|W(futOgEiFBSD7jfe3 ztDzL%5;NC#wI~q*8KSnW5q5L@l{5x~{P9YA{hD%(^*ue#yy>_k_s5dgy3z6JVxD2{ ze-f^v_C$S3iG^EAFcpH(#qpV6S|Z5 z2XDN}Og~}6Wte1)GSnJ~M-ok8&?Z*RpxGCkXe*+Q>1g#~P-`DZ$pz5pgLRhUCp|2J z)8eeA#VoHGP*&o|3ZY91JgS8LRh+a=O?}r8kY5nHexQu|lhc9RPlxs$nSe!C_MQE! zS6|9`*9TW+$B?k6t#3&WNH7P2eEb~`iBvpOe4ox^E5xG)ya%#r#$bY%m*T3KxhR@L zLEvhVHgI6j}DbGGDzS0g?jf&9FA)eZ!xRyLYSF|Z{ zBvlv+3^)WXt`cvhjFtIJZza+8J{HOCO2_MRvhrJk#0Vx-UH};WfF4Qs98v_+5*o^B^s~(z>W)wsfw0Duem<&#Syb2AVWn!O^F7 zt{XH#&&A%am`4|g-PCC1-=&fvOFErw`Ko52nVP0;?BPxLTM}StxzI013m54b!Fq29y_3h`O5=b-Y3Ie0Rz!$T3=ChA4}(edJQdbJG+U=Vo`S5 z-$BBt1HJj9wsWwA*D87`#$Vbc$HZagiAZ%vew&twPi%!_pJdq%vsK2&C!85gPVld# zA}4ffB28=2IzGhk*b4r$*`JGt3In(8Ia8V3A|{ZYHs;FmK82X)EmLcB);KQPv1yJy zFh6ze!$lfCo-592l<+K;LlLmhRi8TLcEcbiupDIE*2Re=zMRd>y^o{qAkM}(%{vX*?&4cu>1?sDELASY($A9)? zwdNID6x|RcYgc}ddXlf|=#{^1?REltTR%59+T>B`vQEm}A=Pf$Bh_X3tvByoj6_|gnn#CM8;O&C zcfN&ibm8HywPoQ1;m|{dj%>uv`I@RlupY5Hj1qv8F6~R%%SYQr(}b5a#2o10VpSQiQ)IO4 zYfppG^AxDhp>gO25R+D^e$Md=uHn zRhZO{$Sk$YRO`xC&E7sa8-K$n`BrfW@@k~5v5Ya|c&k}OK9VbAy?J>=q2y_17S#L8 zUuI)g2^BZ}h%BV!qU895+ZJq)gGq>Kw?oLQ+~DjDJ+Mh?d}wv0akbX_@nV{a8ACpi zML46zmcH5fj=-&O+}{HBj8Bgib}HpNbm2`av)W9r+H{Ul(hn5L1sgo4`-r`DAa+p| z4_^i>1DnqN+p|L)mL;`lf1$*D39RE;VCtN-HF{&cxf6afw#mZ+x4>v&a2hu|Stx1| zaqjFA&hidAevvZiR03uUA{q47S+C#v#mgjdQbZDERJAa}?)Y<+2J2?!A6@4kZY<+PEubVIL~{xFVFi$cWmKy}{xIaZagJ6b)>3e*In2 zTHjR*TQ`Vxt7JD}!kn1aMl&)Zi{CH1N?8SnR8*_3aOp9>%v@vRv0rz%g_r@RUbH%i zKhaMCc*E6u)Ez=QbG6B?oqpQpSOH7*9Dy*%wrM;KmT8>QaBUE(IESg4P5o!b`I1aB zJ=}yrwXCf6D}FGfo(}0Q%b{paeqBfzVi@=ywxeB;@67bb_KDo zi2zTSRQ1M2_d%bpJZ5(|`Xqf%R@SjTrUn`ef;YSA#yc%y9A?-_mL8ifEoLuA!Pl{2 zPOkRY70c52-I(%t^FMj@7kGr-I=9w)ZoK=pL|KRL1P-#3k;tP9)sJnLI>qbPG-|a3 z%*JA<*dFWlZhmrw82R)~D7#f^*qMcAP$WM7i^-^=zt5MxRV74y^~R`c{QM6|evZ)pILT>8orLx7a}BMlyzuI zfxU{zwdz+ZVMLv!%C4`1>dO%OCMO@*g$pJgF4P%(+Q_f+L^*dZ$Xr?KyYKX0zn|N@ z)deS8{OrZ}P3}{Syz*}YQul7VMu$v5j{N$876<1XZQ|6p4@@fO^~QM&Z6V;Ta8+85 za%qmE9PJ)~TTrk6%#rr^8iU9>+>#==?s0v6_^*%oc8GZ9gm0XeyAMW~&*v;aYs+e8 z)`X9VokgUh!n$;QJJb`KBzALp|0r?a&c9`H5#9SNLQeV7QZ4Ys8dsXD%UK7!UQ>_& z3*TupL@LY5f`kTfuO>(HReWZj!#|oX%7d$0TcEfz&I3e5I*CYMHlpLwO&yN|Byp=X zT#D^BG+jZRXRDumBwy&wB5xCAZrwZz8KvO+)hZ>$p=4oIY2b=t%*Wr~V0DdN=C>)A z#&lVmnirEHKgu^8I_)qN2;?`(m<7<~KD!=@cr>Gt=adDZlsjwiq|qio64du9K6zrC zC9C>17|Yi`ZxNfZqBnGH<+tBjAGf!&*S1S%yj~-e!*kYa!jUF6K9G82EG|7LRJaK6 zlnDX7f!@selrKOLoqBYKZl|*$Qh!iQ4MI&+~FgNJwA< z1a)8s!$!CB?f-?jGvLi}JS%)NUEwf><*nHKiDNNEaE=)CIz59#$8S+buK%=Z2x&1R zYg5g0y~}+Gmo)4QC&NW8DOhbyvxAFF`|X$28(R2M=)!5_c1-&67-j`TJ907X+%Mi} z!%ny6l^2?)WisL5^RRX}#k^V&aZK=X+bsbxz)dWvrCQ0O6pe&FtZeQ+;Pm=Oq(8v- z$M#f){jWA(*V~9HwAlr%fm%c~k+_tTp1#Y?j&c;?&r+V5 ziQgH{b-}x0o>4COeoXQ2}syr{@_6o$JwLr_GYmC$;4U=`a-rMrot%4l{Rs1Z~ z75KKrfMynKY1~~r<~B}X;vRljZ?R;qlQxS)|Lt;=PGx~T5n-2sMzTcgkiol5DVMt}gM!LL!Da`{(o!-?a>AtL zm!N9Xap8=?z1=ahfP-3J9iSt*gL%M#CDgYsibcMwAnM z{+gy3meaQ3L;9!0eBli3I)C~~>%(Vp!6<)pw+=arn@=w^pW10d&k5x~-%pRY(d1Z~ z%+l{A8y<#c*XTux3QJW^uVi#GG)-skGYP+tdQUZ0nyR3{au$IAW}i=I7p&#RB21l7PQCPCEdu1>|f zTmDa2Gj~6niGHU#u`+N#KK)J*?W7#tApJAP{bh1OoOr$yyS<*U9=km`PSiR4doVx( z$k(7HfY?szBH?x!ucO6q4FLCci16;XC;4Um{-9YM*u2;Y!%P$Tgqv*>m`|M;$LlI7 zeuo=!)^DCh{3MNf$Sfr;gFfzEmsK&TelyY&4X zo9Ua&L$cyXh&&5v1#ChlRy||VpU6a}!9Rn~%hoC$1*Amac3CQZ$^>88uysO`DXZOy zz=L&hc1+d4lHWjDdNq$+zV(TRBW;Q0vabx4z-SSRmx6-8e|oWJ{nxzBDZ&G1`!;%+)4R9SNky!Y3 zjbjim1X^ zK4-Y5JZIDpY_zo-xw-Zd&gi&IBRt6ZyU@-sn8tO%KD+23Tv2&I{8-)_0mZFc43J?3 z9|OY%cu8X9RCiwB~;gG(Q`hmL&$#QRY?v_kd<|ES^G3|x<8JxpD#$4%yuVvTlF zHAY4|uGq0};X?48%tq@SFL?NjyaA5s4j7#BRu7?1?ALA{o&!^WZnjZ)ER?n=Y9kx z0)VTGXdl8CAQsQq;T$9t1^a=H5qW!zU6l(t!@Ie>Apg2g>!2!NiGdCz6q;S@2YqE! z9*aIxWq#%Of1SNmLtIT0E{Zz?gS)%C6MS%YcXxtY@WEYz26vZ0aMuvr-GV#8LSWDP zowNVIo}0N^wYsXix@wg@oLpr^jEfs#%0dz?Z0th8=BFR#J}(g0k3F*i4RC7S?0;(9 zYTEn=cd>f8iOL6*`DVm{)9ZO}XTRLQwuXF3g}KCTD3qje8^^?&MctKU6t7l}M3mq& z>7l>3zCNTQmC?fq>MaU~O^X>BFE*~O${=2|2GJLs3)Gx!Fk{Hl?p4keDFT<5_QB%n zJ$4;W83RBgj)pS#h%wE$b|n19eYsOOm~c)C)9xoOxhyn1!`>>i#H<39!r|81&%?>G zk8ZOTXK-yM`P|Mz12sN6)s_S49o3wFqPBFn3=I`6HUxR-KI!$4S4a2=E=RqP@Qd)2 z9~V^(bpI91ym0CMEgU{{3Z?M|D$Eh_Fmz{Sc_~b`^5Epx;Y+u#{0e%JHO;4_H8K7y z?6l^t#pK$Z?WXUyWcXb#ZSX3-yxQ9Gbq*-^G+SLer#SZ~7Se%WiI6*1texj_#*`dx zs}lXk(j@RBgh#d_AyK^W}O zKxN^_UXDCRZPI4eY%V!YoC~i1imPun@vpBKYv_GrXSYyIpQ1FcJZhTVgWHfwW7ee7 ztZOp9&J?RqB{6k3=6dfD8?ui56g<7udrxW*d_{1XZ%K0MV4S4jm!>$MACr7p`839nKzHolo%dCIBo`LS$e`qBC=S=*srm-bOvHb7~>;s1yj}{tV2i<;uE6+gW#Ep2xP_ z*e@>9a(TlU&_ty?eMeD8m;Qfi0fOe%iofKpXmT#VnLcsBG?%FUiY=OmS^X7F@Iyus zWrQa3aXrl}J4NzMD~u6gKKPG|+38Qiw|d{_>-bm$Y(xcyUpuBdsD?hI3E{C_uB6<} zVm}G8m}w7d$jNZGl&m%UhPcbh%LgNhez%BRt$vWi6lkv~RzIWzE2u7G!<2BHow9K(m!%$q~~1f7DT z!RIn9{U0|!{VVa}P+r`J^og%~yqdMawK@%z|jK=+z}T^Z80eWa6zPA}j$XjbK$vXX_T&v&)He)w9v zFub@v!J|QGgg6l#&;9MyYY3hM|Kl^a$jgjH71b}saj*nz9&hi{l^F6AUqNze5*7CM zEgdurXM)<99^&YlYjglhX$y3z-dwE~Z#u_o)Ie(A+qsZHaI=s&zI!)FxP+x|+4Rhc zE(E^cx>;6WXBn0&2ClI4(p@jh!*A|ocS67Yb8hi8d)myF8g^LlC{RY>a9a!O>uH5w zuZqGlz*Jbix%pgZ$6>UcsD{FZtWD$?9rp>OmiJoj4S~)?zbUy6gqdH+4^XS3NRUAc zmv!y)An1M`nA2N}cf4lXfyW3wB2wh`-e>ym6KEmgB-*-Gh|St`@@>OB^(K1Pp@Yk? z>d+}6D4;^Jua{(qGQ2AHE}qQ7rLt4rgt<8{ji4&DL{MSMVedIcfm5Uhh6Av_+ho8{ zWfyg~j%PQN)e%|Or`Pk;S%dm>nYl%r(_804QvYG5&aH!`(R<3zX)|)1?!0V-&f&nJ zc13~9v+N|9Hen#tn7QT3Z+?BDsKm;$fs?9wRnGAxlk4m4Em_C?Z+pH{_iI)G;Ie4D z35~NGOJF;JOQ#D;JlWigMp69_3z$-|9^7BwM;_(6JB#6>FigA8vE1b6;~R?c zxYvsXlYbyV!iWS^z)>L_@|tjJdg8+7azY29qvLF3N}N5Y^Kq%2UYHx|p(o{MkL3+3}7x~2K#u*7ViDWKH0R6+-&+E3dt=hmMs|IMHg;X7F^ zsx9vA^Xrr>NCe{I|5)25x;t;BoKu`%9e%a84LHbmzS)iqcLT8?su?Vy(br7V1B{wl zux@s$E>Zat!-(rMiL*hJ*xqF#PiK7nUe%p0p^ra^B@A~myf3)NHFk1{s~@a1F486; z<142^d67)bv`oJ|@4mfPwcU&-xkh%A*d8OQz3}W>>6vx}baDu6r&-)5JpqRJuUsR7 zgMO|!%_4i_D|9?g-j*YP3qGRV2tTgi(KJ{i3IsE2^Y2fA4Z-WsVS)wM6&2j{h#@3g))(O8J!CO|0kpL#G&3DNqss&B+hG zQI_+}m`uU`@kmw!hHr54uv+sb(VyEYV$80_9zT7G~af-*l0NIef@$L#}&TeL;=%86VuS>reFzk=6_<=Tx!VJ^DmlwuF6r`CGj z2)}W87Ix#aW=h_99G)2`@!TJC<*7P4=Gr@HJd3K3eH=P+?|E<@Cu$l7NpXv``NlmU zhO*(m3b!U9A+86JAapSyM-7~tEpt!w1`J8M1A#wi|ELVJh z5B|A02$@4mLfjk0HeK^OKHM|rk9MO{g*3}p!*N5{@0L5q?89h*@P-|JSbs&pP(noh z(q~bP&Vj{=p@=CyiBE<#FdIL=W!V3du}{ODa3)M0;sRq76Y8f^p*`g(-IF;956Pr> zS6AA)Y&1@O@!xjqI8Ngxsrfjm^1a;3SJTpP#ZDCGkE?x}r^9~kuc#Tw*-J@B024A$ zt{~SM*AA{@y1^JQ5T_0hgo%l^;5nSu!qiqt7z$F7i1@fB7n`9gHyO z40d2u{TF=DrfL4JT8|F``yHsNUw!*>uklqh2nU|ATq3%^()@?U;@$mG=gZG%FMN{` ziI?A@ZKD2qUO|9gc%95x6b7fMsg+qD%bcE`E7awFOeh#5 zaF0t7+K9M)l*|iD9?$do{W(-$LD%IndmWwZ9gPJYA9I57B zyxnZX(P1nR)u5)1s#vc+`ww0v2)krMe+f7d>UhdApWhN_qW$5*$y8n7@Y%hDx1A`79b zB?_%Dh8MZQ`S^Kq;^k%}fZg3m8f9{o?Kg(s>2#gjUF(aRnWJ^_A8(o#A6WOKf1^$1 zgfmB(WOOyc^dyflU!h|yisX`5Hv#{}0Y8JrF?vV@DDZzPT_P&-rOQ}zyIu`fu4oze zhb;dU6D7`c5kRi6R1%7ivj6xywXoY+gW?pE%Jf^f<9BR~+sK9cO4rKKhM1JT}w?K0glHWeofakg%~`G56EP z=84{G!L7(Gs^`RfYBl{yK;&3c!v6W;ZMa&XA26>FX`vDF8YC?kJYs! z9iGN|OjEU;_bg_!a$m~fuA-WN{f>c~hwH0IUSdFheX*v6Xa))dhl$KU$%YpFSk&E( zF7RkOZtsyeZE97V1O&K7F^nRwIR3W$sNExNA)4ejmN8|OcHq@dggP20fqX#!{ z#^RpODB%&Qwbk3M`2jc7`F@yZHyrUuaeh`}@2C zhA+69?=Yyd6FN3YE84zz#(|S7LmR#ESdI5W6w5Bq9qW7Az9X=`+eCDCsyzWQFch7P z0YAP{;Zb_t+1?qvzRn$TFWLGX{!H^*x6-9dLFO{7e+>N3d8eP;DdSvKyT&_icy0`i zKIRWkI32#u5lJXY7xT41iF>hs|2$T|bZ={1WElq+J;4*XU9Oa}j+r-32yxL8aASWB zYk3uRw$I0fS=1sRsVuq~uG|%dD0owMPo>$ZQ2U7}$l}W%fgG8tEAr3fRle|G0omNl70T4R7ip zyB7N=6V@K})HZI+ggj5_9l48Enr;GMnZ#zl#tRw=+HKChyILC-Eo~YY^WzSF4L9V$ z2){7EX<{7pX{%AE?A9XjCMB;7>d-4troiUD)$;PZZ&$VoPM(jO%n zFX&Tdq{hLYm9O_-+98AKiWM_LT2f)WctJwA4YjJ1duX_p|FpMcn_7%rjIDlVr z&ODqN3FFjD=ca%Hl`lGC#HvEZ>9*c(O`XQLR@5S;oxZ@7I!t1t#QOODey1B*rV{8iORv+@6*C>Uf4 zTWj&&-`+s|N%WJ-{33Q$9))%+q)h`rc+z9=1PDlzxG;dBkxRh#_kK*LKP9zVfZ?w0 zf6oel#e0yj@;}~$fHVp{42Eii(o*tk2i>n3*Wl@4;4t$dcMigSv+32-y6v}@wOSA*yX81AZ)}>g45vo;lo>TK zT4;HhibR3SuSQ6^=RMnWNB8_fo+;&rOH8`_F!+NI8q&W6=P;Zj@+Hs#*J;D_5=SvS zY3n;atjt=Mc|QT?K>fGq#)yK})?X;w3L(|iZ8{IB#sPyjhuu>CvsLWslN+Uj{+0i){*YTj&VeeG0 zyzat2Gpm97zdhWAgZVG!(5@PYUB2!j+rV{nmJXy#3OMH8YG`^94w*GW*I78Yb(CD z6T_AMO5nd@*nV9CXvj`cNJE+K=6)v+9Wz~pJL2WDIc~+Wl3-|8L0S8J5tSJAQXoq5 zo8Sk9EJZu&yC|T;K9O z#-k6@NNfX)No~*kOsGjwvx7>LjI;ly^3D8LR-r0?$!HqP72&>#BJRWGpOViGN%HTG z?R22k_jpj1mNtqqHQ4={fSmtkq{T(vfzRx&5l<_mSv?~n--fmUBnW@!P`kx*e^{^` zQUnGWQ=C1eAJyYxrRQ@BsQHKnc!J3T{qO?sur>eHE3lD64uU{5ln~7$Zq@)cDxdf* zGOoL2aTBe*>$HUaBMU}2uPh^vtiuT0R}nrd1wIK~$JKooH%o=F&|=QL7=QIT zo#N&{?~%j4f|&e&pi2kVe|V3IKd<~f?r$7;8K{8V{v0szAviF~)6NEFC&fPALn&zq z8c(xUSPlF#t^$uPs|%O`0vCHW|LX0o;Lg*bz!MI0xNsP0*?Q#8!tjQ1+fn2}2Ob?3 zj|7S5jYaIekw7^RoPohcWRW{gL69>dNrT-y2?jwpXx2%7kkNp@F0|xmd>dWAUf@~};%10KIc z(Br!Txucf7XlaRv-^{oiKX+~u%n%MFL{kRvUz~@Ek~}uG1k)RAbO5yiB{iV4m4att zKCK*Qk%U|ZOhN#;`>^N|N$orkO>H1{#7b({Nm00)DXO!#yOtowK!^(8l*i;)Xy@Zs zLO8Zk(_H*La{{EyOPeVmb9ae;YVz9)3r3m@!MGa!ElKIap|qmnCE(*J+|lguA!YxC zmN{OG87V6kD2D~7prSQoInxk+sa`>MjRMC<{)>>*{EhXyu?QL;niw(o?Rz7%!82=S zmrgPi@J_|2?!@{!qX0(izZZe0~@2ogAVk4f0v;*S~|=E z9YRYXyx5csRs6|~%jibh^*roIf1{ppi8zbmJN2e?85TmvC_QLZ42IMkraHgxv)OQA zx*2@w9cwMw0;PX<`iO+{my2z7lnExkkBbw%zItqN)`deCZ;Wyeo6WQ9LBy6K^+X2na6Yx|Ma#-0Bx8=*x=i0&! z$4~UJS!JXfKRq;L4SIb>^=0y`1KnUy?;68#2s}it@C`YVlPBs$_X{lp*{cK@*Lj`R zwZ5>n_=KATi7YUPma`Q`o02E;!-9Ue8@)HY_aZx-&p6afYY=@xz*hNB&pqHKna+0S z`WnJ65o=2BP@aWIoLpxQC`(T=R($8m z$5~TmF>Wi~p1V-|+YqZqF^6Q~&;>Z3=YQv{L1;$-t4=0WPCX}2{?;QN&#MVAlg*&31$AbO{5Czc*PLBK z$xExDxoch5auFZ$7gK=#mH6>O3|TC<55=KXUy6OaGAMrdUfh(zI>%%21}PJi&Mq<< z-a|Y9Ri*eDwE+)sAc7f&l^r z78X&abgv@mO?S|zSxJHS(!y1uE6-AYLS!vV^L@5&o^Sk2(D}+0qS-xW88{0SCQd(z zhk;E$+ZI$99wsQXW`6&C1IG6rls!)xXus-1E5}1qM`6Uag?o#K^5=c;bc1!;A`fCs z*E?D%aJ;o7AZy|p3z}X_p0_b!r*%otrxm|BYo1KKA3Hh{e;#iI*}9z=&|)iNDgh-* zP5Ww~uDVYx#wUL7wFr$utj|x({=Itgx-E>Po(|ac^#pe!oA!}z7Xs3B3_q>JvmR2L z?S!7cC@u83N_9z0RHlvbPyqrgQoqB9Z(70+i^Lkx3D2Vn09gK^;w)4e>xX7vf}GiF zS3OC{I$RNc7JQ1$>hh8JV+q_$fJzkCzj@_bZM|;of=^(~H*f{V`-CSXd_`&YN;%sy zi}T~!!D-Ke9iUz)=!%;3uEM8UrIFE-Zl57Vc>3bNhjy}JIQ%Ucw-;6T(*Tk$F!VLb z#@BiRpx4?;;0DcyoBO!9msFBO-qSwxv#SV3xE#5k48XnuBdIEjXfA>%sX%8?5!?tJ zWND2Y*j(p9HrH z%8H(`4S_VM{=yHe!3R>KT}7V{L|I`_YUt_;gG=$AIP~EnmBK?2v#T8=Apoy>b`szR zdGO4ZL1BW;gxqzgAmp&60zxMj&~GmGl^X*s8C!k)dGoyA?N#aCBR&-?c$u$U%tj6L zj@Jw@X{(^^19VadG-z&V+r8?{B5z{nLYHjhcF>;NXQo5+7eJB?wFUAYR(DjN3q4dzo9irwpd&HW2rL=8gg<4#E9;TvC zWNI{iQAv4gHDU1caT`a_1=;iBbeh?(TF!nlj2wp%txYf#5&n5QNtle{qqw^KAvDf9 zH+?V~b$_S@fe2@4ygivO_5`dkYx93^`PzTlZ!KFL343(IgX-fah^gR%k(JQ)x*1W_ zvo|m^6A?#;ZaFlxpbC#l9xp!jDV^MZyUq_ z{xIJ3uRH+)SmDB=y8ouaBDnq~^IW3Jjf3Xjcf&B$g{q$*P`uZlx?#!_Cqq@Yw4kz- z$dUa7vuP?hfFKGWQXWH1DDBi!KQlSW_wI=9$IZQiu&^|dKo2AeQ|iV=U~f|o>BUy^ zLhhMSq89v8$9 zM2fk_$h>7Cm{Qqb$#Fs!L4Q2Nx^y5^X3bTq${J39XyoWN;)JT>&T;-twErN8-bqV%Sxr zJ^L4BPeB%Fn*y#>Xyk(?7{Ua1qPNr`H9uT7ywD1K-I?>MKM_B0>FkI&k}S3$Lr?6U zLYY3gM2LU8fqitNF@leUo9&~C9(Q!lk&CNLp}=JaVcOU3A6%57W|1wvvwz#fcj@$J zQOXq}c|CfH^&&R4gpAAcx4&6blv*r5 zIeZXBj6IXFaRu$-IH0b0Iq4{4JF*Q>7aCTQ;u<_G^zGPHJ6Qz+~)^oUv|{*h(r$Tap2oY4DX zmxAib@Fil5fw*ftiF_xH%G(eEWNX?*fn5w26tobiy;;+Q{O7r-St?bk-is}yku_FX z=`$}y_cv<9H4c4JEH34RBqK`fX$EFe8BaGO5h28@&LXJaCVYwX*bAJ$dvIWmrQ3~f zQ&9Hi7{YLK6^lBHw9k5@v*R6{X{c;U1ghNCX3FC#bEG&`dI=ah??zmQ<;&vi0>hx7 zbx@$AtK~Y{9O0)Fw|XQ^fH7T?YQ4}er#IK8U#pbqM#(U=+qTI)b7#Iws^2pD9&K$k z_BMunS6d&Fd>@uhpOer*_>y2t!aVDt8w*`_jJYfy?X`73XEuRyGi+jEn`#?A_PF(vB;VJ}Ypu6ji0*9!^Z)Pi&O=yV00_Ym^A&kbKVXXDcBLmGfTx6hRb1&AuU}e@bMJ)+4 zL%~W7@rzL37NGShU+LK%_ZQPr)~4e!cr@{D?~k!GrbE6+Z$B^SgKH9VW3Nc{<$6f; zoHXhXb?i$?*c6!xbWA*Ap^=8h&rFh-Z2_S_TE_Z2=3onjX*ss5ntL;F5@LremX@_4 z_2&l(v}zh_P6o@34i-5(Itbx0DUC9q-*n;ODak+L=#mDwB;z4ht{sPte55D|)0`Kx z^P3e%!KQEIMc%uJ375nZv8krS@xFq?ko5#gphP+cNea5ZC?Nd`Y`eXVfgNQ%6x5E_ z-zqOvr21_;IR~pKqgy;dlTD_u=YV8!F)y?SQDsn=88G-eC}S{}lD)P-TLQhCB4S`z z&Zq;F$0cAUXNk(+7@_w;(5}QFhBVc5_i4@9jmE-;Q?Y>Gh`Z$bY@zbkd zGKuCA=7!`fkvDT6aiwtIyz$JfyfiPw zL6R}pV=l+B#q#J`foAxo`W+?oyTX(tePxLk-6 zC1>-C=X}0b(nP&r6GFbO6M-^()RELw1Y@1DXc7K>gJ2vDeqv1%<^`OsNvH20z?-UI z9!KxrOb{S77F&cIj4EJW7?ZE0;md+^>>9c|Xl-7$yC=5}3@l9HW#_6;^(+^QHcgYO*i&W}G#7K6 zKc)&@Xp~$iIv{yQ;_c_qa1!JQ6$`9dh9t(FOOr&XJT`K+I7zzI1)#}gC#oO9;hzY1 zsemCFY(%?-swBcSa)=p>&T^~0AhYbZ)t3=XkEX2si=o~BKBVndTML{Veq-uLHhuoB z)t*J2&h%!y@S%PqyhZnOXcZ>`)7q(kEOJq?yvgd>TxZWQqn%ZNV@6;L~|}u z&IPMll4<$Rj z=FI+-Xd+Tooom>_Qo-I==>q~{GgU3_30T5o@!Crg2)m1gS~(%hW>rxcUCFf$P4;^2xu^c) zqu&0ts3!&Us!0{}1}Kn$8j&JtTozTTOTtF;Jv@xP93W)kzS2l?(4Y#fd1LU|)62M;}~x>c0P`ibgTHeSs9heklO3Gnr`T5j9U zin=Zvi`BLJm{hZAU|epP+5hL?l(^9eD&YbM&|qzS-#7yJT$_fn&~9SF^aPJMY*i!n z-ER&8Iwqyj9LC5f3YS7MzB<0z6MM9*q^to2GY@CCk@55J6Z~C{o0bP*0-yp7V~m0u z$CW1C3|~o$IX4pKE{}NN#E*R%0sQ2NN9Eo>1@wr$>X(#)VEvgUGsOc4w=?fBiAg~v z3i0%rjAoulg9>(rlFCBd0G;sJl65p@E5ORPO7{Pv%I@K4rSUchLxO{m8&Rg>QjO7&k3q1~0*cGA(7`yU zV>h)pF51Zxq48?r&gzKIClg`^Bh#U$%kUaNpXBXf?RxD2-O?SA3;{Toi;-hK7kf7y zFkZK?CIEMuUbIS4B%-87c0hhZk0>K&0M+F`hAZS6}K%IjbpbZmBKMy+G z+e096q{6ba9TX#I4x3s+Dd}sTx}Hbpp*Yhcft?E*EMSJ_SDcv!yc&J&n+igD>dK2< zC1E^=If?WX>60G-CxKW}7ibnfqf~HsP5F| zVYrc7baJ*~FX!6knGY9|)MQx8uB8=$)`M@|mE8 zHzS&ETlQVZ{H^x=)D&d}c0{xl)K}7#RlKHe6 z)#Nk8#!jGq35hm|NNHWG6VLl$pmU>zME2B(#XNB{0?dx4cU;!c!tFJtfv-q>JcM>jU{FcZXcDj)gj#SEA(L}Yh8IOq2oHl5uAh`4ji>sww3Z<$t z8>dQ?Nvx+6p3C#-*LK3m?8^}fF-h>8RlG+8MjCIkchK(y#qZd|JZ)FF5}EZtaH$ank*9GHEFX5&+ssw!I>Kn(Qut=#BUfL5QScE4FU{ji9L2->pql3I+& z<0uOwFfyiEy6~K14RtdeeSx~(xC12D49J#ows*s_beNSnL$Nz=Q=+t2cB#cz@H>Hb z_ieaXi&syS-tDm1)5RL#LqG|Xhlha#L_@1l?Lk)t$Vg|aSqZ4_>Qt#=SGZCt@RKNk zwy~!0tzJK1MINa4dnm5juW(5z>3c{KR8!RSa6S1(d#j2DxIdHnZ(($8niqmwWxM@aZ0jggA*HTIqVVWHI^^3v@KiyAD&XIgPT81T)lrr z=e*x;@PgsT^mc-ocwnAQ_|VuSI=cZ2yR5GXKY5J<(!^;hHkzFbP(MLno(~N}CPTdJ z&5p%>8&IZ%18pg}oU{#ibot7#Wt+9Ody_4Bi7#7}72bL~CMYy4#;E+aGJCsf*lTfO ziJ3qeO1|v`smpZP964&n^}om999KT<`2^k?4wA7(A)(>9qVXG~6smXFHLz*}TPgz&B-_a_R#_SBP)#=}b3M zfS!0b*QP#SIb@YUf?@)XBIhmifnLRktuS1M)0md2Aeh2OuTlQ<;=0uzealdzCVJ)#2SE zA8=EbnUI%lYv~KBnDNf~3V4BAEaSvp_=;aHwq|+L^8)w;QZ=tZRnxF?oHbM>L&;%G zz?<=5hnouzE=M`%IcoGDcHD<^+=qN9SVH3R=7xxeD) zT^&T*@4XJg(1=w8YsEVxMTr5=6tAR_J}|Ql=!qLaP8zd;#M?bc6$e|K+-QpCOK|nK zr+^7zhBF6_vr?$N_8iqsC}@V_=r1Z0!ao@H&C>@$&&mUYNz5`h@H%gcB%vN%D#e$& zcp$1=f0Zuh!rFn_LxQr+6uZrkQ)&X#9TE?1%x?qX;`x^?4$JDIEMp{O2S>UzL_6t1 zFX0w3H^3DX@rg9DnqPxpaL*){U!!U9V@IiIQJ^8MUr`|z9ktat(kfZtpJxu-g}jiE z)_PD#_p|o6EafQ_LH@pv?yqRPfUZS`sJVKh=L{N&DYnT8AMyZ!RMq2h+V9(KVFduq zX10kCPSUeZ-SqxKKjafeVw{8sB3wqg2IK^h^gH0NI{JE!sDqgY?vig0>1M`0lUFB* z0HV$%C=IAsCnl7x4=EIab5Qc|R=l*;#WlrI1UF20M3#1WjS9Hsk;e%fw1T9oQe$4WcrE>)a-%sH;a4l8uy^)wi3J(SG&=cA!R%IDjj2Trajb#u)TJ zHx8uK=43In2eE^VhG-}yS6B{9g^^PiE}4W*3F~fksauJvi^^7f%?NS0HI+UMpQ!y} z#u Date: Wed, 20 Sep 2023 12:52:24 +0100 Subject: [PATCH 03/11] Autogenerate docs for whole package --- docs/conf.py | 6 ++++++ docs/index.rst | 9 ++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 1b64a89..6070a9a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -38,6 +38,7 @@ "sphinx.ext.doctest", "sphinx.ext.autodoc", "sphinx.ext.autosummary", + "sphinx.ext.napoleon", ] # -- Options for HTML output @@ -52,3 +53,8 @@ def setup(app): # type: ignore """Tasks to perform during app setup.""" app.add_css_file("css/custom.css") + + +# -- Options for autosummary extension + +autosummary_generate = True diff --git a/docs/index.rst b/docs/index.rst index e398334..5b1c04f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,15 +1,18 @@ Welcome to rctab-api's documentation! ================================================ -.. automodule:: rctab.main - :members: - .. toctree:: :maxdepth: 2 :caption: Contents: sometext +.. autosummary:: + :toctree: _autosummary + :recursive: + + rctab + Indices and tables ================== From 6345ae4109317d6e659da231eb482c79d71afb11 Mon Sep 17 00:00:00 2001 From: Iain-S <25081046+Iain-S@users.noreply.github.com> Date: Wed, 20 Sep 2023 13:18:12 +0100 Subject: [PATCH 04/11] Toctree captions --- docs/index.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index 5b1c04f..6511da9 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -3,13 +3,14 @@ Welcome to rctab-api's documentation! .. toctree:: :maxdepth: 2 - :caption: Contents: + :caption: Contents sometext .. autosummary:: :toctree: _autosummary :recursive: + :caption: Docstrings rctab From e8c08f539aff9faf5ca2ed1a53ec7ce7f2386655 Mon Sep 17 00:00:00 2001 From: Iain-S <25081046+Iain-S@users.noreply.github.com> Date: Wed, 20 Sep 2023 13:27:31 +0100 Subject: [PATCH 05/11] Hide toctrees --- docs/index.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/index.rst b/docs/index.rst index 6511da9..95604da 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -4,6 +4,7 @@ Welcome to rctab-api's documentation! .. toctree:: :maxdepth: 2 :caption: Contents + :hidden: sometext From 8699a86fde52bcb29fec7d904adaadc9ff5a985d Mon Sep 17 00:00:00 2001 From: Iain-S <25081046+Iain-S@users.noreply.github.com> Date: Wed, 20 Sep 2023 16:48:43 +0100 Subject: [PATCH 06/11] Move RCTab.md to rctab repo --- RCTab.md | 40 ---------------------------------------- 1 file changed, 40 deletions(-) delete mode 100644 RCTab.md diff --git a/RCTab.md b/RCTab.md deleted file mode 100644 index 803ab49..0000000 --- a/RCTab.md +++ /dev/null @@ -1,40 +0,0 @@ -# RCTab Docs - -## Overview - -RCTab is an Azure subscription management system. It is made up of - -- The web server (in this repo). -- The [CLI](https://github.com/alan-turing-institute/rctab-cli), which allows command line interaction with this API and handles authentication. -- Three [Function Apps](https://github.com/alan-turing-institute/rctab-functions), which run background jobs to interact with Azure: - - Billing: Gets usage data from Azure and posts to this API. - - Status: Gets information about subscriptions from Azure, such as state and RBAC and posts to this API. - - Controller: Gets list of subscriptions and their desired state from the API and then enables or disables subscriptions on Azure. -- An [authentication library](https://github.com/alan-turing-institute/fastapimsal), which handles authentication using Microsoft's MSAL library for FastAPI. - -In a typical setup: - -- The web server and three function apps are deployed to Azure. -- End users can log in to the web server's frontend to check their subscriptions' balances. -- The Billing and Status function apps run on a schedule to collect information about subscriptions and post it to the web server. -- The Controller function app will run on a schedule to check which subscriptions need to be turned off or turned on for. -- The CLI can be installed by admins on their local machines to check or adjust subscriptions' budgets. -- Admins can also connect directly to the database (e.g. with `psql`) if they need more detail than the CLI or frontend provides. -- The web server will email users about changes to their subscriptions. - -## Minimal Setup - -The simplest setup would comprise the web server, database and one or more function apps running on your local machine. -The steps to set this up are: - -1. Clone this repo and follow the development instructions in the [./README.md](./README.md) to set up a database & web server and to register an application with Active Directory. -2. Clone the Function Apps [repo](https://github.com/alan-turing-institute/rctab-functions) and follow the installation instructions in that README too. - Note that the function apps rely on several environment variables to be able to communicate with the web server. - In particular, they need to be given the hostname and port of the web server and each function app needs the private half of a key pair (the public halves are set as environment variables on the web server). -3. As per their documentation, each of the function apps requires a different set of permissions to function correctly. - Once you have the right role assignments, you should log in to Azure with Visual Studio or the Azure CLI so that the function apps can authenticate as you. -4. With the web server listening, you can run the Status and/or Billing function apps to populate the database. -5. With data in your database, you can do any of the following: - - View it via the web server frontend - - Approve some budget for your subscription to spend with the RCTab CLI - - Disable and enable subscriptions by running the Controller function app From 3a4aeed697f0543c9cf57e38fdf985cf84c79868 Mon Sep 17 00:00:00 2001 From: Iain-S <25081046+Iain-S@users.noreply.github.com> Date: Wed, 20 Sep 2023 17:36:12 +0100 Subject: [PATCH 07/11] Add docs build badge to readme --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index f009268..2915fee 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,10 @@ # RCTab API -![dev-deploy](https://github.com/alan-turing-institute/rctab-api/actions/workflows/dev-test-deploy.yml/badge.svg) ![docker-deploy](https://github.com/alan-turing-institute/rctab-api/actions/workflows/deploy.yml/badge.svg) ![tests](https://github.com/alan-turing-institute/rctab-api/actions/workflows/test.yml/badge.svg) ![linter](https://github.com/alan-turing-institute/rctab-api/actions/workflows/linter.yml/badge.svg) - [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit)](https://github.com/pre-commit/pre-commit) -![Coverage Badge](https://rctabtestcoverageblob.blob.core.windows.net/rctabcoveragecontainer/coverage-badge.svg) +[![Documentation Status](https://readthedocs.org/projects/rctab-api/badge/?version=latest)](https://rctab-api.readthedocs.io/en/latest/?badge=latest) The RCTab API webserver. From a6e1746cb5e5324bf1ede9f8f61126e34e69b308 Mon Sep 17 00:00:00 2001 From: Iain-S <25081046+Iain-S@users.noreply.github.com> Date: Fri, 22 Sep 2023 16:42:35 +0100 Subject: [PATCH 08/11] Move setup to docs from README --- README.md | 405 +---------------------------------- docs/conf.py | 6 + docs/content/installation.md | 1 + docs/content/logging.md | 118 ++++++++++ docs/content/setup.md | 284 ++++++++++++++++++++++++ docs/index.rst | 12 +- docs/sometext.rst | 4 - 7 files changed, 421 insertions(+), 409 deletions(-) create mode 100644 docs/content/installation.md create mode 100644 docs/content/logging.md create mode 100644 docs/content/setup.md delete mode 100644 docs/sometext.rst diff --git a/README.md b/README.md index 2915fee..480771a 100644 --- a/README.md +++ b/README.md @@ -8,407 +8,4 @@ The RCTab API webserver. -## Additional Components - -This is the server component of the RCTab Azure subscription management system. -The [RCTab.md](./RCTab.md) document gives an overview of the whole system. - -## Development Setup - -1. Clone the repo. -1. [Set up Poetry](#set-up-Poetry) -1. [Set up Pre-Commit](#set-up-pre-commit) -1. You will need to set some environment variables. - Instructions are in the [Server Configuration](#server-configuration) section. -1. You have the option of either installing the RCTab API natively or building a Docker container. - **Note** that unit tests are currently only supported with the "Run Natively" method. - - For the former, see [Run Natively](#run-natively) - - For the latter, see [Run in a Container](#run-in-a-container). - -### Set up Poetry - -Make sure you have [Poetry](https://python-poetry.org/docs/) installed. -Poetry will manage our virtual environments, as well as our dependencies, which are specified in [pyproject.toml](./pyproject.toml) and [poetry.lock](./poetry.lock). - -Start by setting up the environment: - -```bash -poetry env use python3 -``` - -Now spawn a poetry shell, this will create a virtual environment for the project. -**Keep this virtual environment activated for the remaining steps.** - -```bash -poetry shell -``` - -Install Python dependencies specified in the `poetry.lock` file: - -```bash -poetry install -``` - -All required packages should now be installed. - -### Set up Pre-Commit - -Linting is managed by [Pre-Commit](https://pre-commit.com). -Install the Pre-Commit hooks: - -```bash -pre-commit install -``` - -and run them with: - -```bash -pre-commit run --all-files -``` - -They should all pass. - -### Server Configuration - -The web app is configured through a series of environment variables, read by a Pydantic [BaseSettings](https://pydantic-docs.helpmanual.io/usage/settings/) class. -Create a minimal `.env` file for the web app by copying the example environment file: - -```bash -cp example.env .env -``` - -If you end up using a PostgreSQL port other than `5432` or a password other than `password` in the [PostgreSQL Container](#postgresql-container) section then you should edit `.env` to specify `DB_PORT` and/or `DB_PASSWORD`. -For the full range of settings, see [Settings.py](rctab/settings.py) - -Create a minimal `.auth.env` file for [Microsoft Authentication Library (MSAL)](https://learn.microsoft.com/en-us/azure/active-directory/develop/msal-overview) by copying the example: - -```bash -cp example.auth.env .auth.env -``` - -Note that these are only suitable for getting a development environment up and running. -Never add `.env` nor `.auth.env` to version control and do not use the default password or session secret in a real deployment. - -[//]: # (For the full explanation of the auth settings, see register the app.) -[//]: # (If you wish to send email notifications, set up a SendGrid account and then replace `{YOUR_SENDGRID_KEY} with a SendGrid API key.) - -### Run Natively - -#### Pre-requisites - -Make sure you are inside the virtual environment and have [Set up Poetry](#set-up-Poetry). - -If you are using macOS, you may need to install some libraries that are needed for PDF generation. -You might want to do this with Homebrew: - -```bash -brew install cairo pango libffi -``` - -If you continue to get "library not found" errors when running the RCTab API or unit tests then you may need to find the `brew` library directory and prepend it to your DYLD_LIBRARY_PATH environment variable with something like: - -```bash -export DYLD_LIBRARY_PATH="/opt/homebrew/lib/:$DYLD_LIBRARY_PATH" -``` - -#### PostgreSQL Container - -The RCTab API web server needs a PostgreSQL database to store Azure subscription and user details. -You can install PostgreSQL on your development machine (e.g. with Homebrew) or use a container. -If you want to use the latter option, you can install Docker and run: - -```bash -docker create \ - --name rctab_db \ - --publish 5432:5432 \ - --env POSTGRES_PASSWORD=password \ - --volume "$(pwd)/.postgresdata":"/var/lib/postgresql/data" \ - postgres:11 -``` - -This will create a container based on the latest PostgreSQL 11 image on DockerHub and - -- name it `rctab_db` -- expose port 5432 on the container as port 5432 on the host -- set the default `postgres` user's password to `password` - -It also creates a directory called `.postgresdata` in the current directory and mount it on the container as the default PostgreSQL data directory. -This is optional but makes it easy to delete the test data with `rm -r .postgresdata`. - -You can now start the container with - -```bash -docker start rctab_db -``` - -You can stop it at any time with `docker stop rctab_db`. - -#### Create database schema - -Before you start the API we must create the database schema: - -```bash -scripts/prestart.sh -``` - -#### Running tests - -##### Manually - -With the Poetry shell activated and our PostgreSQL database running, we can run all tests with - -```bash -TESTING=true pytest tests/ -``` - -The `TESTING=true` env var is important so that database commits are rolled back between each unit test. - -**Note:** This will remove the contents of any [postgreSQL containers](#postgresql-container) you have running. If you don't want to lose them use [the helper script](#with-the-helper-script). - -##### With the helper script - -With the Poetry shell activated but no PostgreSQL database running (to avoid port conflicts), we can run all tests with: - -```bash -./scripts/runtests.sh -``` - -[runtests.sh](./scripts/runtests.sh) is a convenience script that creates a temporary database, using Docker or Podman, for the duration of the test suite. - -By default, the script will pull and run a Postgres container, set appropriate environment variables and run all tests. -The container will be cleaned up after the tests conclude (or fail). -The container image will not be removed. -To use Podman instead of Docker, use the `-p` flag. -Extra arguments can be passed to pytest using the `-e flag`. -_e.g._ `./scripts/runtests.sh -e '-vvv'`. -To see all options run `./scripts/runtests.sh -h`. - -Note: Running this tests this way, or manually, will remove any existing data in the local database. To run the tests without removing existing data use: - -```bash -./scripts/testcode.sh -``` - -This does the same thing as `runtests.sh` but any existing/running databases are stopped first to ensure the testing doesn't wipe them. They are then restarted once the script has finished. This means your databases will not be removed by running tests. - -#### Run the API in development mode - -To start the API server run: - -```bash -uvicorn rctab:app --reload --reload-dir rctab -``` - -You should be able to view the Login page, but you will not be able to log in until you have completed the [Application Registration](#application-registration) steps. - -### Run in a Container - -#### Build - -A container image of the web app can be built using the [Dockerfile](./Dockerfile). - -For example, - -```bash -docker build -t rctab:latest . -``` - -or, using Podman, - -```bash -podman build -t rctab:latest . -``` - -#### Run - -It is easiest to start a database and RCTab API server with the [`docker-compose-local.yaml`](./compose/docker-compose-local.yaml) file: - -```bash -docker-compose -f compose/docker-compose-local.yaml up -d -``` - -and stop it with: - -```bash -docker-compose -f compose/docker-compose-local.yaml down -``` - -Podman also supports Docker Compose since Podman 3.0.0. -This will require starting the `podman.service` systemd unit and pointing Docker Compose to `podman.socket` using the `DOCKER_HOST` environment variable. - -For example, - -```bash -systemctl --user start podman.service -export DOCKER_HOST="unix://$XDG_RUNTIME_DIR/podman/podman.sock" -``` - -If you do not want Docker Compose to start a database for you (e.g. because you have manually started a separate [PostgreSQL Container](#postgresql-container) or because you are connecting to an external database), you can use the other Compose file: - -```bash -docker-compose -f compose/docker-compose-external.yaml up -d -``` - -**Note** that environment variables declared in the compose file will override those from env files. - -#### Visit the homepage - -You should be able to view the login page at `http://localhost:8000`, but you will not be able to log in until you have completed the [Application Registration](#application-registration) steps. - -## Application Registration - -RCTab uses the Microsoft Authentication Library (MSAL) for authentication. -[This](https://docs.microsoft.com/en-us/azure/active-directory/develop/web-app-quickstart?pivots=devlang-python#how-the-sample-works) diagram gives some idea of the authentication flow. -For it to work, you will need to change the dummy `TENANT_ID`, `CLIENT_ID` and `CLIENT_SECRET` values in `.auth.env` that were created in the [Server Configuration](#server-configuration) step. - -If you are joining an existing RCTab project, someone will have already registered an application with Azure Active Directory (AD). -In that case, whoever registered the application will need to share the Azure AD Tenant ID, the Application ID (a.k.a. Client ID) and Client Secret with you. - -If an application hasn't already been registered, Microsoft's instructions are [here](https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app#configure-platform-settings). -Your organisation should already have an AD Tenant ID, the Application ID will be given to you during the registration process, and you will need to generate a Client Secret. - -## Enterprise Agreement (EA) Routes - -EA routes (those in [rctab/routers/accounting](rctab/routers/accounting)) use JSON Web Tokens ([JWT](https://jwt.io/introduction)) to authenticate. -Some EA routes are only used by the RCTab function apps. -The apps sign a JWT with a private key, and the API must have the corresponding public key to verify the token. -There are instructions for generating the key pairs in the function apps' repo. -Once generated, you can come back here and append the public keys to the `.env` file. - -You will want to do something like: - -```bash -echo USAGE_FUNC_PUBLIC_KEY={public-key-contents} >> .env -``` - -where `public-key-contents` is a string containing the contents of the public key. - -## Infrastructure Deployment - -RCTab is deployed via a GitHub action which builds an image and pushes it to DockerHub. -The Docker image is pushed by GitHub whenever a new Release is made and pulled by Azure whenever the RCTab API web app restarts. - -To change the deployed infrastructure or to deploy a new Pulumi [stack](https://www.pulumi.com/docs/intro/concepts/stack/), see the [deploy_infrastructure](docs/deploy_infrastructure.md) document. - -If you need to manually build a Docker image and push it to DockerHub, the steps are (approximately): - -1. `docker build -t my-dockerhub-id/rctab:my-image-tag .` -1. `docker push my-dockerhub-id/rctab:my-image-tag` - -## Logging guidelines - -### Logging module - -Import the `logging` module in the Python scripts or modules where you want to log information. - -```python -import logging -``` - -### Log levels - -The default logging threshold is `WARNING`. To change this, set a `LOG_LEVEL` environment variable to the desired log level. - -```bash -export LOG_LEVEL=INFO -``` - -When logging a message, choose the level that corresponds to the severity of the message. The following guideline might be useful: - -- `INFO`: General information about the application’s progress. -- `WARNING`: Indication of potential issues or unexpected behavior that may need attention. -- `DEBUG`: Detailed information, typically useful only for debugging purposes. -- `ERROR`: Record of error events that might still allow the application to continue running. -- `CRITICAL`: Severe error event that may cause the application to terminate. - -### Log messages - -Log messages should be descriptive and provide context that help to understand the log entry. - -Use a `Logger` to log message. First, create an instance of a `Logger` object to log messages: - -```python -# Logger for current module -logger = logging.getLogger(__name__) - -# Log messages with different severity -logger.info("This is just a message with some information.") -logger.warning("Something unexpected occured.") -logger.error("An error occured.") -logger.exception("This logs an error message along with the traceback.") -``` - -This creates a `Logger` on module level, where `__name__` is the module’s name in the Python package namespace. -To log a message, choose the function that reflects the appropriate severity. When exceptions occur,`logger.exception()` can be very useful for debugging, since it logs the error message along the traceback. - -### Centralised Logging - -#### Azure Application Insights - -It can be useful to collect and view the Python logs produced by the RCTab API and the Azure function apps in one place. -To do so, the logs can be sent to [Azure Application Insights](https://learn.microsoft.com/en-us/azure/azure-monitor/app/app-insights-overview?tabs=net), which can, amongst other things, collect and store trace logging data. - -You need to provide a connection string to an Azure Application Insights resource to which the logs can be sent. - -- If you want to set up a new Azure Application Insights resource, follow these [instructions](https://learn.microsoft.com/en-us/azure/azure-monitor/app/create-new-resource?tabs=net#create-an-application-insights-resource-1). -- To find the connection string, navigate to the Azure Application Insights resource on the Azure portal. -- Add the connection string either - - to the `.env` file, if you run the RCTab API locally: - `CENTRAL_LOGGING_CONNECTION_STRING="my-connection-string"` - - or to the Azure portal Application Settings, if you deployed the RCTab API to Azure: - - On the left hand panel under `Settings` select `Configruation` - - Under the tab `Application settings`, click `+ New application setting` - - Provide the name `CENTRAL_LOGGING_CONNECTION_STRING` and as value the connection string to your Azure Application Insights resource. - - Save the new setting. - -#### Custom logging functionality - -Once you added a connection string to the `.env` file or to the Application Settings on the Azure portal, by default, all logging from `rctab/` and further down in the Python module hierarchy are sent to the Application Insights. -You can change this by providing a name for the logger in `set_log_handler(name="my-logger-name")` during `startup()` in `main.py`. - -The custom functionality to centralise logging can be found in `rctab/logutils.py`. -It uses the [OpenCensus Python SDK](https://learn.microsoft.com/en-us/azure/azure-monitor/app/opencensus-python) to send logs to the Application Insights. - -The function `set_log_handler()` in `rctab/logutils.py` - -- Gets a logger of provided name (default: `rctab`) -- Adds an `AzureLogHandler` to this logger if a connection string is provided. -- Adds a filter that appends custom dimensions in form of a key-value pair (e.g. `{"logger_name": "logger_rctab"`) to each log record. - The custom dimensions are hard-coded in the function definition of `set_log_handler()` and can be changed there. - -#### View logs on Azure portal - -To view the log messages, - -- Go to the Application Insights resource on the Azure portal. -- Navigate to `Logs`. -- The logs are in the `traces` table under the `Tables` tab. - -An example query to explore the table contents could be - -```kusto -traces -| extend logger_name = tostring(customDimensions.logger_name) -| extend module = tostring(customDimensions.module) -| extend line_number = tostring(customDimensions.lineNumber) -``` - -Running the query will show you the logs (within the specified `Time range`) in a table layout including columns for the logger name, the module that logged the message and the corresponding line in the code. - -You can further filter the log messages on the Azure portal. -For example - -```kusto -traces -| extend logger_name = tostring(customDimensions.logger_name) -| extend module = tostring(customDimensions.module) -| extend line_number = tostring(customDimensions.lineNumber) -| where module == "main" -| where message contains "Starting server" -``` - -The log levels used with the Python `Logger` correspond to `severityLevel` in `traces` on the Azure portal. `severityLevel` uses integer numbers. For convenience, these numbers can be easily substituted with the more familiar corresponding log levels by adding the following line to the `kusto` query: - -```kusto -| extend log_level = case(severityLevel == 1, "INFO", severityLevel == 2, "WARNING", severityLevel == 3,"ERROR", "UNKNOWN") -``` +You can find the full RCTab docs [here](https://rctab.readthedocs.io/), the online API documentation [here](https://rctab-api.readthedocs.io/) and the documentation source code in [docs/](docs/). diff --git a/docs/conf.py b/docs/conf.py index 6070a9a..14cf1d9 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -39,6 +39,7 @@ "sphinx.ext.autodoc", "sphinx.ext.autosummary", "sphinx.ext.napoleon", + "myst_parser", ] # -- Options for HTML output @@ -58,3 +59,8 @@ def setup(app): # type: ignore # -- Options for autosummary extension autosummary_generate = True + +# -- Options for MyST + +# myst_heading_slug_func = my_slug_func +myst_heading_anchors = 5 diff --git a/docs/content/installation.md b/docs/content/installation.md new file mode 100644 index 0000000..25267fe --- /dev/null +++ b/docs/content/installation.md @@ -0,0 +1 @@ +# Installation diff --git a/docs/content/logging.md b/docs/content/logging.md new file mode 100644 index 0000000..ca78fd5 --- /dev/null +++ b/docs/content/logging.md @@ -0,0 +1,118 @@ + +# Logging + +## Logging module + +Import the `logging` module in the Python scripts or modules where you want to log information. + +```python +import logging +``` + +## Log levels + +The default logging threshold is `WARNING`. To change this, set a `LOG_LEVEL` environment variable to the desired log level. + +```bash +export LOG_LEVEL=INFO +``` + +When logging a message, choose the level that corresponds to the severity of the message. The following guideline might be useful: + +- `INFO`: General information about the application’s progress. +- `WARNING`: Indication of potential issues or unexpected behavior that may need attention. +- `DEBUG`: Detailed information, typically useful only for debugging purposes. +- `ERROR`: Record of error events that might still allow the application to continue running. +- `CRITICAL`: Severe error event that may cause the application to terminate. + +## Log messages + +Log messages should be descriptive and provide context that help to understand the log entry. + +Use a `Logger` to log message. First, create an instance of a `Logger` object to log messages: + +```python +# Logger for current module +logger = logging.getLogger(__name__) + +# Log messages with different severity +logger.info("This is just a message with some information.") +logger.warning("Something unexpected occured.") +logger.error("An error occured.") +logger.exception("This logs an error message along with the traceback.") +``` + +This creates a `Logger` on module level, where `__name__` is the module’s name in the Python package namespace. +To log a message, choose the function that reflects the appropriate severity. When exceptions occur,`logger.exception()` can be very useful for debugging, since it logs the error message along the traceback. + +## Centralised Logging + +### Azure Application Insights + +It can be useful to collect and view the Python logs produced by the RCTab API and the Azure function apps in one place. +To do so, the logs can be sent to [Azure Application Insights](https://learn.microsoft.com/en-us/azure/azure-monitor/app/app-insights-overview?tabs=net), which can, amongst other things, collect and store trace logging data. + +You need to provide a connection string to an Azure Application Insights resource to which the logs can be sent. + +- If you want to set up a new Azure Application Insights resource, follow these [instructions](https://learn.microsoft.com/en-us/azure/azure-monitor/app/create-new-resource?tabs=net#create-an-application-insights-resource-1). +- To find the connection string, navigate to the Azure Application Insights resource on the Azure portal. +- Add the connection string either + - to the `.env` file, if you run the RCTab API locally: + `CENTRAL_LOGGING_CONNECTION_STRING="my-connection-string"` + - or to the Azure portal Application Settings, if you deployed the RCTab API to Azure: + - On the left hand panel under `Settings` select `Configruation` + - Under the tab `Application settings`, click `+ New application setting` + - Provide the name `CENTRAL_LOGGING_CONNECTION_STRING` and as value the connection string to your Azure Application Insights resource. + - Save the new setting. + +### Custom logging functionality + +Once you added a connection string to the `.env` file or to the Application Settings on the Azure portal, by default, all logging from `rctab/` and further down in the Python module hierarchy are sent to the Application Insights. +You can change this by providing a name for the logger in `set_log_handler(name="my-logger-name")` during `startup()` in `main.py`. + +The custom functionality to centralise logging can be found in `rctab/logutils.py`. +It uses the [OpenCensus Python SDK](https://learn.microsoft.com/en-us/azure/azure-monitor/app/opencensus-python) to send logs to the Application Insights. + +The function `set_log_handler()` in `rctab/logutils.py` + +- Gets a logger of provided name (default: `rctab`) +- Adds an `AzureLogHandler` to this logger if a connection string is provided. +- Adds a filter that appends custom dimensions in form of a key-value pair (e.g. `{"logger_name": "logger_rctab"`) to each log record. + The custom dimensions are hard-coded in the function definition of `set_log_handler()` and can be changed there. + +### View logs on Azure portal + +To view the log messages, + +- Go to the Application Insights resource on the Azure portal. +- Navigate to `Logs`. +- The logs are in the `traces` table under the `Tables` tab. + +An example query to explore the table contents could be + +```text +traces +| extend logger_name = tostring(customDimensions.logger_name) +| extend module = tostring(customDimensions.module) +| extend line_number = tostring(customDimensions.lineNumber) +``` + +Running the query will show you the logs (within the specified `Time range`) in a table layout including columns for the logger name, the module that logged the message and the corresponding line in the code. + +You can further filter the log messages on the Azure portal. +For example + +```text +traces +| extend logger_name = tostring(customDimensions.logger_name) +| extend module = tostring(customDimensions.module) +| extend line_number = tostring(customDimensions.lineNumber) +| where module == "main" +| where message contains "Starting server" +``` + +The log levels used with the Python `Logger` correspond to `severityLevel` in `traces` on the Azure portal. `severityLevel` uses integer numbers. For convenience, these numbers can be easily substituted with the more familiar corresponding log levels by adding the following line to the `kusto` query: + +```text +| extend log_level = case(severityLevel == 1, "INFO", severityLevel == 2, "WARNING", severityLevel == 3,"ERROR", "UNKNOWN") +``` diff --git a/docs/content/setup.md b/docs/content/setup.md new file mode 100644 index 0000000..afd0e26 --- /dev/null +++ b/docs/content/setup.md @@ -0,0 +1,284 @@ +# Setup + +## Local Setup + +To run the API locally (e.g. for development): + +1. Clone the repo. +1. [Set up Poetry](#set-up-poetry) +1. [Set up Pre-Commit](#set-up-pre-commit) +1. You will need to set some environment variables. + Instructions are in the [Server Configuration](#server-configuration) section. +1. You have the option of either installing the RCTab API natively or building a Docker container. + **Note** that unit tests are currently only supported with the "Run Natively" method. + - For the former, see [Run Natively](#run-natively) + - For the latter, see [Run in a Container](#run-in-a-container). + +### Set up Poetry + +Make sure you have [Poetry](https://python-poetry.org/docs/) installed. + +Start by setting up the environment: + +```bash +poetry env use python3 +``` + +Now spawn a poetry shell, this will create a virtual environment for the project. +**Keep this virtual environment activated for the remaining steps.** + +```bash +poetry shell +``` + +Install Python dependencies specified in the ``poetry.lock`` file: + +```bash +poetry install +``` + +All required packages should now be installed. + +### Set up Pre-Commit + +Linting is managed by [Pre-Commit](https://pre-commit.com). +Install the Pre-Commit hooks: + +```bash +pre-commit install +``` + +and run them with: + +```bash +pre-commit run --all-files +``` + +They should all pass. + +### Server Configuration + +The web app is configured through a series of environment variables, read by a Pydantic [BaseSettings](https://pydantic-docs.helpmanual.io/usage/settings/) class. +Create a minimal `.env` file for the web app by copying the example environment file: + +```bash +cp example.env .env +``` + +If you end up using a PostgreSQL port other than `5432` or a password other than `password` in the [PostgreSQL Container](#postgresql-container) section then you should edit `.env` to specify `DB_PORT` and/or `DB_PASSWORD`. +For the full range of settings, see the ``settings.py`` module. + +Create a minimal `.auth.env` file for [Microsoft Authentication Library (MSAL)](https://learn.microsoft.com/en-us/azure/active-directory/develop/msal-overview) by copying the example: + +```bash +cp example.auth.env .auth.env +``` + +Note that these are only suitable for getting a development environment up and running. +Never add `.env` nor `.auth.env` to version control and do not use the default password or session secret in a real deployment. +For the full explanation of the auth settings, see [Application Registration](#application-registration). + +[//]: # (If you wish to send email notifications, set up a SendGrid account and then replace `{YOUR_SENDGRID_KEY} with a SendGrid API key.) + +### Run Natively + +#### Pre-requisites + +Make sure you are inside the virtual environment and have [Set up Poetry](#set-up-poetry). + +If you are using macOS, you may need to install some libraries that are needed for PDF generation. +You might want to do this with Homebrew: + +```bash +brew install cairo pango libffi +``` + +If you continue to get "library not found" errors when running the RCTab API or unit tests then you may need to find the `brew` library directory and prepend it to your DYLD_LIBRARY_PATH environment variable with something like: + +```bash +export DYLD_LIBRARY_PATH="/opt/homebrew/lib/:$DYLD_LIBRARY_PATH" +``` + +#### PostgreSQL Container + +The RCTab API web server needs a PostgreSQL database to store Azure subscription and user details. +You can install PostgreSQL on your development machine (e.g. with Homebrew) or use a container. +If you want to use the latter option, you can install Docker and run: + +```bash +docker create \ + --name rctab_db \ + --publish 5432:5432 \ + --env POSTGRES_PASSWORD=password \ + --volume "$(pwd)/.postgresdata":"/var/lib/postgresql/data" \ + postgres:14 +``` + +This will create a container based on the latest PostgreSQL 14 image on DockerHub and + +- name it `rctab_db` +- expose port 5432 on the container as port 5432 on the host +- set the default `postgres` user's password to `password` + +It also creates a directory called `.postgresdata` in the current directory and mount it on the container as the default PostgreSQL data directory. +This is optional but makes it easy to delete the test data with `rm -r .postgresdata`. + +You can now start the container with + +```bash +docker start rctab_db +``` + +You can stop it at any time with `docker stop rctab_db`. + +#### Create database schema + +Before you start the API we must create the database schema: + +```bash +scripts/prestart.sh +``` + +#### Running tests + +##### Manually + +With the Poetry shell activated and our PostgreSQL database running, we can run all tests with + +```bash +TESTING=true pytest tests/ +``` + +The `TESTING=true` env var is important so that database commits are rolled back between each unit test. + +**Note:** This will remove the contents of any [postgreSQL containers](#postgresql-container) you have running. If you don't want to lose them use [the helper script](#with-the-helper-script). + +##### With the helper script + +With the Poetry shell activated but no PostgreSQL database running (to avoid port conflicts), we can run all tests with: + +```bash +./scripts/runtests.sh +``` + +`runtests.sh` is a convenience script that creates a temporary database, using Docker or Podman, for the duration of the test suite. + +By default, the script will pull and run a Postgres container, set appropriate environment variables and run all tests. +The container will be cleaned up after the tests conclude (or fail). +The container image will not be removed. +To use Podman instead of Docker, use the `-p` flag. +Extra arguments can be passed to pytest using the `-e flag`. +_e.g._ `./scripts/runtests.sh -e '-vvv'`. +To see all options run `./scripts/runtests.sh -h`. + +Note: Running this tests this way, or manually, will remove any existing data in the local database. To run the tests without removing existing data use: + +```bash +./scripts/testcode.sh +``` + +This does the same thing as `runtests.sh` but any existing/running databases are stopped first to ensure the testing doesn't wipe them. They are then restarted once the script has finished. This means your databases will not be removed by running tests. + +#### Run the API in development mode + +To start the API server run: + +```bash +uvicorn rctab:app --reload --reload-dir rctab +``` + +You should be able to view the Login page, but you will not be able to log in until you have completed the [Application Registration](#application-registration) steps. + +### Run in a Container + +#### Build + +A container image of the web app can be built using the [Dockerfile](https://github.com/alan-turing-institute/rctab-api/tree/main/Dockerfile). + +For example, + +```bash +docker build -t rctab:latest . +``` + +or, using Podman, + +```bash +podman build -t rctab:latest . +``` + +#### Run + +It is easiest to start a database and RCTab API server with the [`docker-compose-local.yaml`](https://github.com/alan-turing-institute/rctab-api/tree/main/compose/docker-compose-local.yaml) file: + +```bash +docker-compose -f compose/docker-compose-local.yaml up -d +``` + +and stop it with: + +```bash +docker-compose -f compose/docker-compose-local.yaml down +``` + +Podman also supports Docker Compose since Podman 3.0.0. +This will require starting the `podman.service` systemd unit and pointing Docker Compose to `podman.socket` using the `DOCKER_HOST` environment variable. + +For example, + +```bash +systemctl --user start podman.service +export DOCKER_HOST="unix://$XDG_RUNTIME_DIR/podman/podman.sock" +``` + +If you do not want Docker Compose to start a database for you (e.g. because you have manually started a separate [PostgreSQL Container](#postgresql-container) or because you are connecting to an external database), you can use the other Compose file: + +```bash +docker-compose -f compose/docker-compose-external.yaml up -d +``` + +**Note** that environment variables declared in the compose file will override those from env files. + +#### Visit the homepage + +You should be able to view the login page at `http://localhost:8000`, but you will not be able to log in until you have completed the [Application Registration](#application-registration) steps. + +## Application Registration + +RCTab uses the Microsoft Authentication Library (MSAL) for authentication. +[This](https://docs.microsoft.com/en-us/azure/active-directory/develop/web-app-quickstart?pivots=devlang-python#how-the-sample-works) diagram gives some idea of the authentication flow. +For it to work, you will need to change the dummy `TENANT_ID`, `CLIENT_ID` and `CLIENT_SECRET` values in `.auth.env` that were created in the [Server Configuration](#server-configuration) step. + +If you are joining an existing RCTab project, someone will have already registered an application with Azure Active Directory (AD). +In that case, whoever registered the application will need to share the Azure AD Tenant ID, the Application ID (a.k.a. Client ID) and Client Secret with you. + +If an application hasn't already been registered, Microsoft's instructions are [here](https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app#configure-platform-settings). +Your organisation should already have an AD Tenant ID, the Application ID will be given to you during the registration process, and you will need to generate a Client Secret. + +## Accounting Routes + +Accounting routes (those in [rctab/routers/accounting](https://github.com/alan-turing-institute/rctab-api/tree/main/rctab/routers/accounting)) use JSON Web Tokens ([JWT](https://jwt.io/introduction)) to authenticate. +Some routes are only used by the RCTab function apps. +The apps sign a JWT with a private key, and the API must have the corresponding public key to verify the token. +There are instructions for generating the key pairs in the Function Apps' docs. +Once generated, you can come back here and append the public keys to your `.env` file. + +You will want to do something like: + +```bash +echo USAGE_FUNC_PUBLIC_KEY={public-key-contents} >> .env +``` + +where `public-key-contents` is a string containing the contents of the Usage function's public key. + +## Infrastructure Deployment + +New versions of the RCTab API are deployed via a GitHub workflow which builds an image and pushes it to DockerHub. +The Docker image is pushed by GitHub whenever a new release is made and pulled by Azure whenever the RCTab API web app restarts. + +To change the deployed infrastructure or to deploy a new Pulumi [stack](https://www.pulumi.com/docs/intro/concepts/stack/), see the RCTab Infrastructure docs. + +If you need to manually build a Docker image and push it to DockerHub, the steps are (approximately): + +1. `docker build -t my-dockerhub-id/rctab:my-image-tag .` +1. `docker push my-dockerhub-id/rctab:my-image-tag` diff --git a/docs/index.rst b/docs/index.rst index 95604da..fedca69 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,12 +1,22 @@ Welcome to rctab-api's documentation! ================================================ +.. toctree:: + :maxdepth: 2 + :caption: External Links + :glob: + :hidden: + + RCTab docs home + .. toctree:: :maxdepth: 2 :caption: Contents :hidden: + :glob: - sometext + Home + content/* .. autosummary:: :toctree: _autosummary diff --git a/docs/sometext.rst b/docs/sometext.rst deleted file mode 100644 index 4d116f0..0000000 --- a/docs/sometext.rst +++ /dev/null @@ -1,4 +0,0 @@ -Some Text ---------- - -Lorem ipsum... From 4ea107f0aaae4148b59d48732364d749f2b875d4 Mon Sep 17 00:00:00 2001 From: Iain-S <25081046+Iain-S@users.noreply.github.com> Date: Fri, 22 Sep 2023 17:10:48 +0100 Subject: [PATCH 09/11] Fix linter errors --- docs/conf.py | 1 - poetry.lock | 2 +- pyproject.toml | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 14cf1d9..f9f5a12 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -62,5 +62,4 @@ def setup(app): # type: ignore # -- Options for MyST -# myst_heading_slug_func = my_slug_func myst_heading_anchors = 5 diff --git a/poetry.lock b/poetry.lock index aad397e..e43348b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -4002,4 +4002,4 @@ docs = ["myst-parser", "sphinx-rtd-theme", "sphinxcontrib-napoleon"] [metadata] lock-version = "2.0" python-versions = ">=3.10 <3.12" -content-hash = "5125da5d64a2d6172dc041e8da2dab86898946768e4d9f38d582d4eaa032b04a" +content-hash = "038ba677880087bfbac9293c594a85febc6965ee94d821e594b78cc0a96eb2d3" diff --git a/pyproject.toml b/pyproject.toml index 460ad9a..88f38b3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,7 +56,7 @@ pydocstyle = "^6.3.0" hypothesis = "^6.82.6" [tool.poetry.extras] -docs = ["sphinx-rtd-theme", "sphinxcontrib-napoleon", "myst-parser", "sphinx-subprojecttoctree"] +docs = ["sphinx-rtd-theme", "sphinxcontrib-napoleon", "myst-parser"] [tool.isort] profile = "black" From 19c1f20e58e8c28b694a21650164583467b99a66 Mon Sep 17 00:00:00 2001 From: Iain-S <25081046+Iain-S@users.noreply.github.com> Date: Fri, 22 Sep 2023 17:27:21 +0100 Subject: [PATCH 10/11] Install --all-extras in GH workflow --- .github/workflows/linter.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 76edb2b..ec68670 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -44,6 +44,6 @@ jobs: run: poetry check --lock - name: Install dependencies - run: poetry install + run: poetry install --all-extras - uses: pre-commit/action@v3.0.0 From 4bfa54929d126afe2c2a1772b8f254f5a26a3bbe Mon Sep 17 00:00:00 2001 From: Iain-S <25081046+Iain-S@users.noreply.github.com> Date: Wed, 11 Oct 2023 15:59:25 +0100 Subject: [PATCH 11/11] Change method of patching settings and db in docs --- docs/conf.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index f9f5a12..a3155fb 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,19 +1,25 @@ """Configuration file for the Sphinx documentation builder.""" +import os from importlib import metadata -from unittest.mock import MagicMock +from unittest.mock import patch -import databases -import pydantic import sphinx_rtd_theme # pylint: disable=invalid-name -# Patch settings base class to avoid having to set env vars - -pydantic.BaseSettings = MagicMock() # type: ignore -databases.Database = MagicMock() # type: ignore -# pylint: disable=wrong-import-position -import rctab +# Set mandatory env vars +os.environ["SESSION_EXPIRE_TIME_MINUTES"] = "1" +os.environ["SESSION_SECRET"] = "don't use this in production" +os.environ["CLIENT_ID"] = "00000000-0000-0000-0000-000000000000" +os.environ["CLIENT_SECRET"] = "this is a secret" +os.environ["TENANT_ID"] = "00000000-0000-0000-0000-000000000000" +os.environ["DB_HOST"] = "localhost" +os.environ["DB_PASSWORD"] = "notarealpassword" +os.environ["DB_USER"] = "the_username" + +with patch("databases.Database"): + # pylint: disable=wrong-import-position + import rctab # pylint: enable=wrong-import-position