django-deacon 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Deacon
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.
@@ -0,0 +1,122 @@
1
+ Metadata-Version: 2.4
2
+ Name: django-deacon
3
+ Version: 1.0.0
4
+ Summary: Django app that adds Deacon, an AI support chat that answers from your docs, to every page.
5
+ Keywords: django,deacon,chat widget,support,ai,chatbot,ai-chatbot,ask-ai,support-chat
6
+ Author: Deacon
7
+ Author-email: Deacon <hello@heydeacon.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 5 - Production/Stable
11
+ Classifier: Environment :: Web Environment
12
+ Classifier: Framework :: Django
13
+ Classifier: Framework :: Django :: 5.2
14
+ Classifier: Framework :: Django :: 6.0
15
+ Classifier: Framework :: Django :: 6.1
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3 :: Only
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Programming Language :: Python :: 3.14
24
+ Requires-Dist: django>=5.2
25
+ Requires-Python: >=3.11
26
+ Project-URL: Homepage, https://heydeacon.com
27
+ Project-URL: Repository, https://github.com/heydeacon/django-deacon
28
+ Project-URL: Issues, https://github.com/heydeacon/django-deacon/issues
29
+ Description-Content-Type: text/markdown
30
+
31
+ # django-deacon
32
+
33
+ [Deacon](https://heydeacon.com) is an AI support agent that answers questions from your own docs and content, and says when it doesn't know. This app adds its chat widget to every page of a Django site.
34
+
35
+ You need a Deacon account, and there's a free plan.
36
+
37
+ ## Install
38
+
39
+ ```sh
40
+ pip install django-deacon
41
+ ```
42
+
43
+ ## Set it up
44
+
45
+ Add the app and your key to `settings.py`:
46
+
47
+ ```python
48
+ INSTALLED_APPS = [
49
+ # ...the rest of your apps
50
+ "django_deacon",
51
+ ]
52
+
53
+ DEACON_KEY = "pk_your_widget_key"
54
+ ```
55
+
56
+ Then load the tag in the `<head>` of your base template:
57
+
58
+ ```django
59
+ {% load deacon %}
60
+ <head>
61
+ {# ...the rest of your head #}
62
+ {% deacon_widget %}
63
+ </head>
64
+ ```
65
+
66
+ To find your key, open Install from the button at the top of the Deacon dashboard. The snippet there has it in `data-key`, and it starts with `pk_`. It isn't a secret, since every visitor's browser reads it, but you can read it from the environment like any setting: `DEACON_KEY = os.environ.get("DEACON_KEY")`. If the key is missing, the tag prints nothing and logs a warning that says why, so your pages still render. A setting of the wrong type raises `ImproperlyConfigured`, which says why.
67
+
68
+ Deacon only runs on the sites listed on Install. A listed domain covers its subdomains, so `example.com` covers `app.example.com`. If your site lives somewhere else, such as `example.fly.dev`, add that address on Install too.
69
+
70
+ When you run `python manage.py runserver`, Install notices the widget on `127.0.0.1:8000` and lets you add it in one click. Chats on a development address don't count towards your plan.
71
+
72
+ If you tell Deacon who is signed in, with the `window.deaconUser` script from [Deacon's install guide](https://heydeacon.com/agent/install.md), put that script above `{% deacon_widget %}` in the head, because the widget reads it as it starts.
73
+
74
+ ## Options
75
+
76
+ | Setting | Type | Default | What it does |
77
+ | ------------------- | ------- | -------- | ------------ |
78
+ | `DEACON_KEY` | str | none | Your Deacon key. Without it the tag prints nothing and logs a warning. |
79
+ | `DEACON_CONTEXT` | dict | none | Details Deacon takes into account when it answers, the same on every page, such as `{"product": "api", "docs_version": "3.2"}`. Up to 12 keys, each value a string, number or boolean. Anyone can read them in the page's HTML, so never put a secret here. |
80
+ | `DEACON_PAGE_VIEWS` | bool | `True` | The same script counts visits for Deacon's website analytics. Set this to `False` to send no page views or JavaScript errors from your site. Chat still works. |
81
+
82
+ ```python
83
+ DEACON_KEY = "pk_your_widget_key"
84
+ DEACON_CONTEXT = {"product": "api", "docs_version": "3.2"}
85
+ DEACON_PAGE_VIEWS = False
86
+ ```
87
+
88
+ ## Content Security Policy
89
+
90
+ If your site sends a Content Security Policy with a nonce, the tag carries the request's nonce. It reads it from Django's own CSP support (Django 6.0 and later), which needs the `django.template.context_processors.csp` context processor, or from [django-csp](https://pypi.org/project/django-csp/)'s `request.csp_nonce`, which needs `django.template.context_processors.request`.
91
+
92
+ The policy also needs Deacon's address, `https://heydeacon.com`, in `script-src`, `connect-src` and `frame-src`, and `'unsafe-inline'` in `style-src`, because the widget writes its styles where a nonce can't reach. Keep the nonce out of `style-src`, since a nonce there makes browsers ignore `'unsafe-inline'`:
93
+
94
+ ```python
95
+ from django.utils.csp import CSP
96
+
97
+ SECURE_CSP = {
98
+ "default-src": [CSP.SELF],
99
+ "script-src": [CSP.SELF, CSP.NONCE, "https://heydeacon.com"],
100
+ "style-src": [CSP.SELF, CSP.UNSAFE_INLINE],
101
+ "connect-src": [CSP.SELF, "https://heydeacon.com"],
102
+ "frame-src": ["https://heydeacon.com"],
103
+ }
104
+ ```
105
+
106
+ ## Opening the chat from your own button
107
+
108
+ The widget comes with its own button. To use one of yours instead, hide Deacon's in Appearance in the dashboard and call `window.Deacon.open()` from your button or link. `Deacon.close()` and `Deacon.toggle()` work too.
109
+
110
+ ## What it adds
111
+
112
+ The tag prints the same tag Install gives you:
113
+
114
+ ```html
115
+ <script src="https://heydeacon.com/widget/v1/loader.js" data-key="pk_your_widget_key" async></script>
116
+ ```
117
+
118
+ It loads in the background without holding up the page. Works with Django 5.2 and later, on Python 3.11 and later.
119
+
120
+ ## License
121
+
122
+ MIT
@@ -0,0 +1,92 @@
1
+ # django-deacon
2
+
3
+ [Deacon](https://heydeacon.com) is an AI support agent that answers questions from your own docs and content, and says when it doesn't know. This app adds its chat widget to every page of a Django site.
4
+
5
+ You need a Deacon account, and there's a free plan.
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ pip install django-deacon
11
+ ```
12
+
13
+ ## Set it up
14
+
15
+ Add the app and your key to `settings.py`:
16
+
17
+ ```python
18
+ INSTALLED_APPS = [
19
+ # ...the rest of your apps
20
+ "django_deacon",
21
+ ]
22
+
23
+ DEACON_KEY = "pk_your_widget_key"
24
+ ```
25
+
26
+ Then load the tag in the `<head>` of your base template:
27
+
28
+ ```django
29
+ {% load deacon %}
30
+ <head>
31
+ {# ...the rest of your head #}
32
+ {% deacon_widget %}
33
+ </head>
34
+ ```
35
+
36
+ To find your key, open Install from the button at the top of the Deacon dashboard. The snippet there has it in `data-key`, and it starts with `pk_`. It isn't a secret, since every visitor's browser reads it, but you can read it from the environment like any setting: `DEACON_KEY = os.environ.get("DEACON_KEY")`. If the key is missing, the tag prints nothing and logs a warning that says why, so your pages still render. A setting of the wrong type raises `ImproperlyConfigured`, which says why.
37
+
38
+ Deacon only runs on the sites listed on Install. A listed domain covers its subdomains, so `example.com` covers `app.example.com`. If your site lives somewhere else, such as `example.fly.dev`, add that address on Install too.
39
+
40
+ When you run `python manage.py runserver`, Install notices the widget on `127.0.0.1:8000` and lets you add it in one click. Chats on a development address don't count towards your plan.
41
+
42
+ If you tell Deacon who is signed in, with the `window.deaconUser` script from [Deacon's install guide](https://heydeacon.com/agent/install.md), put that script above `{% deacon_widget %}` in the head, because the widget reads it as it starts.
43
+
44
+ ## Options
45
+
46
+ | Setting | Type | Default | What it does |
47
+ | ------------------- | ------- | -------- | ------------ |
48
+ | `DEACON_KEY` | str | none | Your Deacon key. Without it the tag prints nothing and logs a warning. |
49
+ | `DEACON_CONTEXT` | dict | none | Details Deacon takes into account when it answers, the same on every page, such as `{"product": "api", "docs_version": "3.2"}`. Up to 12 keys, each value a string, number or boolean. Anyone can read them in the page's HTML, so never put a secret here. |
50
+ | `DEACON_PAGE_VIEWS` | bool | `True` | The same script counts visits for Deacon's website analytics. Set this to `False` to send no page views or JavaScript errors from your site. Chat still works. |
51
+
52
+ ```python
53
+ DEACON_KEY = "pk_your_widget_key"
54
+ DEACON_CONTEXT = {"product": "api", "docs_version": "3.2"}
55
+ DEACON_PAGE_VIEWS = False
56
+ ```
57
+
58
+ ## Content Security Policy
59
+
60
+ If your site sends a Content Security Policy with a nonce, the tag carries the request's nonce. It reads it from Django's own CSP support (Django 6.0 and later), which needs the `django.template.context_processors.csp` context processor, or from [django-csp](https://pypi.org/project/django-csp/)'s `request.csp_nonce`, which needs `django.template.context_processors.request`.
61
+
62
+ The policy also needs Deacon's address, `https://heydeacon.com`, in `script-src`, `connect-src` and `frame-src`, and `'unsafe-inline'` in `style-src`, because the widget writes its styles where a nonce can't reach. Keep the nonce out of `style-src`, since a nonce there makes browsers ignore `'unsafe-inline'`:
63
+
64
+ ```python
65
+ from django.utils.csp import CSP
66
+
67
+ SECURE_CSP = {
68
+ "default-src": [CSP.SELF],
69
+ "script-src": [CSP.SELF, CSP.NONCE, "https://heydeacon.com"],
70
+ "style-src": [CSP.SELF, CSP.UNSAFE_INLINE],
71
+ "connect-src": [CSP.SELF, "https://heydeacon.com"],
72
+ "frame-src": ["https://heydeacon.com"],
73
+ }
74
+ ```
75
+
76
+ ## Opening the chat from your own button
77
+
78
+ The widget comes with its own button. To use one of yours instead, hide Deacon's in Appearance in the dashboard and call `window.Deacon.open()` from your button or link. `Deacon.close()` and `Deacon.toggle()` work too.
79
+
80
+ ## What it adds
81
+
82
+ The tag prints the same tag Install gives you:
83
+
84
+ ```html
85
+ <script src="https://heydeacon.com/widget/v1/loader.js" data-key="pk_your_widget_key" async></script>
86
+ ```
87
+
88
+ It loads in the background without holding up the page. Works with Django 5.2 and later, on Python 3.11 and later.
89
+
90
+ ## License
91
+
92
+ MIT
@@ -0,0 +1,65 @@
1
+ [project]
2
+ name = "django-deacon"
3
+ version = "1.0.0"
4
+ description = "Django app that adds Deacon, an AI support chat that answers from your docs, to every page."
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ requires-python = ">=3.11"
9
+ dependencies = ["Django>=5.2"]
10
+ keywords = [
11
+ "django",
12
+ "deacon",
13
+ "chat widget",
14
+ "support",
15
+ "ai",
16
+ "chatbot",
17
+ "ai-chatbot",
18
+ "ask-ai",
19
+ "support-chat",
20
+ ]
21
+ classifiers = [
22
+ "Development Status :: 5 - Production/Stable",
23
+ "Environment :: Web Environment",
24
+ "Framework :: Django",
25
+ "Framework :: Django :: 5.2",
26
+ "Framework :: Django :: 6.0",
27
+ "Framework :: Django :: 6.1",
28
+ "Intended Audience :: Developers",
29
+ "Operating System :: OS Independent",
30
+ "Programming Language :: Python :: 3",
31
+ "Programming Language :: Python :: 3 :: Only",
32
+ "Programming Language :: Python :: 3.11",
33
+ "Programming Language :: Python :: 3.12",
34
+ "Programming Language :: Python :: 3.13",
35
+ "Programming Language :: Python :: 3.14",
36
+ ]
37
+
38
+ [[project.authors]]
39
+ name = "Deacon"
40
+ email = "hello@heydeacon.com"
41
+
42
+ [project.urls]
43
+ Homepage = "https://heydeacon.com"
44
+ Repository = "https://github.com/heydeacon/django-deacon"
45
+ Issues = "https://github.com/heydeacon/django-deacon/issues"
46
+
47
+ [dependency-groups]
48
+ dev = [
49
+ "django-csp>=4.0",
50
+ "pytest>=9.1",
51
+ "pytest-django>=4.14",
52
+ "ruff>=0.17.2",
53
+ ]
54
+
55
+ [build-system]
56
+ requires = ["uv_build>=0.13.5,<0.14"]
57
+ build-backend = "uv_build"
58
+
59
+ [tool.pytest.ini_options]
60
+ DJANGO_SETTINGS_MODULE = "tests.settings"
61
+ pythonpath = ["."]
62
+ testpaths = ["tests"]
63
+
64
+ [tool.ruff]
65
+ line-length = 100
@@ -0,0 +1,62 @@
1
+ [project]
2
+ name = "django-deacon"
3
+ version = "1.0.0"
4
+ description = "Django app that adds Deacon, an AI support chat that answers from your docs, to every page."
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ authors = [{ name = "Deacon", email = "hello@heydeacon.com" }]
9
+ requires-python = ">=3.11"
10
+ dependencies = ["Django>=5.2"]
11
+ keywords = [
12
+ "django",
13
+ "deacon",
14
+ "chat widget",
15
+ "support",
16
+ "ai",
17
+ "chatbot",
18
+ "ai-chatbot",
19
+ "ask-ai",
20
+ "support-chat",
21
+ ]
22
+ classifiers = [
23
+ "Development Status :: 5 - Production/Stable",
24
+ "Environment :: Web Environment",
25
+ "Framework :: Django",
26
+ "Framework :: Django :: 5.2",
27
+ "Framework :: Django :: 6.0",
28
+ "Framework :: Django :: 6.1",
29
+ "Intended Audience :: Developers",
30
+ "Operating System :: OS Independent",
31
+ "Programming Language :: Python :: 3",
32
+ "Programming Language :: Python :: 3 :: Only",
33
+ "Programming Language :: Python :: 3.11",
34
+ "Programming Language :: Python :: 3.12",
35
+ "Programming Language :: Python :: 3.13",
36
+ "Programming Language :: Python :: 3.14",
37
+ ]
38
+
39
+ [project.urls]
40
+ Homepage = "https://heydeacon.com"
41
+ Repository = "https://github.com/heydeacon/django-deacon"
42
+ Issues = "https://github.com/heydeacon/django-deacon/issues"
43
+
44
+ [dependency-groups]
45
+ dev = [
46
+ "django-csp>=4.0",
47
+ "pytest>=9.1",
48
+ "pytest-django>=4.14",
49
+ "ruff>=0.17.2",
50
+ ]
51
+
52
+ [build-system]
53
+ requires = ["uv_build>=0.13.5,<0.14"]
54
+ build-backend = "uv_build"
55
+
56
+ [tool.pytest.ini_options]
57
+ DJANGO_SETTINGS_MODULE = "tests.settings"
58
+ pythonpath = ["."]
59
+ testpaths = ["tests"]
60
+
61
+ [tool.ruff]
62
+ line-length = 100
File without changes
@@ -0,0 +1,6 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class DeaconConfig(AppConfig):
5
+ name = "django_deacon"
6
+ verbose_name = "Deacon"
@@ -0,0 +1,96 @@
1
+ import json
2
+ import logging
3
+
4
+ from django import template
5
+ from django.conf import settings
6
+ from django.core.exceptions import ImproperlyConfigured
7
+ from django.utils.html import escape
8
+ from django.utils.safestring import mark_safe
9
+
10
+ register = template.Library()
11
+ logger = logging.getLogger("django_deacon")
12
+
13
+ LOADER_SRC = "https://heydeacon.com/widget/v1/loader.js"
14
+
15
+ MISSING_KEY = (
16
+ "django-deacon: the DEACON_KEY setting is missing, so the page has no Deacon widget. In "
17
+ "Deacon, open Install (the button at the top of the dashboard) and copy the data-key value "
18
+ "from the snippet. It starts with pk_."
19
+ )
20
+
21
+
22
+ @register.simple_tag(takes_context=True)
23
+ def deacon_widget(context):
24
+ """
25
+ The tag Install gives, with the key from the DEACON_KEY setting. With no key it prints
26
+ nothing and logs a warning, so a missing setting never takes a page down.
27
+ """
28
+ key = str(getattr(settings, "DEACON_KEY", None) or "").strip()
29
+
30
+ if key == "":
31
+ logger.warning(MISSING_KEY)
32
+ return ""
33
+
34
+ deacon_context = getattr(settings, "DEACON_CONTEXT", None)
35
+ page_views = getattr(settings, "DEACON_PAGE_VIEWS", True)
36
+ _check_context(deacon_context)
37
+ _check_page_views(page_views)
38
+
39
+ attributes = {"data-key": key}
40
+ if deacon_context is not None:
41
+ attributes["data-context"] = json.dumps(
42
+ deacon_context, separators=(",", ":"), ensure_ascii=False
43
+ )
44
+ if page_views is False:
45
+ attributes["data-no-page-views"] = True
46
+
47
+ return _loader_tag(attributes, _csp_nonce(context))
48
+
49
+
50
+ def _csp_nonce(context):
51
+ """
52
+ The request's Content Security Policy nonce: from Django's own CSP support (6.0 and
53
+ later, through its csp context processor), else from django-csp's request.csp_nonce, or
54
+ None when the project has no nonce.
55
+ """
56
+ nonce = context.get("csp_nonce")
57
+ if nonce is None:
58
+ nonce = getattr(context.get("request"), "csp_nonce", None)
59
+
60
+ return None if nonce is None else str(nonce)
61
+
62
+
63
+ def _check_context(deacon_context):
64
+ """A wrong type is a mistake in the project's settings, so it raises."""
65
+ if deacon_context is None:
66
+ return
67
+
68
+ if not isinstance(deacon_context, dict) or not all(
69
+ isinstance(value, str | int | float | bool) for value in deacon_context.values()
70
+ ):
71
+ raise ImproperlyConfigured(
72
+ "django-deacon: the DEACON_CONTEXT setting must be a dict whose values are strings, "
73
+ "numbers or booleans."
74
+ )
75
+
76
+
77
+ def _check_page_views(page_views):
78
+ if not isinstance(page_views, bool):
79
+ raise ImproperlyConfigured(
80
+ "django-deacon: the DEACON_PAGE_VIEWS setting must be True or False."
81
+ )
82
+
83
+
84
+ def _loader_tag(attributes, nonce):
85
+ """
86
+ The tag Install gives, with any of its other attributes between the key and async, and
87
+ the nonce after them. An attribute that is True is written with no value, as async is.
88
+ """
89
+ rendered = "".join(
90
+ f" {name}" if value is True else f' {name}="{escape(value)}"'
91
+ for name, value in attributes.items()
92
+ )
93
+
94
+ nonce_attribute = "" if nonce is None else f' nonce="{escape(nonce)}"'
95
+
96
+ return mark_safe(f'<script src="{LOADER_SRC}"{rendered} async{nonce_attribute}></script>')