From 0124a33e6c4e894c464cadbff826079a1d0121a9 Mon Sep 17 00:00:00 2001
From: alexferl <me@alexferl.com>
Date: Wed, 3 Apr 2024 14:26:56 -0400
Subject: [PATCH 1/5] update project, merge PRs

Signed-off-by: alexferl <me@alexferl.com>
---
 .editorconfig                                |  16 +
 .github/workflows/test.yaml                  |  71 +++
 .pre-commit-config.yaml                      |  34 +-
 .travis.yml                                  |  15 -
 AUTHORS                                      |   8 -
 Makefile                                     |  47 +-
 Pipfile                                      |  13 +
 Pipfile.lock                                 | 374 ++++++++++++
 README.md                                    |  32 +-
 VERSION                                      |   1 +
 dev_requirements.txt                         |   6 -
 docs/Makefile                                | 177 ------
 docs/_themes/LICENSE                         |  37 --
 docs/_themes/README                          |  31 -
 docs/_themes/flask/layout.html               |  24 -
 docs/_themes/flask/relations.html            |  19 -
 docs/_themes/flask/static/flasky.css_t       | 577 -------------------
 docs/_themes/flask/theme.conf                |   9 -
 docs/_themes/flask_small/layout.html         |  22 -
 docs/_themes/flask_small/static/flasky.css_t | 287 ---------
 docs/_themes/flask_small/theme.conf          |  10 -
 docs/_themes/flask_theme_support.py          |  89 ---
 docs/conf.py                                 | 286 ---------
 docs/index.rst                               | 206 -------
 examples/basic_auth/app.py                   |   4 +-
 examples/basic_auth/app_oldap.py             |   8 +-
 examples/blueprints/blueprints/config.py     |   8 +-
 examples/groups/app_oldap.py                 |   6 +-
 flask_simpleldap/__init__.py                 |  66 +--
 pyproject.toml                               |   3 +
 requirements-dev.txt                         |  19 +
 requirements.txt                             |  13 +-
 setup.cfg                                    |  29 +
 setup.py                                     |  40 --
 tests/__init__.py                            |   0
 tests/test_simpleldap.py                     |  32 +
 36 files changed, 659 insertions(+), 1960 deletions(-)
 create mode 100644 .editorconfig
 create mode 100644 .github/workflows/test.yaml
 delete mode 100644 .travis.yml
 delete mode 100644 AUTHORS
 create mode 100644 Pipfile
 create mode 100644 Pipfile.lock
 create mode 100644 VERSION
 delete mode 100644 dev_requirements.txt
 delete mode 100644 docs/Makefile
 delete mode 100644 docs/_themes/LICENSE
 delete mode 100644 docs/_themes/README
 delete mode 100644 docs/_themes/flask/layout.html
 delete mode 100644 docs/_themes/flask/relations.html
 delete mode 100644 docs/_themes/flask/static/flasky.css_t
 delete mode 100644 docs/_themes/flask/theme.conf
 delete mode 100644 docs/_themes/flask_small/layout.html
 delete mode 100644 docs/_themes/flask_small/static/flasky.css_t
 delete mode 100644 docs/_themes/flask_small/theme.conf
 delete mode 100644 docs/_themes/flask_theme_support.py
 delete mode 100644 docs/conf.py
 delete mode 100644 docs/index.rst
 create mode 100644 pyproject.toml
 create mode 100644 requirements-dev.txt
 create mode 100644 setup.cfg
 delete mode 100644 setup.py
 create mode 100644 tests/__init__.py
 create mode 100644 tests/test_simpleldap.py

diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..15a145a
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,16 @@
+root = true
+
+[*]
+end_of_line = lf
+insert_final_newline = true
+charset = utf-8
+trim_trailing_whitespace = true
+indent_style = space
+indent_size = 2
+
+[*.py]
+indent_size = 4
+
+[Makefile]
+indent_style = tab
+indent_size = 4
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
new file mode 100644
index 0000000..98f942b
--- /dev/null
+++ b/.github/workflows/test.yaml
@@ -0,0 +1,71 @@
+name: Test
+on:
+  push:
+    branches:
+      - master
+  pull_request:
+    branches:
+      - master
+jobs:
+  build:
+    runs-on: '${{ matrix.os }}'
+    strategy:
+      matrix:
+        include:
+          - os: ubuntu-latest
+            python-version: '3.8'
+            FLASK: 3.0.2
+          - os: ubuntu-latest
+            python-version: '3.8'
+            FLASK: 2.3.3
+          - os: ubuntu-latest
+            python-version: '3.8'
+            FLASK: 2.2.5
+          - os: ubuntu-latest
+            python-version: '3.9'
+            FLASK: 3.0.2
+          - os: ubuntu-latest
+            python-version: '3.9'
+            FLASK: 2.3.3
+          - os: ubuntu-latest
+            python-version: '3.9'
+            FLASK: 2.2.5
+          - os: ubuntu-latest
+            python-version: '3.10'
+            FLASK: 3.0.2
+          - os: ubuntu-latest
+            python-version: '3.10'
+            FLASK: 2.3.3
+          - os: ubuntu-latest
+            python-version: '3.10'
+            FLASK: 2.2.5
+          - os: ubuntu-latest
+            python-version: '3.11'
+            FLASK: 3.0.2
+          - os: ubuntu-latest
+            python-version: '3.11'
+            FLASK: 2.3.3
+          - os: ubuntu-latest
+            python-version: '3.11'
+            FLASK: 2.2.5
+          - os: ubuntu-latest
+            python-version: '3.12'
+            FLASK: 3.0.2
+          - os: ubuntu-latest
+            python-version: '3.12'
+            FLASK: 2.3.3
+          - os: ubuntu-latest
+            python-version: '3.12'
+            FLASK: 2.2.5
+    steps:
+      - name: 'Set up Python ${{ matrix.python-version }}'
+        uses: actions/setup-python@v2
+        with:
+          python-version: '${{ matrix.python-version }}'
+      - uses: actions/checkout@v2
+      - run: pip install pytest Flask==$FLASK
+        env:
+          FLASK: '${{ matrix.FLASK }}'
+      - run: pip install -r requirements-dev.txt .
+      - run: pip install .
+      - run: pytest
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index c74020e..fe8e9bf 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,15 +1,21 @@
 repos:
-- repo: https://github.com/pre-commit/pre-commit-hooks
-  rev: v4.1.0
-  hooks:
-  - id: trailing-whitespace
-  - id: end-of-file-fixer
-  - id: mixed-line-ending
-    args: ['--fix=lf']
-    description: Forces to replace line ending by the UNIX 'lf' character.
-- repo: https://github.com/psf/black
-  rev: 22.1.0
-  hooks:
-  - id: black
-    language_version: python3
-    args: [-t, py310]
+  - repo: https://github.com/pre-commit/pre-commit-hooks
+    rev: v4.5.0
+    hooks:
+      - id: trailing-whitespace
+      - id: end-of-file-fixer
+      - id: mixed-line-ending
+        args: ['--fix=lf']
+        description: Forces to replace line ending by the UNIX 'lf' character.
+  - repo: https://github.com/psf/black
+    rev: 24.3.0
+    hooks:
+      - id: black
+        language_version: python3
+  - repo: https://github.com/d-ryzhykau/pipenv-lock-pre-commit
+    rev: 0.5.0
+    hooks:
+      - id: pipenv-lock
+      - id: pipenv-verify
+      - id: pipenv-requirements
+      - id: pipenv-requirements-dev
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 2ddc001..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,15 +0,0 @@
-language: python
-sudo: required
-dist: focal
-python:
-  - "3.7"
-  - "3.8"
-  - "3.9"
-env:
-  - FLASK=2.0.2
-  - FLASK=1.1.4
-  - FLASK=1.0.4
-install:
-  - pip install Flask==$FLASK
-  - pip install -r dev_requirements.txt
-script: python setup.py test
diff --git a/AUTHORS b/AUTHORS
deleted file mode 100644
index 0850a48..0000000
--- a/AUTHORS
+++ /dev/null
@@ -1,8 +0,0 @@
-==================================
- AUTHORS (in chronological order)
-==================================
-
-Alexandre Ferland (admiralobvious)
-Chris Seymour (iiSeymour)
-Timothy Allen (OptiverTimAll)
-Jose Manuel (jm66)
diff --git a/Makefile b/Makefile
index 16ef367..291af74 100644
--- a/Makefile
+++ b/Makefile
@@ -1,47 +1,24 @@
-.PHONY: help dev clean update test lint pre-commit
-
-VENV_NAME?=venv
-VENV_ACTIVATE=. $(VENV_NAME)/bin/activate
-PYTHON=${VENV_NAME}/bin/python3
+.PHONY: help dev test lint pre-commit
 
 .DEFAULT: help
 help:
 	@echo "make dev"
-	@echo "       prepare development environment, use only once"
-	@echo "make clean"
-	@echo "       delete development environment"
-	@echo "make update"
-	@echo "       update dependencies"
+	@echo "	prepare development environment"
 	@echo "make test"
-	@echo "       run tests"
+	@echo "	run tests"
 	@echo "make lint"
-	@echo "       run black"
+	@echo "	run black"
 	@echo "make pre-commit"
-	@echo "       run pre-commit hooks"
+	@echo "	run pre-commit hooks"
 
 dev:
-	make venv
-
-venv: $(VENV_NAME)/bin/activate
-$(VENV_NAME)/bin/activate:
-	test -d $(VENV_NAME) || virtualenv -p python3 $(VENV_NAME)
-	${PYTHON} -m pip install -U pip
-	${PYTHON} -m pip install -r dev_requirements.txt
-	$(VENV_NAME)/bin/pre-commit install
-	touch $(VENV_NAME)/bin/activate
-
-clean:
-	rm -rf venv
-
-update:
-	${PYTHON} -m pip install -U -r dev_requirements.txt
-	$(VENV_NAME)/bin/pre-commit install
+	pipenv install --dev
 
-test: venv
-	${PYTHON} -m pytest
+test:
+	pipenv run pytest
 
-lint: venv
-	$(VENV_NAME)/bin/black -t py310 --exclude $(VENV_NAME) .
+lint:
+	pipenv run black .
 
-pre-commit: venv
-	$(VENV_NAME)/bin/pre-commit
+pre-commit:
+	pipenv run pre-commit
diff --git a/Pipfile b/Pipfile
new file mode 100644
index 0000000..4113b29
--- /dev/null
+++ b/Pipfile
@@ -0,0 +1,13 @@
+[[source]]
+url = "https://pypi.org/simple"
+verify_ssl = true
+name = "pypi"
+
+[packages]
+flask = "==3.0.2"
+python-ldap = "==3.4.4"
+
+[dev-packages]
+black = "==24.3.0"
+pre-commit = "==3.5.0"
+pytest = "==8.1.1"
diff --git a/Pipfile.lock b/Pipfile.lock
new file mode 100644
index 0000000..f8328c1
--- /dev/null
+++ b/Pipfile.lock
@@ -0,0 +1,374 @@
+{
+    "_meta": {
+        "hash": {
+            "sha256": "5fbd42bf4fcec6b59a08e107f90dca4cb5bb8a9312a2234733e7d413cce97960"
+        },
+        "pipfile-spec": 6,
+        "requires": {},
+        "sources": [
+            {
+                "name": "pypi",
+                "url": "https://pypi.org/simple",
+                "verify_ssl": true
+            }
+        ]
+    },
+    "default": {
+        "blinker": {
+            "hashes": [
+                "sha256:c3f865d4d54db7abc53758a01601cf343fe55b84c1de4e3fa910e420b438d5b9",
+                "sha256:e6820ff6fa4e4d1d8e2747c2283749c3f547e4fee112b98555cdcdae32996182"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==1.7.0"
+        },
+        "click": {
+            "hashes": [
+                "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28",
+                "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==8.1.7"
+        },
+        "flask": {
+            "hashes": [
+                "sha256:3232e0e9c850d781933cf0207523d1ece087eb8d87b23777ae38456e2fbe7c6e",
+                "sha256:822c03f4b799204250a7ee84b1eddc40665395333973dfb9deebfe425fefcb7d"
+            ],
+            "index": "pypi",
+            "markers": "python_version >= '3.8'",
+            "version": "==3.0.2"
+        },
+        "itsdangerous": {
+            "hashes": [
+                "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44",
+                "sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==2.1.2"
+        },
+        "jinja2": {
+            "hashes": [
+                "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa",
+                "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==3.1.3"
+        },
+        "markupsafe": {
+            "hashes": [
+                "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf",
+                "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff",
+                "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f",
+                "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3",
+                "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532",
+                "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f",
+                "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617",
+                "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df",
+                "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4",
+                "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906",
+                "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f",
+                "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4",
+                "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8",
+                "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371",
+                "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2",
+                "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465",
+                "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52",
+                "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6",
+                "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169",
+                "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad",
+                "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2",
+                "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0",
+                "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029",
+                "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f",
+                "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a",
+                "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced",
+                "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5",
+                "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c",
+                "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf",
+                "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9",
+                "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb",
+                "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad",
+                "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3",
+                "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1",
+                "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46",
+                "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc",
+                "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a",
+                "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee",
+                "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900",
+                "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5",
+                "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea",
+                "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f",
+                "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5",
+                "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e",
+                "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a",
+                "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f",
+                "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50",
+                "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a",
+                "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b",
+                "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4",
+                "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff",
+                "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2",
+                "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46",
+                "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b",
+                "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf",
+                "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5",
+                "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5",
+                "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab",
+                "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd",
+                "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==2.1.5"
+        },
+        "pyasn1": {
+            "hashes": [
+                "sha256:3a35ab2c4b5ef98e17dfdec8ab074046fbda76e281c5a706ccd82328cfc8f64c",
+                "sha256:cca4bb0f2df5504f02f6f8a775b6e416ff9b0b3b16f7ee80b5a3153d9b804473"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==0.6.0"
+        },
+        "pyasn1-modules": {
+            "hashes": [
+                "sha256:831dbcea1b177b28c9baddf4c6d1013c24c3accd14a1873fffaa6a2e905f17b6",
+                "sha256:be04f15b66c206eed667e0bb5ab27e2b1855ea54a842e5037738099e8ca4ae0b"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==0.4.0"
+        },
+        "python-ldap": {
+            "hashes": [
+                "sha256:7edb0accec4e037797705f3a05cbf36a9fde50d08c8f67f2aef99a2628fab828"
+            ],
+            "index": "pypi",
+            "markers": "python_version >= '3.6'",
+            "version": "==3.4.4"
+        },
+        "werkzeug": {
+            "hashes": [
+                "sha256:3aac3f5da756f93030740bc235d3e09449efcf65f2f55e3602e1d851b8f48795",
+                "sha256:e39b645a6ac92822588e7b39a692e7828724ceae0b0d702ef96701f90e70128d"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==3.0.2"
+        }
+    },
+    "develop": {
+        "black": {
+            "hashes": [
+                "sha256:2818cf72dfd5d289e48f37ccfa08b460bf469e67fb7c4abb07edc2e9f16fb63f",
+                "sha256:41622020d7120e01d377f74249e677039d20e6344ff5851de8a10f11f513bf93",
+                "sha256:4acf672def7eb1725f41f38bf6bf425c8237248bb0804faa3965c036f7672d11",
+                "sha256:4be5bb28e090456adfc1255e03967fb67ca846a03be7aadf6249096100ee32d0",
+                "sha256:4f1373a7808a8f135b774039f61d59e4be7eb56b2513d3d2f02a8b9365b8a8a9",
+                "sha256:56f52cfbd3dabe2798d76dbdd299faa046a901041faf2cf33288bc4e6dae57b5",
+                "sha256:65b76c275e4c1c5ce6e9870911384bff5ca31ab63d19c76811cb1fb162678213",
+                "sha256:65c02e4ea2ae09d16314d30912a58ada9a5c4fdfedf9512d23326128ac08ac3d",
+                "sha256:6905238a754ceb7788a73f02b45637d820b2f5478b20fec82ea865e4f5d4d9f7",
+                "sha256:79dcf34b33e38ed1b17434693763301d7ccbd1c5860674a8f871bd15139e7837",
+                "sha256:7bb041dca0d784697af4646d3b62ba4a6b028276ae878e53f6b4f74ddd6db99f",
+                "sha256:7d5e026f8da0322b5662fa7a8e752b3fa2dac1c1cbc213c3d7ff9bdd0ab12395",
+                "sha256:9f50ea1132e2189d8dff0115ab75b65590a3e97de1e143795adb4ce317934995",
+                "sha256:a0c9c4a0771afc6919578cec71ce82a3e31e054904e7197deacbc9382671c41f",
+                "sha256:aadf7a02d947936ee418777e0247ea114f78aff0d0959461057cae8a04f20597",
+                "sha256:b5991d523eee14756f3c8d5df5231550ae8993e2286b8014e2fdea7156ed0959",
+                "sha256:bf21b7b230718a5f08bd32d5e4f1db7fc8788345c8aea1d155fc17852b3410f5",
+                "sha256:c45f8dff244b3c431b36e3224b6be4a127c6aca780853574c00faf99258041eb",
+                "sha256:c7ed6668cbbfcd231fa0dc1b137d3e40c04c7f786e626b405c62bcd5db5857e4",
+                "sha256:d7de8d330763c66663661a1ffd432274a2f92f07feeddd89ffd085b5744f85e7",
+                "sha256:e19cb1c6365fd6dc38a6eae2dcb691d7d83935c10215aef8e6c38edee3f77abd",
+                "sha256:e2af80566f43c85f5797365077fb64a393861a3730bd110971ab7a0c94e873e7"
+            ],
+            "index": "pypi",
+            "markers": "python_version >= '3.8'",
+            "version": "==24.3.0"
+        },
+        "cfgv": {
+            "hashes": [
+                "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9",
+                "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==3.4.0"
+        },
+        "click": {
+            "hashes": [
+                "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28",
+                "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==8.1.7"
+        },
+        "distlib": {
+            "hashes": [
+                "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784",
+                "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"
+            ],
+            "version": "==0.3.8"
+        },
+        "filelock": {
+            "hashes": [
+                "sha256:5ffa845303983e7a0b7ae17636509bc97997d58afeafa72fb141a17b152284cb",
+                "sha256:a79895a25bbefdf55d1a2a0a80968f7dbb28edcd6d4234a0afb3f37ecde4b546"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==3.13.3"
+        },
+        "identify": {
+            "hashes": [
+                "sha256:10a7ca245cfcd756a554a7288159f72ff105ad233c7c4b9c6f0f4d108f5f6791",
+                "sha256:c4de0081837b211594f8e877a6b4fad7ca32bbfc1a9307fdd61c28bfe923f13e"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==2.5.35"
+        },
+        "iniconfig": {
+            "hashes": [
+                "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3",
+                "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==2.0.0"
+        },
+        "mypy-extensions": {
+            "hashes": [
+                "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d",
+                "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"
+            ],
+            "markers": "python_version >= '3.5'",
+            "version": "==1.0.0"
+        },
+        "nodeenv": {
+            "hashes": [
+                "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2",
+                "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"
+            ],
+            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'",
+            "version": "==1.8.0"
+        },
+        "packaging": {
+            "hashes": [
+                "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5",
+                "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==24.0"
+        },
+        "pathspec": {
+            "hashes": [
+                "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08",
+                "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==0.12.1"
+        },
+        "platformdirs": {
+            "hashes": [
+                "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068",
+                "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==4.2.0"
+        },
+        "pluggy": {
+            "hashes": [
+                "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981",
+                "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==1.4.0"
+        },
+        "pre-commit": {
+            "hashes": [
+                "sha256:5804465c675b659b0862f07907f96295d490822a450c4c40e747d0b1c6ebcb32",
+                "sha256:841dc9aef25daba9a0238cd27984041fa0467b4199fc4852e27950664919f660"
+            ],
+            "index": "pypi",
+            "markers": "python_version >= '3.8'",
+            "version": "==3.5.0"
+        },
+        "pytest": {
+            "hashes": [
+                "sha256:2a8386cfc11fa9d2c50ee7b2a57e7d898ef90470a7a34c4b949ff59662bb78b7",
+                "sha256:ac978141a75948948817d360297b7aae0fcb9d6ff6bc9ec6d514b85d5a65c044"
+            ],
+            "index": "pypi",
+            "markers": "python_version >= '3.8'",
+            "version": "==8.1.1"
+        },
+        "pyyaml": {
+            "hashes": [
+                "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5",
+                "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc",
+                "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df",
+                "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741",
+                "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206",
+                "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27",
+                "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595",
+                "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62",
+                "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98",
+                "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696",
+                "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290",
+                "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9",
+                "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d",
+                "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6",
+                "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867",
+                "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47",
+                "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486",
+                "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6",
+                "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3",
+                "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007",
+                "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938",
+                "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0",
+                "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c",
+                "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735",
+                "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d",
+                "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28",
+                "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4",
+                "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba",
+                "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8",
+                "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef",
+                "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5",
+                "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd",
+                "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3",
+                "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0",
+                "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515",
+                "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c",
+                "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c",
+                "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924",
+                "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34",
+                "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43",
+                "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859",
+                "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673",
+                "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54",
+                "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a",
+                "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b",
+                "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab",
+                "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa",
+                "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c",
+                "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585",
+                "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d",
+                "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"
+            ],
+            "markers": "python_version >= '3.6'",
+            "version": "==6.0.1"
+        },
+        "setuptools": {
+            "hashes": [
+                "sha256:0ff4183f8f42cd8fa3acea16c45205521a4ef28f73c6391d8a25e92893134f2e",
+                "sha256:c21c49fb1042386df081cb5d86759792ab89efca84cf114889191cd09aacc80c"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==69.2.0"
+        },
+        "virtualenv": {
+            "hashes": [
+                "sha256:961c026ac520bac5f69acb8ea063e8a4f071bcc9457b9c1f28f6b085c511583a",
+                "sha256:e08e13ecdca7a0bd53798f356d5831434afa5b07b93f0abdf0797b7a06ffe197"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==20.25.1"
+        }
+    }
+}
diff --git a/README.md b/README.md
index 126c350..58ad28b 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,9 @@
-Flask-SimpleLDAP [![Build Status](https://app.travis-ci.com/alexferl/flask-simpleldap.svg?branch=master)](https://app.travis-ci.com/alexferl/flask-simpleldap)
-================
-
+# Flask-SimpleLDAP
 Flask-SimpleLDAP provides LDAP authentication for Flask.
 
-Flask-SimpleLDAP is compatible with and tested on Python 3.7+.
-
-Quickstart
-----------
+Flask-SimpleLDAP is compatible with and tested on Python 3.8+.
 
+## Quickstart
 First, install Flask-SimpleLDAP:
 
 ```shell
@@ -16,10 +12,10 @@ pip install flask-simpleldap
 
 
 Flask-SimpleLDAP depends, and will install for you, recent versions of Flask
-(0.12.4 or later) and [python-ldap](https://python-ldap.org/).
+(2.2.5 or later) and [python-ldap](https://python-ldap.org/).
 Please consult the [python-ldap installation instructions](https://www.python-ldap.org/en/latest/installing.html) if you get an error during installation.
 
-Next, add an ``LDAP`` instance to your code and at least the three
+Next, add an `LDAP` instance to your code and at least the three
 required configuration options. The complete sample from
 [examples/basic_auth/app.py](examples/basic_auth/app.py) looks like this:
 
@@ -52,18 +48,18 @@ of the LDAP user, e.g. `me@mydomain.com`.
 
 Once you get the basic example working, check out the more complex ones:
 
-* [examples/groups](examples/groups) demostrates using:
-  * `@ldap.login_required` for form/cookie-based auth, instead of basic HTTP authentication.
-  * `@ldap.group_required()` to restrict access to pages based on the user's LDAP groups.
-* [examples/blueprints](examples/blueprints) implements the same functionality, but uses Flask's
-[application factories](http://flask.pocoo.org/docs/patterns/appfactories/)
-and [blueprints](http://flask.pocoo.org/docs/blueprints/).
+- [examples/groups](examples/groups) demonstrates using:
+  - `@ldap.login_required` for form/cookie-based auth, instead of basic HTTP authentication.
+  - `@ldap.group_required()` to restrict access to pages based on the user's LDAP groups.
+- [examples/blueprints](examples/blueprints) implements the same functionality, but uses Flask's
+[application factories](https://flask.palletsprojects.com/en/3.0.x/patterns/appfactories/)
+and [blueprints](https://flask.palletsprojects.com/en/3.0.x/blueprints/).
 
 
 OpenLDAP
 --------
 
-Add the ``LDAP`` instance to your code and depending on your OpenLDAP
+Add the `LDAP` instance to your code and depending on your OpenLDAP
 configuration, add the following at least LDAP_USER_OBJECT_FILTER and
 LDAP_USER_OBJECT_FILTER.
 
@@ -102,8 +98,6 @@ if __name__ == "__main__":
     app.run()
 ```
 
-Resources
----------
+## Resources
 
-- [Documentation](http://flask-simpleldap.readthedocs.org/en/latest/)
 - [PyPI](https://pypi.python.org/pypi/Flask-SimpleLDAP)
diff --git a/VERSION b/VERSION
new file mode 100644
index 0000000..227cea2
--- /dev/null
+++ b/VERSION
@@ -0,0 +1 @@
+2.0.0
diff --git a/dev_requirements.txt b/dev_requirements.txt
deleted file mode 100644
index 4c1a726..0000000
--- a/dev_requirements.txt
+++ /dev/null
@@ -1,6 +0,0 @@
-black==24.3.0
-pre-commit==2.17.0
-python-ldap==3.4.0  # here instead of requirements.txt so rtfd can build
-Sphinx==2.1.2
-
--r requirements.txt
diff --git a/docs/Makefile b/docs/Makefile
deleted file mode 100644
index 372cedc..0000000
--- a/docs/Makefile
+++ /dev/null
@@ -1,177 +0,0 @@
-# Makefile for Sphinx documentation
-#
-
-# You can set these variables from the command line.
-SPHINXOPTS    =
-SPHINXBUILD   = sphinx-build
-PAPER         =
-BUILDDIR      = _build
-
-# User-friendly check for sphinx-build
-ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
-$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
-endif
-
-# Internal variables.
-PAPEROPT_a4     = -D latex_paper_size=a4
-PAPEROPT_letter = -D latex_paper_size=letter
-ALLSPHINXOPTS   = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
-# the i18n builder cannot share the environment and doctrees with the others
-I18NSPHINXOPTS  = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
-
-.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
-
-help:
-	@echo "Please use \`make <target>' where <target> is one of"
-	@echo "  html       to make standalone HTML files"
-	@echo "  dirhtml    to make HTML files named index.html in directories"
-	@echo "  singlehtml to make a single large HTML file"
-	@echo "  pickle     to make pickle files"
-	@echo "  json       to make JSON files"
-	@echo "  htmlhelp   to make HTML files and a HTML help project"
-	@echo "  qthelp     to make HTML files and a qthelp project"
-	@echo "  devhelp    to make HTML files and a Devhelp project"
-	@echo "  epub       to make an epub"
-	@echo "  latex      to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
-	@echo "  latexpdf   to make LaTeX files and run them through pdflatex"
-	@echo "  latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
-	@echo "  text       to make text files"
-	@echo "  man        to make manual pages"
-	@echo "  texinfo    to make Texinfo files"
-	@echo "  info       to make Texinfo files and run them through makeinfo"
-	@echo "  gettext    to make PO message catalogs"
-	@echo "  changes    to make an overview of all changed/added/deprecated items"
-	@echo "  xml        to make Docutils-native XML files"
-	@echo "  pseudoxml  to make pseudoxml-XML files for display purposes"
-	@echo "  linkcheck  to check all external links for integrity"
-	@echo "  doctest    to run all doctests embedded in the documentation (if enabled)"
-
-clean:
-	rm -rf $(BUILDDIR)/*
-
-html:
-	$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
-	@echo
-	@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
-
-dirhtml:
-	$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
-	@echo
-	@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
-
-singlehtml:
-	$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
-	@echo
-	@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
-
-pickle:
-	$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
-	@echo
-	@echo "Build finished; now you can process the pickle files."
-
-json:
-	$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
-	@echo
-	@echo "Build finished; now you can process the JSON files."
-
-htmlhelp:
-	$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
-	@echo
-	@echo "Build finished; now you can run HTML Help Workshop with the" \
-	      ".hhp project file in $(BUILDDIR)/htmlhelp."
-
-qthelp:
-	$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
-	@echo
-	@echo "Build finished; now you can run "qcollectiongenerator" with the" \
-	      ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
-	@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Flask-SimpleLDAP.qhcp"
-	@echo "To view the help file:"
-	@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Flask-SimpleLDAP.qhc"
-
-devhelp:
-	$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
-	@echo
-	@echo "Build finished."
-	@echo "To view the help file:"
-	@echo "# mkdir -p $$HOME/.local/share/devhelp/Flask-SimpleLDAP"
-	@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Flask-SimpleLDAP"
-	@echo "# devhelp"
-
-epub:
-	$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
-	@echo
-	@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
-
-latex:
-	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
-	@echo
-	@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
-	@echo "Run \`make' in that directory to run these through (pdf)latex" \
-	      "(use \`make latexpdf' here to do that automatically)."
-
-latexpdf:
-	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
-	@echo "Running LaTeX files through pdflatex..."
-	$(MAKE) -C $(BUILDDIR)/latex all-pdf
-	@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
-
-latexpdfja:
-	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
-	@echo "Running LaTeX files through platex and dvipdfmx..."
-	$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
-	@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
-
-text:
-	$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
-	@echo
-	@echo "Build finished. The text files are in $(BUILDDIR)/text."
-
-man:
-	$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
-	@echo
-	@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
-
-texinfo:
-	$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
-	@echo
-	@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
-	@echo "Run \`make' in that directory to run these through makeinfo" \
-	      "(use \`make info' here to do that automatically)."
-
-info:
-	$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
-	@echo "Running Texinfo files through makeinfo..."
-	make -C $(BUILDDIR)/texinfo info
-	@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
-
-gettext:
-	$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
-	@echo
-	@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
-
-changes:
-	$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
-	@echo
-	@echo "The overview file is in $(BUILDDIR)/changes."
-
-linkcheck:
-	$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
-	@echo
-	@echo "Link check complete; look for any errors in the above output " \
-	      "or in $(BUILDDIR)/linkcheck/output.txt."
-
-doctest:
-	$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
-	@echo "Testing of doctests in the sources finished, look at the " \
-	      "results in $(BUILDDIR)/doctest/output.txt."
-
-xml:
-	$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
-	@echo
-	@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
-
-pseudoxml:
-	$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
-	@echo
-	@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
diff --git a/docs/_themes/LICENSE b/docs/_themes/LICENSE
deleted file mode 100644
index 8daab7e..0000000
--- a/docs/_themes/LICENSE
+++ /dev/null
@@ -1,37 +0,0 @@
-Copyright (c) 2010 by Armin Ronacher.
-
-Some rights reserved.
-
-Redistribution and use in source and binary forms of the theme, with or
-without modification, are permitted provided that the following conditions
-are met:
-
-* Redistributions of source code must retain the above copyright
-  notice, this list of conditions and the following disclaimer.
-
-* Redistributions in binary form must reproduce the above
-  copyright notice, this list of conditions and the following
-  disclaimer in the documentation and/or other materials provided
-  with the distribution.
-
-* The names of the contributors may not be used to endorse or
-  promote products derived from this software without specific
-  prior written permission.
-
-We kindly ask you to only use these themes in an unmodified manner just
-for Flask and Flask-related products, not for unrelated projects.  If you
-like the visual style and want to use it for your own projects, please
-consider making some larger changes to the themes (such as changing
-font faces, sizes, colors or margins).
-
-THIS THEME IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
-ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
-LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
-CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
-SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
-INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
-CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
-ARISING IN ANY WAY OUT OF THE USE OF THIS THEME, EVEN IF ADVISED OF THE
-POSSIBILITY OF SUCH DAMAGE.
diff --git a/docs/_themes/README b/docs/_themes/README
deleted file mode 100644
index b3292bd..0000000
--- a/docs/_themes/README
+++ /dev/null
@@ -1,31 +0,0 @@
-Flask Sphinx Styles
-===================
-
-This repository contains sphinx styles for Flask and Flask related
-projects.  To use this style in your Sphinx documentation, follow
-this guide:
-
-1. put this folder as _themes into your docs folder.  Alternatively
-   you can also use git submodules to check out the contents there.
-2. add this to your conf.py:
-
-   sys.path.append(os.path.abspath('_themes'))
-   html_theme_path = ['_themes']
-   html_theme = 'flask'
-
-The following themes exist:
-
-- 'flask' - the standard flask documentation theme for large
-  projects
-- 'flask_small' - small one-page theme.  Intended to be used by
-  very small addon libraries for flask.
-
-The following options exist for the flask_small theme:
-
-   [options]
-   index_logo = ''              filename of a picture in _static
-                                to be used as replacement for the
-                                h1 in the index.rst file.
-   index_logo_height = 120px    height of the index logo
-   github_fork = ''             repository name on github for the
-                                "fork me" badge
diff --git a/docs/_themes/flask/layout.html b/docs/_themes/flask/layout.html
deleted file mode 100644
index 19c43fb..0000000
--- a/docs/_themes/flask/layout.html
+++ /dev/null
@@ -1,24 +0,0 @@
-{%- extends "basic/layout.html" %}
-{%- block extrahead %}
-  {{ super() }}
-  {% if theme_touch_icon %}
-  <link rel="apple-touch-icon" href="{{ pathto('_static/' ~ theme_touch_icon, 1) }}" />
-  {% endif %}
-  <meta name="viewport" content="width=device-width, initial-scale=0.9, maximum-scale=0.9">
-{% endblock %}
-{%- block relbar2 %}{% endblock %}
-{% block header %}
-  {{ super() }}
-  {% if pagename == 'index' %}
-  <div class=indexwrapper>
-  {% endif %}
-{% endblock %}
-{%- block footer %}
-  <div class="footer">
-    &copy; Copyright {{ copyright }}.
-    Created using <a href="http://sphinx.pocoo.org/">Sphinx</a>.
-  </div>
-  {% if pagename == 'index' %}
-  </div>
-  {% endif %}
-{%- endblock %}
diff --git a/docs/_themes/flask/relations.html b/docs/_themes/flask/relations.html
deleted file mode 100644
index 3bbcde8..0000000
--- a/docs/_themes/flask/relations.html
+++ /dev/null
@@ -1,19 +0,0 @@
-<h3>Related Topics</h3>
-<ul>
-  <li><a href="{{ pathto(master_doc) }}">Documentation overview</a><ul>
-  {%- for parent in parents %}
-  <li><a href="{{ parent.link|e }}">{{ parent.title }}</a><ul>
-  {%- endfor %}
-    {%- if prev %}
-      <li>Previous: <a href="{{ prev.link|e }}" title="{{ _('previous chapter')
-        }}">{{ prev.title }}</a></li>
-    {%- endif %}
-    {%- if next %}
-      <li>Next: <a href="{{ next.link|e }}" title="{{ _('next chapter')
-        }}">{{ next.title }}</a></li>
-    {%- endif %}
-  {%- for parent in parents %}
-  </ul></li>
-  {%- endfor %}
-  </ul></li>
-</ul>
diff --git a/docs/_themes/flask/static/flasky.css_t b/docs/_themes/flask/static/flasky.css_t
deleted file mode 100644
index 5906e75..0000000
--- a/docs/_themes/flask/static/flasky.css_t
+++ /dev/null
@@ -1,577 +0,0 @@
-/*
- * flasky.css_t
- * ~~~~~~~~~~~~
- *
- * :copyright: Copyright 2010 by Armin Ronacher.
- * :license: Flask Design License, see LICENSE for details.
- */
-
-{% set page_width = '940px' %}
-{% set sidebar_width = '220px' %}
- 
-@import url("basic.css");
- 
-/* -- page layout ----------------------------------------------------------- */
- 
-body {
-    font-family: 'Georgia', serif;
-    font-size: 17px;
-    background-color: white;
-    color: #000;
-    margin: 0;
-    padding: 0;
-}
-
-div.document {
-    width: {{ page_width }};
-    margin: 30px auto 0 auto;
-}
-
-div.documentwrapper {
-    float: left;
-    width: 100%;
-}
-
-div.bodywrapper {
-    margin: 0 0 0 {{ sidebar_width }};
-}
-
-div.sphinxsidebar {
-    width: {{ sidebar_width }};
-}
-
-hr {
-    border: 1px solid #B1B4B6;
-}
- 
-div.body {
-    background-color: #ffffff;
-    color: #3E4349;
-    padding: 0 30px 0 30px;
-}
-
-img.floatingflask {
-    padding: 0 0 10px 10px;
-    float: right;
-}
- 
-div.footer {
-    width: {{ page_width }};
-    margin: 20px auto 30px auto;
-    font-size: 14px;
-    color: #888;
-    text-align: right;
-}
-
-div.footer a {
-    color: #888;
-}
-
-div.related {
-    display: none;
-}
- 
-div.sphinxsidebar a {
-    color: #444;
-    text-decoration: none;
-    border-bottom: 1px dotted #999;
-}
-
-div.sphinxsidebar a:hover {
-    border-bottom: 1px solid #999;
-}
- 
-div.sphinxsidebar {
-    font-size: 14px;
-    line-height: 1.5;
-}
-
-div.sphinxsidebarwrapper {
-    padding: 18px 10px;
-}
-
-div.sphinxsidebarwrapper p.logo {
-    padding: 0 0 20px 0;
-    margin: 0;
-    text-align: center;
-}
- 
-div.sphinxsidebar h3,
-div.sphinxsidebar h4 {
-    font-family: 'Garamond', 'Georgia', serif;
-    color: #444;
-    font-size: 24px;
-    font-weight: normal;
-    margin: 0 0 5px 0;
-    padding: 0;
-}
-
-div.sphinxsidebar h4 {
-    font-size: 20px;
-}
- 
-div.sphinxsidebar h3 a {
-    color: #444;
-}
-
-div.sphinxsidebar p.logo a,
-div.sphinxsidebar h3 a,
-div.sphinxsidebar p.logo a:hover,
-div.sphinxsidebar h3 a:hover {
-    border: none;
-}
- 
-div.sphinxsidebar p {
-    color: #555;
-    margin: 10px 0;
-}
-
-div.sphinxsidebar ul {
-    margin: 10px 0;
-    padding: 0;
-    color: #000;
-}
- 
-div.sphinxsidebar input {
-    border: 1px solid #ccc;
-    font-family: 'Georgia', serif;
-    font-size: 1em;
-}
- 
-/* -- body styles ----------------------------------------------------------- */
- 
-a {
-    color: #004B6B;
-    text-decoration: underline;
-}
- 
-a:hover {
-    color: #6D4100;
-    text-decoration: underline;
-}
- 
-div.body h1,
-div.body h2,
-div.body h3,
-div.body h4,
-div.body h5,
-div.body h6 {
-    font-family: 'Garamond', 'Georgia', serif;
-    font-weight: normal;
-    margin: 30px 0px 10px 0px;
-    padding: 0;
-}
-
-{% if theme_index_logo %}
-div.indexwrapper h1 {
-    text-indent: -999999px;
-    background: url({{ theme_index_logo }}) no-repeat center center;
-    height: {{ theme_index_logo_height }};
-}
-{% endif %}
-div.body h1 { margin-top: 0; padding-top: 0; font-size: 240%; }
-div.body h2 { font-size: 180%; }
-div.body h3 { font-size: 150%; }
-div.body h4 { font-size: 130%; }
-div.body h5 { font-size: 100%; }
-div.body h6 { font-size: 100%; }
- 
-a.headerlink {
-    color: #ddd;
-    padding: 0 4px;
-    text-decoration: none;
-}
- 
-a.headerlink:hover {
-    color: #444;
-    background: #eaeaea;
-}
- 
-div.body p, div.body dd, div.body li {
-    line-height: 1.4em;
-}
-
-div.admonition {
-    background: #fafafa;
-    margin: 20px -30px;
-    padding: 10px 30px;
-    border-top: 1px solid #ccc;
-    border-bottom: 1px solid #ccc;
-}
-
-div.admonition tt.xref, div.admonition a tt {
-    border-bottom: 1px solid #fafafa;
-}
-
-dd div.admonition {
-    margin-left: -60px;
-    padding-left: 60px;
-}
-
-div.admonition p.admonition-title {
-    font-family: 'Garamond', 'Georgia', serif;
-    font-weight: normal;
-    font-size: 24px;
-    margin: 0 0 10px 0;
-    padding: 0;
-    line-height: 1;
-}
-
-div.admonition p.last {
-    margin-bottom: 0;
-}
-
-div.highlight {
-    background-color: white;
-}
-
-dt:target, .highlight {
-    background: #FAF3E8;
-}
-
-div.note {
-    background-color: #eee;
-    border: 1px solid #ccc;
-}
- 
-div.seealso {
-    background-color: #ffc;
-    border: 1px solid #ff6;
-}
- 
-div.topic {
-    background-color: #eee;
-}
- 
-p.admonition-title {
-    display: inline;
-}
- 
-p.admonition-title:after {
-    content: ":";
-}
-
-pre, tt {
-    font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
-    font-size: 0.9em;
-}
-
-img.screenshot {
-}
-
-tt.descname, tt.descclassname {
-    font-size: 0.95em;
-}
-
-tt.descname {
-    padding-right: 0.08em;
-}
-
-img.screenshot {
-    -moz-box-shadow: 2px 2px 4px #eee;
-    -webkit-box-shadow: 2px 2px 4px #eee;
-    box-shadow: 2px 2px 4px #eee;
-}
-
-table.docutils {
-    border: 1px solid #888;
-    -moz-box-shadow: 2px 2px 4px #eee;
-    -webkit-box-shadow: 2px 2px 4px #eee;
-    box-shadow: 2px 2px 4px #eee;
-}
-
-table.docutils td, table.docutils th {
-    border: 1px solid #888;
-    padding: 0.25em 0.7em;
-}
-
-table.field-list, table.footnote {
-    border: none;
-    -moz-box-shadow: none;
-    -webkit-box-shadow: none;
-    box-shadow: none;
-}
-
-table.footnote {
-    margin: 15px 0;
-    width: 100%;
-    border: 1px solid #eee;
-    background: #fdfdfd;
-    font-size: 0.9em;
-}
-
-table.footnote + table.footnote {
-    margin-top: -15px;
-    border-top: none;
-}
-
-table.field-list th {
-    padding: 0 0.8em 0 0;
-}
-
-table.field-list td {
-    padding: 0;
-}
-
-table.footnote td.label {
-    width: 0px;
-    padding: 0.3em 0 0.3em 0.5em;
-}
-
-table.footnote td {
-    padding: 0.3em 0.5em;
-}
-
-dl {
-    margin: 0;
-    padding: 0;
-}
-
-dl dd {
-    margin-left: 30px;
-}
-
-blockquote {
-    margin: 0 0 0 30px;
-    padding: 0;
-}
-
-ul, ol {
-    margin: 10px 0 10px 30px;
-    padding: 0;
-}
- 
-pre {
-    background: #eee;
-    padding: 7px 30px;
-    margin: 15px -30px;
-    line-height: 1.3em;
-}
-
-dl pre, blockquote pre, li pre {
-    margin-left: -60px;
-    padding-left: 60px;
-}
-
-dl dl pre {
-    margin-left: -90px;
-    padding-left: 90px;
-}
- 
-tt {
-    background-color: #ecf0f3;
-    color: #222;
-    /* padding: 1px 2px; */
-}
-
-tt.xref, a tt {
-    background-color: #FBFBFB;
-    border-bottom: 1px solid white;
-}
-
-a.reference {
-    text-decoration: none;
-    border-bottom: 1px dotted #004B6B;
-}
-
-a.reference:hover {
-    border-bottom: 1px solid #6D4100;
-}
-
-a.footnote-reference {
-    text-decoration: none;
-    font-size: 0.7em;
-    vertical-align: top;
-    border-bottom: 1px dotted #004B6B;
-}
-
-a.footnote-reference:hover {
-    border-bottom: 1px solid #6D4100;
-}
-
-a:hover tt {
-    background: #EEE;
-}
-
-
-@media screen and (max-width: 870px) {
-
-    div.sphinxsidebar {
-        display: none;
-    }
-
-    div.document {
-       width: 100%;
-
-    }
-
-    div.documentwrapper {
-        margin-left: 0;
-        margin-top: 0;
-        margin-right: 0;
-        margin-bottom: 0;
-    }
-
-    div.bodywrapper {
-        margin-top: 0;
-        margin-right: 0;
-        margin-bottom: 0;
-        margin-left: 0;
-    }
-
-    ul {
-        margin-left: 0;
-    }
-
-    .document {
-        width: auto;
-    }
-
-    .footer {
-        width: auto;
-    }
-
-    .bodywrapper {
-        margin: 0;
-    }
-
-    .footer {
-        width: auto;
-    }
-
-    .github {
-        display: none;
-    }
-
-
-
-}
-
-
-
-@media screen and (max-width: 875px) {
-
-    body {
-        margin: 0;
-        padding: 20px 30px;
-    }
-
-    div.documentwrapper {
-        float: none;
-        background: white;
-    }
-
-    div.sphinxsidebar {
-        display: block;
-        float: none;
-        width: 102.5%;
-        margin: 50px -30px -20px -30px;
-        padding: 10px 20px;
-        background: #333;
-        color: white;
-    }
-
-    div.sphinxsidebar h3, div.sphinxsidebar h4, div.sphinxsidebar p,
-    div.sphinxsidebar h3 a {
-        color: white;
-    }
-
-    div.sphinxsidebar a {
-        color: #aaa;
-    }
-
-    div.sphinxsidebar p.logo {
-        display: none;
-    }
-
-    div.document {
-        width: 100%;
-        margin: 0;
-    }
-
-    div.related {
-        display: block;
-        margin: 0;
-        padding: 10px 0 20px 0;
-    }
-
-    div.related ul,
-    div.related ul li {
-        margin: 0;
-        padding: 0;
-    }
-
-    div.footer {
-        display: none;
-    }
-
-    div.bodywrapper {
-        margin: 0;
-    }
-
-    div.body {
-        min-height: 0;
-        padding: 0;
-    }
-
-    .rtd_doc_footer {
-        display: none;
-    }
-
-    .document {
-        width: auto;
-    }
-
-    .footer {
-        width: auto;
-    }
-
-    .footer {
-        width: auto;
-    }
-
-    .github {
-        display: none;
-    }
-}
-
-
-/* scrollbars */
-
-::-webkit-scrollbar {
-    width: 6px;
-    height: 6px;
-}
-
-::-webkit-scrollbar-button:start:decrement,
-::-webkit-scrollbar-button:end:increment {
-    display: block;
-    height: 10px;
-}
-
-::-webkit-scrollbar-button:vertical:increment {
-    background-color: #fff;
-}
-
-::-webkit-scrollbar-track-piece {
-    background-color: #eee;
-    -webkit-border-radius: 3px;
-}
-
-::-webkit-scrollbar-thumb:vertical {
-    height: 50px;
-    background-color: #ccc;
-    -webkit-border-radius: 3px;
-}
-
-::-webkit-scrollbar-thumb:horizontal {
-    width: 50px;
-    background-color: #ccc;
-    -webkit-border-radius: 3px;
-}
-
-/* misc. */
-
-.revsys-inline {
-    display: none!important;
-}
\ No newline at end of file
diff --git a/docs/_themes/flask/theme.conf b/docs/_themes/flask/theme.conf
deleted file mode 100644
index 18c720f..0000000
--- a/docs/_themes/flask/theme.conf
+++ /dev/null
@@ -1,9 +0,0 @@
-[theme]
-inherit = basic
-stylesheet = flasky.css
-pygments_style = flask_theme_support.FlaskyStyle
-
-[options]
-index_logo = ''
-index_logo_height = 120px
-touch_icon = 
diff --git a/docs/_themes/flask_small/layout.html b/docs/_themes/flask_small/layout.html
deleted file mode 100644
index aa1716a..0000000
--- a/docs/_themes/flask_small/layout.html
+++ /dev/null
@@ -1,22 +0,0 @@
-{% extends "basic/layout.html" %}
-{% block header %}
-  {{ super() }}
-  {% if pagename == 'index' %}
-  <div class=indexwrapper>
-  {% endif %}
-{% endblock %}
-{% block footer %}
-  {% if pagename == 'index' %}
-  </div>
-  {% endif %}
-{% endblock %}
-{# do not display relbars #}
-{% block relbar1 %}{% endblock %}
-{% block relbar2 %}
-  {% if theme_github_fork %}
-    <a href="http://github.com/{{ theme_github_fork }}"><img style="position: fixed; top: 0; right: 0; border: 0;"
-    src="http://s3.amazonaws.com/github/ribbons/forkme_right_darkblue_121621.png" alt="Fork me on GitHub" /></a>
-  {% endif %}
-{% endblock %}
-{% block sidebar1 %}{% endblock %}
-{% block sidebar2 %}{% endblock %}
diff --git a/docs/_themes/flask_small/static/flasky.css_t b/docs/_themes/flask_small/static/flasky.css_t
deleted file mode 100644
index fe2141c..0000000
--- a/docs/_themes/flask_small/static/flasky.css_t
+++ /dev/null
@@ -1,287 +0,0 @@
-/*
- * flasky.css_t
- * ~~~~~~~~~~~~
- *
- * Sphinx stylesheet -- flasky theme based on nature theme.
- *
- * :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS.
- * :license: BSD, see LICENSE for details.
- *
- */
- 
-@import url("basic.css");
- 
-/* -- page layout ----------------------------------------------------------- */
- 
-body {
-    font-family: 'Georgia', serif;
-    font-size: 17px;
-    color: #000;
-    background: white;
-    margin: 0;
-    padding: 0;
-}
-
-div.documentwrapper {
-    float: left;
-    width: 100%;
-}
-
-div.bodywrapper {
-    margin: 40px auto 0 auto;
-    width: 700px;
-}
-
-hr {
-    border: 1px solid #B1B4B6;
-}
- 
-div.body {
-    background-color: #ffffff;
-    color: #3E4349;
-    padding: 0 30px 30px 30px;
-}
-
-img.floatingflask {
-    padding: 0 0 10px 10px;
-    float: right;
-}
- 
-div.footer {
-    text-align: right;
-    color: #888;
-    padding: 10px;
-    font-size: 14px;
-    width: 650px;
-    margin: 0 auto 40px auto;
-}
- 
-div.footer a {
-    color: #888;
-    text-decoration: underline;
-}
- 
-div.related {
-    line-height: 32px;
-    color: #888;
-}
-
-div.related ul {
-    padding: 0 0 0 10px;
-}
- 
-div.related a {
-    color: #444;
-}
- 
-/* -- body styles ----------------------------------------------------------- */
- 
-a {
-    color: #004B6B;
-    text-decoration: underline;
-}
- 
-a:hover {
-    color: #6D4100;
-    text-decoration: underline;
-}
-
-div.body {
-    padding-bottom: 40px; /* saved for footer */
-}
- 
-div.body h1,
-div.body h2,
-div.body h3,
-div.body h4,
-div.body h5,
-div.body h6 {
-    font-family: 'Garamond', 'Georgia', serif;
-    font-weight: normal;
-    margin: 30px 0px 10px 0px;
-    padding: 0;
-}
-
-{% if theme_index_logo %}
-div.indexwrapper h1 {
-    text-indent: -999999px;
-    background: url({{ theme_index_logo }}) no-repeat center center;
-    height: {{ theme_index_logo_height }};
-}
-{% endif %}
- 
-div.body h2 { font-size: 180%; }
-div.body h3 { font-size: 150%; }
-div.body h4 { font-size: 130%; }
-div.body h5 { font-size: 100%; }
-div.body h6 { font-size: 100%; }
- 
-a.headerlink {
-    color: white;
-    padding: 0 4px;
-    text-decoration: none;
-}
- 
-a.headerlink:hover {
-    color: #444;
-    background: #eaeaea;
-}
- 
-div.body p, div.body dd, div.body li {
-    line-height: 1.4em;
-}
-
-div.admonition {
-    background: #fafafa;
-    margin: 20px -30px;
-    padding: 10px 30px;
-    border-top: 1px solid #ccc;
-    border-bottom: 1px solid #ccc;
-}
-
-div.admonition p.admonition-title {
-    font-family: 'Garamond', 'Georgia', serif;
-    font-weight: normal;
-    font-size: 24px;
-    margin: 0 0 10px 0;
-    padding: 0;
-    line-height: 1;
-}
-
-div.admonition p.last {
-    margin-bottom: 0;
-}
-
-div.highlight{
-    background-color: white;
-}
-
-dt:target, .highlight {
-    background: #FAF3E8;
-}
-
-div.note {
-    background-color: #eee;
-    border: 1px solid #ccc;
-}
- 
-div.seealso {
-    background-color: #ffc;
-    border: 1px solid #ff6;
-}
- 
-div.topic {
-    background-color: #eee;
-}
- 
-div.warning {
-    background-color: #ffe4e4;
-    border: 1px solid #f66;
-}
- 
-p.admonition-title {
-    display: inline;
-}
- 
-p.admonition-title:after {
-    content: ":";
-}
-
-pre, tt {
-    font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
-    font-size: 0.85em;
-}
-
-img.screenshot {
-}
-
-tt.descname, tt.descclassname {
-    font-size: 0.95em;
-}
-
-tt.descname {
-    padding-right: 0.08em;
-}
-
-img.screenshot {
-    -moz-box-shadow: 2px 2px 4px #eee;
-    -webkit-box-shadow: 2px 2px 4px #eee;
-    box-shadow: 2px 2px 4px #eee;
-}
-
-table.docutils {
-    border: 1px solid #888;
-    -moz-box-shadow: 2px 2px 4px #eee;
-    -webkit-box-shadow: 2px 2px 4px #eee;
-    box-shadow: 2px 2px 4px #eee;
-}
-
-table.docutils td, table.docutils th {
-    border: 1px solid #888;
-    padding: 0.25em 0.7em;
-}
-
-table.field-list, table.footnote {
-    border: none;
-    -moz-box-shadow: none;
-    -webkit-box-shadow: none;
-    box-shadow: none;
-}
-
-table.footnote {
-    margin: 15px 0;
-    width: 100%;
-    border: 1px solid #eee;
-}
-
-table.field-list th {
-    padding: 0 0.8em 0 0;
-}
-
-table.field-list td {
-    padding: 0;
-}
-
-table.footnote td {
-    padding: 0.5em;
-}
-
-dl {
-    margin: 0;
-    padding: 0;
-}
-
-dl dd {
-    margin-left: 30px;
-}
- 
-pre {
-    padding: 0;
-    margin: 15px -30px;
-    padding: 8px;
-    line-height: 1.3em;
-    padding: 7px 30px;
-    background: #eee;
-    border-radius: 2px;
-    -moz-border-radius: 2px;
-    -webkit-border-radius: 2px;
-}
-
-dl pre {
-    margin-left: -60px;
-    padding-left: 60px;
-}
-
-tt {
-    background-color: #ecf0f3;
-    color: #222;
-    /* padding: 1px 2px; */
-}
-
-tt.xref, a tt {
-    background-color: #FBFBFB;
-}
-
-a:hover tt {
-    background: #EEE;
-}
diff --git a/docs/_themes/flask_small/theme.conf b/docs/_themes/flask_small/theme.conf
deleted file mode 100644
index 542b462..0000000
--- a/docs/_themes/flask_small/theme.conf
+++ /dev/null
@@ -1,10 +0,0 @@
-[theme]
-inherit = basic
-stylesheet = flasky.css
-nosidebar = true
-pygments_style = flask_theme_support.FlaskyStyle
-
-[options]
-index_logo = ''
-index_logo_height = 120px
-github_fork = ''
diff --git a/docs/_themes/flask_theme_support.py b/docs/_themes/flask_theme_support.py
deleted file mode 100644
index 0dcf53b..0000000
--- a/docs/_themes/flask_theme_support.py
+++ /dev/null
@@ -1,89 +0,0 @@
-# flasky extensions.  flasky pygments style based on tango style
-from pygments.style import Style
-from pygments.token import (
-    Keyword,
-    Name,
-    Comment,
-    String,
-    Error,
-    Number,
-    Operator,
-    Generic,
-    Whitespace,
-    Punctuation,
-    Other,
-    Literal,
-)
-
-
-class FlaskyStyle(Style):
-    background_color = "#f8f8f8"
-    default_style = ""
-
-    styles = {
-        # No corresponding class for the following:
-        # Text:                     "", # class:  ''
-        Whitespace: "underline #f8f8f8",  # class: 'w'
-        Error: "#a40000 border:#ef2929",  # class: 'err'
-        Other: "#000000",  # class 'x'
-        Comment: "italic #8f5902",  # class: 'c'
-        Comment.Preproc: "noitalic",  # class: 'cp'
-        Keyword: "bold #004461",  # class: 'k'
-        Keyword.Constant: "bold #004461",  # class: 'kc'
-        Keyword.Declaration: "bold #004461",  # class: 'kd'
-        Keyword.Namespace: "bold #004461",  # class: 'kn'
-        Keyword.Pseudo: "bold #004461",  # class: 'kp'
-        Keyword.Reserved: "bold #004461",  # class: 'kr'
-        Keyword.Type: "bold #004461",  # class: 'kt'
-        Operator: "#582800",  # class: 'o'
-        Operator.Word: "bold #004461",  # class: 'ow' - like keywords
-        Punctuation: "bold #000000",  # class: 'p'
-        # because special names such as Name.Class, Name.Function, etc.
-        # are not recognized as such later in the parsing, we choose them
-        # to look the same as ordinary variables.
-        Name: "#000000",  # class: 'n'
-        Name.Attribute: "#c4a000",  # class: 'na' - to be revised
-        Name.Builtin: "#004461",  # class: 'nb'
-        Name.Builtin.Pseudo: "#3465a4",  # class: 'bp'
-        Name.Class: "#000000",  # class: 'nc' - to be revised
-        Name.Constant: "#000000",  # class: 'no' - to be revised
-        Name.Decorator: "#888",  # class: 'nd' - to be revised
-        Name.Entity: "#ce5c00",  # class: 'ni'
-        Name.Exception: "bold #cc0000",  # class: 'ne'
-        Name.Function: "#000000",  # class: 'nf'
-        Name.Property: "#000000",  # class: 'py'
-        Name.Label: "#f57900",  # class: 'nl'
-        Name.Namespace: "#000000",  # class: 'nn' - to be revised
-        Name.Other: "#000000",  # class: 'nx'
-        Name.Tag: "bold #004461",  # class: 'nt' - like a keyword
-        Name.Variable: "#000000",  # class: 'nv' - to be revised
-        Name.Variable.Class: "#000000",  # class: 'vc' - to be revised
-        Name.Variable.Global: "#000000",  # class: 'vg' - to be revised
-        Name.Variable.Instance: "#000000",  # class: 'vi' - to be revised
-        Number: "#990000",  # class: 'm'
-        Literal: "#000000",  # class: 'l'
-        Literal.Date: "#000000",  # class: 'ld'
-        String: "#4e9a06",  # class: 's'
-        String.Backtick: "#4e9a06",  # class: 'sb'
-        String.Char: "#4e9a06",  # class: 'sc'
-        String.Doc: "italic #8f5902",  # class: 'sd' - like a comment
-        String.Double: "#4e9a06",  # class: 's2'
-        String.Escape: "#4e9a06",  # class: 'se'
-        String.Heredoc: "#4e9a06",  # class: 'sh'
-        String.Interpol: "#4e9a06",  # class: 'si'
-        String.Other: "#4e9a06",  # class: 'sx'
-        String.Regex: "#4e9a06",  # class: 'sr'
-        String.Single: "#4e9a06",  # class: 's1'
-        String.Symbol: "#4e9a06",  # class: 'ss'
-        Generic: "#000000",  # class: 'g'
-        Generic.Deleted: "#a40000",  # class: 'gd'
-        Generic.Emph: "italic #000000",  # class: 'ge'
-        Generic.Error: "#ef2929",  # class: 'gr'
-        Generic.Heading: "bold #000080",  # class: 'gh'
-        Generic.Inserted: "#00A000",  # class: 'gi'
-        Generic.Output: "#888",  # class: 'go'
-        Generic.Prompt: "#745334",  # class: 'gp'
-        Generic.Strong: "bold #000000",  # class: 'gs'
-        Generic.Subheading: "bold #800080",  # class: 'gu'
-        Generic.Traceback: "bold #a40000",  # class: 'gt'
-    }
diff --git a/docs/conf.py b/docs/conf.py
deleted file mode 100644
index 47beca9..0000000
--- a/docs/conf.py
+++ /dev/null
@@ -1,286 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Flask-SimpleLDAP documentation build configuration file, created by
-# sphinx-quickstart on Sun Aug 10 00:23:02 2014.
-#
-# This file is execfile()d with the current directory set to its
-# containing dir.
-#
-# Note that not all possible configuration values are present in this
-# autogenerated file.
-#
-# All configuration values have a default; values that are commented out
-# serve to show the default.
-
-import sys
-import os
-from mock import Mock as MagicMock
-
-
-class Mock(MagicMock):
-    @classmethod
-    def __getattr__(cls, name):
-        return Mock()
-
-
-MOCK_MODULES = ["ldap"]
-sys.modules.update((mod_name, Mock()) for mod_name in MOCK_MODULES)
-
-# If extensions (or modules to document with autodoc) are in another directory,
-# add these directories to sys.path here. If the directory is relative to the
-# documentation root, use os.path.abspath to make it absolute, like shown here.
-sys.path.insert(0, os.path.abspath(".."))
-
-# -- General configuration -----------
-
-# If your documentation needs a minimal Sphinx version, state it here.
-# needs_sphinx = '1.0'
-
-# Add any Sphinx extension module names here, as strings. They can be
-# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
-# ones.
-extensions = [
-    "sphinx.ext.autodoc",
-    "sphinx.ext.intersphinx",
-]
-
-# Add any paths that contain templates here, relative to this directory.
-templates_path = ["_templates"]
-
-# The suffix of source filenames.
-source_suffix = ".rst"
-
-# The encoding of source files.
-# source_encoding = 'utf-8-sig'
-
-# The master toctree document.
-master_doc = "index"
-
-# General information about the project.
-project = "Flask-SimpleLDAP"
-copyright = "2022, Alexandre Ferland"
-
-# The version info for the project you're documenting, acts as replacement for
-# |version| and |release|, also used in various other places throughout the
-# built documents.
-#
-# The short X.Y version.
-version = "1.4.0"
-# The full version, including alpha/beta/rc tags.
-release = "1.4.0"
-
-# The language for content autogenerated by Sphinx. Refer to documentation
-# for a list of supported languages.
-# language = None
-
-# There are two options for replacing |today|: either, you set today to some
-# non-false value, then it is used:
-# today = ''
-# Else, today_fmt is used as the format for a strftime call.
-# today_fmt = '%B %d, %Y'
-
-# List of patterns, relative to source directory, that match files and
-# directories to ignore when looking for source files.
-exclude_patterns = ["_build"]
-
-# The reST default role (used for this markup: `text`) to use for all
-# documents.
-# default_role = None
-
-# If true, '()' will be appended to :func: etc. cross-reference text.
-# add_function_parentheses = True
-
-# If true, the current module name will be prepended to all description
-# unit titles (such as .. function::).
-# add_module_names = True
-
-# If true, sectionauthor and moduleauthor directives will be shown in the
-# output. They are ignored by default.
-# show_authors = False
-
-# The name of the Pygments (syntax highlighting) style to use.
-pygments_style = "sphinx"
-
-# A list of ignored prefixes for module index sorting.
-# modindex_common_prefix = []
-
-# If true, keep warnings as "system message" paragraphs in the built documents.
-# keep_warnings = False
-
-
-# -- Options for HTML output ----------------------------------------------
-
-sys.path.append(os.path.abspath("_themes"))
-html_theme_path = ["_themes"]
-html_theme = "flask_small"
-html_theme_options = {
-    "index_logo": "",  # TODO
-    "github_fork": "alexferl/flask-simpleldap",
-}
-
-# The name for this set of Sphinx documents.  If None, it defaults to
-# "<project> v<release> documentation".
-# html_title = None
-
-# A shorter title for the navigation bar.  Default is the same as html_title.
-# html_short_title = None
-
-# The name of an image file (relative to this directory) to place at the top
-# of the sidebar.
-# html_logo = None
-
-# The name of an image file (within the static path) to use as favicon of the
-# docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32
-# pixels large.
-# html_favicon = None
-
-# Add any paths that contain custom static files (such as style sheets) here,
-# relative to this directory. They are copied after the builtin static files,
-# so a file named "default.css" will overwrite the builtin "default.css".
-html_static_path = ["_static"]
-
-# Add any extra paths that contain custom files (such as robots.txt or
-# .htaccess) here, relative to this directory. These files are copied
-# directly to the root of the documentation.
-# html_extra_path = []
-
-# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
-# using the given strftime format.
-# html_last_updated_fmt = '%b %d, %Y'
-
-# If true, SmartyPants will be used to convert quotes and dashes to
-# typographically correct entities.
-# html_use_smartypants = True
-
-# Custom sidebar templates, maps document names to template names.
-# html_sidebars = {}
-
-# Additional templates that should be rendered to pages, maps page names to
-# template names.
-# html_additional_pages = {}
-
-# If false, no module index is generated.
-# html_domain_indices = True
-
-# If false, no index is generated.
-# html_use_index = True
-
-# If true, the index is split into individual pages for each letter.
-# html_split_index = False
-
-# If true, links to the reST sources are added to the pages.
-# html_show_sourcelink = True
-
-# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
-# html_show_sphinx = True
-
-# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
-# html_show_copyright = True
-
-# If true, an OpenSearch description file will be output, and all pages will
-# contain a <link> tag referring to it.  The value of this option must be the
-# base URL from which the finished HTML is served.
-# html_use_opensearch = ''
-
-# This is the file name suffix for HTML files (e.g. ".xhtml").
-# html_file_suffix = None
-
-# Output file base name for HTML help builder.
-htmlhelp_basename = "Flask-SimpleLDAPdoc"
-
-
-# -- Options for LaTeX output ---------------------------------------------
-
-latex_elements = {
-    # The paper size ('letterpaper' or 'a4paper').
-    #'papersize': 'letterpaper',
-    # The font size ('10pt', '11pt' or '12pt').
-    #'pointsize': '10pt',
-    # Additional stuff for the LaTeX preamble.
-    #'preamble': '',
-}
-
-# Grouping the document tree into LaTeX files. List of tuples
-# (source start file, target name, title,
-#  author, documentclass [howto, manual, or own class]).
-latex_documents = [
-    (
-        "index",
-        "Flask-SimpleLDAP.tex",
-        "Flask-SimpleLDAP Documentation",
-        "Alexandre Ferland",
-        "manual",
-    ),
-]
-
-# The name of an image file (relative to this directory) to place at the top of
-# the title page.
-# latex_logo = None
-
-# For "manual" documents, if this is true, then toplevel headings are parts,
-# not chapters.
-# latex_use_parts = False
-
-# If true, show page references after internal links.
-# latex_show_pagerefs = False
-
-# If true, show URL addresses after external links.
-# latex_show_urls = False
-
-# Documents to append as an appendix to all manuals.
-# latex_appendices = []
-
-# If false, no module index is generated.
-# latex_domain_indices = True
-
-
-# -- Options for manual page output ---------------------------------------
-
-# One entry per manual page. List of tuples
-# (source start file, name, description, authors, manual section).
-man_pages = [
-    (
-        "index",
-        "flask-simpleldap",
-        "Flask-SimpleLDAP Documentation",
-        ["Alexandre Ferland"],
-        1,
-    )
-]
-
-# If true, show URL addresses after external links.
-# man_show_urls = False
-
-
-# -- Options for Texinfo output -------------------------------------------
-
-# Grouping the document tree into Texinfo files. List of tuples
-# (source start file, target name, title, author,
-#  dir menu entry, description, category)
-texinfo_documents = [
-    (
-        "index",
-        "Flask-SimpleLDAP",
-        "Flask-SimpleLDAP Documentation",
-        "Alexandre Ferland",
-        "Flask-SimpleLDAP",
-        "One line description of project.",
-        "Miscellaneous",
-    ),
-]
-
-# Documents to append as an appendix to all manuals.
-# texinfo_appendices = []
-
-# If false, no module index is generated.
-# texinfo_domain_indices = True
-
-# How to display URL addresses: 'footnote', 'no', or 'inline'.
-# texinfo_show_urls = 'footnote'
-
-# If true, do not generate a @detailmenu in the "Top" node's menu.
-# texinfo_no_detailmenu = False
-
-
-# Example configuration for intersphinx: refer to the Python standard library.
-intersphinx_mapping = {"http://docs.python.org/": None}
diff --git a/docs/index.rst b/docs/index.rst
deleted file mode 100644
index e4bb3ac..0000000
--- a/docs/index.rst
+++ /dev/null
@@ -1,206 +0,0 @@
-.. Flask-SimpleLDAP documentation master file, created by
-   sphinx-quickstart on Sat Aug  9 19:44:30 2014.
-   You can adapt this file completely to your liking, but it should at least
-   contain the root `toctree` directive.
-
-Welcome to Flask-SimpleLDAP's documentation!
-============================================
-
-Flask-SimpleLDAP provides LDAP authentication for Flask.
-
-
-Quickstart
-----------
-
-First, install Flask-SimpleLDAP:
-
-    .. code-block:: bash
-
-      pip install flask-simpleldap
-
-Flask-SimpleLDAP depends, and will install for you, recent versions of Flask
-(0.12.4 or later) and pyldap. Flask-SimpleLDAP is compatible
-with and tested on Python 3.7+.
-
-Next, add a :class:`~flask_simpleldap.LDAP` to your code and at least the three
-required configuration options:
-
-    .. code-block:: python
-
-      from flask import Flask
-      from flask_simpleldap import LDAP
-
-      app = Flask(__name__)
-      # app.config["LDAP_HOST"] = "ldap.example.org"  # defaults to localhost
-      app.config["LDAP_BASE_DN"] = "OU=users,dc=example,dc=org"
-      app.config["LDAP_USERNAME"] = "CN=user,OU=Users,DC=example,DC=org"
-      app.config["LDAP_PASSWORD"] = "password"
-
-      ldap = LDAP(app)
-
-      @app.route("/ldap")
-      @ldap.login_required
-      def ldap_protected():
-          return "Success!"
-
-
-      if __name__ == "__main__":
-          app.run()
-
-
-Configuration
--------------
-
-:class:`~flask_simpleldap.LDAP` understands the following configuration
-directives:
-
-================================== ================================================================
-``LDAP_HOST``                      The host name or IP address of your LDAP server.
-                                   Default: 'localhost'.
-``LDAP_PORT``                      The port number of your LDAP server. Default: 389.
-``LDAP_SCHEMA``                    The LDAP schema to use between 'ldap' and 'ldaps'.
-                                   Default: 'ldap'.
-``LDAP_SOCKET_PATH``               If ``LDAP_SCHEMA`` is set to `ldapi`, the
-                                   path to the Unix socket path. Default: `/`.
-``LDAP_USERNAME``                  **Required**: The user name used to bind.
-``LDAP_PASSWORD``                  **Required**: The password used to bind.
-``LDAP_TIMEOUT``                   How long (seconds) a connection can take to be opened
-                                   before timing out. Default: 10.
-``LDAP_USE_SSL``                   Set to ``True`` if your server uses SSL.
-                                   Default: ``False``.
-``LDAP_USE_TLS``                   Set to ``True`` if your server uses TLS.
-                                   Default: ``False``.
-``LDAP_REQUIRE_CERT``              Set to ``True`` if your server requires a certificate.
-                                   Default: ``False``.
-``LDAP_CERT_PATH``                 Path to the certificate if ``LDAP_REQUIRE_CERT`` is
-                                   ``True``.
-``LDAP_BASE_DN``                   **Required**: The distinguished name to use as the search base.
-``LDAP_OBJECTS_DN``                The field to use as the objects' distinguished name.
-                                   Default: 'distinguishedName'.
-``LDAP_USER_FIELDS``               ``list`` of fields to return when searching for a user's
-                                   object details. Default: ``list`` (all).
-``LDAP_USER_OBJECT_FILTER``        The filter to use when searching for a user object.
-                                   Default: '(&(objectclass=Person)(userPrincipalName=%s))'
-``LDAP_USERS_OBJECT_FILTER``       The filter to use when searching for users objects.
-                                   Default: 'objectclass=Person'
-``LDAP_USER_GROUPS_FIELD``         The field to return when searching for a user's
-                                   groups. Default: 'memberOf'.
-``LDAP_GROUPS_OBJECT_FILTER``      The filter to use when searching for groups objects.
-                                   Default: 'objectclass=Group'
-``LDAP_GROUP_FIELDS``              ``list`` of fields to return when searching for a group's
-                                   object details. Default: ``list`` (all).
-``LDAP_GROUP_OBJECT_FILTER``       The filter to use when searching for a group object.
-                                   Default: '(&(objectclass=Group)(userPrincipalName=%s))'
-``LDAP_GROUP_MEMBERS_FIELD``       The field to return when searching for a group's members.
-                                   Default: 'member'
-``LDAP_LOGIN_VIEW``                Views decorated with :meth:`.login_required()` or
-                                   :meth:`.group_required()` will redirect
-                                   unauthenticated requests to this view. Default:
-                                   'login'.
-``LDAP_REALM_NAME``                Views decorated with
-                                   :meth:`.basic_auth_required()` will use this as
-                                   the "realm" part of HTTP Basic Authentication when
-                                   responding to unauthenticated requests.
-``LDAP_OPENLDAP``                  Set to ``True`` if your server is running OpenLDAP.
-                                   Default: ``False``
-``LDAP_GROUP_MEMBER_FILTER``       The group member filter to use when using OpenLDAP.
-                                   Default: '*'
-``LDAP_GROUP_MEMBER_FILTER_FIELD`` The group member filter field to use when using OpenLDAP.
-                                   Default: '*'
-``LDAP_CUSTOM_OPTIONS``            ``dict`` of ldap options you want to set in this format: {option: value}.
-                                   Default: ``None``
-================================== ================================================================
-
-
-API
-===
-
-Classes
--------
-
-.. autoclass:: flask_simpleldap.LDAP
-   :members:
-
-
-History
--------
-
-Changes:
-
-- 1.4.0 July 16, 2019
-
-  - This release drops support for `Python 2.7 <https://pythonclock.org/>`_. If you're still on Python 2.7, you can use `v1.3.3 <https://github.com/alexferl/flask-simpleldap/releases/tag/v1.3.3>`_.
-
-  - Fixes:
-
-    - `#62 <https://github.com/admiralobvious/flask-simpleldap/issues/62>`_ get_object_details returning None
-
-- 1.3.0 July 14, 2019
-
-  - Thanks to the contributors, this release fixes issues related to bind_user and fixes some issues related to filtering.
-
-    - `#51 <https://github.com/admiralobvious/flask-simpleldap/pull/51>`_ Referral chasing crash
-
-    - `#54 <https://github.com/admiralobvious/flask-simpleldap/pull/54>`_ Fixes #44 - Error in bind_user method, also fixes #60 and #61
-
-    - `#56 <https://github.com/admiralobvious/flask-simpleldap/pull/56>`_ OpenLDAP section has Incorrect LDAP_GROUP_OBJECT_FILTER
-
-    - `#57 <https://github.com/admiralobvious/flask-simpleldap/pull/57>`_ next vaule: Priority use request.full_path
-
-    - `#59 <https://github.com/admiralobvious/flask-simpleldap/pull/59>`_ get_object_details to take query_filter and fallback to LDAP_USER_OBJECT_FILTER or LDAP_GROUP_OBJECT_FILTER
-
-
-- 1.2.0 September 26, 2017
-
-  - Changed get_group_members() and get_user_groups() returning strings instead of bytes in PY3.
-
-- 1.1.2 July 17, 2017
-
-  - Merge GitHub PR `#30 <https://github.com/admiralobvious/flask-simpleldap/pull/30>`_,
-    Fix for python3
-  - Fix decoding bytes in PY3 for @ldap.group_required.
-
-- 1.1.1 April 10, 2017
-
-  - Merge GitHub pull `#26 <https://github.com/admiralobvious/flask-simpleldap/pull/26>`_,
-    Fix set_option call to LDAP for SSL CERT
-
-- 1.1.0 June 7, 2016
-
-  - Add the ability the pass any valid pyldap config options via the LDAP_CUSTOM_OPTIONS configuration directive.
-
-- 1.0.1 June 5, 2016
-
-  - Fix ldap filter import.
-
-- 1.0.0 June 4, 2016
-
-  - Python 3.x support. Switched from python-ldap to pyldap which is a fork with Python 3.x support.
-
-- 0.4.0: September 5, 2015
-
-  - Added support for OpenLDAP directories. Thanks to `@jm66 <https://github.com/jm66>`_ on GitHub.
-
-- 0.3.0: January 21, 2015
-
-  - Fix Github issue `#10 <https://github.com/admiralobvious/flask-simpleldap/issues/10>`_,
-    Redirect users back to the page they originally requested after authenticating
-
-  - Fix GitHub issue `#12 <https://github.com/admiralobvious/flask-simpleldap/issues/12>`_,
-    Only trust .bind_user() with a non-empty password
-
-- 0.2.0: December 7, 2014
-
-  - Added HTTP Basic Authentication. Thanks to `@OptiverTimAll <https://github.com/optivertimall>`_ on GitHub.
-  - Fix GitHub issue `#4 <https://github.com/admiralobvious/flask-simpleldap/issues/4>`_,
-    User or group queries are vulnerable to LDAP injection.
-    Make sure you update your filters to use '%s' instead of the old '{}'!
-
-- 0.1.1: September 6, 2014
-
-  - Fix GitHub issue `#3 <https://github.com/admiralobvious/flask-simpleldap/issues/3>`_,
-    Not compatible with uppercase distinguished names.
-
-- 0.1: August 9, 2014
-
-  - Initial Release
diff --git a/examples/basic_auth/app.py b/examples/basic_auth/app.py
index 12bad8b..e214831 100644
--- a/examples/basic_auth/app.py
+++ b/examples/basic_auth/app.py
@@ -2,7 +2,7 @@
 from flask_simpleldap import LDAP
 
 app = Flask(__name__)
-#app.config["LDAP_HOST"] = "ldap.example.org"  # defaults to localhost
+# app.config["LDAP_HOST"] = "ldap.example.org"  # defaults to localhost
 app.config["LDAP_BASE_DN"] = "OU=users,dc=example,dc=org"
 app.config["LDAP_USERNAME"] = "CN=user,OU=Users,DC=example,DC=org"
 app.config["LDAP_PASSWORD"] = "password"
@@ -16,7 +16,7 @@
 @app.route("/")
 @ldap.basic_auth_required
 def index():
-    return "Welcome, {0}!".format(g.ldap_username)
+    return f"Welcome, {g.ldap_username}!"
 
 
 if __name__ == "__main__":
diff --git a/examples/basic_auth/app_oldap.py b/examples/basic_auth/app_oldap.py
index dce343b..21d056d 100644
--- a/examples/basic_auth/app_oldap.py
+++ b/examples/basic_auth/app_oldap.py
@@ -21,9 +21,9 @@
 app.config["LDAP_GROUP_OBJECT_FILTER"] = "(&(objectclass=groupOfUniqueNames)(cn=%s))"
 app.config["LDAP_GROUPS_OBJECT_FILTER"] = "objectclass=groupOfUniqueNames"
 app.config["LDAP_GROUP_FIELDS"] = ["cn", "entryDN", "member", "description"]
-app.config[
-    "LDAP_GROUP_MEMBER_FILTER"
-] = "(&(cn=*)(objectclass=groupOfUniqueNames)(member=%s))"
+app.config["LDAP_GROUP_MEMBER_FILTER"] = (
+    "(&(cn=*)(objectclass=groupOfUniqueNames)(member=%s))"
+)
 app.config["LDAP_GROUP_MEMBER_FILTER_FIELD"] = "cn"
 
 ldap = LDAP(app)
@@ -32,7 +32,7 @@
 @app.route("/")
 @ldap.basic_auth_required
 def index():
-    return "Welcome, {0}!".format(g.ldap_username)
+    return f"Welcome, {g.ldap_username}!"
 
 
 if __name__ == "__main__":
diff --git a/examples/blueprints/blueprints/config.py b/examples/blueprints/blueprints/config.py
index 8c0a5f3..6e8baa7 100644
--- a/examples/blueprints/blueprints/config.py
+++ b/examples/blueprints/blueprints/config.py
@@ -7,9 +7,9 @@ class BaseConfig(object):
     DEBUG = True
 
     # LDAP
-    LDAP_HOST = "ldap.example.org"
-    LDAP_BASE_DN = "OU=users,dc=example,dc=org"
-    LDAP_USERNAME = "CN=user,OU=Users,DC=example,DC=org"
-    LDAP_PASSWORD = "password"
+    # LDAP_HOST = "ldap.example.org"  # defaults to localhost
+    LDAP_BASE_DN = "dc=example,dc=org"
+    LDAP_USERNAME = "cn=admin,dc=example,dc=org"
+    LDAP_PASSWORD = "admin"
     LDAP_LOGIN_VIEW = "core.login"
     LDAP_CUSTOM_OPTIONS = {ldap.OPT_REFERRALS: 0}
diff --git a/examples/groups/app_oldap.py b/examples/groups/app_oldap.py
index 4fd547a..2cb4001 100644
--- a/examples/groups/app_oldap.py
+++ b/examples/groups/app_oldap.py
@@ -19,9 +19,9 @@
 app.config["LDAP_GROUP_OBJECT_FILTER"] = "(&(objectclass=groupOfUniqueNames)(cn=%s))"
 app.config["LDAP_GROUPS_OBJECT_FILTER"] = "objectclass=groupOfUniqueNames"
 app.config["LDAP_GROUP_FIELDS"] = ["cn", "entryDN", "member", "description"]
-app.config[
-    "LDAP_GROUP_MEMBER_FILTER"
-] = "(&(cn=*)(objectclass=groupOfUniqueNames)(member=%s))"
+app.config["LDAP_GROUP_MEMBER_FILTER"] = (
+    "(&(cn=*)(objectclass=groupOfUniqueNames)(member=%s))"
+)
 app.config["LDAP_GROUP_MEMBER_FILTER_FIELD"] = "cn"
 
 ldap = LDAP(app)
diff --git a/flask_simpleldap/__init__.py b/flask_simpleldap/__init__.py
index 6d73cfd..858f5c8 100644
--- a/flask_simpleldap/__init__.py
+++ b/flask_simpleldap/__init__.py
@@ -46,14 +46,15 @@ def init_app(app):
         app.config.setdefault("LDAP_OBJECTS_DN", "distinguishedName")
         app.config.setdefault("LDAP_USER_FIELDS", [])
         app.config.setdefault("LDAP_USER_GROUPS_FIELD", "memberOf")
-        app.config.setdefault("LDAP_USER_OBJECT_FILTER",
-                              "(&(objectclass=Person)(userPrincipalName=%s))")
-        app.config.setdefault("LDAP_USERS_OBJECT_FILTER",
-                              "objectclass=Person")
+        app.config.setdefault(
+            "LDAP_USER_OBJECT_FILTER", "(&(objectclass=Person)(userPrincipalName=%s))"
+        )
+        app.config.setdefault("LDAP_USERS_OBJECT_FILTER", "objectclass=Person")
         app.config.setdefault("LDAP_GROUP_FIELDS", [])
         app.config.setdefault("LDAP_GROUP_MEMBERS_FIELD", "member")
-        app.config.setdefault("LDAP_GROUP_OBJECT_FILTER",
-                              "(&(objectclass=Group)(userPrincipalName=%s))")
+        app.config.setdefault(
+            "LDAP_GROUP_OBJECT_FILTER", "(&(objectclass=Group)(userPrincipalName=%s))"
+        )
         app.config.setdefault("LDAP_GROUPS_OBJECT_FILTER", "objectclass=Group")
         app.config.setdefault("LDAP_LOGIN_VIEW", "login")
         app.config.setdefault("LDAP_REALM_NAME", "LDAP authentication")
@@ -63,22 +64,19 @@ def init_app(app):
         app.config.setdefault("LDAP_CUSTOM_OPTIONS", None)
 
         if app.config["LDAP_USE_SSL"] or app.config["LDAP_USE_TLS"]:
-            ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT,
-                            ldap.OPT_X_TLS_NEVER)
+            ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)
 
         if app.config["LDAP_REQUIRE_CERT"]:
-            ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT,
-                            ldap.OPT_X_TLS_DEMAND)
-            ldap.set_option(ldap.OPT_X_TLS_CACERTFILE,
-                            app.config["LDAP_CERT_PATH"])
+            ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_DEMAND)
+            ldap.set_option(ldap.OPT_X_TLS_CACERTFILE, app.config["LDAP_CERT_PATH"])
 
         if app.config["LDAP_BASE_DN"] is None:
             raise LDAPException("LDAP_BASE_DN cannot be None!")
 
         if app.config["LDAP_SCHEMA"] != "ldapi":
             for option in ["USERNAME", "PASSWORD"]:
-                if app.config["LDAP_{0}".format(option)] is None:
-                    raise LDAPException("LDAP_{0} cannot be None!".format(option))
+                if app.config[f"LDAP_{option}"] is None:
+                    raise LDAPException(f"LDAP_{option} cannot be None!")
 
     @staticmethod
     def _set_custom_options(conn):
@@ -97,16 +95,9 @@ def initialize(self):
 
         try:
             if current_app.config["LDAP_SCHEMA"] == "ldapi":
-                uri = "{0}://{1}".format(
-                    current_app.config["LDAP_SCHEMA"],
-                    current_app.config["LDAP_SOCKET_PATH"],
-                )
+                uri = f"{current_app.config['LDAP_SCHEMA']}://{current_app.config['LDAP_SOCKET_PATH']}"
             else:
-                uri = "{0}://{1}:{2}".format(
-                    current_app.config["LDAP_SCHEMA"],
-                    current_app.config["LDAP_HOST"],
-                    current_app.config["LDAP_PORT"],
-                )
+                uri = f"{current_app.config['LDAP_SCHEMA']}://{current_app.config['LDAP_HOST']}:{current_app.config['LDAP_PORT']}"
             conn = ldap.initialize(uri)
             conn.set_option(
                 ldap.OPT_NETWORK_TIMEOUT, current_app.config["LDAP_TIMEOUT"]
@@ -189,14 +180,18 @@ def get_users(self, fields=None, dn_only=False):
             fields = fields or current_app.config["LDAP_USER_FIELDS"]
             if current_app.config["LDAP_OPENLDAP"]:
                 records = conn.search_s(
-                    current_app.config["LDAP_BASE_DN"], ldap.SCOPE_SUBTREE,
+                    current_app.config["LDAP_BASE_DN"],
+                    ldap.SCOPE_SUBTREE,
                     current_app.config["LDAP_USERS_OBJECT_FILTER"],
-                    fields)
+                    fields,
+                )
             else:
                 records = conn.search_s(
-                    current_app.config["LDAP_BASE_DN"], ldap.SCOPE_SUBTREE,
+                    current_app.config["LDAP_BASE_DN"],
+                    ldap.SCOPE_SUBTREE,
                     current_app.config["LDAP_USERS_OBJECT_FILTER"],
-                    fields)
+                    fields,
+                )
             conn.unbind_s()
             if records:
                 if dn_only:
@@ -208,8 +203,9 @@ def get_users(self, fields=None, dn_only=False):
         except ldap.LDAPError as e:
             raise LDAPException(self.error(e.args))
 
-    def get_object_details(self, user=None, group=None, query_filter=None,
-                           dn_only=False):
+    def get_object_details(
+        self, user=None, group=None, query_filter=None, dn_only=False
+    ):
         """Returns a ``dict`` with the object's (user or group) details.
 
         :param str user: Username of the user object you want details for.
@@ -410,10 +406,10 @@ def wrapped(*args, **kwargs):
             if g.user is None:
                 next_path = request.full_path or request.path
                 if next_path == "/?":
-                    return redirect(
-                        url_for(current_app.config["LDAP_LOGIN_VIEW"]))
-                return redirect(url_for(current_app.config["LDAP_LOGIN_VIEW"],
-                                        next=next_path))
+                    return redirect(url_for(current_app.config["LDAP_LOGIN_VIEW"]))
+                return redirect(
+                    url_for(current_app.config["LDAP_LOGIN_VIEW"], next=next_path)
+                )
             return func(*args, **kwargs)
 
         return wrapped
@@ -470,7 +466,9 @@ def basic_auth_required(self, func):
 
         def make_auth_required_response():
             response = make_response("Unauthorized", 401)
-            response.headers['WWW-Authenticate'] = f'Basic realm="{current_app.config["LDAP_REALM_NAME"]}"'
+            response.headers["WWW-Authenticate"] = (
+                f'Basic realm="{current_app.config["LDAP_REALM_NAME"]}"'
+            )
             return response
 
         @wraps(func)
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..fed528d
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,3 @@
+[build-system]
+requires = ["setuptools"]
+build-backend = "setuptools.build_meta"
diff --git a/requirements-dev.txt b/requirements-dev.txt
new file mode 100644
index 0000000..fc11e9c
--- /dev/null
+++ b/requirements-dev.txt
@@ -0,0 +1,19 @@
+-i https://pypi.org/simple
+black==24.3.0; python_version >= '3.8'
+cfgv==3.4.0; python_version >= '3.8'
+click==8.1.7; python_version >= '3.7'
+distlib==0.3.8
+filelock==3.13.3; python_version >= '3.8'
+identify==2.5.35; python_version >= '3.8'
+iniconfig==2.0.0; python_version >= '3.7'
+mypy-extensions==1.0.0; python_version >= '3.5'
+nodeenv==1.8.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'
+packaging==24.0; python_version >= '3.7'
+pathspec==0.12.1; python_version >= '3.8'
+platformdirs==4.2.0; python_version >= '3.8'
+pluggy==1.4.0; python_version >= '3.8'
+pre-commit==3.5.0; python_version >= '3.8'
+pytest==8.1.1; python_version >= '3.8'
+pyyaml==6.0.1; python_version >= '3.6'
+setuptools==69.2.0; python_version >= '3.8'
+virtualenv==20.25.1; python_version >= '3.7'
diff --git a/requirements.txt b/requirements.txt
index a506103..8169b26 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,2 +1,11 @@
-Flask==2.3.2
-mock==4.0.3  # for ci
+-i https://pypi.org/simple
+blinker==1.7.0; python_version >= '3.8'
+click==8.1.7; python_version >= '3.7'
+flask==3.0.2; python_version >= '3.8'
+itsdangerous==2.1.2; python_version >= '3.7'
+jinja2==3.1.3; python_version >= '3.7'
+markupsafe==2.1.5; python_version >= '3.7'
+pyasn1==0.6.0; python_version >= '3.8'
+pyasn1-modules==0.4.0; python_version >= '3.8'
+python-ldap==3.4.4; python_version >= '3.6'
+werkzeug==3.0.2; python_version >= '3.8'
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..d7153e4
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,29 @@
+[metadata]
+name = Flask-SimpleLDAP
+version = file: VERSION
+author = Alexandre Ferland
+author_email = me@alexferl.com
+url = https://github.com/alexferl/flask-simpleldap
+description = LDAP authentication extension for Flask
+long_description = file: README.md
+long_description_content_type = text/markdown
+license = MIT
+classifiers =
+    Environment :: Web Environment
+    Intended Audience :: Developers
+    License :: OSI Approved :: MIT License
+    Operating System :: OS Independent
+    Programming Language :: Python :: 3.8
+    Programming Language :: Python :: 3.9
+    Programming Language :: Python :: 3.10
+    Programming Language :: Python :: 3.11
+    Programming Language :: Python :: 3.12
+    Topic :: Software Development :: Libraries :: Python Modules
+
+[options]
+zip_safe = False
+include_package_data = True
+python_requires = >=3.8
+install_requires =
+    Flask>=2.2.5
+    python-ldap>=3.0.0
diff --git a/setup.py b/setup.py
deleted file mode 100644
index 3897a38..0000000
--- a/setup.py
+++ /dev/null
@@ -1,40 +0,0 @@
-"""
-Flask-SimpleLDAP
-----------------
-
-LDAP authentication extension for Flask
-"""
-from setuptools import setup
-
-from pathlib import Path
-
-this_directory = Path(__file__).parent
-long_description = (this_directory / "README.md").read_text()
-
-
-setup(
-    name="Flask-SimpleLDAP",
-    version="1.4.0",
-    url="https://github.com/alexferl/flask-simpleldap",
-    license="MIT",
-    author="Alexandre Ferland",
-    author_email="me@alexferl.com",
-    description="LDAP authentication extension for Flask",
-    long_description=long_description,
-    long_description_content_type="text/markdown",
-    packages=["flask_simpleldap"],
-    zip_safe=False,
-    include_package_data=True,
-    platforms="any",
-    install_requires=["Flask>=0.12.4", "python-ldap>=3.0.0"],
-    classifiers=[
-        "Environment :: Web Environment",
-        "Intended Audience :: Developers",
-        "License :: OSI Approved :: MIT License",
-        "Operating System :: OS Independent",
-        "Programming Language :: Python :: 3.7",
-        "Programming Language :: Python :: 3.8",
-        "Programming Language :: Python :: 3.9",
-        "Topic :: Software Development :: Libraries :: Python Modules",
-    ],
-)
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_simpleldap.py b/tests/test_simpleldap.py
new file mode 100644
index 0000000..1e5a792
--- /dev/null
+++ b/tests/test_simpleldap.py
@@ -0,0 +1,32 @@
+import pytest
+from flask import Flask
+from flask_simpleldap import LDAP, LDAPException
+
+
+def test_instantiate():
+    app = Flask(__name__)
+    app.config["LDAP_BASE_DN"] = "OU=users,dc=example,dc=org"
+    app.config["LDAP_USERNAME"] = "CN=user,OU=Users,DC=example,DC=org"
+    app.config["LDAP_PASSWORD"] = "password"
+    LDAP(app)
+
+
+def test_instantiate_no_dn():
+    app = Flask(__name__)
+    with pytest.raises(LDAPException, match="LDAP_BASE_DN cannot be None!"):
+        LDAP(app)
+
+
+def test_instantiate_no_username():
+    app = Flask(__name__)
+    app.config["LDAP_BASE_DN"] = "OU=users,dc=example,dc=org"
+    with pytest.raises(LDAPException, match="LDAP_USERNAME cannot be None!"):
+        LDAP(app)
+
+
+def test_instantiate_no_password():
+    app = Flask(__name__)
+    app.config["LDAP_BASE_DN"] = "OU=users,dc=example,dc=org"
+    app.config["LDAP_USERNAME"] = "CN=user,OU=Users,DC=example,DC=org"
+    with pytest.raises(LDAPException, match="LDAP_PASSWORD cannot be None!"):
+        LDAP(app)

From 8bf68208bd7f1ed16bae85b0fd0502cd033ae57d Mon Sep 17 00:00:00 2001
From: alexferl <me@alexferl.com>
Date: Wed, 3 Apr 2024 14:32:46 -0400
Subject: [PATCH 2/5] add apt deps

Signed-off-by: alexferl <me@alexferl.com>
---
 .github/workflows/test.yaml | 7 +++++++
 README.md                   | 4 ++--
 2 files changed, 9 insertions(+), 2 deletions(-)

diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index 98f942b..4028323 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -58,6 +58,13 @@ jobs:
             python-version: '3.12'
             FLASK: 2.2.5
     steps:
+      - name: Install apt dependencies
+        run: |
+          set -ex
+          sudo apt update
+          sudo apt install -y ldap-utils slapd enchant-2 libldap2-dev libsasl2-dev apparmor-utils
+      - name: Disable AppArmor
+        run: sudo aa-disable /usr/sbin/slapd
       - name: 'Set up Python ${{ matrix.python-version }}'
         uses: actions/setup-python@v2
         with:
diff --git a/README.md b/README.md
index 58ad28b..dd908ea 100644
--- a/README.md
+++ b/README.md
@@ -34,7 +34,7 @@ ldap = LDAP(app)
 @app.route("/")
 @ldap.basic_auth_required
 def index():
-    return "Welcome, {0}!".format(g.ldap_username)
+    return f"Welcome, {g.ldap_username}!"
 
 if __name__ == "__main__":
     app.run()
@@ -92,7 +92,7 @@ ldap = LDAP(app)
 @app.route("/")
 @ldap.basic_auth_required
 def index():
-    return "Welcome, {0}!".format(g.ldap_username)
+    return f"Welcome, {g.ldap_username}!"
 
 if __name__ == "__main__":
     app.run()

From 2dd0d99cee59b1d5a7d09535ed36503bf0baa172 Mon Sep 17 00:00:00 2001
From: alexferl <me@alexferl.com>
Date: Wed, 3 Apr 2024 14:35:44 -0400
Subject: [PATCH 3/5] update version

Signed-off-by: alexferl <me@alexferl.com>
---
 .github/workflows/test.yaml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index 4028323..3100a1d 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -66,10 +66,10 @@ jobs:
       - name: Disable AppArmor
         run: sudo aa-disable /usr/sbin/slapd
       - name: 'Set up Python ${{ matrix.python-version }}'
-        uses: actions/setup-python@v2
+        uses: actions/setup-python@v4
         with:
           python-version: '${{ matrix.python-version }}'
-      - uses: actions/checkout@v2
+      - uses: actions/checkout@v4
       - run: pip install pytest Flask==$FLASK
         env:
           FLASK: '${{ matrix.FLASK }}'

From 9699defaaabe053af5cfa6f95a9ad853e79d6561 Mon Sep 17 00:00:00 2001
From: alexferl <me@alexferl.com>
Date: Wed, 3 Apr 2024 14:38:34 -0400
Subject: [PATCH 4/5] update setup-python version

Signed-off-by: alexferl <me@alexferl.com>
---
 .github/workflows/test.yaml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index 3100a1d..800d02a 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -66,7 +66,7 @@ jobs:
       - name: Disable AppArmor
         run: sudo aa-disable /usr/sbin/slapd
       - name: 'Set up Python ${{ matrix.python-version }}'
-        uses: actions/setup-python@v4
+        uses: actions/setup-python@v5
         with:
           python-version: '${{ matrix.python-version }}'
       - uses: actions/checkout@v4

From 1a3b0d19309aeeb85d3418f0bd868c2688e69ad7 Mon Sep 17 00:00:00 2001
From: alexferl <me@alexferl.com>
Date: Wed, 3 Apr 2024 15:07:32 -0400
Subject: [PATCH 5/5] add config docs

Signed-off-by: alexferl <me@alexferl.com>
---
 README.md | 48 +++++++++++++++++++++++++++++++++++-------------
 1 file changed, 35 insertions(+), 13 deletions(-)

diff --git a/README.md b/README.md
index dd908ea..9e205b8 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,5 @@
 # Flask-SimpleLDAP
-Flask-SimpleLDAP provides LDAP authentication for Flask.
-
-Flask-SimpleLDAP is compatible with and tested on Python 3.8+.
+Flask-SimpleLDAP provides LDAP authentication for Flask and is compatible with and tested on Python 3.8+.
 
 ## Quickstart
 First, install Flask-SimpleLDAP:
@@ -10,8 +8,7 @@ First, install Flask-SimpleLDAP:
 pip install flask-simpleldap
 ```
 
-
-Flask-SimpleLDAP depends, and will install for you, recent versions of Flask
+Flask-SimpleLDAP depends, and will install for you, a recent version of Flask
 (2.2.5 or later) and [python-ldap](https://python-ldap.org/).
 Please consult the [python-ldap installation instructions](https://www.python-ldap.org/en/latest/installing.html) if you get an error during installation.
 
@@ -56,12 +53,10 @@ Once you get the basic example working, check out the more complex ones:
 and [blueprints](https://flask.palletsprojects.com/en/3.0.x/blueprints/).
 
 
-OpenLDAP
---------
-
+## OpenLDAP
 Add the `LDAP` instance to your code and depending on your OpenLDAP
-configuration, add the following at least LDAP_USER_OBJECT_FILTER and
-LDAP_USER_OBJECT_FILTER.
+configuration, add the following at least `LDAP_USER_OBJECT_FILTER` and
+`LDAP_USER_OBJECT_FILTER`.
 
 ```python
 from flask import Flask, g
@@ -98,6 +93,33 @@ if __name__ == "__main__":
     app.run()
 ```
 
-## Resources
-
-- [PyPI](https://pypi.python.org/pypi/Flask-SimpleLDAP)
+## Configuration
+| Setting                          | Description                                                                                                                                               |
+|----------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `LDAP_HOST`                      | The host name or IP address of your LDAP server. Default: `"localhost"`.                                                                                  |
+| `LDAP_PORT`                      | The port number of your LDAP server. Default: `389`.                                                                                                      |
+| `LDAP_SCHEMA`                    | The LDAP schema to use between `"ldap"`, `"ldapi"` and `"ldaps"`. Default: `"ldap"`.                                                                      |
+| `LDAP_SOCKET_PATH`               | If `LDAP_SCHEMA` is set to `"ldapi"`, the path to the Unix socket path. Default: `"/"`.                                                                   |
+| `LDAP_USERNAME`                  | **Required**: The username used to bind.                                                                                                                  |
+| `LDAP_PASSWORD`                  | **Required**: The password used to bind.                                                                                                                  |
+| `LDAP_TIMEOUT`                   | How long (seconds) a connection can take to be opened before timing out. Default: `10`.                                                                   |
+| `LDAP_LOGIN_VIEW`                | Views decorated with `.login_required()` or`.group_required()` will redirect unauthenticated requests to this view. Default: `"login"`.                   |
+| `LDAP_REALM_NAME`                | Views decorated with `.basic_auth_required()` will use this as the "realm" part of HTTP Basic Authentication when responding to unauthenticated requests. |
+| `LDAP_OPENLDAP`                  | Set to `True` if your server is running OpenLDAP. Default: `False`.                                                                                       |
+| `LDAP_USE_SSL`                   | Set to `True` if your server uses SSL. Default: `False`.                                                                                                  |
+| `LDAP_USE_TLS`                   | Set to `True` if your server uses TLS. Default: `False`.                                                                                                  |
+| `LDAP_REQUIRE_CERT`              | Set to `True` if your server requires a certificate. Default: `False`.                                                                                    |
+| `LDAP_CERT_PATH`                 | Path to the certificate if `LDAP_REQUIRE_CERT` is `True`.                                                                                                 |
+| `LDAP_CUSTOM_OPTIONS`            | `dict` of ldap options you want to set in this format: `{option: value}`. Default: `None`.                                                                |
+| `LDAP_BASE_DN`                   | **Required**: The distinguished name to use as the search base.                                                                                           |
+| `LDAP_OBJECTS_DN`                | The field to use as the objects' distinguished name. Default: `"distinguishedName"`.                                                                      |
+| `LDAP_USER_FIELDS`               | `list` of fields to return when searching for a user's object details. Default: `[]` (all).                                                               |
+| `LDAP_USER_GROUPS_FIELD`         | The field to return when searching for a user's groups. Default: `"memberOf"`.                                                                            |
+| `LDAP_USER_OBJECT_FILTER`        | The filter to use when searching for a user object. Default: `"(&(objectclass=Person)(userPrincipalName=%s))"`                                            |
+| `LDAP_USERS_OBJECT_FILTER`       | The filter to use when searching for users objects. Default: `"objectclass=Person"`                                                                       |
+| `LDAP_GROUP_FIELDS`              | `list` of fields to return when searching for a group's object details. Default: `[]` (all).                                                              |
+| `LDAP_GROUP_MEMBER_FILTER`       | The group member filter to use when using OpenLDAP. Default: `"*"`.                                                                                       |
+| `LDAP_GROUP_MEMBER_FILTER_FIELD` | The group member filter field to use when using OpenLDAP. Default: `"*"`.                                                                                 |
+| `LDAP_GROUP_MEMBERS_FIELD`       | The field to return when searching for a group's members. Default: `"member"`.                                                                            |
+| `LDAP_GROUP_OBJECT_FILTER`       | The filter to use when searching for a group object. Default: `"(&(objectclass=Group)(userPrincipalName=%s))"`.                                           |
+| `LDAP_GROUPS_OBJECT_FILTER`      | The filter to use when searching for groups objects. Default: `"objectclass=Group"`.                                                                      |