New Upstream Release - djangorestframework-api-key

Ready changes

Summary

Merged new upstream version: 2.2.0 (was: 2.0.0).

Resulting package

Built on 2022-10-21T08:56 (took 2m30s)

The resulting binary packages can be installed (if you have the apt repository enabled) by running one of:

apt install -t fresh-releases python-djangorestframework-api-key-docapt install -t fresh-releases python3-djangorestframework-api-key

Lintian Result

Diff

diff --git a/.gitignore b/.gitignore
index 64c4504..9aeb664 100644
--- a/.gitignore
+++ b/.gitignore
@@ -105,3 +105,5 @@ venv.bak/
 
 # databases
 *.sqlite3
+
+.idea/
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a57fff2..383dd7d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,23 @@ All notable changes to this project will be documented in this file.
 
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
+## 2.2.0 - 2022-03-11
+
+### Added
+
+- Added support for Django config detection for different versions (PR #187)
+
+### Changed
+
+- Add official support for Django 3.2 and Python 3.9 and 3.10 (PR #189)
+- Bumped `hashed_key` field's `max_length` from 100 to 150 to address length issue with `argon2-cffi` (PR #193)
+
+## 2.1.0 - 2021-09-24
+
+### Added
+
+- Add support for custom API `keyword`. (Pull #175)
+
 ## 2.0.0 - 2020-04-07
 
 **NOTE**: this release drops compatibility with certain Python and Django versions, but contains no other breaking changes. See [Upgrade to 2.0](https://florimondmanca.github.io/djangorestframework-api-key/upgrade/2.0/) for detailed migration steps.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 07290a9..2a1f47a 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -61,16 +61,19 @@ scripts/serve
 Build the documentation using:
 
 ```bash
-scripts/docs
+scripts/docs build
 ```
 
 ## Notes to maintainers
 
 ### Releasing
 
-- Create a PR with the following:
+- Create a release PR with the following:
   - Bump the package version by editing `__version__.py`.
   - Update the changelog with any relevant PRs merged since the last version: bug fixes, new features, changes, deprecations, removals.
-- Merge the PR.
-- Run `$ scripts/publish` on `master`.
-- Tag the commit and push the tag to the remote.
+- Once the release PR is merged, create a [new release](https://github.com/florimondmanca/djangorestframework-api-key/releases/new), including:
+    - Tag version, like `2.1.0`.
+    - Release title, `Version 2.1.0`.
+    - Description copied from the changelog.
+- Once created, this release will be automatically uploaded to PyPI via a publish job on Azure Pipelines.
+- Deploy the docs using: `scripts/docs gh-deploy`
diff --git a/README.md b/README.md
index f1ef777..0a41d20 100644
--- a/README.md
+++ b/README.md
@@ -31,7 +31,7 @@ API key permissions for the [Django REST Framework](https://www.django-rest-fram
 
 ### Should I use API keys?
 
-There are important security aspects you need to consider before switching to an API key access control scheme. We've listed some of these in [Security caveats](security.md#caveats), including serving your API over HTTPS.
+There are important security aspects you need to consider before switching to an API key access control scheme. We've listed some of these in [Security caveats](docs/security.md#caveats), including serving your API over HTTPS.
 
 Besides, see [Why and when to use API keys](https://cloud.google.com/endpoints/docs/openapi/when-why-api-key#top_of_page) for hints on whether API keys can fit your use case.
 
diff --git a/ci/azure-pipelines.yml b/ci/azure-pipelines.yml
index 34b3f19..f40ef10 100644
--- a/ci/azure-pipelines.yml
+++ b/ci/azure-pipelines.yml
@@ -1,10 +1,10 @@
-trigger:
-  - master
-
-pr:
-  - master
-
 resources:
+  repositories:
+    - repository: templates
+      type: github
+      endpoint: github
+      name: florimondmanca/azure-pipelines-templates
+      ref: refs/tags/4.0
   containers:
     - container: pg11
       image: postgres:11
@@ -15,78 +15,60 @@ resources:
         POSTGRES_PASSWORD: postgres
         POSTGRES_DB: default
 
+trigger:
+  - master
+  - refs/tags/*
+
+pr:
+  - master
+
 variables:
-  CI: true
-  PIP_CACHE_DIR: $(Pipeline.Workspace)/.cache/pip
+  - name: CI
+    value: "true"
+  - name: PIP_CACHE_DIR
+    value: $(Pipeline.Workspace)/.cache/pip
+  - group: pypi-credentials
 
-jobs:
-  - job: Check
-    steps:
-      - template: templates/install.yml
+stages:
+  - stage: test
+    jobs:
+      - template: job--python-check.yml@templates
         parameters:
-          pythonVersion: "3.8"
-      - bash: scripts/check
-        displayName: "Run checks"
+          pythonVersion: "3.10"
 
-  - job: Docs
-    steps:
-      - template: templates/install.yml
+      - template: job--python-docs-build.yml@templates
         parameters:
-          pythonVersion: "3.8"
-      - bash: scripts/docs build
-        displayName: "Build docs"
-
-  - job: Linux
-    strategy:
-      matrix:
-        py3.6-dj2.2:
-          pythonVersion: "3.6"
-          djangoVersion: "2.2.*"
-        py3.7-dj2.2:
-          pythonVersion: "3.7"
-          djangoVersion: "2.2.*"
-        py3.8-dj3.0:
-          pythonVersion: "3.8"
-          djangoVersion: "3.0.*"
-          uploadCoverage: true
+          pythonVersion: "3.10"
 
-    steps:
-      - template: templates/install.yml
+      - template: job--python-test.yml@templates
         parameters:
-          pythonVersion: $(pythonVersion)
-          djangoVersion: $(djangoVersion)
-      - bash: scripts/test
-        displayName: "Run tests"
-      - bash: |
-          if [ -f .coverage ]; then
-            python -m pip install codecov;
-            codecov --required;
-          fi
-        condition: eq(variables['uploadCoverage'], true)
-        env:
-          CODECOV_TOKEN: $(codecovToken)
-        displayName: "Upload coverage"
+          jobs:
+            py36_dj22:
+              variables:
+                DJANGO_VERSION: "2.2.*"
 
-  - job: Windows
-    pool:
-      vmImage: "vs2017-win2016"
-    steps:
-      - template: templates/install.yml
-        parameters:
-          pythonVersion: "3.7"
-          djangoVersion: "3.0.*"
-      - bash: scripts/test
-        displayName: "Run tests"
+            py37_dj32:
+              variables:
+                DJANGO_VERSION: "3.2.*"
+
+            py310_dj32:
+              coverage: true
+              variables:
+                DJANGO_VERSION: "3.2.*"
+
+            py38_windows:
+              os: windows
+
+            py38_postgres:
+              services:
+                postgres: pg11
+              variables:
+                DATABASE_URL: "postgresql://postgres:postgres@localhost:5432/default"
 
-  - job: Postgres
-    services:
-      postgres: pg11
-    steps:
-      - template: templates/install.yml
+  - stage: publish
+    condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/')
+    jobs:
+      - template: job--python-publish.yml@templates
         parameters:
-          pythonVersion: "3.7"
-          djangoVersion: "3.0.*"
-      - bash: scripts/test
-        env:
-          DATABASE_URL: "postgresql://postgres:postgres@localhost:5432/default"
-        displayName: "Run tests"
+          token: $(pypiToken)
+          pythonVersion: "3.10"
diff --git a/ci/templates/install.yml b/ci/templates/install.yml
deleted file mode 100644
index eeaddba..0000000
--- a/ci/templates/install.yml
+++ /dev/null
@@ -1,26 +0,0 @@
-parameters:
-  - name: pythonVersion
-    type: string
-  - name: djangoVersion
-    type: string
-    default: ""
-
-steps:
-  - task: UsePythonVersion@0
-    inputs:
-      versionSpec: "${{ parameters.pythonVersion }}"
-    displayName: "Use Python ${{ parameters.pythonVersion }}"
-
-  - task: Cache@2
-    inputs:
-      key: "pip | $(Agent.OS) | requirements.txt"
-      restoreKeys: |
-        pip | $(Agent.OS)
-        pip
-      path: $(PIP_CACHE_DIR)
-
-  - bash: scripts/install
-    env:
-      ${{ if parameters.djangoVersion }}:
-        DJANGO_VERSION: ${{ parameters.djangoVersion }}
-    displayName: "Install dependencies"
diff --git a/debian/changelog b/debian/changelog
index 2c670db..7a9601a 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,10 @@
+djangorestframework-api-key (2.2.0-1) UNRELEASED; urgency=low
+
+  * New upstream release.
+  * Drop patch fix-copyright-years.patch, present upstream.
+
+ -- Debian Janitor <janitor@jelmer.uk>  Fri, 21 Oct 2022 08:54:00 -0000
+
 djangorestframework-api-key (2.0.0-4) unstable; urgency=medium
 
   [ Debian Janitor ]
diff --git a/debian/patches/Fix_mkdocs_yml b/debian/patches/Fix_mkdocs_yml
index ea4f068..df53539 100644
--- a/debian/patches/Fix_mkdocs_yml
+++ b/debian/patches/Fix_mkdocs_yml
@@ -9,18 +9,20 @@ djangorestframework-api-key (1.4.1-1) unstable; urgency=medium
  mkdocs.yml | 22 +++++++++++-----------
  1 file changed, 11 insertions(+), 11 deletions(-)
 
---- a/mkdocs.yml
-+++ b/mkdocs.yml
-@@ -2,7 +2,7 @@
+Index: djangorestframework-api-key.git/mkdocs.yml
+===================================================================
+--- djangorestframework-api-key.git.orig/mkdocs.yml
++++ djangorestframework-api-key.git/mkdocs.yml
+@@ -2,7 +2,7 @@ site_name: Django REST Framework API Key
  site_url: https://florimondmanca.github.io/djangorestframework-api-key
- copyright: © 2018-present Florimond Manca
+ copyright: © 2018-2020 Florimond Manca
  theme:
 -  name: "material"
 +  name: "readthedocs"
    palette:
      primary: "red"
      accent: "red"
-@@ -22,13 +22,13 @@
+@@ -22,13 +22,13 @@ nav:
    - Changelog: https://github.com/florimondmanca/djangorestframework-api-key/tree/master/CHANGELOG.md
    - License: https://github.com/florimondmanca/djangorestframework-api-key/tree/master/LICENSE
  
diff --git a/debian/patches/Remove_privacy_breaches_in_doc b/debian/patches/Remove_privacy_breaches_in_doc
index 34475ef..60dc466 100644
--- a/debian/patches/Remove_privacy_breaches_in_doc
+++ b/debian/patches/Remove_privacy_breaches_in_doc
@@ -11,8 +11,10 @@ djangorestframework-api-key (1.4.1-1) unstable; urgency=medium
  docs/index.md | 17 -----------------
  1 file changed, 17 deletions(-)
 
---- a/docs/index.md
-+++ b/docs/index.md
+Index: djangorestframework-api-key.git/docs/index.md
+===================================================================
+--- djangorestframework-api-key.git.orig/docs/index.md
++++ djangorestframework-api-key.git/docs/index.md
 @@ -6,22 +6,6 @@
        Django REST Framework
      </a>
diff --git a/debian/patches/fix-copyright-years.patch b/debian/patches/fix-copyright-years.patch
deleted file mode 100644
index c910fec..0000000
--- a/debian/patches/fix-copyright-years.patch
+++ /dev/null
@@ -1,17 +0,0 @@
-From 89259bfc8e15688f5d513a812ea9e8c41f298d60 Mon Sep 17 00:00:00 2001
-From: florimondmanca <florimond.manca@gmail.com>
-Date: Sat, 6 Jun 2020 10:26:58 +0200
-Subject: [PATCH] Fix docs site copyright notice
-Bug-Debian: https://bugs.debian.org/956920
-Last-Update: 2020-07-12
-
---- a/mkdocs.yml
-+++ b/mkdocs.yml
-@@ -1,6 +1,6 @@
- site_name: Django REST Framework API Key
- site_url: https://florimondmanca.github.io/djangorestframework-api-key
--copyright: © 2018-present Florimond Manca
-+copyright: © 2018-2020 Florimond Manca
- theme:
-   name: "readthedocs"
-   palette:
diff --git a/debian/patches/series b/debian/patches/series
index 2e588f3..59a1d54 100644
--- a/debian/patches/series
+++ b/debian/patches/series
@@ -1,3 +1,2 @@
 Fix_mkdocs_yml
 Remove_privacy_breaches_in_doc
-fix-copyright-years.patch
diff --git a/docs/guide.md b/docs/guide.md
index 289bcf4..b435ee2 100644
--- a/docs/guide.md
+++ b/docs/guide.md
@@ -80,13 +80,15 @@ See also [Setting the permission policy](http://www.django-rest-framework.org/ap
 By default, clients must pass their API key via the `Authorization` header. It must be formatted as follows:
 
 ```
-Authorization: Api-Key ********
+Authorization: Api-Key <API_KEY>
 ```
 
-where `********` refers to the generated API key.
+where `<API_KEY>` refers to the full generated API key (see [Creating and managing API keys](#creating-and-managing-api-keys) below).
 
 To know under which conditions access is granted, please see [Grant scheme](security.md#grant-scheme).
 
+If wanting to also customize the keyword used for parsing the Api-Key, please see [API key Custom Keyword](guide.md#api-key-custom-keyword)
+
 #### Custom header
 
 You can set the `API_KEY_CUSTOM_HEADER` setting to a non-`None` value to require clients to pass their API key in a custom header instead of the `Authorization` header.
@@ -103,10 +105,10 @@ API_KEY_CUSTOM_HEADER = "HTTP_X_API_KEY"
 then clients must make authorized requests using:
 
 ```
-X-Api-Key: ********
+X-Api-Key: <API_KEY>
 ```
 
-where `********` refers to the generated API key.
+where `<API_KEY>` refers to the full generated API key.
 
 Please refer to [HttpRequest.META](https://docs.djangoproject.com/en/2.2/ref/request-response/#django.http.HttpRequest.META) for more information on headers in Django.
 
@@ -116,6 +118,9 @@ Please refer to [HttpRequest.META](https://docs.djangoproject.com/en/2.2/ref/req
 
 When it is installed, `djangorestframework-api-key` adds an "API Key Permissions" section to the Django admin site where you can create, view and revoke API keys.
 
+!!! note
+    Upon creating an API key from the admin, the full API key is shown only once in a success message banner. **This is what should be passed in authorization headers.** After creation, only the prefix of the API key is shown in the admin site, mostly for identification purposes. If you lose the full API key, you'll need to regenerate a new one.
+
 #### Programmatic usage
 
 API keys can be created, viewed and revoked programmatically by manipulating the `APIKey` model.
@@ -156,7 +161,7 @@ class ProjectListView(APIView):
 
     def get(self, request):
         """Retrieve a project based on the request API key."""
-        key = request.META["HTTP_AUTHORIZATION"]
+        key = request.META["HTTP_AUTHORIZATION"].split()[1]
         api_key = APIKey.objects.get_from_key(key)
         project = Project.objects.get(api_key=api_key)
 ```
@@ -170,7 +175,7 @@ This package provides various customization APIs that allow you to extend its ba
 If the built-in `APIKey` model doesn't fit your needs, you can create your own by subclassing `AbstractAPIKey`. This is particularly useful if you need to **store extra information** or **link API keys to other models** using a `ForeignKey` or a `ManyToManyField`.
 
 !!! warning
-    Associating API keys to users, directly or indirectly, can present a security risk. See also: [Should I use API keys?](/#should-i-use-api-keys).
+    Associating API keys to users, directly or indirectly, can present a security risk. See also: [Should I use API keys?](https://florimondmanca.github.io/djangorestframework-api-key/#should-i-use-api-keys).
  
 #### Example
 
@@ -286,24 +291,50 @@ class HasOrganizationAPIKey(BaseHasAPIKey):
 You can then use `HasOrganizationAPIKey` as described in [Setting permissions](#setting-permissions).
 
 !!! tip
-    If you need to customize `.has_permission()` or `.has_object_permission()`, feel free to read the [source code](https://github.com/florimondmanca/djangorestframework-api-key/blob/master/rest_framework_api_key/permissions.py).
+    If you need to customize `.has_permission()` or `.has_object_permission()`, feel free to read the [source code](https://github.com/florimondmanca/djangorestframework-api-key/blob/master/src/rest_framework_api_key/permissions.py).
 
 #### API key parsing
 
 By default, API key permission classes retrieve the API key from the `Authorization` header or a custom header, as described in [Making authorized requests](#making-authorized-requests).
 
-You can override this behavior by redefining the `.get_key()` method on your custom permission class. It accepts the [HttpRequest](https://docs.djangoproject.com/en/2.2/ref/request-response/#httprequest-objects) object as unique argument and should return the API key as an `str` if one was found, or `None` otherwise.
+You can customize or override this behavior in several ways.
+
+If you are building an API for an application you do not control that requires a specific header keyword, e.g. a client that sends API keys using the `Bearer` keyword as follows:
+
+```
+Authorization: Bearer <API_KEY>
+```
+
+Then you can subclass `KeyParser` with a custom `keyword`, and attach it to a custom permission class, like so:
+
+```python
+# settings.py
+from rest_framework_api_key.models import HasAPIKey
+from rest_framework_api_key.permissions import BaseHasAPIKey, KeyParser
+
+class BearerKeyParser(KeyParser):
+    keyword = "Bearer"
+
+class HasAPIKey(BaseHasAPIKey):
+    model = APIKey  # Or a custom model
+    key_parser = BearerKeyParser()
+```
+
+You can also override the default header-based parsing completely.
+
+To do so, redefine the `.get_key()` method on your custom permission class. This method accepts the [HttpRequest](https://docs.djangoproject.com/en/2.2/ref/request-response/#httprequest-objects) object as unique argument and should return the API key as an `str` if one was found, or `None` otherwise.
 
 For example, here's how you could retrieve the API key from a cookie:
 
 ```python
-class HasOrganizationAPIKey(BaseHasAPIKey):
-    # ...
+class HasAPIKey(BaseHasAPIKey):
+    model = APIKey  # Or a custom model
+
     def get_key(self, request):
         return request.COOKIES.get("api_key")
 ```
 
-If your custom key parsing algorithm is complex, you may want to define it as a separate component. To do so, build a class which implements the `.get()` method with the same signature as `.get_key()`, and set it as the `.key_parser`:
+If your custom key parsing algorithm is more complex, you may want to define it as a separate component. To do so, build a key parser class, which must implement the `.get()` method with the same signature as `.get_key()`, then set it as the `.key_parser`, as follows:
 
 ```python
 class CookieKeyParser:
@@ -311,8 +342,8 @@ class CookieKeyParser:
         cookie_name = getattr(settings, "API_KEY_COOKIE_NAME", "api_key")
         return request.COOKIES.get(cookie_name)
 
-class HasOrganizationAPIKey(BaseHasAPIKey):
-    # ...
+class HasAPIKey(BaseHasAPIKey):
+    model = APIKey  # Or a custom model
     key_parser = CookieKeyParser()
 ```
 
@@ -341,14 +372,14 @@ class OrganizationAPIKey(AbstractAPIKey):
     # ...
 ```
 
-If you want to replace the key generation algorithm entirely, you can create your own `KeyGenerator` class. It must implement the `.generate()` and `.verify()` methods. At this point, it's probably best to read the [source code](https://github.com/florimondmanca/djangorestframework-api-key/blob/master/rest_framework_api_key/crypto.py) for the built-in `KeyGenerator`.
+If you want to replace the key generation algorithm entirely, you can create your own `KeyGenerator` class. It must implement the `.generate()` and `.verify()` methods. At this point, it's probably best to read the [source code](https://github.com/florimondmanca/djangorestframework-api-key/blob/master/src/rest_framework_api_key/crypto.py) for the built-in `KeyGenerator`.
 
 !!! check
     If the signature of your `.generate()` method is different from the built-in one, you'll need to override `.assign_key()` in your custom API key manager as well.
     
     Likewise, if `.verify()` must accept anything else than the `key` and `hashed_key`, you'll need to override `.is_valid()` on your custom API key model.
     
-    See [models.py](https://github.com/florimondmanca/djangorestframework-api-key/blob/master/rest_framework_api_key/models.py) for the source code of `BaseAPIKeyManager`.
+    See [models.py](https://github.com/florimondmanca/djangorestframework-api-key/blob/master/src/rest_framework_api_key/models.py) for the source code of `BaseAPIKeyManager`.
 
 ## Typing support
 
diff --git a/docs/index.md b/docs/index.md
index 39da9ea..6b0b3de 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -43,7 +43,7 @@ Besides, see [Why and when to use API keys](https://cloud.google.com/endpoints/d
 API keys are ideal in the following situations:
 
 - Blocking anonymous traffic.
-- Implementing API key-based [throttling](https://www.django-rest-framework.org/api-guide/throttling/). (Note that Django REST Framework already has may built-in utilities for this use case.)
+- Implementing API key-based [throttling](https://www.django-rest-framework.org/api-guide/throttling/). (Note that Django REST Framework already has many built-in utilities for this use case.)
 - Identifying usage patterns by logging request information along with the API key.
 
 They can also present enough security for authorizing internal services, such as your API server and an internal frontend application.
diff --git a/docs/security.md b/docs/security.md
index 969fe55..de5961f 100644
--- a/docs/security.md
+++ b/docs/security.md
@@ -9,7 +9,7 @@ An API key is composed of two items:
 - A prefix `P`, which is a generated string of 8 characters.
 - A secret key `SK`, which is a generated string of 32 characters.
 
-The generated key that clients use to [make authorized requests](#making-authorized-requests) is `GK = P.SK`. It is treated with the same level of care than passwords:
+The generated key that clients use to [make authorized requests](#making-authorized-requests) is `GK = P.SK`. It is treated with the same level of care as passwords:
 
 - Only a hashed version is stored in the database. The hash is computed using the default password hasher. [^1]
 - The generated key is shown only once to the client upon API key creation.
@@ -31,9 +31,9 @@ Access is granted if and only if all of the following is true:
 
 [API keys ≠ Security](https://nordicapis.com/why-api-keys-are-not-enough/): depending on your situation, you should probably not use API keys only to authorize your clients.
 
-Besides, you do NOT recommend using this package for authentication, i.e. retrieving user information from API keys.
+Besides, it is NOT recommended to use this package for authentication, i.e. retrieving user information from API keys.
 
-Inded, **using API keys shifts the responsability of Information Security on your clients**. This induces risks, especially if detaining an API key gives access to confidential information or write operations. For example, an attacker could impersonate clients if they let their API keys leak.
+Indeed, **using API keys shifts the responsability of Information Security on your clients**. This induces risks, especially if detaining an API key gives access to confidential information or write operations. For example, an attacker could impersonate clients if they let their API keys leak.
 
 As a best practice, you should apply the _Principle of Least Privilege_: allow only those who require resources to access those specific resources. In other words: **if your client needs to access an endpoint, add API permissions on that endpoint only** instead of the whole API.
 
diff --git a/mkdocs.yml b/mkdocs.yml
index 640af5a..ee1e5be 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -1,6 +1,6 @@
 site_name: Django REST Framework API Key
 site_url: https://florimondmanca.github.io/djangorestframework-api-key
-copyright: © 2018-present Florimond Manca
+copyright: © 2018-2020 Florimond Manca
 theme:
   name: "material"
   palette:
diff --git a/requirements.txt b/requirements.txt
index 1cb4da0..a647603 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,26 +1,30 @@
 -e .
 
 # Django environment.
-django[argon2,bcrypt]==3.0.*
-djangorestframework==3.10.*
+django[argon2,bcrypt]==3.2.*
+djangorestframework==3.12.*
 dj-database-url
 django-dotenv
 
 # PostgreSQL testing.
 psycopg2-binary
 
+# Packaging.
+twine
+wheel
+
 # Tooling.
 autoflake
-black
+black==21.11b1
 flake8
 flake8-bugbear
 flake8-comprehensions
-isort
+isort==5.*
 mkdocs==1.*
-mkdocs-material==4.*
-pymdown-extensions==6.*
+mkdocs-material==8.*
+pymdown-extensions==9.*
 mypy
-pytest==5.*
-pytest-django==3.5.*
+pytest==6.*
+pytest-django==4.*
 pytest-cov
 seed-isort-config
diff --git a/scripts/build b/scripts/build
new file mode 100755
index 0000000..312c348
--- /dev/null
+++ b/scripts/build
@@ -0,0 +1,12 @@
+#!/bin/sh -e
+
+PREFIX=""
+if [ -d "venv" ] ; then
+  PREFIX="venv/bin/"
+fi
+
+set -x
+
+${PREFIX}python setup.py sdist bdist_wheel
+${PREFIX}twine check dist/*
+rm -r build
diff --git a/scripts/check b/scripts/check
index 0aeb8a4..57455c9 100755
--- a/scripts/check
+++ b/scripts/check
@@ -2,12 +2,12 @@
 
 . scripts/env
 
-export SOURCE_FILES="src/rest_framework_api_key test_project/ tests/conftest.py"
-
+export SOURCE_FILES="src/rest_framework_api_key test_project/ tests"
+export MYPY_SOURCE_FILES="src/rest_framework_api_key test_project tests/conftest.py"
 set -x
 
 ${PREFIX}black --check --diff --target-version=py36 $SOURCE_FILES
 ${PREFIX}flake8 $SOURCE_FILES
-${PREFIX}mypy $SOURCE_FILES
-${PREFIX}isort --check --diff --project=djangorestframework-api-key --recursive $SOURCE_FILES
+${PREFIX}mypy $MYPY_SOURCE_FILES
+${PREFIX}isort --check --diff $SOURCE_FILES
 ${PREFIX}python scripts/makemigrations --check
diff --git a/scripts/clean b/scripts/clean
deleted file mode 100755
index 0134b41..0000000
--- a/scripts/clean
+++ /dev/null
@@ -1,24 +0,0 @@
-#!/bin/sh -e
-
-if [ -d 'dist' ] ; then
-    rm -r dist
-fi
-
-if [ -d 'build' ] ; then
-    rm -r build
-fi
-
-if [ -d 'site' ] ; then
-    rm -r site
-fi
-
-if [ -d 'htmlcov' ] ; then
-    rm -r htmlcov
-fi
-
-if [ -d "src/djangorestframework_api_key.egg-info" ] ; then
-    rm -r src/djangorestframework_api_key.egg-info
-fi
-
-find src/rest_framework_api_key -type f -name "*.py[co]" -delete
-find src/rest_framework_api_key -type d -name __pycache__ -delete
diff --git a/scripts/docs b/scripts/docs
index 5baca7d..b5763dc 100755
--- a/scripts/docs
+++ b/scripts/docs
@@ -2,19 +2,6 @@
 
 . scripts/env
 
-COMMAND=$1
-USAGE="USAGE: scripts/docs (build|serve)"
-
-if [ ! $COMMAND ]; then
-  echo $USAGE
-  exit 1
-fi
-
-if [ "$COMMAND" != "build" ] && [ "$COMMAND" != "serve" ]; then
-  echo $USAGE
-  exit 1
-fi
-
 set -x
 
-${PREFIX}mkdocs $COMMAND
+${PREFIX}mkdocs "$@"
diff --git a/scripts/lint b/scripts/lint
index 9829f27..49fa490 100755
--- a/scripts/lint
+++ b/scripts/lint
@@ -2,11 +2,11 @@
 
 . scripts/env
 
-export SOURCE_FILES="src/rest_framework_api_key test_project tests/conftest.py"
+export SOURCE_FILES="src/rest_framework_api_key test_project tests"
 
 set -x
 
 ${PREFIX}autoflake --in-place --recursive $SOURCE_FILES
 ${PREFIX}seed-isort-config --application-directories=src
-${PREFIX}isort --project=rest_framework_api_key --recursive --apply $SOURCE_FILES
+${PREFIX}isort $SOURCE_FILES
 ${PREFIX}black --target-version=py36 $SOURCE_FILES
diff --git a/scripts/publish b/scripts/publish
index 8521a62..4c139fb 100755
--- a/scripts/publish
+++ b/scripts/publish
@@ -2,29 +2,6 @@
 
 . scripts/env
 
-export PACKAGE="rest_framework_api_key"
-export VERSION=`cat src/${PACKAGE}/__version__.py | grep __version__ | sed "s/__version__ = //" | sed "s/'//g"`
+set -x
 
-if ! command -v "${PREFIX}twine" &>/dev/null ; then
-    echo "Unable to find the 'twine' command."
-    echo "Install from PyPI, using '${PREFIX}pip install twine'."
-    exit 1
-fi
-
-if ! ${PREFIX}pip show wheel &>/dev/null ; then
-    echo "Unable to find the 'wheel' command."
-    echo "Install from PyPI, using '${PREFIX}pip install wheel'."
-    exit 1
-fi
-
-scripts/clean
-
-${PREFIX}python setup.py sdist bdist_wheel
 ${PREFIX}twine upload dist/*
-${PREFIX}mkdocs gh-deploy
-
-echo "You probably want to also tag the version now:"
-echo "git tag -a ${VERSION} -m 'version ${VERSION}'"
-echo "git push --tags"
-
-scripts/clean
diff --git a/setup.cfg b/setup.cfg
index 8121d1c..c197f07 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -7,13 +7,9 @@ disallow_untyped_defs = True
 ignore_missing_imports = True
 
 [tool:isort]
-combine_as_imports = True
-force_grid_wrap = 0
-include_trailing_comma = True
+profile = black
 known_first_party = rest_framework_api_key,tests
 known_third_party = dj_database_url,django,dotenv,pytest,rest_framework,setuptools,test_project
-line_length = 88
-multi_line_output = 3
 
 [tool:pytest]
 testpaths = tests
diff --git a/setup.py b/setup.py
index ea50fc3..5656e91 100644
--- a/setup.py
+++ b/setup.py
@@ -44,10 +44,12 @@ setup(
         "Programming Language :: Python :: 3.6",
         "Programming Language :: Python :: 3.7",
         "Programming Language :: Python :: 3.8",
+        "Programming Language :: Python :: 3.9",
+        "Programming Language :: Python :: 3.10",
         "Environment :: Web Environment",
         "Topic :: Software Development :: Libraries :: Python Modules",
         "Framework :: Django",
         "Framework :: Django :: 2.2",
-        "Framework :: Django :: 3.0",
+        "Framework :: Django :: 3.2",
     ],
 )
diff --git a/src/rest_framework_api_key/__init__.py b/src/rest_framework_api_key/__init__.py
index e327823..4d11ebb 100644
--- a/src/rest_framework_api_key/__init__.py
+++ b/src/rest_framework_api_key/__init__.py
@@ -1,5 +1,8 @@
+import django
+
 from .__version__ import __version__
 
-default_app_config = "rest_framework_api_key.apps.RestFrameworkApiKeyConfig"
+if django.VERSION < (3, 2):
+    default_app_config = "rest_framework_api_key.apps.RestFrameworkApiKeyConfig"
 
 __all__ = ["__version__", "default_app_config"]
diff --git a/src/rest_framework_api_key/__version__.py b/src/rest_framework_api_key/__version__.py
index 8c0d5d5..8a124bf 100644
--- a/src/rest_framework_api_key/__version__.py
+++ b/src/rest_framework_api_key/__version__.py
@@ -1 +1 @@
-__version__ = "2.0.0"
+__version__ = "2.2.0"
diff --git a/src/rest_framework_api_key/migrations/0005_auto_20220110_1102.py b/src/rest_framework_api_key/migrations/0005_auto_20220110_1102.py
new file mode 100644
index 0000000..6ccd860
--- /dev/null
+++ b/src/rest_framework_api_key/migrations/0005_auto_20220110_1102.py
@@ -0,0 +1,29 @@
+# Generated by Django 3.2.11 on 2022-01-10 11:02
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("rest_framework_api_key", "0004_prefix_hashed_key"),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name="apikey",
+            name="hashed_key",
+            field=models.CharField(editable=False, max_length=150),
+        ),
+        migrations.AlterField(
+            model_name="apikey",
+            name="id",
+            field=models.CharField(
+                editable=False,
+                max_length=150,
+                primary_key=True,
+                serialize=False,
+                unique=True,
+            ),
+        ),
+    ]
diff --git a/src/rest_framework_api_key/models.py b/src/rest_framework_api_key/models.py
index 4317b83..378f200 100644
--- a/src/rest_framework_api_key/models.py
+++ b/src/rest_framework_api_key/models.py
@@ -73,9 +73,9 @@ class APIKeyManager(BaseAPIKeyManager):
 class AbstractAPIKey(models.Model):
     objects = APIKeyManager()
 
-    id = models.CharField(max_length=100, unique=True, primary_key=True, editable=False)
+    id = models.CharField(max_length=150, unique=True, primary_key=True, editable=False)
     prefix = models.CharField(max_length=8, unique=True, editable=False)
-    hashed_key = models.CharField(max_length=100, editable=False)
+    hashed_key = models.CharField(max_length=150, editable=False)
     created = models.DateTimeField(auto_now_add=True, db_index=True)
     name = models.CharField(
         max_length=50,
diff --git a/src/rest_framework_api_key/permissions.py b/src/rest_framework_api_key/permissions.py
index 284aa67..29d6144 100644
--- a/src/rest_framework_api_key/permissions.py
+++ b/src/rest_framework_api_key/permissions.py
@@ -8,6 +8,8 @@ from .models import AbstractAPIKey, APIKey
 
 
 class KeyParser:
+    keyword = "Api-Key"
+
     def get(self, request: HttpRequest) -> typing.Optional[str]:
         custom_header = getattr(settings, "API_KEY_CUSTOM_HEADER", None)
 
@@ -23,7 +25,7 @@ class KeyParser:
             return None
 
         try:
-            _, key = authorization.split("Api-Key ")
+            _, key = authorization.split("{} ".format(self.keyword))
         except ValueError:
             key = None
 
diff --git a/test_project/heroes/apps.py b/test_project/heroes/apps.py
index 4a55fc1..4aca123 100644
--- a/test_project/heroes/apps.py
+++ b/test_project/heroes/apps.py
@@ -2,4 +2,5 @@ from django.apps import AppConfig
 
 
 class HeroesConfig(AppConfig):
-    name = "heroes"
+    default_auto_field = "django.db.models.BigAutoField"
+    name = "test_project.heroes"
diff --git a/test_project/heroes/migrations/0003_alter_hero_id.py b/test_project/heroes/migrations/0003_alter_hero_id.py
new file mode 100644
index 0000000..148877b
--- /dev/null
+++ b/test_project/heroes/migrations/0003_alter_hero_id.py
@@ -0,0 +1,20 @@
+# Generated by Django 3.2.9 on 2021-12-05 17:09
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("heroes", "0002_prefix_hashed_key"),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name="hero",
+            name="id",
+            field=models.BigAutoField(
+                auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
+            ),
+        ),
+    ]
diff --git a/test_project/heroes/migrations/0004_auto_20220110_1102.py b/test_project/heroes/migrations/0004_auto_20220110_1102.py
new file mode 100644
index 0000000..7341a35
--- /dev/null
+++ b/test_project/heroes/migrations/0004_auto_20220110_1102.py
@@ -0,0 +1,29 @@
+# Generated by Django 3.2.11 on 2022-01-10 11:02
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("heroes", "0003_alter_hero_id"),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name="heroapikey",
+            name="hashed_key",
+            field=models.CharField(editable=False, max_length=150),
+        ),
+        migrations.AlterField(
+            model_name="heroapikey",
+            name="id",
+            field=models.CharField(
+                editable=False,
+                max_length=150,
+                primary_key=True,
+                serialize=False,
+                unique=True,
+            ),
+        ),
+    ]
diff --git a/test_project/project/settings.py b/test_project/project/settings.py
index feaeef4..2227edc 100644
--- a/test_project/project/settings.py
+++ b/test_project/project/settings.py
@@ -55,7 +55,7 @@ WSGI_APPLICATION = "project.wsgi.application"
 
 # Database
 
-DATABASES = {"default": dj_database_url.config(default="sqlite://db.sqlite3")}
+DATABASES = {"default": dj_database_url.config(default="sqlite:///db.sqlite3")}
 
 
 # Password validation
@@ -69,6 +69,15 @@ AUTH_PASSWORD_VALIDATORS = [
     {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
 ]
 
+# Password hashers
+
+PASSWORD_HASHERS = [
+    "django.contrib.auth.hashers.Argon2PasswordHasher",
+    "django.contrib.auth.hashers.PBKDF2PasswordHasher",
+    "django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher",
+    "django.contrib.auth.hashers.BCryptSHA256PasswordHasher",
+]
+
 
 # Internationalization
 
diff --git a/tests/conftest.py b/tests/conftest.py
index 31f2e31..658db33 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -94,6 +94,7 @@ def fixture_key_header_config(request: typing.Any) -> typing.Iterator[dict]:
 @pytest.fixture(name="build_create_request")
 def fixture_build_create_request(key_header_config: dict) -> typing.Callable:
     from rest_framework.test import APIRequestFactory, force_authenticate
+
     from rest_framework_api_key.models import AbstractAPIKey
 
     def build_create_request(model: typing.Type[AbstractAPIKey]) -> typing.Callable:
@@ -102,7 +103,8 @@ def fixture_build_create_request(key_header_config: dict) -> typing.Callable:
         _MISSING = object()
 
         def create_request(
-            authenticated: bool = False, **kwargs: typing.Any,
+            authenticated: bool = False,
+            **kwargs: typing.Any,
         ) -> HttpRequest:
             headers = {}
 
diff --git a/tests/test_admin.py b/tests/test_admin.py
index 146927f..b45dd5d 100644
--- a/tests/test_admin.py
+++ b/tests/test_admin.py
@@ -3,15 +3,14 @@ from django.contrib.admin import site
 from django.contrib.messages import get_messages
 from django.contrib.messages.middleware import MessageMiddleware
 from django.contrib.sessions.middleware import SessionMiddleware
-from django.test import RequestFactory
 from django.http.request import HttpRequest
+from django.test import RequestFactory
+from test_project.heroes.admin import HeroAPIKeyModelAdmin
+from test_project.heroes.models import Hero, HeroAPIKey
 
 from rest_framework_api_key.admin import APIKeyModelAdmin
 from rest_framework_api_key.models import APIKey
 
-from test_project.heroes.admin import HeroAPIKeyModelAdmin
-from test_project.heroes.models import Hero, HeroAPIKey
-
 
 def build_admin_request(rf: RequestFactory) -> HttpRequest:
     request = rf.post("/")
diff --git a/tests/test_compatibility.py b/tests/test_compatibility.py
new file mode 100644
index 0000000..ff09936
--- /dev/null
+++ b/tests/test_compatibility.py
@@ -0,0 +1,18 @@
+import django
+import pytest
+
+import rest_framework_api_key
+
+
+@pytest.mark.skipif(
+    django.VERSION < (3, 2), reason="app config is automatically defined by django"
+)
+def test_app_config_not_defined():
+    assert hasattr(rest_framework_api_key, "default_app_config") is False
+
+
+@pytest.mark.skipif(
+    django.VERSION >= (3, 2), reason="app config is not automatically defined by django"
+)
+def test_app_config_defined():
+    assert hasattr(rest_framework_api_key, "default_app_config") is True
diff --git a/tests/test_model.py b/tests/test_model.py
index bfd0293..a08d34c 100644
--- a/tests/test_model.py
+++ b/tests/test_model.py
@@ -3,9 +3,10 @@ import string
 import pytest
 from django.core.exceptions import ValidationError
 from django.db.utils import IntegrityError
+from test_project.heroes.models import Hero, HeroAPIKey
+
 from rest_framework_api_key.models import APIKey
 
-from test_project.heroes.models import HeroAPIKey, Hero
 from .dateutils import NOW, TOMORROW, YESTERDAY
 
 pytestmark = pytest.mark.django_db
@@ -83,3 +84,9 @@ def test_api_key_manager_get_from_key_invalid_key():
     invalid_key = f"{prefix}.foobar"
     with pytest.raises(APIKey.DoesNotExist):
         APIKey.objects.get_from_key(invalid_key)
+
+
+def test_api_key_str():
+    _, generated_key = APIKey.objects.create_key(name="test")
+    retrieved_key = APIKey.objects.get_from_key(generated_key)
+    assert str(retrieved_key) == "test"
diff --git a/tests/test_permissions.py b/tests/test_permissions.py
index 5a607aa..4990fab 100644
--- a/tests/test_permissions.py
+++ b/tests/test_permissions.py
@@ -5,8 +5,11 @@ from django.conf.global_settings import PASSWORD_HASHERS
 from django.test import override_settings
 from rest_framework import generics, permissions
 from rest_framework.response import Response
+
+from rest_framework_api_key.models import APIKey
+from rest_framework_api_key.permissions import BaseHasAPIKey, HasAPIKey, KeyParser
+
 from .utils import create_view_with_permissions
-from rest_framework_api_key.permissions import HasAPIKey
 
 pytestmark = pytest.mark.django_db
 
@@ -99,3 +102,23 @@ def test_object_permission(create_request):
     request = create_request(authorization=None)
     response = view(request)
     assert response.status_code == 403
+
+
+def test_keyparser_keyword_override(create_request, key_header_config):
+    class BearerKeyParser(KeyParser):
+        keyword = "Bearer"
+
+    class BearerHasAPIKey(BaseHasAPIKey):
+        model = APIKey
+        key_parser = BearerKeyParser()
+
+    bearer_view = create_view_with_permissions(BearerHasAPIKey)
+
+    keyword = "Bearer"
+
+    def get_authorization(key):
+        return key_header_config["default"].format(key=key).replace("Api-Key", keyword)
+
+    request = create_request(authorization=get_authorization)
+    response = bearer_view(request)
+    assert response.status_code == 200
diff --git a/tests/test_permissions_combination.py b/tests/test_permissions_combination.py
index 0798d5e..c29253c 100644
--- a/tests/test_permissions_combination.py
+++ b/tests/test_permissions_combination.py
@@ -2,6 +2,7 @@ import pytest
 from rest_framework.permissions import IsAuthenticated
 
 from rest_framework_api_key.permissions import HasAPIKey
+
 from .utils import create_view_with_permissions
 
 pytestmark = pytest.mark.django_db
diff --git a/tests/test_permissions_custom.py b/tests/test_permissions_custom.py
index 4930c5d..53e5e68 100644
--- a/tests/test_permissions_custom.py
+++ b/tests/test_permissions_custom.py
@@ -1,7 +1,7 @@
 import pytest
-
-from test_project.heroes.models import HeroAPIKey, Hero
+from test_project.heroes.models import Hero, HeroAPIKey
 from test_project.heroes.permissions import HasHeroAPIKey
+
 from .utils import create_view_with_permissions
 
 pytestmark = pytest.mark.django_db
diff --git a/tests/utils.py b/tests/utils.py
index 2372d63..bad88b7 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -1,8 +1,8 @@
 import typing
 
 from rest_framework.decorators import api_view, permission_classes
-from rest_framework.response import Response
 from rest_framework.permissions import BasePermission
+from rest_framework.response import Response
 
 
 def create_view_with_permissions(

More details

Full run details