├── djp ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── showplugins.py ├── hookspecs.py └── __init__.py ├── docs ├── .gitignore ├── requirements.txt ├── _templates │ └── base.html ├── Makefile ├── writing_tests.md ├── index.md ├── installing_plugins.md ├── plugin_hooks.md ├── creating_a_plugin.md └── conf.py ├── tests ├── test_project │ ├── __init__.py │ ├── app1 │ │ └── __init__.py │ ├── urls.py │ ├── settings.py │ └── middleware.py ├── plugins │ ├── installed_apps.py │ ├── settings.py │ ├── urlpatterns.py │ ├── asgi_wrapper.py │ └── middleware.py └── test_django_plugins.py ├── .gitignore ├── .readthedocs.yaml ├── .github └── workflows │ ├── test.yml │ └── publish.yml ├── pyproject.toml ├── README.md └── LICENSE /djp/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | _build 2 | -------------------------------------------------------------------------------- /tests/test_project/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /djp/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/test_project/app1/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx 2 | furo 3 | sphinx-autobuild 4 | sphinx-copybutton 5 | myst-parser 6 | cogapp 7 | -------------------------------------------------------------------------------- /tests/plugins/installed_apps.py: -------------------------------------------------------------------------------- 1 | import djp 2 | 3 | 4 | @djp.hookimpl 5 | def installed_apps(): 6 | return ["tests.test_project.app1"] 7 | -------------------------------------------------------------------------------- /tests/plugins/settings.py: -------------------------------------------------------------------------------- 1 | import djp 2 | 3 | 4 | @djp.hookimpl 5 | def settings(current_settings): 6 | current_settings["FROM_PLUGIN"] = "x" 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .venv 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | venv 6 | .eggs 7 | .pytest_cache 8 | *.egg-info 9 | .DS_Store 10 | dist 11 | build 12 | -------------------------------------------------------------------------------- /docs/_templates/base.html: -------------------------------------------------------------------------------- 1 | {%- extends "!base.html" %} 2 | 3 | {% block site_meta %} 4 | {{ super() }} 5 | 6 | {% endblock %} -------------------------------------------------------------------------------- /tests/test_project/urls.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse 2 | from django.urls import path 3 | import djp 4 | 5 | urlpatterns = [ 6 | path("", lambda request: HttpResponse("Hello world")) 7 | ] + djp.urlpatterns() 8 | -------------------------------------------------------------------------------- /tests/plugins/urlpatterns.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from django.http import HttpResponse 3 | import djp 4 | 5 | 6 | @djp.hookimpl 7 | def urlpatterns(): 8 | return [path("from-plugin/", lambda request: HttpResponse("Hello from a plugin"))] 9 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | os: ubuntu-22.04 5 | tools: 6 | python: "3.12" 7 | 8 | sphinx: 9 | configuration: docs/conf.py 10 | 11 | formats: 12 | - pdf 13 | - epub 14 | 15 | python: 16 | install: 17 | - requirements: docs/requirements.txt 18 | -------------------------------------------------------------------------------- /djp/management/commands/showplugins.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | from djp import get_plugins 3 | import json 4 | 5 | 6 | class Command(BaseCommand): 7 | help = "Show installed plugins" 8 | 9 | def handle(self, *args, **options): 10 | plugins = get_plugins() 11 | print(json.dumps(plugins, indent=2)) 12 | -------------------------------------------------------------------------------- /tests/test_project/settings.py: -------------------------------------------------------------------------------- 1 | import djp 2 | 3 | SECRET_KEY = "django-insecure-test-key" 4 | DEBUG = True 5 | ALLOWED_HOSTS = ["*"] 6 | 7 | INSTALLED_APPS = ( 8 | "django.contrib.admin", 9 | "django.contrib.auth", 10 | "django.contrib.contenttypes", 11 | ) 12 | 13 | MIDDLEWARE = tuple() 14 | 15 | ROOT_URLCONF = "tests.test_project.urls" 16 | 17 | DATABASES = { 18 | "default": { 19 | "ENGINE": "django.db.backends.sqlite3", 20 | "NAME": ":memory:", 21 | } 22 | } 23 | 24 | TEMPLATES = [ 25 | { 26 | "BACKEND": "django.template.backends.django.DjangoTemplates", 27 | "APP_DIRS": True, 28 | } 29 | ] 30 | 31 | USE_TZ = True 32 | 33 | djp.settings(globals()) 34 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push, pull_request] 4 | 5 | permissions: 6 | contents: read 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Set up Python ${{ matrix.python-version }} 17 | uses: actions/setup-python@v5 18 | with: 19 | python-version: ${{ matrix.python-version }} 20 | cache: pip 21 | cache-dependency-path: pyproject.toml 22 | - name: Install dependencies 23 | run: | 24 | pip install '.[test]' 25 | - name: Run tests 26 | run: | 27 | python -m pytest 28 | 29 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = djp 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | 22 | livehtml: 23 | sphinx-autobuild -b html "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(0) 24 | -------------------------------------------------------------------------------- /tests/plugins/asgi_wrapper.py: -------------------------------------------------------------------------------- 1 | import djp 2 | 3 | 4 | @djp.hookimpl 5 | def asgi_wrapper(): 6 | return wrap 7 | 8 | 9 | def wrap(app): 10 | async def wrapper(scope, receive, send): 11 | if scope["type"] == "http" and scope["path"] == "/hello": 12 | await send( 13 | { 14 | "type": "http.response.start", 15 | "status": 200, 16 | "headers": [ 17 | [b"content-type", b"text/plain"], 18 | ], 19 | } 20 | ) 21 | await send( 22 | { 23 | "type": "http.response.body", 24 | "body": b"Hello world", 25 | } 26 | ) 27 | else: 28 | await app(scope, receive, send) 29 | 30 | return wrapper 31 | -------------------------------------------------------------------------------- /tests/plugins/middleware.py: -------------------------------------------------------------------------------- 1 | import djp 2 | 3 | 4 | @djp.hookimpl(specname="middleware", tryfirst=True) 5 | def middleware1(): 6 | return [ 7 | "tests.test_project.middleware.Middleware", 8 | "tests.test_project.middleware.Middleware2", 9 | "tests.test_project.middleware.Middleware3", 10 | djp.Before("tests.test_project.middleware.MiddlewareBefore"), 11 | djp.After("tests.test_project.middleware.MiddlewareAfter"), 12 | ] 13 | 14 | 15 | @djp.hookimpl(specname="middleware") 16 | def middleware2(): 17 | return [ 18 | djp.Position( 19 | "tests.test_project.middleware.Middleware4", 20 | before="tests.test_project.middleware.Middleware2", 21 | ), 22 | djp.Position( 23 | "tests.test_project.middleware.Middleware5", 24 | before="tests.test_project.middleware.Middleware3", 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /djp/hookspecs.py: -------------------------------------------------------------------------------- 1 | from pluggy import HookimplMarker 2 | from pluggy import HookspecMarker 3 | 4 | hookspec = HookspecMarker("djp") 5 | hookimpl = HookimplMarker("djp") 6 | 7 | 8 | @hookspec 9 | def installed_apps(): 10 | """Return a list of Django app strings to be added to INSTALLED_APPS""" 11 | 12 | 13 | @hookspec 14 | def middleware(): 15 | """ 16 | Return a list of Django middleware class strings to be added to MIDDLEWARE. 17 | Optionally wrap with djp.Before() or djp.After() to specify ordering, 18 | or wrap with djp.Position(name, before=other_name) to insert before another 19 | or djp.Position(name, after=other_name) to insert after another. 20 | """ 21 | 22 | 23 | @hookspec 24 | def urlpatterns(): 25 | """Return a list of url patterns to be added to urlpatterns""" 26 | 27 | 28 | @hookspec 29 | def settings(current_settings): 30 | """Modify current_settings in place to finish configuring settings.py""" 31 | 32 | 33 | @hookspec 34 | def asgi_wrapper(): 35 | """Returns an ASGI middleware callable to wrap our ASGI application with""" 36 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "djp" 3 | version = "0.3.1" 4 | description = "A plugin system for Django" 5 | readme = "README.md" 6 | requires-python = ">=3.8" 7 | authors = [{name = "Simon Willison"}] 8 | license = {text = "Apache-2.0"} 9 | classifiers = [ 10 | "License :: OSI Approved :: Apache Software License", 11 | "Framework :: Django", 12 | ] 13 | dependencies = [ 14 | "django", 15 | "pluggy", 16 | ] 17 | 18 | [build-system] 19 | requires = ["setuptools"] 20 | build-backend = "setuptools.build_meta" 21 | 22 | [project.urls] 23 | Homepage = "https://github.com/simonw/djp" 24 | Changelog = "https://github.com/simonw/djp/releases" 25 | Issues = "https://github.com/simonw/djp/issues" 26 | CI = "https://github.com/simonw/djp/actions" 27 | 28 | 29 | [project.optional-dependencies] 30 | test = ["pytest", "pytest-django", "pytest-env", "pytest-asyncio", "httpx"] 31 | 32 | [tool.pytest.ini_options] 33 | DJANGO_SETTINGS_MODULE = "tests.test_project.settings" 34 | pythonpath = ["."] 35 | asyncio_default_fixture_loop_scope = "function" 36 | 37 | [tool.pytest_env] 38 | DJP_PLUGINS_DIR = "tests/plugins" 39 | -------------------------------------------------------------------------------- /tests/test_project/middleware.py: -------------------------------------------------------------------------------- 1 | def request_note(request, response, note): 2 | if not hasattr(request, "_notes"): 3 | request._notes = [] 4 | request._notes.append(note) 5 | response._request = request 6 | 7 | 8 | class Middleware: 9 | def __init__(self, get_response): 10 | self.get_response = get_response 11 | 12 | def __call__(self, request): 13 | response = self.get_response(request) 14 | response["X-DJP-Middleware"] = "Middleware" 15 | request_note(request, response, "Middleware") 16 | return response 17 | 18 | 19 | class Middleware2: 20 | def __init__(self, get_response): 21 | self.get_response = get_response 22 | 23 | def __call__(self, request): 24 | response = self.get_response(request) 25 | request_note(request, response, self.__class__.__name__) 26 | return response 27 | 28 | 29 | class Middleware3(Middleware2): 30 | pass 31 | 32 | 33 | class Middleware4(Middleware2): 34 | pass 35 | 36 | 37 | class Middleware5(Middleware2): 38 | pass 39 | 40 | 41 | class MiddlewareBefore: 42 | def __init__(self, get_response): 43 | self.get_response = get_response 44 | 45 | def __call__(self, request): 46 | response = self.get_response(request) 47 | response["X-DJP-Middleware-Before"] = "MiddlewareBefore" 48 | request_note(request, response, "MiddlewareBefore") 49 | return response 50 | 51 | 52 | class MiddlewareAfter: 53 | def __init__(self, get_response): 54 | self.get_response = get_response 55 | 56 | def __call__(self, request): 57 | response = self.get_response(request) 58 | response["X-DJP-Middleware-After"] = "MiddlewareAfter" 59 | request_note(request, response, "MiddlewareAfter") 60 | return response 61 | -------------------------------------------------------------------------------- /docs/writing_tests.md: -------------------------------------------------------------------------------- 1 | # Writing tests 2 | 3 | The following projects include examples of tests written against a DJP plugin: 4 | 5 | - [django-plugin-django-header](https://github.com/simonw/django-plugin-django-header) demonstrates [a simple test](https://github.com/simonw/django-plugin-django-header/blob/main/tests/test_django_plugin_django_header.py) that confirms that custom middleware is working by checking for anex expected HTTP header in the response. 6 | - [django-plugin-blog](https://github.com/simonw/django-plugin-blog) has [tests](https://github.com/simonw/django-plugin-blog/blob/main/tests/test_django_plugin_blog.py) for a full application that includes custom models, views and templates. 7 | 8 | Both of these projects follow the pattern described in [Using pytest-django with a reusable Django application](https://til.simonwillison.net/django/pytest-django). 9 | 10 | ## Running tests 11 | 12 | If you created your plugin {ref}`using cookiecutter ` you will have an initial test that you can run, and then extend. 13 | 14 | To run the tests for your plugin it's best to create a dedicated virtual environment: 15 | ```bash 16 | cd django-plugin-example 17 | python -m venv venv 18 | source venv/bin/activate 19 | python -m pip install -e '.[test]' 20 | python -m pytest 21 | ``` 22 | The output should look like this: 23 | ``` 24 | ============================= test session starts ============================== 25 | platform darwin -- Python 3.10.10, pytest-8.3.3, pluggy-1.5.0 26 | django: version: 5.1.1, settings: tests.test_project.settings (from ini) 27 | rootdir: /private/tmp/django-plugin-example 28 | configfile: pyproject.toml 29 | plugins: django-4.9.0 30 | collected 1 item 31 | 32 | tests/test_django_plugin_example.py . [100%] 33 | 34 | ============================== 1 passed in 0.05s =============================== 35 | ``` 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DJP: Django Plugins 2 | 3 | [![PyPI](https://img.shields.io/pypi/v/djp.svg)](https://pypi.org/project/djp/) 4 | [![Tests](https://github.com/simonw/djp/actions/workflows/test.yml/badge.svg)](https://github.com/simonw/djp/actions/workflows/test.yml) 5 | [![Changelog](https://img.shields.io/github/v/release/simonw/djp?include_prereleases&label=changelog)](https://github.com/simonw/djp/releases) 6 | [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/simonw/djp/blob/main/LICENSE) 7 | 8 | A plugin system for Django 9 | 10 | Visit **[djp.readthedocs.io](https://djp.readthedocs.io/)** for full documentation, including how to install plugins and how to write new plugins. 11 | 12 | See [DJP: A plugin system for Django](https://simonwillison.net/2024/Sep/25/djp-a-plugin-system-for-django/) for an introduction to this project. 13 | 14 | ## Installation 15 | 16 | Install this library using `pip`: 17 | ```bash 18 | pip install djp 19 | ``` 20 | 21 | ## Configuration 22 | 23 | Add this to the **end** of your `settings.py` file: 24 | ```python 25 | import djp 26 | 27 | # ... existing settings.py contents 28 | 29 | djp.settings(globals()) 30 | ``` 31 | Then add this to your URL configuration in `urls.py`: 32 | ```python 33 | urlpatterns = [ 34 | # ... 35 | ] + djp.urlpatterns() 36 | ``` 37 | 38 | ## Usage 39 | 40 | Installing a plugin in the same environment as your Django application should cause that plugin to automatically add the necessary 41 | 42 | ## Development 43 | 44 | To contribute to this library, first checkout the code. Then create a new virtual environment: 45 | ```bash 46 | cd djp 47 | python -m venv venv 48 | source venv/bin/activate 49 | ``` 50 | Now install the dependencies and test dependencies: 51 | ```bash 52 | python -m pip install -e '.[test]' 53 | ``` 54 | To run the tests: 55 | ```bash 56 | python -m pytest 57 | ``` 58 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Python Package 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | permissions: 8 | contents: read 9 | 10 | jobs: 11 | test: 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Set up Python ${{ matrix.python-version }} 19 | uses: actions/setup-python@v5 20 | with: 21 | python-version: ${{ matrix.python-version }} 22 | cache: pip 23 | cache-dependency-path: pyproject.toml 24 | - name: Install dependencies 25 | run: | 26 | pip install '.[test]' 27 | - name: Run tests 28 | run: | 29 | python -m pytest 30 | build: 31 | runs-on: ubuntu-latest 32 | needs: [test] 33 | steps: 34 | - uses: actions/checkout@v4 35 | - name: Set up Python 36 | uses: actions/setup-python@v5 37 | with: 38 | python-version: "3.12" 39 | cache: pip 40 | cache-dependency-path: pyproject.toml 41 | - name: Install dependencies 42 | run: | 43 | pip install setuptools wheel build 44 | - name: Build 45 | run: | 46 | python -m build 47 | - name: Store the distribution packages 48 | uses: actions/upload-artifact@v4 49 | with: 50 | name: python-packages 51 | path: dist/ 52 | publish: 53 | name: Publish to PyPI 54 | runs-on: ubuntu-latest 55 | if: startsWith(github.ref, 'refs/tags/') 56 | needs: [build] 57 | environment: release 58 | permissions: 59 | id-token: write 60 | steps: 61 | - name: Download distribution packages 62 | uses: actions/download-artifact@v4 63 | with: 64 | name: python-packages 65 | path: dist/ 66 | - name: Publish to PyPI 67 | uses: pypa/gh-action-pypi-publish@release/v1 68 | 69 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # DJP: Django Plugins 2 | 3 | A plugin system for Django, based on [Pluggy](https://pluggy.readthedocs.io/). 4 | 5 | See [DJP: A plugin system for Django](https://simonwillison.net/2024/Sep/25/djp-a-plugin-system-for-django/) for an introduction to this project. 6 | 7 | ## Why plugins? 8 | 9 | Django has long promoted the idea of [reusable apps](https://docs.djangoproject.com/en/5.1/intro/reusable-apps/), and there is a thriving ecosystem of open source extensions to the framework. 10 | 11 | Many of these require the user to manually configure them, by modifying their `settings.py` to add new strings to `INSTALLED_APPS` or `MIDDLEWARE`, or by adding new entries to their URL configuration. 12 | 13 | DJP addresses this limitation: you can configure DJP once for a project, after which any DJP-compliant plugins you install will be able to automatically modify your Django configuration to enable their functionality. 14 | 15 | ## Available plugins 16 | 17 | [django-plugin-django-header](https://github.com/simonw/django-plugin-django-header) is an example plugin that adds a `Django-Composition` HTTP header to every HTTP response containing the name of a random composition by Django Reinhardt. 18 | 19 | [django-plugin-blog](https://github.com/simonw/django-plugin-blog) implements a full blog application for Django, with entries and tags and an Atom feed and a configured Django admin interface. Installing this plugin adds the blog under the `/blog/` URL path. 20 | 21 | [django-plugin-database-url](https://github.com/simonw/django-plugin-database-url) configures Django to connect to the database defined by the `DATABASE_URL` environment variable. 22 | 23 | [django-plugin-datasette](https://github.com/simonw/django-plugin-datasette) adds a [Datasette](https://datasette.io) instance to Django, providing a read-only UI and JSON API for exploring data in any SQLite databases configured using Django’s `DATABASES` setting. 24 | 25 | ## Documentation 26 | 27 | ```{toctree} 28 | --- 29 | maxdepth: 3 30 | --- 31 | installing_plugins 32 | creating_a_plugin 33 | writing_tests 34 | plugin_hooks 35 | ``` 36 | -------------------------------------------------------------------------------- /tests/test_django_plugins.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.test.client import Client 3 | import httpx 4 | import pytest 5 | 6 | 7 | def test_middleware_order(): 8 | assert settings.MIDDLEWARE == [ 9 | "tests.test_project.middleware.MiddlewareBefore", 10 | "tests.test_project.middleware.Middleware", 11 | "tests.test_project.middleware.Middleware4", 12 | "tests.test_project.middleware.Middleware2", 13 | "tests.test_project.middleware.Middleware5", 14 | "tests.test_project.middleware.Middleware3", 15 | "tests.test_project.middleware.MiddlewareAfter", 16 | ] 17 | 18 | 19 | def test_middleware(): 20 | response = Client().get("/") 21 | assert response["X-DJP-Middleware-After"] == "MiddlewareAfter" 22 | assert response["X-DJP-Middleware"] == "Middleware" 23 | assert response["X-DJP-Middleware-Before"] == "MiddlewareBefore" 24 | request = response._request 25 | assert hasattr(request, "_notes") 26 | assert request._notes == [ 27 | "MiddlewareAfter", 28 | "Middleware3", 29 | "Middleware5", 30 | "Middleware2", 31 | "Middleware4", 32 | "Middleware", 33 | "MiddlewareBefore", 34 | ] 35 | 36 | 37 | def test_urlpatterns(): 38 | response = Client().get("/from-plugin/") 39 | assert response.content == b"Hello from a plugin" 40 | 41 | 42 | def test_settings(): 43 | assert settings.FROM_PLUGIN == "x" 44 | 45 | 46 | def test_installed_apps(): 47 | assert "tests.test_project.app1" in settings.INSTALLED_APPS 48 | 49 | 50 | @pytest.mark.asyncio 51 | async def test_asgi_wrapper(): 52 | from django.core.asgi import get_asgi_application 53 | from djp import asgi_wrapper 54 | 55 | application = get_asgi_application() 56 | wrapped_application = asgi_wrapper(application) 57 | 58 | async with httpx.AsyncClient( 59 | transport=httpx.ASGITransport(app=wrapped_application), 60 | base_url="http://testserver", 61 | ) as client: 62 | response = await client.get("http://testserver/hello") 63 | assert response.status_code == 200 64 | assert response.text == "Hello world" 65 | -------------------------------------------------------------------------------- /docs/installing_plugins.md: -------------------------------------------------------------------------------- 1 | # Installing plugins 2 | 3 | To install plugins that use this system you will first need to configure the plugin system in your Django application. 4 | 5 | Install this library using `pip`: 6 | ```bash 7 | pip install djp 8 | ``` 9 | 10 | ## Modifying your configuration 11 | 12 | Add this to the **end** of your `settings.py` file: 13 | ```python 14 | import djp 15 | 16 | # ... existing settings.py contents 17 | 18 | djp.settings(globals()) 19 | ``` 20 | Then add this to your URL configuration in `urls.py`: 21 | ```python 22 | urlpatterns = [ 23 | # ... 24 | ] + djp.urlpatterns() 25 | ``` 26 | 27 | ## To use ASGI middleware 28 | 29 | If you want to use plugins that use the {ref}`asgi_wrapper() ` hook, you will need to modify your `asgi.py` file to look like this: 30 | 31 | ```python 32 | import os 33 | import djp 34 | from django.core.asgi import get_asgi_application 35 | 36 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "helloworld.settings") 37 | 38 | application = djp.asgi_wrapper(get_asgi_application()) 39 | ``` 40 | 41 | ## Adding plugins to your environment 42 | 43 | You can now install plugins, using `pip` or your package manager of choice. 44 | 45 | Try this example plugin, which adds a custom HTTP header with a random composition written by Django Reinhardt: 46 | 47 | ```bash 48 | pip install django-plugin-django-header 49 | ``` 50 | 51 | Now run `curl` against your application to see the new header: 52 | 53 | ```bash 54 | curl -I http://localhost:8000/ 55 | ``` 56 | 57 | ## Listing installed plugins 58 | 59 | The `showplugins` management command lists the plugins that are installed in your current environment: 60 | 61 | ```bash 62 | ./manage.py showplugins 63 | ``` 64 | Example output: 65 | ```json 66 | [ 67 | { 68 | "name": "django-plugin-blog", 69 | "hooks": [ 70 | "installed_apps", 71 | "middleware", 72 | "settings", 73 | "urlpatterns" 74 | ], 75 | "version": "0.1" 76 | }, 77 | { 78 | "name": "django-plugin-django-header", 79 | "hooks": [ 80 | "middleware" 81 | ], 82 | "version": "0.1" 83 | } 84 | ] 85 | ``` 86 | 87 | ## Loading plugins from a directory 88 | 89 | You can also set the `DJP_PLUGINS_DIR` environment variable to point to a directory which contains `*.py` files implementing plugins. 90 | 91 | This can be useful for plugin development, and is also used by DJP's own automated tests. 92 | -------------------------------------------------------------------------------- /docs/plugin_hooks.md: -------------------------------------------------------------------------------- 1 | # Plugin Hooks 2 | 3 | The following plugin hooks can be used by plugins. 4 | 5 | ## installed_apps() 6 | 7 | Return a list of Django app strings to be added to `INSTALLED_APPS`. 8 | 9 | Example implementation: 10 | 11 | ```python 12 | import djp 13 | 14 | @djp.hookimpl 15 | def installed_apps(): 16 | return ["my_plugin_app"] 17 | ``` 18 | 19 | ## middleware() 20 | 21 | Return a list of Django middleware class strings to be added to MIDDLEWARE. 22 | 23 | Middleware can optionally be wrapped with `djp.Before()` or `djp.After()` to specify ordering relative to existing middleware. These will then be added to the beginning or end of the middleware list, respectively. 24 | 25 | Example implementation: 26 | 27 | ```python 28 | import djp 29 | 30 | @djp.hookimpl 31 | def middleware(): 32 | return [ 33 | djp.Before("django.middleware.common.CommonMiddleware"), 34 | "my_plugin.middleware.MyPluginMiddleware", 35 | djp.After("django.middleware.clickjacking.XFrameOptionsMiddleware") 36 | ] 37 | ``` 38 | 39 | Sometimes you may want a middleware to be inserted at an exact position relative to another middleware. You can specify that with `djp.Position()`: 40 | 41 | ```python 42 | @djp.hookimpl 43 | def middleware(): 44 | return [ 45 | # This will insert the middleware directly before CommonMiddleware 46 | djp.Position( 47 | "my_plugin.middleware.MyPluginMiddleware", 48 | before="django.middleware.common.CommonMiddleware" 49 | ), 50 | # And this will be inserted directly after CommonMiddleware 51 | djp.Position( 52 | "my_plugin.middleware.MyPluginMiddleware2", 53 | after="django.middleware.common.CommonMiddleware" 54 | ), 55 | ] 56 | ``` 57 | 58 | ## urlpatterns() 59 | 60 | Return a list of URL patterns to be added to `urlpatterns`. 61 | 62 | Example implementation: 63 | 64 | ```python 65 | import djp 66 | from django.urls import path 67 | from . import views 68 | 69 | @djp.hookimpl 70 | def urlpatterns(): 71 | return [ 72 | path("my-plugin/", views.my_plugin_view), 73 | ] 74 | ``` 75 | 76 | ## settings(current_settings) 77 | 78 | Modify the current Django settings in-place to configure additional settings. 79 | 80 | `current_settings` is a dictionary representing the current settings in `settings.py`. 81 | 82 | Example implementation: 83 | 84 | ```python 85 | import djp 86 | 87 | @djp.hookimpl 88 | def settings(current_settings): 89 | current_settings["DATABASES"] = { 90 | "default": { 91 | "ENGINE": "django.db.backends.sqlite3", 92 | "NAME": "mydatabase", 93 | } 94 | } 95 | ``` 96 | 97 | (plugin_hook_asgi_wrapper)= 98 | ## asgi_wrapper() 99 | 100 | Return a function that can wrap the Django ASGI application with new ASGI middleware. 101 | 102 | Example implementation: 103 | 104 | ```python 105 | import djp 106 | 107 | @djp.hookimpl 108 | def asgi_wrapper(): 109 | return wrap 110 | 111 | def wrap(app): 112 | async def wrapper(scope, receive, send): 113 | if scope["type"] == "http" and scope["path"] == "/hello": 114 | await send({ 115 | "type": "http.response.start", 116 | "status": 200, 117 | "headers": [ 118 | [b"content-type", b"text/plain"], 119 | ], 120 | }) 121 | await send({ 122 | "type": "http.response.body", 123 | "body": b"Hello world", 124 | }) 125 | else: 126 | await app(scope, receive, send) 127 | 128 | return wrapper 129 | ``` 130 | -------------------------------------------------------------------------------- /docs/creating_a_plugin.md: -------------------------------------------------------------------------------- 1 | # Creating a plugin 2 | 3 | A Plugin is a Python package, usually named with `django-plugin-` as a prefix. 4 | 5 | (cookiecutter)= 6 | 7 | ## Using cookiecutter 8 | 9 | You can use the [simonw/django-plugin](https://github.com/simonw/django-plugin) to create an initial skeleton for your plugin, including automated tests, continuous integration and publishing to PyPI using GitHub Actions. 10 | 11 | Install [cookiecutter](https://github.com/cookiecutter/cookiecutter): 12 | 13 | ```bash 14 | pipx install cookiecutter # or pip install 15 | ``` 16 | Then run the template like this: 17 | ```bash 18 | cookiecutter gh:simonw/django-plugin 19 | ``` 20 | The template will ask you a number of questions. Here's an example run: 21 | 22 | ``` 23 | [1/6] plugin_name (): django-plugin-example 24 | [2/6] description (): A simple example plugin 25 | [3/6] hyphenated (django-plugin-example): 26 | [4/6] underscored (django_plugin_example): 27 | [5/6] github_username (): simonw 28 | [6/6] author_name (): Simon Willison 29 | ``` 30 | This creates a directory called `django-plugin-example` containing the skeleton of the plugin: 31 | 32 | ``` 33 | django-plugin-example 34 | django-plugin-example/django_plugin_example 35 | django-plugin-example/django_plugin_example/__init__.py 36 | django-plugin-example/LICENSE 37 | django-plugin-example/pyproject.toml 38 | django-plugin-example/tests 39 | django-plugin-example/tests/test_django_plugin_example.py 40 | django-plugin-example/tests/test_project 41 | django-plugin-example/tests/test_project/__init__.py 42 | django-plugin-example/tests/test_project/settings.py 43 | django-plugin-example/tests/test_project/urls.py 44 | django-plugin-example/__init__.py 45 | django-plugin-example/README.md 46 | django-plugin-example/.gitignore 47 | django-plugin-example/.github 48 | django-plugin-example/.github/workflows 49 | django-plugin-example/.github/workflows/publish.yml 50 | django-plugin-example/.github/workflows/test.yml 51 | ``` 52 | 53 | ## Creating a plugin without the template 54 | 55 | Your plugin should have a `pyproject.toml` file that defines it, looking something like this: 56 | 57 | ### pyproject.toml 58 | 59 | ```toml 60 | [project] 61 | name = "django-plugin-special-header" 62 | version = "0.1" 63 | description = "Add a HTTP header to a Django app" 64 | readme = "README.md" 65 | authors = [{name = "Simon Willison"}] 66 | license = {text = "Apache-2.0"} 67 | classifiers = [ 68 | "License :: OSI Approved :: Apache Software License" 69 | ] 70 | dependencies = [ 71 | "django", 72 | "djp", 73 | ] 74 | 75 | [project.entry-points.djp] 76 | django_plugin_special_header = "django_plugin_special_header" 77 | ``` 78 | The key part here is the `[project.entry-points.djp]` section. This tells the plugins system how to load the plugin - it should look for the `django_plugin_special_header` package or module. 79 | 80 | ### Plugin directory structure 81 | 82 | Next, create the directory structure. For this plugin that will look like this: 83 | 84 | ``` 85 | django-plugin-special-header/ 86 | django_plugin_special_header/ 87 | __init__.py 88 | middleware.py 89 | pyproject.toml 90 | README.md 91 | ``` 92 | The `__init__.py` file should contain the plugin hook implementations. For this middleware example that will look like this: 93 | 94 | ```python 95 | import djp 96 | 97 | 98 | @djp.hookimpl 99 | def middleware(): 100 | return ["django_plugin_secial_header.middleware.SpecialHeaderMiddleware"] 101 | ``` 102 | The `middleware.py` file should contain the actual middleware implementation. Here's an example: 103 | 104 | ```python 105 | class SpecialHeaderMiddleware: 106 | def __init__(self, get_response): 107 | self.get_response = get_response 108 | 109 | def __call__(self, request): 110 | response = self.get_response(request) 111 | response["Special-Header"] = "This is a special HTTP header" 112 | return response 113 | ``` 114 | 115 | ## Trying out the plugin 116 | 117 | In local development you can add this plugin to your existing Django environment by running this command: 118 | 119 | ```bash 120 | pip install -e path/to/django-plugin-example 121 | ``` 122 | -------------------------------------------------------------------------------- /djp/__init__.py: -------------------------------------------------------------------------------- 1 | from .hookspecs import hookimpl 2 | from . import hookspecs 3 | import itertools 4 | import os 5 | import pathlib 6 | from pluggy import PluginManager 7 | import sys 8 | from typing import List 9 | import types 10 | 11 | pm = PluginManager("djp") 12 | pm.add_hookspecs(hookspecs) 13 | pm.load_setuptools_entrypoints("djp") 14 | 15 | 16 | def _module_from_path(path, name): 17 | # Adapted from http://sayspy.blogspot.com/2011/07/how-to-import-module-from-just-file.html 18 | mod = types.ModuleType(name) 19 | mod.__file__ = path 20 | with open(path, "r") as file: 21 | code = compile(file.read(), path, "exec", dont_inherit=True) 22 | exec(code, mod.__dict__) 23 | return mod 24 | 25 | 26 | plugins_dir = os.environ.get("DJP_PLUGINS_DIR") 27 | if plugins_dir: 28 | for filepath in pathlib.Path(plugins_dir).glob("*.py"): 29 | mod = _module_from_path(str(filepath), name=filepath.stem) 30 | try: 31 | pm.register(mod) 32 | except ValueError as ex: 33 | print(ex, file=sys.stderr) 34 | # Plugin already registered 35 | pass 36 | 37 | 38 | class Before: 39 | def __init__(self, item: str): 40 | self.item = item 41 | 42 | 43 | class After: 44 | def __init__(self, item: str): 45 | self.item = item 46 | 47 | 48 | class Position: 49 | def __init__(self, item: str, before=None, after=None): 50 | assert not (before and after), "Cannot specify both before and after" 51 | self.item = item 52 | self.before = before 53 | self.after = after 54 | 55 | 56 | def installed_apps() -> List[str]: 57 | return ["djp"] + list(itertools.chain(*pm.hook.installed_apps())) 58 | 59 | 60 | def middleware(current_middleware: List[str]): 61 | before = [] 62 | after = [] 63 | default = [] 64 | position_items = [] 65 | 66 | for batch in pm.hook.middleware(): 67 | for item in batch: 68 | if isinstance(item, Before): 69 | before.append(item.item) 70 | elif isinstance(item, After): 71 | after.append(item.item) 72 | elif isinstance(item, Position): 73 | position_items.append(item) 74 | elif isinstance(item, str): 75 | default.append(item) 76 | else: 77 | raise ValueError(f"Invalid item in middleware hook: {item}") 78 | 79 | combined = before + to_list(current_middleware) + default + after 80 | 81 | # Handle Position items 82 | for item in position_items: 83 | if item.before: 84 | try: 85 | idx = combined.index(item.before) 86 | combined.insert(idx, item.item) 87 | except ValueError: 88 | raise ValueError(f"Cannot find item to insert before: {item.before}") 89 | elif item.after: 90 | try: 91 | idx = combined.index(item.after) 92 | combined.insert(idx + 1, item.item) 93 | except ValueError: 94 | raise ValueError(f"Cannot find item to insert after: {item.after}") 95 | 96 | return combined 97 | 98 | 99 | def urlpatterns(): 100 | return list(itertools.chain(*pm.hook.urlpatterns())) 101 | 102 | 103 | def settings(current_settings): 104 | # First wrap INSTALLED_APPS 105 | installed_apps_ = to_list(current_settings["INSTALLED_APPS"]) 106 | installed_apps_ += installed_apps() 107 | current_settings["INSTALLED_APPS"] = installed_apps_ 108 | 109 | # Now MIDDLEWARE 110 | current_settings["MIDDLEWARE"] = middleware(current_settings["MIDDLEWARE"]) 111 | 112 | # Now apply any other settings() hooks 113 | pm.hook.settings(current_settings=current_settings) 114 | 115 | 116 | def get_plugins(): 117 | plugins = [] 118 | plugin_to_distinfo = dict(pm.list_plugin_distinfo()) 119 | for plugin in pm.get_plugins(): 120 | plugin_info = { 121 | "name": plugin.__name__, 122 | "hooks": [h.name for h in pm.get_hookcallers(plugin)], 123 | } 124 | distinfo = plugin_to_distinfo.get(plugin) 125 | if distinfo: 126 | plugin_info["version"] = distinfo.version 127 | plugin_info["name"] = ( 128 | getattr(distinfo, "name", None) or distinfo.project_name 129 | ) 130 | plugins.append(plugin_info) 131 | return plugins 132 | 133 | 134 | def to_list(tuple_or_list): 135 | if isinstance(tuple_or_list, tuple): 136 | return list(tuple_or_list) 137 | return tuple_or_list 138 | 139 | 140 | def asgi_wrapper(application): 141 | for wrapper in pm.hook.asgi_wrapper(): 142 | application = wrapper(application) 143 | return application 144 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import datetime 4 | from subprocess import PIPE, Popen 5 | 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | # If extensions (or modules to document with autodoc) are in another directory, 16 | # add these directories to sys.path here. If the directory is relative to the 17 | # documentation root, use os.path.abspath to make it absolute, like shown here. 18 | # 19 | # import os 20 | # import sys 21 | # sys.path.insert(0, os.path.abspath('.')) 22 | 23 | 24 | # -- General configuration ------------------------------------------------ 25 | 26 | # If your documentation needs a minimal Sphinx version, state it here. 27 | # 28 | # needs_sphinx = '1.0' 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = ["myst_parser", "sphinx_copybutton"] 34 | myst_enable_extensions = ["colon_fence"] 35 | 36 | # Add any paths that contain templates here, relative to this directory. 37 | templates_path = ["_templates"] 38 | 39 | # The suffix(es) of source filenames. 40 | # You can specify multiple suffix as a list of string: 41 | # 42 | # source_suffix = ['.rst', '.md'] 43 | source_suffix = ".rst" 44 | 45 | # The master toctree document. 46 | master_doc = "index" 47 | 48 | # General information about the project. 49 | project = "DJP" 50 | author = "Simon Willison" 51 | copyright = "{}, {}".format(datetime.date.today().year, author) 52 | 53 | 54 | # The version info for the project you're documenting, acts as replacement for 55 | # |version| and |release|, also used in various other places throughout the 56 | # built documents. 57 | # 58 | # The short X.Y version. 59 | pipe = Popen("git describe --tags --always", stdout=PIPE, shell=True) 60 | git_version = pipe.stdout.read().decode("utf8") 61 | 62 | if git_version: 63 | version = git_version.rsplit("-", 1)[0] 64 | release = git_version 65 | else: 66 | version = "" 67 | release = "" 68 | 69 | # The language for content autogenerated by Sphinx. Refer to documentation 70 | # for a list of supported languages. 71 | # 72 | # This is also used if you do content translation via gettext catalogs. 73 | # Usually you set "language" from the command line for these cases. 74 | language = "en" 75 | 76 | # List of patterns, relative to source directory, that match files and 77 | # directories to ignore when looking for source files. 78 | # This patterns also effect to html_static_path and html_extra_path 79 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 80 | 81 | # The name of the Pygments (syntax highlighting) style to use. 82 | pygments_style = "sphinx" 83 | 84 | # If true, `todo` and `todoList` produce output, else they produce nothing. 85 | todo_include_todos = False 86 | 87 | 88 | # -- Options for HTML output ---------------------------------------------- 89 | 90 | # The theme to use for HTML and HTML Help pages. See the documentation for 91 | # a list of builtin themes. 92 | # 93 | html_theme = "furo" 94 | 95 | # Theme options are theme-specific and customize the look and feel of a theme 96 | # further. For a list of options available for each theme, see the 97 | # documentation. 98 | 99 | html_theme_options = {} 100 | html_title = project 101 | 102 | # Add any paths that contain custom static files (such as style sheets) here, 103 | # relative to this directory. They are copied after the builtin static files, 104 | # so a file named "default.css" will overwrite the builtin "default.css". 105 | html_static_path = [] 106 | 107 | 108 | # -- Options for HTMLHelp output ------------------------------------------ 109 | 110 | # Output file base name for HTML help builder. 111 | htmlhelp_basename = "project-doc" 112 | 113 | 114 | # -- Options for LaTeX output --------------------------------------------- 115 | 116 | latex_elements = { 117 | # The paper size ('letterpaper' or 'a4paper'). 118 | # 119 | # 'papersize': 'letterpaper', 120 | # The font size ('10pt', '11pt' or '12pt'). 121 | # 122 | # 'pointsize': '10pt', 123 | # Additional stuff for the LaTeX preamble. 124 | # 125 | # 'preamble': '', 126 | # Latex figure (float) alignment 127 | # 128 | # 'figure_align': 'htbp', 129 | } 130 | 131 | # Grouping the document tree into LaTeX files. List of tuples 132 | # (source start file, target name, title, 133 | # author, documentclass [howto, manual, or own class]). 134 | latex_documents = [ 135 | ( 136 | master_doc, 137 | "project.tex", 138 | project, 139 | author, 140 | "manual", 141 | ) 142 | ] 143 | 144 | 145 | # -- Options for manual page output --------------------------------------- 146 | 147 | # One entry per manual page. List of tuples 148 | # (source start file, name, description, authors, manual section). 149 | man_pages = [ 150 | ( 151 | master_doc, 152 | "project", 153 | project, 154 | [author], 155 | 1, 156 | ) 157 | ] 158 | 159 | 160 | # -- Options for Texinfo output ------------------------------------------- 161 | 162 | # Grouping the document tree into Texinfo files. List of tuples 163 | # (source start file, target name, title, author, 164 | # dir menu entry, description, category) 165 | texinfo_documents = [ 166 | ( 167 | master_doc, 168 | "project", 169 | project, 170 | author, 171 | "project", 172 | "Documentation for {}".format(project), 173 | "Miscellaneous", 174 | ) 175 | ] 176 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------