django-request-token


Namedjango-request-token JSON
Version 2.3 PyPI version JSON
download
home_pagehttps://github.com/yunojuno/django-request-token
SummaryJWT-backed Django app for managing querystring tokens.
upload_time2023-11-14 11:58:27
maintainerYunoJuno
docs_urlNone
authorYunoJuno
requires_python>=3.8,<4.0
licenseMIT
keywords
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage
            ## Supported versions

This project supports Django 3.2+ and Python 3.8+. The latest version
supported is Django 4.1 running on Python 3.11.

## Django Request Token

Django app that uses JWT to manage one-time and expiring tokens to
protect URLs.

This app currently requires the use of PostgreSQL.

### Background

This project was borne out of our experiences at YunoJuno with 'expiring
links' - which is a common use case of providing users with a URL that
performs a single action, and may bypass standard authentication. A
well-known use of this is the ubiquitous 'unsubscribe' link you find
at the bottom of newsletters. You click on the link and it immediately
unsubscribes you, irrespective of whether you are already authenticated
or not.

If you google "temporary url", "one-time link" or something similar you
will find lots of StackOverflow articles on supporting this in Django -
it's pretty obvious, you have a dedicated token url, and you store the
tokens in a model - when they are used you expire the token, and it
can't be used again. This works well, but it falls down in a number of
areas:

* Hard to support multiple endpoints (views)

If you want to support the same functionality (expiring links) for more
than one view in your project, you either need to have multiple models
and token handlers, or you need to store the specific view function and
args in the model; neither of these is ideal.

* Hard to debug

If you use have a single token url view that proxies view functions, you
need to store the function name, args and it then becomes hard to
support - when someone claims that they clicked on
`example.com/t/<token>`, you can't tell what that would resolve to
without looking it up in the database - which doesn't work for customer
support.

* Hard to support multiple scenarios

Some links expire, others have usage quotas - some have both. Links may
be for use by a single user, or multiple users.

This project is intended to provide an easy-to-support mechanism for
'tokenising' URLs without having to proxy view functions - you can build
well-formed Django URLs and views, and then add request token support
afterwards.

### Use Cases

This project supports three core use cases, each of which is modelled
using the `login_mode` attribute of a request token:

1. Public link with payload
2. ~~Single authenticated request~~ (DEPRECATED: use `django-visitor-pass`)
3. ~~Auto-login~~ (DEPRECATED: use `django-magic-link`)

**Public Link** (`RequestToken.LOGIN_MODE_NONE`)

In this mode (the default for a new token), there is no authentication,
and no assigned user. The token is used as a mechanism for attaching a
payload to the link. An example of this might be a custom registration
or affiliate link, that renders the standard template with additional
information extracted from the token - e.g. the name of the affiliate,
or the person who invited you to register.

```python
# a token that can be used to access a public url, without authenticating
# as a user, but carrying a payload (affiliate_id).
token = RequestToken.objects.create_token(
    scope="foo",
    login_mode=RequestToken.LOGIN_MODE_NONE,
    data={
        'affiliate_id': 1
    }
)

...

@use_request_token(scope="foo")
function view_func(request):
    # extract the affiliate id from an token _if_ one is supplied
    affiliate_id = (
        request.token.data['affiliate_id']
        if hasattr(request, 'token')
        else None
    )
```

**Single Request** (`RequestToken.LOGIN_MODE_REQUEST`)

In Request mode, the request.user property is overridden by the user
specified in the token, but only for a single request. This is useful
for responding to a single action (e.g. RSVP, unsubscribe). If the user
then navigates onto another page on the site, they will not be
authenticated. If the user is already authenticated, but as a different
user to the one in the token, then they will receive a 403 response.

```python
# this token will identify the request.user as a given user, but only for
# a single request - not the entire session.
token = RequestToken.objects.create_token(
    scope="foo",
    login_mode=RequestToken.LOGIN_MODE_REQUEST,
    user=User.objects.get(username="hugo")
)

...

@use_request_token(scope="foo")
function view_func(request):
    assert request.user == User.objects.get(username="hugo")
```
**Auto-login** (`RequestToken.LOGIN_MODE_SESSION`)

This is the nuclear option, and must be treated with extreme care. Using
a Session token will automatically log the user in for an entire
session, giving the user who clicks on the link full access the token
user's account. This is useful for automatic logins. A good example of
this is the email login process on medium.com, which takes an email
address (no password) and sends out a login link.

Session tokens have a default expiry of ten minutes.

```python
# this token will log in as the given user for the entire session -
# NB use with caution.
token = RequestToken.objects.create_token(
    scope="foo",
    login_mode=RequestToken.LOGIN_MODE_SESSION,
    user=User.objects.get(username="hugo")
)
```

### Implementation

The project contains middleware and a view function decorator that
together validate request tokens added to site URLs.

**request_token.models.RequestToken** - stores the token details

Step 1 is to create a `RequestToken` - this has various attributes that
can be used to modify its behaviour, and mandatory property - `scope`.
This is a text value - it can be anything you like - it is used by the
function decorator (described below) to confirm that the token given
matches the function being called - i.e. the `token.scope` must match
the function decorator scope kwarg:

```python
token = RequestToken(scope="foo")

# this will raise a 403 without even calling the function
@use_request_token(scope="bar")
def incorrect_scope(request):
    pass

# this will call the function as expected
@use_request_token(scope="foo")
def correct_scope(request):
    pass
```

The token itself - the value that must be appended to links as a
querystring argument - is a JWT - and comes from the
`RequestToken.jwt()` method. For example, if you were sending out an
email, you might render the email as an HTML template like this:

```html
{% if token %}
    <a href="{{url}}?rt={{token.jwt}}>click here</a>
{% else %}
    <a href="{{url}}">click here</a>
{% endif %}
```

If you haven't come across JWT before you can find out more on the
[jwt.io](https://jwt.io/) website. The token produced will include the
following JWT claims (available as the property `RequestToken.claims`:

* `max`: maximum times the token can be used
* `sub`: the scope
* `mod`: the login mode
* `jti`: the token id
* `aud`: (optional) the user the token represents
* `exp`: (optional) the expiration time of the token
* `iat`: (optional) the time the token was issued
* `ndf`: (optional) the not-before-time of the token

**request_token.models.RequestTokenLog** - stores usage data for tokens

Each time a token is used successfully, a log object is written to the
database. This provided an audit log of the usage, and it stores client
IP address and user agent, so can be used to debug issues. This can be
disabled using the `REQUEST_TOKEN_DISABLE_LOGS` setting. The logs table
can be maintained using the management command as described below.

**request_token.middleware.RequestTokenMiddleware** - decodes and verifies tokens

The `RequestTokenMiddleware` will look for a querystring token value
(the argument name defaults to 'rt' and can overridden using the
`JWT_QUERYSTRING_ARG` setting), and if it finds one it will verify the
token (using the JWT decode verification). If the token is verified, it
will fetch the token object from the database and perform additional
validation against the token attributes. If the token checks out it is
added to the incoming request as a `token` attribute. This way you can
add arbitrary data (stored on the token) to incoming requests.

If the token has a user specified, then the `request.user` is updated to
reflect this. The middleware must run after the Django auth middleware,
and before any custom middleware that inspects / monkey-patches the
`request.user`.

If the token cannot be verified it returns a 403.

**request_token.decorators.use_request_token** - applies token
permissions to views

A function decorator that takes one mandatory kwargs (`scope`) and one
optional kwargs (`required`). The `scope` is used to match tokens to
view functions - it's just a straight text match - the value can be
anything you like, but if the token scope is 'foo', then the
corresponding view function decorator scope must match. The `required`
kwarg is used to indicate whether the view **must** have a token in
order to be used, or not. This defaults to False - if a token **is**
provided, then it will be validated, if not, the view function is called
as is.

If the scopes do not match then a 403 is returned.

If required is True and no token is provided the a 403 is returned.

### Installation

Download / install the app using pip:

```shell
pip install django-request-token
```

Add the app `request_token` to your `INSTALLED_APPS` Django setting:

```python
# settings.py
INSTALLED_APPS = (
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'request_token',
    ...
)
```

Add the middleware to your settings, **after** the standard
authentication middleware, and before any custom middleware that uses
the `request.user`.

```python
MIDDLEWARE_CLASSES = [
    # default django middleware
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'request_token.middleware.RequestTokenMiddleware',
]
```

You can now add `RequestToken` objects, either via the shell (or within
your app) or through the admin interface. Once you have added a
`RequestToken` you can add the token JWT to your URLs (using the `jwt()`
method):

```python
>>> token = RequestToken.objects.create_token(scope="foo")
>>> url = "https://example.com/foo?rt=" + token.jwt()
```

You now have a request token enabled URL. You can use this token to
protect a view function using the view decorator:

```python
@use_request_token(scope="foo")
function foo(request):
    pass
```

NB The 'scope' argument to the decorator is used to bind the function to
the incoming token - if someone tries to use a valid token on another
URL, this will return a 403.

**NB this currently supports only view functions - not class-based views.**

### Management commands

There is a single management command, `truncate_request_token_log` which can
be used to manage the size of the log table (each token usage is logged to
the database). It supports two arguments - `--max-count` and `--max-days` which
are self-explanatory:

```
$ python manage.py truncate_request_token_log --max-count=100
Truncating request token log records:
-> Retaining last 100 request token log records
-> Truncating request token log records from 2021-08-01 00:00:00
-> Truncating 0 request token log records.
$
```

### Settings

* `REQUEST_TOKEN_QUERYSTRING`

The querystring argument name used to extract the token from incoming
requests, defaults to **rt**.

* `REQUEST_TOKEN_EXPIRY`

Session tokens have a default expiry interval, specified in minutes. The
primary use case (above) dictates that the expiry should be no longer
than it takes to receive and open an email, defaults to **10**
(minutes).

* `REQUEST_TOKEN_403_TEMPLATE`

Specifying the 403-template so that for prettyfying the 403-response,
in production with a setting like:

```python
FOUR03_TEMPLATE = os.path.join(BASE_DIR,'...','403.html')
```

* `REQUEST_TOKEN_DISABLE_LOGS`

Set to `True` to disable the creation of `RequestTokenLog` objects on
each use of a token. This is not recommended in production, as the
auditing of token use is a valuable part of the library.

### Tests

There is a set of `tox` tests.

### License

MIT

### Contributing

This is by no means complete, however, it's good enough to be of value, hence releasing it.
If you would like to contribute to the project, usual Github rules apply:

1. Fork the repo to your own account
2. Submit a pull request
3. Add tests for any new code
4. Follow coding style of existing project

### Acknowledgements

@jpadilla for [PyJWT](https://github.com/jpadilla/pyjwt/)

            

Raw data

            {
    "_id": null,
    "home_page": "https://github.com/yunojuno/django-request-token",
    "name": "django-request-token",
    "maintainer": "YunoJuno",
    "docs_url": null,
    "requires_python": ">=3.8,<4.0",
    "maintainer_email": "code@yunojuno.com",
    "keywords": "",
    "author": "YunoJuno",
    "author_email": "code@yunojuno.com",
    "download_url": "https://files.pythonhosted.org/packages/ab/36/5c6956c6fec71772e359ff2073414ef61cae2d9502461535a696f70fbf57/django_request_token-2.3.tar.gz",
    "platform": null,
    "description": "## Supported versions\n\nThis project supports Django 3.2+ and Python 3.8+. The latest version\nsupported is Django 4.1 running on Python 3.11.\n\n## Django Request Token\n\nDjango app that uses JWT to manage one-time and expiring tokens to\nprotect URLs.\n\nThis app currently requires the use of PostgreSQL.\n\n### Background\n\nThis project was borne out of our experiences at YunoJuno with 'expiring\nlinks' - which is a common use case of providing users with a URL that\nperforms a single action, and may bypass standard authentication. A\nwell-known use of this is the ubiquitous 'unsubscribe' link you find\nat the bottom of newsletters. You click on the link and it immediately\nunsubscribes you, irrespective of whether you are already authenticated\nor not.\n\nIf you google \"temporary url\", \"one-time link\" or something similar you\nwill find lots of StackOverflow articles on supporting this in Django -\nit's pretty obvious, you have a dedicated token url, and you store the\ntokens in a model - when they are used you expire the token, and it\ncan't be used again. This works well, but it falls down in a number of\nareas:\n\n* Hard to support multiple endpoints (views)\n\nIf you want to support the same functionality (expiring links) for more\nthan one view in your project, you either need to have multiple models\nand token handlers, or you need to store the specific view function and\nargs in the model; neither of these is ideal.\n\n* Hard to debug\n\nIf you use have a single token url view that proxies view functions, you\nneed to store the function name, args and it then becomes hard to\nsupport - when someone claims that they clicked on\n`example.com/t/<token>`, you can't tell what that would resolve to\nwithout looking it up in the database - which doesn't work for customer\nsupport.\n\n* Hard to support multiple scenarios\n\nSome links expire, others have usage quotas - some have both. Links may\nbe for use by a single user, or multiple users.\n\nThis project is intended to provide an easy-to-support mechanism for\n'tokenising' URLs without having to proxy view functions - you can build\nwell-formed Django URLs and views, and then add request token support\nafterwards.\n\n### Use Cases\n\nThis project supports three core use cases, each of which is modelled\nusing the `login_mode` attribute of a request token:\n\n1. Public link with payload\n2. ~~Single authenticated request~~ (DEPRECATED: use `django-visitor-pass`)\n3. ~~Auto-login~~ (DEPRECATED: use `django-magic-link`)\n\n**Public Link** (`RequestToken.LOGIN_MODE_NONE`)\n\nIn this mode (the default for a new token), there is no authentication,\nand no assigned user. The token is used as a mechanism for attaching a\npayload to the link. An example of this might be a custom registration\nor affiliate link, that renders the standard template with additional\ninformation extracted from the token - e.g. the name of the affiliate,\nor the person who invited you to register.\n\n```python\n# a token that can be used to access a public url, without authenticating\n# as a user, but carrying a payload (affiliate_id).\ntoken = RequestToken.objects.create_token(\n    scope=\"foo\",\n    login_mode=RequestToken.LOGIN_MODE_NONE,\n    data={\n        'affiliate_id': 1\n    }\n)\n\n...\n\n@use_request_token(scope=\"foo\")\nfunction view_func(request):\n    # extract the affiliate id from an token _if_ one is supplied\n    affiliate_id = (\n        request.token.data['affiliate_id']\n        if hasattr(request, 'token')\n        else None\n    )\n```\n\n**Single Request** (`RequestToken.LOGIN_MODE_REQUEST`)\n\nIn Request mode, the request.user property is overridden by the user\nspecified in the token, but only for a single request. This is useful\nfor responding to a single action (e.g. RSVP, unsubscribe). If the user\nthen navigates onto another page on the site, they will not be\nauthenticated. If the user is already authenticated, but as a different\nuser to the one in the token, then they will receive a 403 response.\n\n```python\n# this token will identify the request.user as a given user, but only for\n# a single request - not the entire session.\ntoken = RequestToken.objects.create_token(\n    scope=\"foo\",\n    login_mode=RequestToken.LOGIN_MODE_REQUEST,\n    user=User.objects.get(username=\"hugo\")\n)\n\n...\n\n@use_request_token(scope=\"foo\")\nfunction view_func(request):\n    assert request.user == User.objects.get(username=\"hugo\")\n```\n**Auto-login** (`RequestToken.LOGIN_MODE_SESSION`)\n\nThis is the nuclear option, and must be treated with extreme care. Using\na Session token will automatically log the user in for an entire\nsession, giving the user who clicks on the link full access the token\nuser's account. This is useful for automatic logins. A good example of\nthis is the email login process on medium.com, which takes an email\naddress (no password) and sends out a login link.\n\nSession tokens have a default expiry of ten minutes.\n\n```python\n# this token will log in as the given user for the entire session -\n# NB use with caution.\ntoken = RequestToken.objects.create_token(\n    scope=\"foo\",\n    login_mode=RequestToken.LOGIN_MODE_SESSION,\n    user=User.objects.get(username=\"hugo\")\n)\n```\n\n### Implementation\n\nThe project contains middleware and a view function decorator that\ntogether validate request tokens added to site URLs.\n\n**request_token.models.RequestToken** - stores the token details\n\nStep 1 is to create a `RequestToken` - this has various attributes that\ncan be used to modify its behaviour, and mandatory property - `scope`.\nThis is a text value - it can be anything you like - it is used by the\nfunction decorator (described below) to confirm that the token given\nmatches the function being called - i.e. the `token.scope` must match\nthe function decorator scope kwarg:\n\n```python\ntoken = RequestToken(scope=\"foo\")\n\n# this will raise a 403 without even calling the function\n@use_request_token(scope=\"bar\")\ndef incorrect_scope(request):\n    pass\n\n# this will call the function as expected\n@use_request_token(scope=\"foo\")\ndef correct_scope(request):\n    pass\n```\n\nThe token itself - the value that must be appended to links as a\nquerystring argument - is a JWT - and comes from the\n`RequestToken.jwt()` method. For example, if you were sending out an\nemail, you might render the email as an HTML template like this:\n\n```html\n{% if token %}\n    <a href=\"{{url}}?rt={{token.jwt}}>click here</a>\n{% else %}\n    <a href=\"{{url}}\">click here</a>\n{% endif %}\n```\n\nIf you haven't come across JWT before you can find out more on the\n[jwt.io](https://jwt.io/) website. The token produced will include the\nfollowing JWT claims (available as the property `RequestToken.claims`:\n\n* `max`: maximum times the token can be used\n* `sub`: the scope\n* `mod`: the login mode\n* `jti`: the token id\n* `aud`: (optional) the user the token represents\n* `exp`: (optional) the expiration time of the token\n* `iat`: (optional) the time the token was issued\n* `ndf`: (optional) the not-before-time of the token\n\n**request_token.models.RequestTokenLog** - stores usage data for tokens\n\nEach time a token is used successfully, a log object is written to the\ndatabase. This provided an audit log of the usage, and it stores client\nIP address and user agent, so can be used to debug issues. This can be\ndisabled using the `REQUEST_TOKEN_DISABLE_LOGS` setting. The logs table\ncan be maintained using the management command as described below.\n\n**request_token.middleware.RequestTokenMiddleware** - decodes and verifies tokens\n\nThe `RequestTokenMiddleware` will look for a querystring token value\n(the argument name defaults to 'rt' and can overridden using the\n`JWT_QUERYSTRING_ARG` setting), and if it finds one it will verify the\ntoken (using the JWT decode verification). If the token is verified, it\nwill fetch the token object from the database and perform additional\nvalidation against the token attributes. If the token checks out it is\nadded to the incoming request as a `token` attribute. This way you can\nadd arbitrary data (stored on the token) to incoming requests.\n\nIf the token has a user specified, then the `request.user` is updated to\nreflect this. The middleware must run after the Django auth middleware,\nand before any custom middleware that inspects / monkey-patches the\n`request.user`.\n\nIf the token cannot be verified it returns a 403.\n\n**request_token.decorators.use_request_token** - applies token\npermissions to views\n\nA function decorator that takes one mandatory kwargs (`scope`) and one\noptional kwargs (`required`). The `scope` is used to match tokens to\nview functions - it's just a straight text match - the value can be\nanything you like, but if the token scope is 'foo', then the\ncorresponding view function decorator scope must match. The `required`\nkwarg is used to indicate whether the view **must** have a token in\norder to be used, or not. This defaults to False - if a token **is**\nprovided, then it will be validated, if not, the view function is called\nas is.\n\nIf the scopes do not match then a 403 is returned.\n\nIf required is True and no token is provided the a 403 is returned.\n\n### Installation\n\nDownload / install the app using pip:\n\n```shell\npip install django-request-token\n```\n\nAdd the app `request_token` to your `INSTALLED_APPS` Django setting:\n\n```python\n# settings.py\nINSTALLED_APPS = (\n    'django.contrib.admin',\n    'django.contrib.auth',\n    'django.contrib.contenttypes',\n    'django.contrib.sessions',\n    'django.contrib.messages',\n    'django.contrib.staticfiles',\n    'request_token',\n    ...\n)\n```\n\nAdd the middleware to your settings, **after** the standard\nauthentication middleware, and before any custom middleware that uses\nthe `request.user`.\n\n```python\nMIDDLEWARE_CLASSES = [\n    # default django middleware\n    'django.contrib.sessions.middleware.SessionMiddleware',\n    'django.middleware.common.CommonMiddleware',\n    'django.middleware.csrf.CsrfViewMiddleware',\n    'django.contrib.auth.middleware.AuthenticationMiddleware',\n    'django.contrib.messages.middleware.MessageMiddleware',\n    'request_token.middleware.RequestTokenMiddleware',\n]\n```\n\nYou can now add `RequestToken` objects, either via the shell (or within\nyour app) or through the admin interface. Once you have added a\n`RequestToken` you can add the token JWT to your URLs (using the `jwt()`\nmethod):\n\n```python\n>>> token = RequestToken.objects.create_token(scope=\"foo\")\n>>> url = \"https://example.com/foo?rt=\" + token.jwt()\n```\n\nYou now have a request token enabled URL. You can use this token to\nprotect a view function using the view decorator:\n\n```python\n@use_request_token(scope=\"foo\")\nfunction foo(request):\n    pass\n```\n\nNB The 'scope' argument to the decorator is used to bind the function to\nthe incoming token - if someone tries to use a valid token on another\nURL, this will return a 403.\n\n**NB this currently supports only view functions - not class-based views.**\n\n### Management commands\n\nThere is a single management command, `truncate_request_token_log` which can\nbe used to manage the size of the log table (each token usage is logged to\nthe database). It supports two arguments - `--max-count` and `--max-days` which\nare self-explanatory:\n\n```\n$ python manage.py truncate_request_token_log --max-count=100\nTruncating request token log records:\n-> Retaining last 100 request token log records\n-> Truncating request token log records from 2021-08-01 00:00:00\n-> Truncating 0 request token log records.\n$\n```\n\n### Settings\n\n* `REQUEST_TOKEN_QUERYSTRING`\n\nThe querystring argument name used to extract the token from incoming\nrequests, defaults to **rt**.\n\n* `REQUEST_TOKEN_EXPIRY`\n\nSession tokens have a default expiry interval, specified in minutes. The\nprimary use case (above) dictates that the expiry should be no longer\nthan it takes to receive and open an email, defaults to **10**\n(minutes).\n\n* `REQUEST_TOKEN_403_TEMPLATE`\n\nSpecifying the 403-template so that for prettyfying the 403-response,\nin production with a setting like:\n\n```python\nFOUR03_TEMPLATE = os.path.join(BASE_DIR,'...','403.html')\n```\n\n* `REQUEST_TOKEN_DISABLE_LOGS`\n\nSet to `True` to disable the creation of `RequestTokenLog` objects on\neach use of a token. This is not recommended in production, as the\nauditing of token use is a valuable part of the library.\n\n### Tests\n\nThere is a set of `tox` tests.\n\n### License\n\nMIT\n\n### Contributing\n\nThis is by no means complete, however, it's good enough to be of value, hence releasing it.\nIf you would like to contribute to the project, usual Github rules apply:\n\n1. Fork the repo to your own account\n2. Submit a pull request\n3. Add tests for any new code\n4. Follow coding style of existing project\n\n### Acknowledgements\n\n@jpadilla for [PyJWT](https://github.com/jpadilla/pyjwt/)\n",
    "bugtrack_url": null,
    "license": "MIT",
    "summary": "JWT-backed Django app for managing querystring tokens.",
    "version": "2.3",
    "project_urls": {
        "Homepage": "https://github.com/yunojuno/django-request-token",
        "Repository": "https://github.com/yunojuno/django-request-token"
    },
    "split_keywords": [],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "967d534319604e8588caf634ba9d358b0a8db8f5e3c557dc2753e384d13b7ba7",
                "md5": "95ab722baf9bcccdf475a3eef768e43b",
                "sha256": "bb858cc77e6af0614b61e3b8f7005f0ebf0a960367639eaebae6795b5eb853cf"
            },
            "downloads": -1,
            "filename": "django_request_token-2.3-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "95ab722baf9bcccdf475a3eef768e43b",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.8,<4.0",
            "size": 29631,
            "upload_time": "2023-11-14T11:58:25",
            "upload_time_iso_8601": "2023-11-14T11:58:25.494543Z",
            "url": "https://files.pythonhosted.org/packages/96/7d/534319604e8588caf634ba9d358b0a8db8f5e3c557dc2753e384d13b7ba7/django_request_token-2.3-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "ab365c6956c6fec71772e359ff2073414ef61cae2d9502461535a696f70fbf57",
                "md5": "770fbb5e0d1786d6481b1c140e8df6cb",
                "sha256": "5ff298e37e5d8bc6b7c054a3c0931724b50a4548ec75745262288832be58421e"
            },
            "downloads": -1,
            "filename": "django_request_token-2.3.tar.gz",
            "has_sig": false,
            "md5_digest": "770fbb5e0d1786d6481b1c140e8df6cb",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.8,<4.0",
            "size": 24591,
            "upload_time": "2023-11-14T11:58:27",
            "upload_time_iso_8601": "2023-11-14T11:58:27.554085Z",
            "url": "https://files.pythonhosted.org/packages/ab/36/5c6956c6fec71772e359ff2073414ef61cae2d9502461535a696f70fbf57/django_request_token-2.3.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2023-11-14 11:58:27",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "yunojuno",
    "github_project": "django-request-token",
    "travis_ci": false,
    "coveralls": true,
    "github_actions": true,
    "tox": true,
    "lcname": "django-request-token"
}
        
Elapsed time: 0.14439s