feat: 3.6

- Added support for Python 3.9.
- Added support for Python 3.10.
- Removed cache headers for all views except 404. Due to that snippets can be
  deleted, it's not trivial to have them removed from upstream caches.
- Bump pygments version to 2.11.
- update dependency psycopg2-binary to v2.9.3
- Addresses bug in cleanup_snippets script [#191](https://github.com/DarrenOfficial/dpaste/issues/191)
- Removed docs, since it's moved to [docs.dpaste.org](https://docs.dpaste.org), [darrenofficial/dpaste-docs](https://github.com/darrenofficial/dpaste-docs) and will be updated there.
- Removed changelog from main branched, it's moved to https://docs.dpaste.org/changelog/

Signed-off-by: Darren <git@darrennathanael.com>
This commit is contained in:
Darren 2022-03-27 09:17:23 -04:00
parent 7a64572cc1
commit 8aa1d51146
No known key found for this signature in database
GPG key ID: E145327EB72009B4
15 changed files with 2 additions and 863 deletions

View file

@ -1,241 +0,0 @@
Changelog
=========
3.6 (master)
------------
- Added support for Python 3.9.
- Added support for Python 3.10.
- Removed cache headers for all views except 404. Due to that snippets can be
deleted, it's not trivial to have them removed from upstream caches.
- Bump pygments version to 2.11.
3.5 (2020-01-08)
----------------
- Mobile view improvements.
- Upgraded django-csp dependency to v3.6 that ships with Django 3.0 support.
3.4 (2019-12-08)
----------------
- Dropped support for Python 3.4.
- Dropped support for Python 3.5.
- Dropped support for Django 1.11. ⚠️
- Dropped support for Django 2.0. ⚠️
- Dropped support for Django 2.1. ⚠️
- Added support for Python 3.8.
- Added support for Django 3.0.
- Snippets which are expired are now deleted as soon as they are requested
by a client. It's not necessary to purge them minutely with the
``cleanup_snipppet`` managemenent command. It's still encouraged to have the
management command setup, just run it daily, so snippets which expired but
never got fetched by a client are deleted properly.
- All pages have sane Expire or Max-Age header.
- Onetime snippets which were never viewed a second time are now deleted if
they reach the default expire date.
- New AppConfig setting ``APPLICATION_NAME`` that can be used to replace the
term "dpaste" throughout the UI.
- New AppConfig setting ``EXTRA_HEAD_HTML`` and similars that can be used to
add custom HTML to each template, to easily override the stock UI of dpaste.
- New "Slim" view that displays the highlighted snippet without header,
options etc, and can be iframed.
- Forced line-break for superlongwordsthatwouldexceedthecanvas.
- Local development is no longer centered around ``pipenv``, instead it's using
docker-compose or the classic virtualenv based setups.
- Error pages are now correctly translated.
- Testsuite and Tox uses pytest instead of a homebrewed testrunner.
3.3.1 (2019-08-04):
-------------------
- Exclude the local settings file from the pypi release.
3.3 (2019-07-12)
----------------
- The compiled static files (CSS, JS) are now shipped with the Pypi package since
its not possible to compile them after installation with pip.
3.2 (2019-06-24)
----------------
- "Edit Snippet" panel is now hidden by default to remove visual noise.
- Linux/Unix browsers now use Ctrl+Enter as a shortcut to submit the form.
- Added a dedicated "Copy Snippet" button to copy the content to the clipboard.
- Added "View Raw" option to optionally render the 'raw' snippet content with a
template rather served as plain text. This was added to hinder abuse.
- Added "Json" to the list of lexers.
- Added 'JSX/React" to the list of lexers.
3.1 (2019-05-16)
----------------
- Django 2.1 support and tests.
- Django 2.2 support and tests.
- General code cleanup by running the entire codebase through black_.
- Right-to-left support for text snippets.
- dart-sass is now used for SASS compilation.
- Updated lexer list.
- "View Raw" feature can be disabled in app config to hinder abuse.
.. _black: https://github.com/ambv/black
3.0 (2018-06-22)
----------------
Huge release. Full cleanup and update of the entire codebase. Details:
- Requires Python 3.4 and up.
- Dropped support for Django 1.8 to 1.10 due to it's general end of support.
The project will likely work well but it's no longer specifically tested.
- All views are now class based and use the latest generic based views sugar.
- Django 1.11 based templates, forms, views, models, etc.
- Added pipenv support for local development.
- Added AppConfig support to set and maintain settings.
- Added "Rendered Text" lexer with support for rST and Markdown.
- Added Content Security Policy features, with django-csp (this is mainly
required for the "rendered" text feature).
- Removed jQuery dependency, all Javascript is native.
- Removed Bootstrap dependency.
- Removed 'Maximum History' limit setting.
- Removed translations.
- Removed "Suspicious" middleware which was never been used, documented,
and also not functional for a while.
- Fixed issues around leading whitespace in lines.
- Fixed CMD+Enter form submission shortcut in Firefox.
2.14 (no public release)
------------------------
- Django 1.11 compatibility. But not Django 2.0 yet.
- Removed "Suspicious" middleware which was never been used, documented,
and also not functional for a while.
2.13 (2017-01-20)
-----------------
- (Backwards incompatible) Removal of django-mptt and therefor the removal of a
tree based snippet list, due to performance reasons with large snippet counts.
Snippets still have a 'parent' relation if it's an answer of another snippet,
however this is no longer a Nested Set. The UI is simplified too and the user
can now only compare an answer to it's parent snippet. I believe this is the
major use case anyway.
- (Backwards incompatible) Removal of the "Gist" button feature.
- Fixed broken 404 view handler in Django 1.9+.
- Python 3.6 and Django 1.10 compatibility and tests.
2.12 (2016-09-06)
-----------------
- Fixed "Content Type" problem with Django 1.10.
- Development requirements now use a different version scheme to be
compatible with older `pip` versions.
2.11 (2016-09-04)
-----------------
- Django 1.10 Support
- R Lexer is enabled by default
- Minor fixes and improvements.
2.10 (2016-03-23)
-----------------
- Dropped Django 1.4 and 1.7 support!
- Full Django 1.8 support
- Full Django 1.9 support
- C++ Lexer is enabled by default
- (Backwards incompatible) All API calls must pass the data within a POST
request. It can't mix POST and GET arguments anymore. This was weird behavior
anyway and is likely no issue for any paste plugin out there.
2.9 (2015-08-12)
----------------
- Full Django 1.7 support
- Full Django 1.8 support
- New Django migrations, with fallback to South migrations if South is
installed. If you want to switch from South to native Django migrations,
and have an existing databsae, fake the initial migrations:
`manage.py migrate --fake-initial`
- Added full i18n support and several languages
- More settings can be overrridden, like the jQuery URL, site name and wether
you want to enable Gthub Gist.
- Ships a middleware that blocks anonymous proxies and TOR nodes. Not enabled
by default.
2.8 (2014-08-02)
----------------
- The API create view has a new argument 'filename' which is used to determine
the lexer out of a given filename.
- Fixed a XSS bug where HTML tags were not properly escaped with the simple
``code`` lexer.
2.7 (2014-06-08)
----------------
- "never" as an expiration choice is enable by default! This creates snippets
in the database which are never purged.
- The API create call now supports to set the exiration time.
- Some simple Bootstrap 3 support.
- Gist fixes on Python 3.
2.6 (2014-04-12)
----------------
- Fix for the rare case of duplicate slug (secret id) generation.
- A new 'code' lexer renders source code with no highlighting.
- Whitespace fixes with tab indention and word wrap mode.
- Installation docs.
2.5 (2014-01-21)
----------------
- IRC lexer is now in the default lexer list.
- One-Time snippet support. Snippets get automatically deleted after the
another user looks at it.
- Toggle wordwrap for code snippets.
- General UI and readability improvements.
2.4 (2014-01-11)
----------------
- API accepts the format or lexer via GET too. You can call an API url like
``example.com/api/?format=json`` and have the body in POST only.
- Added an option to keep snippets forever.
- ABAP lexer is now in the default lexer list.
2.3 (2014-01-07)
----------------
- API Documentation.
- Full test coverage.
- Removed Twitter button from homepage.
- Slug generation is less predictable.
2.2 (2013-12-18)
----------------
- Added documentation_
- Added support for CSRF middleware.
- Windows users can submit the form using Ctrl+Enter.
- The raw view now sends the X-Content-Type-Options=nosniff header.
- Various constants can now be overridden by settings.
- Support for `python setup.py test` to run the tox suite.
.. _documentation: http://dpaste.readthedocs.org/en/latest/
2.1 (2013-12-14)
----------------
- Changes and fixes along the package management.
2.0 (2013-11-29)
----------------
- A huge cleanup and nearly total rewrite.
- dpaste now includes a Django project which is used on www.dpaste.de
as well as hooks to get it integrated into existing projcts.

View file

@ -1,4 +1,5 @@
Copyright (c) 2013 Martin Mahner <martin@mahner.org> Copyright (c) 2013 Martin Mahner <martin@mahner.org>
Copyright (c) 2022 Darren Nathanael <me@darrennathanael.com>
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View file

@ -1,4 +0,0 @@
.highlight { background-color: #f8f8f8; }
.highlight pre { font-size: 1em; }
.sidebar .searchbox { border-top: 1px solid #eaecef;; }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 KiB

View file

@ -1,134 +0,0 @@
===
API
===
API endpoint
============
dpaste provides a simple API endpoint to create new snippets. All you need to
do is a simple ``POST`` request to the API endpoint, usually ``/api/``:
.. http:post:: /api/
Create a new Snippet on this dpaste installation. It returns the full
URL that snippet was created.
**Example request**:
.. code-block:: bash
$ curl -X POST -F "format=url" -F "content=ABC" https:/dpaste.de/api/
Host: dpaste.de
User-Agent: curl/7.54.0
Accept: */*
**Example response**:
.. sourcecode:: json
{
"lexer": "python",
"url": "https://dpaste.de/EBKU",
"content": "ABC"
}
:form content: (required) The UTF-8 encoded string you want to paste.
:form lexer: (optional) The lexer string key used for highlighting. See
the ``CODE_FORMATTER`` property in :ref:`settings` for a full list
of choices. Default: ``_code``.
:form format: (optional) The format of the API response. Choices are:
* ``default`` — Returns a full qualified URL wrapped in quotes.
Example: ``"https://dpaste.de/xsWd"``
* ``url`` — Returns the full qualified URL to the snippet, without surrounding
quotes, but with a line break. Example: ``https://dpaste.de/xsWd\n``
* ``json`` — Returns a JSON object containing the URL, lexer and content of the
the snippet. Example:
.. code-block:: json
{
"url": "https://dpaste.de/xsWd",
"lexer": "python",
"content": "The text body of the snippet."
}
:form expires: (optional) A keyword to indicate the lifetime of a snippet in
seconds. The values are
predefined by the server. Calling this with an invalid value returns a HTTP 400
BadRequest together with a list of valid values. Default: ``2592000``. In the
default configuration valid values are:
* onetime
* never
* 3600
* 604800
* 2592000
:form filename: (optional) A filename which we use to determine a lexer, if
``lexer`` is not set. In case we can't determine a file, the lexer will
fallback to ``plain`` code (no highlighting). A given ``lexer`` will overwrite
any filename! Example:
.. code-block:: json
{
"url": "https://dpaste.de/xsWd",
"lexer": "",
"filename": "python",
"content": "The text body of the snippet."
}
This will create a ``python`` highlighted snippet. However in this example:
.. code-block:: json
{
"url": "https://dpaste.de/xsWd",
"lexer": "php",
"filename": "python",
"content": "The text body of the snippet."
}
Since the lexer is set too, we will create a ``php`` highlighted snippet.
:statuscode 200: No Error.
:statuscode 400: One of the above form options was invalid,
the response will contain a meaningful error message.
.. hint:: If you have a standalone installation and your API returns
``https://dpaste-base-url.example.org`` as the domain, you need to adjust
the setting ``get_base_url`` property. See :ref:`settings`.
Third party API integration
===========================
subdpaste
a Sublime Editor plugin: https://github.com/bartTC/SubDpaste
Marmalade
an Emacs plugin: http://marmalade-repo.org/packages/dpaste_de
atom-dpaste
for the Atom editor: https://atom.io/packages/atom-dpaste
dpaste-magic
an iPython extension: https://pypi.org/project/dpaste-magic/
You can also paste your file content to the API via curl, directly from the
command line:
.. code-block:: bash
$ alias dpaste="curl -F 'format=url' -F 'content=<-' https://dpaste.org/api/"
$ cat foo.txt | dpaste
https://dpaste.de/ke2pB
.. note:: If you wrote or know a third party dpaste plugin or extension,
please open an *Issue* on Github_ and it's added here.
.. _Github: https://github.com/bartTC/dpaste

View file

@ -1,3 +0,0 @@
.. _changelog:
.. include:: ../CHANGELOG.rst

View file

@ -1,178 +0,0 @@
# -*- coding: utf-8 -*-
#
# Configuration file for the Sphinx documentation builder.
#
# This file does only contain a selection of the most common options. For a
# full list see the documentation:
# http://www.sphinx-doc.org/en/master/config
# -- Path setup --------------------------------------------------------------
# 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.
#
# import os
# import sys
# sys.path.insert(0, os.path.abspath('.'))
# -- Project information -----------------------------------------------------
from datetime import datetime
project = 'dpaste'
copyright = f'2013—{datetime.now().year}, Martin Mahner'
author = 'Martin Mahner'
# The short X.Y version
version = ''
# The full version, including alpha/beta/rc tags
release = ''
# -- 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.viewcode',
'sphinxcontrib.httpdomain',
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
#
# source_suffix = ['.rst', '.md']
source_suffix = '.rst'
# The master toctree document.
master_doc = 'index'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = None
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path .
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = "tango"
# -- Options for HTML output -------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
try:
html_theme = "press"
html_css_files = ["custom.css"]
html_sidebars = {'**': ['util/sidetoc.html', 'util/searchbox.html']}
html_theme_options = {
"external_links": [
("🐞 File an issue", "https://github.com/bartTC/dpaste/issues"),
("Github", "https://github.com/bartTC/dpaste"),
("Docker Hub", "https://hub.docker.com/r/barttc/dpaste"),
("dpaste.org", "https://dpaste.org")
]
}
except ImportError:
html_theme = 'alabaster'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#
# html_theme_options = {}
# 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']
# Custom sidebar templates, must be a dictionary that maps document names
# to template names.
#
# The default sidebars (for documents that don't match any pattern) are
# defined by theme itself. Builtin themes are using these templates by
# default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
# 'searchbox.html']``.
#
# html_sidebars = {}
# -- Options for HTMLHelp output ---------------------------------------------
# Output file base name for HTML help builder.
htmlhelp_basename = 'dpastedoc'
# -- 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': '',
# Latex figure (float) alignment
#
# 'figure_align': 'htbp',
}
# 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 = [
(master_doc, 'dpaste.tex', 'dpaste Documentation',
'Martin Mahner', 'manual'),
]
# -- Options for manual page output ------------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(master_doc, 'dpaste', 'dpaste Documentation',
[author], 1)
]
# -- 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 = [
(master_doc, 'dpaste', 'dpaste Documentation',
author, 'dpaste', 'One line description of project.',
'Miscellaneous'),
]
# -- Extension configuration -------------------------------------------------

View file

@ -1,19 +0,0 @@
.. _index:
.. include:: ../README.rst
Documentation
=============
.. toctree::
:maxdepth: 1
installation
testing
management_commands
settings
api
changelog
.. _dpaste.de: https://dpaste.de/
.. _pastebin: https://en.wikipedia.org/wiki/Pastebin

View file

@ -1,139 +0,0 @@
.. _installation:
============
Installation
============
There are various ways to install and deploy dpaste. See the guides below:
dpaste with Docker
==================
dpaste Docker images are available to pull from the `Docker Hub`_.
Quickstart to run a dpaste container image:
.. code:: bash
$ docker run --rm -p 8000:8000 barttc/dpaste:latest
The dpaste image serves the project using uWSGi and is ready for production-like
environments. However it's encouraged to use an external database to store the
data. See the example below for all available options, specifically
``DATABASE_URL``:
.. code:: bash
$ docker run --rm --name db1 --detach postgres:latest
$ docker run --rm -p 12345:12345 \
--link db1 \
-e DATABASE_URL=postgres://postgres@db1:5432/postgres \
-e DEBUG=True \
-e SECRET_KEY=very-secret-key \
-e PORT=12345 \
barttc/dpaste:latest
.. _Docker Hub: https://hub.docker.com/r/barttc/dpaste
Integration into an existing Django project
===========================================
Install the latest dpaste release in your environment. This will install all
necessary dependencies of dpaste as well:
.. code-block:: bash
$ pip install dpaste
Add ``dpaste.apps.dpasteAppConfig`` to your ``INSTALLED_APPS`` list:
.. code-block:: python
INSTALLED_APPS = (
'django.contrib.sessions',
# ...
'dpaste.apps.dpasteAppConfig',
)
Add ``dpaste`` and the (optiona) ``dpaste_api`` url patterns:
.. code-block:: python
urlpatterns = patterns('',
# ...
url(r'my-pastebin/', include('dpaste.urls.dpaste')),
url(r'my-pastebin/api/', include('dpaste.urls.dpaste_api')),
)
Finally, migrate the database schema:
.. code-block:: bash
$ manage.py migrate dpaste
dpaste with docker-compose for local development
================================================
The project's preferred way for local development is docker-compose:
.. code:: bash
$ docker-compose up
This will open the Django runserver on http://127.0.0.1:8000. Changes to the
code are automatically reflected in the Docker container and the runserver
will reload automatically.
Upon first run you will need to migrate the database. Do that in a separate
terminal window:
.. code:: bash
$ docker-compose run --rm app ./manage.py migrate
dpaste with virtualenv for local development
============================================
If you prefer the classic local installation using Virtualenv then you can
do so. There's no magic involved.
Example:
.. code:: bash
$ python3 -m venv .venv
$ source .venv/bin/activate
$ pip install -e .[dev]
$ ./manage.py migrate
$ ./manage.py runserver
CSS and Javascript development
==============================
Static files are stored in the ``client/`` directory and must get compiled
and compressed before being used on the website.
.. code:: bash
$ npm install
There are some helper scripts you can invoke with ``make``
make js
Compile only JS files.
make css
Compile only CSS files.
make css-watch
Same as ``build-css`` but it automatically watches for changes in the
CSS files and re-compiles it.
After compilation the CSS and JS files are stored in ``dpaste/static/``
where they are picked up by Django (and Django's collectstatic command).
.. note::
These files are not commited to the project repository, however they are
part of the pypi wheel package, since users couldn't compile those once
they are within Python's site-packages.

View file

@ -1,46 +0,0 @@
.. _management_commands:
===================
Management Commands
===================
.. _purge_expired_snippets:
Purge expired snippets
======================
Snippets are removed as soon as they exceed their expiration
date and get fetched by a client, however if they never get fetched this isn't
triggered. dpaste ships with a management command ``cleanup_snippets`` that
removes these expired snippets.
It's sufficient to run it daily.
To run it locally do:
.. code-block:: bash
$ pipenv run ./managepy cleanup_snippets
Options
-------
--dry-run Does not actually delete the snippets.
This is useful for local testing.
Setup a Crontab
---------------
A crontab line might look like:
.. code-block:: bash
1 20 * * * /srv/dpaste.de/pipenv run manage.py cleanup_snippets > /dev/null
.. note:: If you use the *database* session backend, you may also need to setup
a crontab that removes the expired entries from the session database.
See the related `Django Documentation`_ for details.
.. _Django Documentation: https://docs.djangoproject.com/en/2.0/ref/django-admin/#django-admin-clearsessions

View file

@ -1,2 +0,0 @@
sphinxcontrib-httpdomain
sphinx_press_theme

View file

@ -1,48 +0,0 @@
.. _settings:
========
Settings
========
When dpaste is installed as a standalone service or integrated into an existing
project there are various settings you can override to adjust dpaste's
behavior.
To do so, you need to override dpaste's AppConfig. This is a feature
`introduced in Django 1.9`_ and allows you to set settings more
programmatically.
See :ref:`current_appconfig` for a full list of settings and functions you
can override.
Example for your custom AppConfig:
==================================
.. code-block:: python
# settings.py
from dpaste.apps import dpasteAppConfig
class MyBetterDpasteAppConfig(dpasteAppConfig):
SLUG_LENGTH = 8
LEXER_DEFAULT = 'js'
# ...
INSTALLED_APPS = [
'myproject.settings.MyBetterDpasteAppConfig',
]
.. _introduced in Django 1.9: https://docs.djangoproject.com/en/1.9/ref/applications/
.. _current_appconfig:
Current AppConfig with default values
=====================================
This is the file content of ``dpaste/apps.py``:
.. literalinclude:: ../dpaste/apps.py
:language: python

View file

@ -1,49 +0,0 @@
.. _testing:
=======
Testing
=======
Testing with Tox
================
dpaste is continuously tested online with Travis_. You can also run the test
suite locally with tox_. Tox automatically tests the project against multiple
Python and Django versions.
.. code-block:: bash
$ pip install tox
Then simply call it from the project directory.
.. code-block:: bash
$ cd dpaste/
$ tox
.. code-block:: text
:caption: Example tox output:
$ tox
py35-django-111 create: /tmp/tox/dpaste/py35-django-111
SKIPPED:InterpreterNotFound: python3.5
py36-django-111 create: /tmp/tox/dpaste/py36-django-111
py36-django-111 installdeps: django>=1.11,<1.12
py36-django-111 inst: /tmp/tox/dpaste/dist/dpaste-3.0a1.zip
...................
----------------------------------------------------------------------
Ran 48 tests in 1.724s
OK
SKIPPED: py35-django-111: InterpreterNotFound: python3.5
SKIPPED: py35-django-20: InterpreterNotFound: python3.5
py36-django-111: commands succeeded
py36-django-20: commands succeeded
congratulations :)
.. _Travis: https://travis-ci.org/bartTC/dpaste
.. _tox: http://tox.readthedocs.org/en/latest/

View file

@ -19,6 +19,7 @@ classifiers =
Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.6
Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Framework :: Django Framework :: Django
[options] [options]