├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── docs ├── index.rst ├── installation.rst ├── requirements.rst └── run-and-demo.rst ├── log_reader ├── __init__.py ├── admin.py ├── apps.py ├── migrations │ └── __init__.py ├── models.py ├── settings.py ├── static │ └── log_reader │ │ ├── css │ │ └── log_reader.css │ │ └── js │ │ └── log_reader.js ├── templates │ └── log_reader │ │ └── admin │ │ └── change_list.html ├── tests.py ├── utils.py └── views.py ├── screenshots └── django_log_reader.png └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | .idea/ 54 | .DS_Store 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | db.sqlite3 64 | db.sqlite3-journal 65 | 66 | # Flask stuff: 67 | instance/ 68 | .webassets-cache 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | 73 | # Sphinx documentation 74 | docs/_build/ 75 | 76 | # PyBuilder 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # IPython 83 | profile_default/ 84 | ipython_config.py 85 | 86 | # pyenv 87 | .python-version 88 | 89 | # pipenv 90 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 91 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 92 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 93 | # install all needed dependencies. 94 | #Pipfile.lock 95 | 96 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 97 | __pypackages__/ 98 | 99 | # Celery stuff 100 | celerybeat-schedule 101 | celerybeat.pid 102 | 103 | # SageMath parsed files 104 | *.sage.py 105 | 106 | # Environments 107 | .env 108 | .venv 109 | env/ 110 | venv/ 111 | ENV/ 112 | env.bak/ 113 | venv.bak/ 114 | 115 | # Spyder project settings 116 | .spyderproject 117 | .spyproject 118 | 119 | # Rope project settings 120 | .ropeproject 121 | 122 | # mkdocs documentation 123 | /site 124 | 125 | # mypy 126 | .mypy_cache/ 127 | .dmypy.json 128 | dmypy.json 129 | 130 | # Pyre type checker 131 | .pyre/ 132 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Iman Karimi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.rst 3 | recursive-include log_reader/static * 4 | recursive-include log_reader/templates * 5 | recursive-include docs * -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Django Log Reader 2 | **Django Log Reader** allows you to read & download log files on the admin page. 3 | 4 | > This version designed for the Linux operating system and uses Linux commands to read files faster. 5 | 6 |
7 | 8 | ## Why Django Log Reader? 9 | 10 | - Reading files based on Linux commands speeds up the display of file content 11 | - Search in files based on Linux commands 12 | - Download the result of the content 13 | - Display all files according to the pattern defined in the `settings.py` 14 | - Simple interface 15 | - Easy integration 16 | 17 |
18 | 19 | ![Django Log Reader](https://raw.githubusercontent.com/imankarimi/django-log-reader/main/screenshots/django_log_reader.png) 20 | 21 | 22 |
23 | 24 | ## How to use it 25 | 26 |
27 | 28 | * Download and install latest version of Django Log Reader: 29 | 30 | ```bash 31 | $ pip install django-log-reader 32 | # or 33 | $ easy_install django-log-reader 34 | ``` 35 | 36 |
37 | 38 | * Add `log_reader` application to the `INSTALLED_APPS` setting of your Django project `settings.py` file: 39 | 40 | ```python 41 | INSTALLED_APPS = ( 42 | # ... 43 | "log_reader.apps.LogReaderConfig", 44 | ) 45 | ``` 46 | 47 |
48 | 49 | * You can Add the following value In your `settings.py` file: 50 | 51 | ```python 52 | # This value specifies the folder for the files. The default value is 'logs' 53 | LOG_READER_DIR_PATH = 'logs' 54 | 55 | # This value specifies the file extensions. The default value is '*.log' 56 | LOG_READER_FILES_PATTERN = '*.log' 57 | 58 | # This value specifies the default file. If there is no filter, the system reads the default file. 59 | LOG_READER_DEFAULT_FILE = 'django.log' 60 | 61 | # The contents of the files are separated based on this pattern. 62 | LOG_READER_SPLIT_PATTERN = "\\n" 63 | 64 | # This value indicates the number of lines of content in the file. Set the number of lines you want to read to this value. 65 | LOG_READER_MAX_READ_LINES = 1000 66 | 67 | # You can exclude files with this value. 68 | LOG_READER_EXCLUDE_FILES = [] 69 | ``` 70 | 71 |
72 | 73 | * Collect static if you are in production environment: 74 | ```bash 75 | $ python manage.py collectstatic 76 | ``` 77 | 78 | * Clear your browser cache 79 | 80 |
81 | 82 | ## Start the app 83 | 84 | ```bash 85 | # Set up the database 86 | $ python manage.py makemigrations 87 | $ python manage.py migrate 88 | 89 | # Create the superuser 90 | $ python manage.py createsuperuser 91 | 92 | # Start the application (development mode) 93 | $ python manage.py runserver # default port 8000 94 | ``` 95 | 96 | * Access the `admin` section in the browser: `http://127.0.0.1:8000/` 97 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Django Log Reader Documentation 2 | ########################################## 3 | 4 | Django Log Reader allows you to read and download log files on the admin page. 5 | 6 | .. note:: 7 | This version designed for the Linux operating system and uses Linux commands to read files faster. 8 | 9 | 10 | Why Django Log Reader? 11 | ========================= 12 | 13 | * Reading files based on Linux commands speeds up the display of file content 14 | * Search in files based on Linux commands 15 | * Download the result of the content 16 | * Display all files according to the pattern defined in the ``settings.py`` 17 | * Simple interface 18 | * Easy integration 19 | 20 | .. image:: https://raw.githubusercontent.com/imankarimi/django-log-reader/main/screenshots/django_log_reader.png 21 | :alt: Django Log Reader 22 | 23 | Contents: 24 | ========= 25 | 26 | .. toctree:: 27 | :maxdepth: 2 28 | 29 | requirements 30 | installation 31 | run-and-demo 32 | 33 | I would love to hear your feedback on this application. If you run into 34 | problems, please file an issue on GitHub_, or contribute to the project by 35 | forking the repository and sending some pull requests. 36 | 37 | .. _GitHub: https://github.com/imankarimi/django-log-reader/issues 38 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | * Download and install latest version of `Django Log Reader`_: 5 | 6 | .. code-block:: console 7 | 8 | $ pip install django-log-reader 9 | 10 | Setup 11 | ------- 12 | 13 | * Add ``log_reader`` application to the ``INSTALLED_APPS`` setting of your Django project ``settings.py`` file: 14 | 15 | .. code-block:: python 16 | 17 | INSTALLED_APPS = ( 18 | ... 19 | "log_reader.apps.LogReaderConfig", 20 | ) 21 | 22 | * You can Add the following value In your ``settings.py`` file: 23 | 24 | .. code-block:: python 25 | 26 | # This value specifies the folder for the files. The default value is 'logs' 27 | LOG_READER_DIR_PATH = 'logs' 28 | 29 | # This value specifies the file extensions. The default value is '*.log' 30 | LOG_READER_FILES_PATTERN = '*.log' 31 | 32 | # This value specifies the default file. If there is no filter, the system reads the default file. 33 | LOG_READER_DEFAULT_FILE = 'django.log' 34 | 35 | # The contents of the files are separated based on this pattern. 36 | LOG_READER_SPLIT_PATTERN = "\\n" 37 | 38 | # This value indicates the number of lines of content in the file. Set the number of lines you want to read to this value. 39 | LOG_READER_MAX_READ_LINES = 1000 40 | 41 | # You can exclude files with this value. 42 | LOG_READER_EXCLUDE_FILES = [] 43 | 44 | 45 | * Collect static if you are in production environment: 46 | 47 | .. code-block:: console 48 | 49 | $ python manage.py collectstatic 50 | 51 | * Clear your browser cache 52 | 53 | 54 | .. _Django Log Reader: https://pypi.org/project/django-admin-two-factor/ 55 | -------------------------------------------------------------------------------- /docs/requirements.rst: -------------------------------------------------------------------------------- 1 | Requirements 2 | ============ 3 | 4 | Django 5 | ------ 6 | Modern Django versions are supported. Currently this list includes Django 2.*, and 3.2 7 | 8 | Python 9 | ------ 10 | The following Python versions are supported: 3.5, 3.6, 3.7 and 3.8 with a 11 | limit to what Django itself supports. As support for older Django versions is 12 | dropped, the minimum version might be raised. See also `What Python version can 13 | I use with Django?`_. 14 | 15 | 16 | .. _What Python version can I use with Django?: 17 | https://docs.djangoproject.com/en/stable/faq/install/#what-python-version-can-i-use-with-django 18 | -------------------------------------------------------------------------------- /docs/run-and-demo.rst: -------------------------------------------------------------------------------- 1 | Run & Demo 2 | ############## 3 | 4 | Run 5 | ----- 6 | 7 | .. code-block:: console 8 | 9 | # Set up the database 10 | $ python manage.py makemigrations 11 | $ python manage.py migrate 12 | 13 | # Create the superuser 14 | $ python manage.py createsuperuser 15 | 16 | # Start the application (development mode) 17 | $ python manage.py runserver # default port 8000 18 | 19 | Access the ``admin`` section in the browser: ``http://127.0.0.1:8000/`` 20 | 21 | Demo 22 | ------ 23 | 24 | .. image:: https://raw.githubusercontent.com/imankarimi/django-log-reader/main/screenshots/django_log_reader.png 25 | :alt: Demo 26 | -------------------------------------------------------------------------------- /log_reader/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imankarimi/django-log-reader/e37fbd07a3420a51e641e23c440e3396509b0d3f/log_reader/__init__.py -------------------------------------------------------------------------------- /log_reader/admin.py: -------------------------------------------------------------------------------- 1 | import django 2 | from django.contrib import admin, messages 3 | from django.template.response import TemplateResponse 4 | from django.urls import path 5 | 6 | from log_reader import settings 7 | from log_reader.models import FileLogReader 8 | from log_reader.utils import get_log_files, read_file_lines 9 | 10 | 11 | @admin.register(FileLogReader) 12 | class FileLogReaderAdmin(admin.ModelAdmin): 13 | list_filter = ['id'] 14 | 15 | def get_urls(self): 16 | info = self.model._meta.app_label, self.model._meta.module_name 17 | return [ 18 | path(r'', self.admin_site.admin_view(self.changelist_view), name='%s_%s_changelist' % info), 19 | ] 20 | 21 | def changelist_view(self, request, extra_context=None): 22 | filename = request.GET.get('file_name', settings.LOG_READER_DEFAULT_FILE) 23 | search = request.GET.get('q', None) or None 24 | is_valid, file_contents = read_file_lines(file_name=filename, search=search) 25 | if not is_valid: 26 | self.message_user(request, file_contents, level=messages.ERROR) 27 | log_files = get_log_files(settings.LOG_READER_DIR_PATH) 28 | 29 | context = dict( 30 | self.admin_site.each_context(request), 31 | log_files=log_files, 32 | file_contents=file_contents if is_valid else [], 33 | file_name=filename, 34 | django_version=django.get_version() 35 | ) 36 | return TemplateResponse(request, "log_reader/admin/change_list.html", context=context) 37 | -------------------------------------------------------------------------------- /log_reader/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class LogReaderConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'log_reader' 7 | verbose_name = 'log reader' 8 | -------------------------------------------------------------------------------- /log_reader/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imankarimi/django-log-reader/e37fbd07a3420a51e641e23c440e3396509b0d3f/log_reader/migrations/__init__.py -------------------------------------------------------------------------------- /log_reader/models.py: -------------------------------------------------------------------------------- 1 | import django 2 | 3 | if django.get_version() >= '4': 4 | from django.utils.translation import gettext_lazy as _ 5 | else: 6 | from django.utils.translation import ugettext_lazy as _ 7 | 8 | 9 | class FileLogReader(object): 10 | class Meta(object): 11 | app_label = 'log_reader' 12 | object_name = 'file_log_readers' 13 | model_name = module_name = 'file_log_readers' 14 | verbose_name = _('file log') 15 | verbose_name_plural = _('file logs') 16 | abstract = False 17 | swapped = False 18 | app_config = "" 19 | 20 | _meta = Meta() 21 | -------------------------------------------------------------------------------- /log_reader/settings.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | 4 | """ log reader config """ 5 | 6 | LOG_READER_DIR_PATH = getattr(settings, 'LOG_READER_DIR_PATH', 'logs') 7 | LOG_READER_FILES_PATTERN = getattr(settings, 'LOG_READER_FILES_PATTERN', '*.log') 8 | LOG_READER_DEFAULT_FILE = getattr(settings, 'LOG_READER_DEFAULT_FILE', 'django.log') 9 | LOG_READER_SPLIT_PATTERN = getattr(settings, 'LOG_READER_SPLIT_PATTERN', "\\n") 10 | # The contents of the files are separated based on this regex. if it couldn't split correctly with both pattern. 11 | # The default value is '[(?i)[0-9]{4}-[0-9]{2}-[0-9]{2}\\s(?:[0-9]{2}:){2}[0-9]{2}.+?(?=[0-9]{4}-[0-9]{2}-[0-9]{2}\\s(?:[0-9]{2}:){2}[0-9]{2}|$)' 12 | LOG_READER_REGEX_SPLIT_PATTERN = '[(?i)[0-9]{4}-[0-9]{2}-[0-9]{2}\\s(?:[0-9]{2}:){2}[0-9]{2}.+?(?=[0-9]{4}-[0-9]{2}-[0-9]{2}\\s(?:[0-9]{2}:){2}[0-9]{2}|$)' 13 | LOG_READER_MAX_READ_LINES = getattr(settings, 'LOG_READER_MAX_READ_LINES', 1000) 14 | LOG_READER_EXCLUDE_FILES = getattr(settings, 'LOG_READER_EXCLUDE_FILES', []) 15 | 16 | -------------------------------------------------------------------------------- /log_reader/static/log_reader/css/log_reader.css: -------------------------------------------------------------------------------- 1 | #DownloadLogFile { 2 | padding: 6px; 3 | border-radius: 5px; 4 | background-color: #79aec8; 5 | color: #ffffff; 6 | cursor: pointer; 7 | border: 1px solid #d0d0d0; 8 | position: absolute; 9 | right: 315px; 10 | top: 156px; 11 | } -------------------------------------------------------------------------------- /log_reader/static/log_reader/js/log_reader.js: -------------------------------------------------------------------------------- 1 | function download(filename, text) { 2 | var element = document.createElement('a'); 3 | element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text)); 4 | element.setAttribute('download', filename); 5 | 6 | element.style.display = 'none'; 7 | document.body.appendChild(element); 8 | 9 | element.click(); 10 | 11 | document.body.removeChild(element); 12 | } -------------------------------------------------------------------------------- /log_reader/templates/log_reader/admin/change_list.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base_site.html" %} 2 | {% load i18n static %} 3 | 4 | {% block title %}Log Reader{% endblock %} 5 | 6 | {% block extrastyle %} 7 | {{ block.super }} 8 | 9 | 10 | 11 | 12 | {% endblock %} 13 | 14 | {% block breadcrumbs %} 15 | 20 | {% endblock %} 21 | 22 | {% block pretitle %} 23 |

File Logs

24 | {% endblock %} 25 | 26 | {% block content %} 27 |
28 |
29 | {% if django_version >= '3' %}
{% endif %} 30 | 31 |
32 | 42 | {% if file_contents %} 43 | 44 | {% endif %} 45 |
46 | 47 | {% if file_contents %} 48 |
49 |
50 | 51 | 52 | 53 | 56 | 59 | 60 | 61 | 62 | {% for content in file_contents reversed %} 63 | {% if content|length > 5 %} 64 | 65 | 66 | 67 | 68 | {% endif %} 69 | {% endfor %} 70 | 71 |
54 |
#
55 |
57 | 58 |
{{ forloop.counter }}{{ content }}
72 |
73 | {% if django_version < '3' %} 74 |

{{ file_contents|length }} log content

75 | {% endif %} 76 |
77 | {% endif %} 78 | 79 | {% if django_version >= '3' %} 80 |

{{ file_contents|length }} log content

81 | {% endif %} 82 | {% if django_version >= '3' %}
{% endif %} 83 | 84 | {% block filters %} 85 | {{ block.super }} 86 |
87 |

{% trans 'Filter' %}

88 |

By File

89 |
    90 | {% for file_name in log_files %} 91 |
  • {{ file_name }}
  • 92 | {% endfor %} 93 |
94 |
95 | {% endblock %} 96 |
97 |
98 | 99 | 112 | {% endblock %} -------------------------------------------------------------------------------- /log_reader/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /log_reader/utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import django 4 | import os 5 | # import re 6 | import subprocess 7 | from fnmatch import fnmatch 8 | from subprocess import PIPE 9 | 10 | if django.get_version() >= '4': 11 | from django.utils.translation import gettext_lazy as _ 12 | else: 13 | from django.utils.translation import ugettext_lazy as _ 14 | 15 | from log_reader import settings 16 | 17 | 18 | def get_log_files(directory): 19 | for (dir_path, dir_names, filenames) in os.walk(directory): 20 | log_files = [] 21 | all_files = list(filter(lambda x: x.find('~') == -1, filenames)) 22 | log_files.extend([x for x in all_files if fnmatch(x, settings.LOG_READER_FILES_PATTERN) and x not in settings.LOG_READER_EXCLUDE_FILES]) 23 | return list(set(log_files)) 24 | return [] 25 | 26 | 27 | def read_file_lines(file_name, search=None): 28 | if file_name not in get_log_files(settings.LOG_READER_DIR_PATH): 29 | return False, _("%s file, not found. Please try again." % file_name) 30 | 31 | try: 32 | file_path = '%s/%s' % (settings.LOG_READER_DIR_PATH, file_name) 33 | 34 | if search: 35 | result = subprocess.run( 36 | ['grep', '-m %s' % settings.LOG_READER_MAX_READ_LINES, search, file_path], 37 | stdout=PIPE, 38 | stderr=PIPE, 39 | encoding="utf8", 40 | ) 41 | else: 42 | result = subprocess.run( 43 | ['tail', '-%s' % settings.LOG_READER_MAX_READ_LINES, file_path], 44 | stdout=PIPE, 45 | stderr=PIPE, 46 | encoding="utf8", 47 | ) 48 | content = repr(result.stdout) if result.stdout else None 49 | except Exception as e: 50 | return False, str(e) 51 | 52 | return True, split_file_content(content) 53 | 54 | 55 | def split_file_content(content): 56 | data = content.split(settings.LOG_READER_SPLIT_PATTERN) if content else [] 57 | # if content and len(res) == 1: 58 | # res = re.findall(settings.LOG_READER_REGEX_SPLIT_PATTERN, content) if content else [] 59 | res = [x for x in data if len(x) > 5] 60 | return res 61 | 62 | -------------------------------------------------------------------------------- /log_reader/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /screenshots/django_log_reader.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imankarimi/django-log-reader/e37fbd07a3420a51e641e23c440e3396509b0d3f/screenshots/django_log_reader.png -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from setuptools import find_packages, setup 4 | 5 | with open(os.path.join(os.path.dirname(__file__), 'README.md')) as readme: 6 | README = readme.read() 7 | 8 | os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir))) 9 | 10 | setup( 11 | name='django-log-reader', 12 | version='1.1.9', 13 | zip_safe=False, 14 | packages=find_packages(), 15 | include_package_data=True, 16 | description='Read & Download log files on the admin page', 17 | long_description=README, 18 | long_description_content_type="text/markdown", 19 | url='https://github.com/imankarimi/django-log-reader', 20 | author='Iman Karimi', 21 | author_email='imankarimi.mail@gmail.com', 22 | license='MIT License', 23 | classifiers=[ 24 | 'Environment :: Web Environment', 25 | 'Framework :: Django', 26 | 'Framework :: Django :: 3.2', 27 | 'Intended Audience :: Developers', 28 | 'License :: OSI Approved :: MIT License', 29 | 'Programming Language :: Python', 30 | 'Programming Language :: Python :: 2.6', 31 | 'Programming Language :: Python :: 2.7', 32 | 'Programming Language :: Python :: 3.2', 33 | 'Programming Language :: Python :: 3.3', 34 | 'Programming Language :: Python :: 3.4', 35 | 'Programming Language :: Python :: 3.5', 36 | 'Programming Language :: Python :: 3.6', 37 | 'Programming Language :: Python :: 3.7', 38 | 'Programming Language :: Python :: 3.8', 39 | 'Environment :: Web Environment', 40 | 'Topic :: Software Development', 41 | ], 42 | ) 43 | --------------------------------------------------------------------------------