django-bots 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.
Files changed (39) hide show
  1. django_bots-1.0.0/LICENSE +21 -0
  2. django_bots-1.0.0/PKG-INFO +200 -0
  3. django_bots-1.0.0/README.md +166 -0
  4. django_bots-1.0.0/pyproject.toml +156 -0
  5. django_bots-1.0.0/setup.cfg +4 -0
  6. django_bots-1.0.0/src/django_bots/__init__.py +15 -0
  7. django_bots-1.0.0/src/django_bots/ai.py +94 -0
  8. django_bots-1.0.0/src/django_bots/apps.py +20 -0
  9. django_bots-1.0.0/src/django_bots/checks.py +70 -0
  10. django_bots-1.0.0/src/django_bots/conf.py +66 -0
  11. django_bots-1.0.0/src/django_bots/crawlers.py +56 -0
  12. django_bots-1.0.0/src/django_bots/data/LICENSE-ai-robots-txt +21 -0
  13. django_bots-1.0.0/src/django_bots/data/VERSIONS.json +3 -0
  14. django_bots-1.0.0/src/django_bots/data/ai_robots.json +1228 -0
  15. django_bots-1.0.0/src/django_bots/middleware.py +118 -0
  16. django_bots-1.0.0/src/django_bots/py.typed +0 -0
  17. django_bots-1.0.0/src/django_bots/templates/django_bots/robots.txt +2 -0
  18. django_bots-1.0.0/src/django_bots/templatetags/__init__.py +0 -0
  19. django_bots-1.0.0/src/django_bots/templatetags/bots.py +31 -0
  20. django_bots-1.0.0/src/django_bots/templatetags/user_agents.py +35 -0
  21. django_bots-1.0.0/src/django_bots/useragent.py +313 -0
  22. django_bots-1.0.0/src/django_bots/utils.py +39 -0
  23. django_bots-1.0.0/src/django_bots/views.py +19 -0
  24. django_bots-1.0.0/src/django_bots.egg-info/PKG-INFO +200 -0
  25. django_bots-1.0.0/src/django_bots.egg-info/SOURCES.txt +37 -0
  26. django_bots-1.0.0/src/django_bots.egg-info/dependency_links.txt +1 -0
  27. django_bots-1.0.0/src/django_bots.egg-info/requires.txt +3 -0
  28. django_bots-1.0.0/src/django_bots.egg-info/top_level.txt +1 -0
  29. django_bots-1.0.0/tests/test_ai.py +216 -0
  30. django_bots-1.0.0/tests/test_checks.py +60 -0
  31. django_bots-1.0.0/tests/test_conf.py +70 -0
  32. django_bots-1.0.0/tests/test_crawlers.py +125 -0
  33. django_bots-1.0.0/tests/test_middleware.py +178 -0
  34. django_bots-1.0.0/tests/test_scripts.py +380 -0
  35. django_bots-1.0.0/tests/test_templatetags.py +113 -0
  36. django_bots-1.0.0/tests/test_useragent.py +130 -0
  37. django_bots-1.0.0/tests/test_useragent_compat.py +64 -0
  38. django_bots-1.0.0/tests/test_utils.py +62 -0
  39. django_bots-1.0.0/tests/test_views.py +73 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 M4p4
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,200 @@
1
+ Metadata-Version: 2.4
2
+ Name: django-bots
3
+ Version: 1.0.0
4
+ Summary: User-agent parsing, crawler detection and AI bot blocking for Django.
5
+ Author-email: M4p4 <homejr@protonmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Changelog, https://github.com/M4p4/django-bots/blob/main/CHANGELOG.md
8
+ Project-URL: Documentation, https://djangobots.readthedocs.io/
9
+ Project-URL: Issues, https://github.com/M4p4/django-bots/issues
10
+ Project-URL: Repository, https://github.com/M4p4/django-bots
11
+ Keywords: ai,bots,crawlers,django,robots.txt,user-agents
12
+ Classifier: Development Status :: 5 - Production/Stable
13
+ Classifier: Framework :: Django
14
+ Classifier: Framework :: Django :: 5.2
15
+ Classifier: Framework :: Django :: 6.0
16
+ Classifier: Framework :: Django :: 6.1
17
+ Classifier: Intended Audience :: Developers
18
+ Classifier: Programming Language :: Python :: 3 :: Only
19
+ Classifier: Programming Language :: Python :: 3.10
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
+ Classifier: Topic :: Internet :: WWW/HTTP
25
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
26
+ Classifier: Typing :: Typed
27
+ Requires-Python: >=3.10
28
+ Description-Content-Type: text/markdown
29
+ License-File: LICENSE
30
+ Requires-Dist: crawler-user-agents>=1.62
31
+ Requires-Dist: django>=5.2
32
+ Requires-Dist: ua-parser<2,>=1
33
+ Dynamic: license-file
34
+
35
+ # django-bots
36
+
37
+ [![PyPI version](https://img.shields.io/pypi/v/django-bots.svg)](https://pypi.org/project/django-bots/)
38
+ [![Python versions](https://img.shields.io/pypi/pyversions/django-bots.svg)](https://pypi.org/project/django-bots/)
39
+ [![CI](https://github.com/M4p4/django-bots/actions/workflows/main.yml/badge.svg)](https://github.com/M4p4/django-bots/actions/workflows/main.yml)
40
+ [![Documentation](https://readthedocs.org/projects/djangobots/badge/?version=stable)](https://djangobots.readthedocs.io/)
41
+
42
+ More and more of the traffic on a website comes from crawlers, scrapers and AI bots
43
+ instead of people. I wrote django-bots to tell them apart in Django, and to keep out
44
+ the AI bots I don't want.
45
+
46
+ django-bots tells you who is on the other end of a request: which browser, operating
47
+ system and device, whether it's a crawler, and whether it's an AI bot. It can serve a
48
+ robots.txt that disallows AI bots and turn them away with a 403. Each feature is
49
+ opt-in, and the package adds no models or migrations.
50
+
51
+ ## Installation
52
+
53
+ ```console
54
+ python -m pip install django-bots
55
+ ```
56
+
57
+ Add the app to `INSTALLED_APPS`:
58
+
59
+ ```python
60
+ INSTALLED_APPS = [
61
+ ...,
62
+ "django_bots",
63
+ ]
64
+ ```
65
+
66
+ ## User agents
67
+
68
+ Add the middleware, and every request gets a lazily parsed `request.user_agent`:
69
+
70
+ ```python
71
+ MIDDLEWARE = [
72
+ ...,
73
+ "django_bots.middleware.UserAgentMiddleware",
74
+ ]
75
+ ```
76
+
77
+ ```python
78
+ def home(request):
79
+ if request.user_agent.is_mobile:
80
+ ...
81
+ request.user_agent.browser # Browser(family="Mobile Safari", version=(5, 1), version_string="5.1")
82
+ request.user_agent.os # OperatingSystem(family="iOS", version=(5, 1), version_string="5.1")
83
+ request.user_agent.device # Device(family="iPhone", brand="Apple", model="iPhone")
84
+ ```
85
+
86
+ Templates get the same checks as filters:
87
+
88
+ ```django
89
+ {% load bots %}
90
+
91
+ {% if request|is_mobile %}
92
+ <a href="/app/">Get the app</a>
93
+ {% endif %}
94
+ ```
95
+
96
+ Parsing uses [ua-parser](https://github.com/ua-parser/uap-python) 1.x with an
97
+ in-process cache, and works for sync and async views.
98
+
99
+ ## Crawlers
100
+
101
+ `is_crawler` matches the patterns from
102
+ [crawler-user-agents](https://github.com/monperrus/crawler-user-agents), which cover
103
+ search engines, SEO tools, uptime monitors and HTTP libraries:
104
+
105
+ ```python
106
+ if request.user_agent.is_crawler:
107
+ ...
108
+ ```
109
+
110
+ ```django
111
+ {% if not request|is_crawler %}
112
+ <script src="/analytics.js"></script>
113
+ {% endif %}
114
+ ```
115
+
116
+ ## AI bots
117
+
118
+ `is_ai_bot` and `ai_bot` match the list from
119
+ [ai.robots.txt](https://github.com/ai-robots-txt/ai.robots.txt), which is bundled with
120
+ the package:
121
+
122
+ ```python
123
+ if request.user_agent.is_ai_bot:
124
+ logger.info("AI bot: %s", request.user_agent.ai_bot) # "GPTBot"
125
+ ```
126
+
127
+ Serve a robots.txt that disallows every AI bot:
128
+
129
+ ```python
130
+ from django.urls import path
131
+
132
+ from django_bots.views import robots_txt
133
+
134
+ urlpatterns = [
135
+ path("robots.txt", robots_txt),
136
+ ]
137
+ ```
138
+
139
+ robots.txt only asks. To refuse AI bots outright, add the blocking middleware near
140
+ the top of `MIDDLEWARE`:
141
+
142
+ ```python
143
+ MIDDLEWARE = [
144
+ "django.middleware.security.SecurityMiddleware",
145
+ "django_bots.middleware.AIBotBlockMiddleware",
146
+ ...,
147
+ ]
148
+ ```
149
+
150
+ `BOTS_AI_ALLOW` keeps the bots you want, for both robots.txt and the middleware:
151
+
152
+ ```python
153
+ BOTS_AI_ALLOW = ["OAI-SearchBot"]
154
+ ```
155
+
156
+ A daily workflow checks for new ai.robots.txt releases, and data updates ship as patch
157
+ releases.
158
+
159
+ ## Upgrading from django-user-agents
160
+
161
+ The `request.user_agent` attributes, the `django_bots.utils` helpers and the
162
+ `user_agents` template filters work the same way as in django-user-agents, so
163
+ upgrading comes down to new app, middleware and import paths. One thing behaves
164
+ differently: `is_bot` is also true for crawlers and AI bots. The
165
+ [migration guide](https://djangobots.readthedocs.io/en/stable/migration.html) walks
166
+ through each step.
167
+
168
+ ## Compatibility
169
+
170
+ | Python | Django |
171
+ |---|---|
172
+ | 3.10, 3.11 | 5.2 |
173
+ | 3.12, 3.13, 3.14 | 5.2, 6.0, 6.1 |
174
+
175
+ ## Data sources and licenses
176
+
177
+ django-bots is released under the MIT license. It uses:
178
+
179
+ - [ai.robots.txt](https://github.com/ai-robots-txt/ai.robots.txt) (MIT), bundled in
180
+ `django_bots/data/` with its license
181
+ - [crawler-user-agents](https://github.com/monperrus/crawler-user-agents) (MIT),
182
+ installed as a dependency
183
+ - [ua-parser](https://github.com/ua-parser/uap-python) (Apache-2.0), installed as a
184
+ dependency
185
+ - the device detection rules of
186
+ [user-agents](https://github.com/selwin/python-user-agents) (MIT), ported into
187
+ `django_bots/useragent.py`
188
+
189
+ `django_bots.DATA_VERSIONS` shows which versions are in use.
190
+
191
+ ## Documentation
192
+
193
+ The full documentation is at [djangobots.readthedocs.io](https://djangobots.readthedocs.io/):
194
+
195
+ - [User agents](https://djangobots.readthedocs.io/en/stable/user-agents.html)
196
+ - [Crawler detection](https://djangobots.readthedocs.io/en/stable/crawlers.html)
197
+ - [AI bots](https://djangobots.readthedocs.io/en/stable/ai-bots.html)
198
+ - [Settings](https://djangobots.readthedocs.io/en/stable/settings.html)
199
+ - [Limits](https://djangobots.readthedocs.io/en/stable/limits.html)
200
+ - [Contributing](https://djangobots.readthedocs.io/en/stable/contributing.html)
@@ -0,0 +1,166 @@
1
+ # django-bots
2
+
3
+ [![PyPI version](https://img.shields.io/pypi/v/django-bots.svg)](https://pypi.org/project/django-bots/)
4
+ [![Python versions](https://img.shields.io/pypi/pyversions/django-bots.svg)](https://pypi.org/project/django-bots/)
5
+ [![CI](https://github.com/M4p4/django-bots/actions/workflows/main.yml/badge.svg)](https://github.com/M4p4/django-bots/actions/workflows/main.yml)
6
+ [![Documentation](https://readthedocs.org/projects/djangobots/badge/?version=stable)](https://djangobots.readthedocs.io/)
7
+
8
+ More and more of the traffic on a website comes from crawlers, scrapers and AI bots
9
+ instead of people. I wrote django-bots to tell them apart in Django, and to keep out
10
+ the AI bots I don't want.
11
+
12
+ django-bots tells you who is on the other end of a request: which browser, operating
13
+ system and device, whether it's a crawler, and whether it's an AI bot. It can serve a
14
+ robots.txt that disallows AI bots and turn them away with a 403. Each feature is
15
+ opt-in, and the package adds no models or migrations.
16
+
17
+ ## Installation
18
+
19
+ ```console
20
+ python -m pip install django-bots
21
+ ```
22
+
23
+ Add the app to `INSTALLED_APPS`:
24
+
25
+ ```python
26
+ INSTALLED_APPS = [
27
+ ...,
28
+ "django_bots",
29
+ ]
30
+ ```
31
+
32
+ ## User agents
33
+
34
+ Add the middleware, and every request gets a lazily parsed `request.user_agent`:
35
+
36
+ ```python
37
+ MIDDLEWARE = [
38
+ ...,
39
+ "django_bots.middleware.UserAgentMiddleware",
40
+ ]
41
+ ```
42
+
43
+ ```python
44
+ def home(request):
45
+ if request.user_agent.is_mobile:
46
+ ...
47
+ request.user_agent.browser # Browser(family="Mobile Safari", version=(5, 1), version_string="5.1")
48
+ request.user_agent.os # OperatingSystem(family="iOS", version=(5, 1), version_string="5.1")
49
+ request.user_agent.device # Device(family="iPhone", brand="Apple", model="iPhone")
50
+ ```
51
+
52
+ Templates get the same checks as filters:
53
+
54
+ ```django
55
+ {% load bots %}
56
+
57
+ {% if request|is_mobile %}
58
+ <a href="/app/">Get the app</a>
59
+ {% endif %}
60
+ ```
61
+
62
+ Parsing uses [ua-parser](https://github.com/ua-parser/uap-python) 1.x with an
63
+ in-process cache, and works for sync and async views.
64
+
65
+ ## Crawlers
66
+
67
+ `is_crawler` matches the patterns from
68
+ [crawler-user-agents](https://github.com/monperrus/crawler-user-agents), which cover
69
+ search engines, SEO tools, uptime monitors and HTTP libraries:
70
+
71
+ ```python
72
+ if request.user_agent.is_crawler:
73
+ ...
74
+ ```
75
+
76
+ ```django
77
+ {% if not request|is_crawler %}
78
+ <script src="/analytics.js"></script>
79
+ {% endif %}
80
+ ```
81
+
82
+ ## AI bots
83
+
84
+ `is_ai_bot` and `ai_bot` match the list from
85
+ [ai.robots.txt](https://github.com/ai-robots-txt/ai.robots.txt), which is bundled with
86
+ the package:
87
+
88
+ ```python
89
+ if request.user_agent.is_ai_bot:
90
+ logger.info("AI bot: %s", request.user_agent.ai_bot) # "GPTBot"
91
+ ```
92
+
93
+ Serve a robots.txt that disallows every AI bot:
94
+
95
+ ```python
96
+ from django.urls import path
97
+
98
+ from django_bots.views import robots_txt
99
+
100
+ urlpatterns = [
101
+ path("robots.txt", robots_txt),
102
+ ]
103
+ ```
104
+
105
+ robots.txt only asks. To refuse AI bots outright, add the blocking middleware near
106
+ the top of `MIDDLEWARE`:
107
+
108
+ ```python
109
+ MIDDLEWARE = [
110
+ "django.middleware.security.SecurityMiddleware",
111
+ "django_bots.middleware.AIBotBlockMiddleware",
112
+ ...,
113
+ ]
114
+ ```
115
+
116
+ `BOTS_AI_ALLOW` keeps the bots you want, for both robots.txt and the middleware:
117
+
118
+ ```python
119
+ BOTS_AI_ALLOW = ["OAI-SearchBot"]
120
+ ```
121
+
122
+ A daily workflow checks for new ai.robots.txt releases, and data updates ship as patch
123
+ releases.
124
+
125
+ ## Upgrading from django-user-agents
126
+
127
+ The `request.user_agent` attributes, the `django_bots.utils` helpers and the
128
+ `user_agents` template filters work the same way as in django-user-agents, so
129
+ upgrading comes down to new app, middleware and import paths. One thing behaves
130
+ differently: `is_bot` is also true for crawlers and AI bots. The
131
+ [migration guide](https://djangobots.readthedocs.io/en/stable/migration.html) walks
132
+ through each step.
133
+
134
+ ## Compatibility
135
+
136
+ | Python | Django |
137
+ |---|---|
138
+ | 3.10, 3.11 | 5.2 |
139
+ | 3.12, 3.13, 3.14 | 5.2, 6.0, 6.1 |
140
+
141
+ ## Data sources and licenses
142
+
143
+ django-bots is released under the MIT license. It uses:
144
+
145
+ - [ai.robots.txt](https://github.com/ai-robots-txt/ai.robots.txt) (MIT), bundled in
146
+ `django_bots/data/` with its license
147
+ - [crawler-user-agents](https://github.com/monperrus/crawler-user-agents) (MIT),
148
+ installed as a dependency
149
+ - [ua-parser](https://github.com/ua-parser/uap-python) (Apache-2.0), installed as a
150
+ dependency
151
+ - the device detection rules of
152
+ [user-agents](https://github.com/selwin/python-user-agents) (MIT), ported into
153
+ `django_bots/useragent.py`
154
+
155
+ `django_bots.DATA_VERSIONS` shows which versions are in use.
156
+
157
+ ## Documentation
158
+
159
+ The full documentation is at [djangobots.readthedocs.io](https://djangobots.readthedocs.io/):
160
+
161
+ - [User agents](https://djangobots.readthedocs.io/en/stable/user-agents.html)
162
+ - [Crawler detection](https://djangobots.readthedocs.io/en/stable/crawlers.html)
163
+ - [AI bots](https://djangobots.readthedocs.io/en/stable/ai-bots.html)
164
+ - [Settings](https://djangobots.readthedocs.io/en/stable/settings.html)
165
+ - [Limits](https://djangobots.readthedocs.io/en/stable/limits.html)
166
+ - [Contributing](https://djangobots.readthedocs.io/en/stable/contributing.html)
@@ -0,0 +1,156 @@
1
+ [build-system]
2
+ build-backend = "setuptools.build_meta"
3
+ requires = [ "setuptools>=77" ]
4
+
5
+ [project]
6
+ name = "django-bots"
7
+ version = "1.0.0"
8
+ description = "User-agent parsing, crawler detection and AI bot blocking for Django."
9
+ readme = "README.md"
10
+ keywords = [ "ai", "bots", "crawlers", "django", "robots.txt", "user-agents" ]
11
+ license = "MIT"
12
+ license-files = [ "LICENSE" ]
13
+ authors = [
14
+ { name = "M4p4", email = "homejr@protonmail.com" },
15
+ ]
16
+ requires-python = ">=3.10"
17
+ classifiers = [
18
+ "Development Status :: 5 - Production/Stable",
19
+ "Framework :: Django",
20
+ "Framework :: Django :: 5.2",
21
+ "Framework :: Django :: 6.0",
22
+ "Framework :: Django :: 6.1",
23
+ "Intended Audience :: Developers",
24
+ "Programming Language :: Python :: 3 :: Only",
25
+ "Programming Language :: Python :: 3.10",
26
+ "Programming Language :: Python :: 3.11",
27
+ "Programming Language :: Python :: 3.12",
28
+ "Programming Language :: Python :: 3.13",
29
+ "Programming Language :: Python :: 3.14",
30
+ "Topic :: Internet :: WWW/HTTP",
31
+ "Topic :: Software Development :: Libraries :: Python Modules",
32
+ "Typing :: Typed",
33
+ ]
34
+ dependencies = [
35
+ "crawler-user-agents>=1.62",
36
+ "django>=5.2",
37
+ "ua-parser>=1,<2",
38
+ ]
39
+ urls.Changelog = "https://github.com/M4p4/django-bots/blob/main/CHANGELOG.md"
40
+ urls.Documentation = "https://djangobots.readthedocs.io/"
41
+ urls.Issues = "https://github.com/M4p4/django-bots/issues"
42
+ urls.Repository = "https://github.com/M4p4/django-bots"
43
+
44
+ [dependency-groups]
45
+ test = [
46
+ "coverage[toml]",
47
+ "pytest",
48
+ "pytest-django",
49
+ "pytest-randomly",
50
+ "user-agents==2.2",
51
+ ]
52
+ # Sphinx 9 needs Python 3.12 or newer.
53
+ docs = [
54
+ "furo; python_version>='3.12'",
55
+ "myst-parser; python_version>='3.12'",
56
+ "sphinx; python_version>='3.12'",
57
+ ]
58
+ django52 = [ "django>=5.2,<5.3" ]
59
+ # Django 6.0 and later need Python 3.12 or newer.
60
+ django60 = [ "django>=6,<6.1; python_version>='3.12'" ]
61
+ django61 = [ "django>=6.1,<6.2; python_version>='3.12'" ]
62
+
63
+ [tool.setuptools]
64
+ packages.find.where = [ "src" ]
65
+ package-data.django_bots = [ "data/*", "py.typed", "templates/django_bots/*" ]
66
+
67
+ [tool.uv]
68
+ # uv only auto-installs the `dev` group, so name the groups a bare `uv run`
69
+ # needs. The full version matrix is exercised by tox, not by the default env.
70
+ default-groups = [ "test", "django52" ]
71
+ conflicts = [
72
+ [
73
+ { group = "django52" },
74
+ { group = "django60" },
75
+ { group = "django61" },
76
+ ],
77
+ ]
78
+
79
+ [tool.ruff]
80
+ lint.select = [
81
+ # flake8-bugbear
82
+ "B",
83
+ # flake8-comprehensions
84
+ "C4",
85
+ # pycodestyle
86
+ "E",
87
+ # Pyflakes errors
88
+ "F",
89
+ # isort
90
+ "I",
91
+ # flake8-simplify
92
+ "SIM",
93
+ # flake8-tidy-imports
94
+ "TID",
95
+ # pyupgrade
96
+ "UP",
97
+ # Pyflakes warnings
98
+ "W",
99
+ ]
100
+ lint.ignore = [
101
+ # flake8-bugbear opinionated rules
102
+ "B9",
103
+ # line-too-long
104
+ "E501",
105
+ # suppressible-exception
106
+ "SIM105",
107
+ # if-else-block-instead-of-if-exp
108
+ "SIM108",
109
+ ]
110
+ lint.extend-safe-fixes = [
111
+ # non-pep585-annotation
112
+ "UP006",
113
+ ]
114
+ lint.isort.required-imports = [ "from __future__ import annotations" ]
115
+
116
+ [tool.pyproject-fmt]
117
+ max_supported_python = "3.14"
118
+
119
+ [tool.mypy]
120
+ mypy_path = "src/"
121
+ namespace_packages = false
122
+ warn_unreachable = true
123
+ enable_error_code = [
124
+ "ignore-without-code",
125
+ "redundant-expr",
126
+ "truthy-bool",
127
+ ]
128
+ strict = true
129
+ overrides = [
130
+ { module = "tests.*", allow_untyped_defs = true, allow_untyped_calls = true },
131
+ { module = "crawleruseragents", ignore_missing_imports = true },
132
+ ]
133
+
134
+ [tool.pytest]
135
+ ini_options.testpaths = [ "tests" ]
136
+ ini_options.pythonpath = [ "." ]
137
+ ini_options.addopts = "-ra --strict-config --strict-markers"
138
+ ini_options.django_find_project = false
139
+ ini_options.DJANGO_SETTINGS_MODULE = "tests.settings"
140
+
141
+ [tool.coverage]
142
+ run.branch = true
143
+ run.data_file = ".coverage/cov"
144
+ run.parallel = true
145
+ run.source = [
146
+ "django_bots",
147
+ "tests",
148
+ ]
149
+ paths.source = [
150
+ "src",
151
+ ".tox/**/site-packages",
152
+ ]
153
+ report.fail_under = 100
154
+ report.show_missing = true
155
+ report.skip_covered = true
156
+ report.skip_empty = true
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,15 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from importlib.metadata import version
5
+ from importlib.resources import files
6
+
7
+ __version__ = version("django-bots")
8
+
9
+ DATA_VERSIONS: dict[str, str] = {
10
+ **json.loads(files(__name__).joinpath("data", "VERSIONS.json").read_text("utf-8")),
11
+ "crawler_user_agents": version("crawler-user-agents"),
12
+ "ua_parser": version("ua-parser"),
13
+ "uap_core": version("ua-parser-builtins"),
14
+ }
15
+ """Versions of the bundled and installed data sources."""
@@ -0,0 +1,94 @@
1
+ """Detect AI bots by user-agent string.
2
+
3
+ The bot list is vendored from `ai.robots.txt
4
+ <https://github.com/ai-robots-txt/ai.robots.txt>`_, copyright (c) 2024
5
+ ai.robots.txt, released under the MIT license.
6
+
7
+ Each name matches case-insensitively at word boundaries in the user-agent string,
8
+ after URLs and email addresses are removed from it. Results are cached per string in
9
+ an LRU of ``BOTS_UA_CACHE_SIZE`` entries.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ import re
16
+ from collections.abc import Callable
17
+ from functools import lru_cache
18
+ from importlib.resources import files
19
+ from typing import Any
20
+
21
+ from django_bots.conf import bots_settings
22
+
23
+ __all__ = ["ai_bot_names", "ai_bots", "is_ai_bot", "match_ai_bot", "robots_rules"]
24
+
25
+
26
+ def _read_data(name: str) -> Any:
27
+ return json.loads(files("django_bots").joinpath("data", name).read_text("utf-8"))
28
+
29
+
30
+ @lru_cache(maxsize=1)
31
+ def ai_bots() -> dict[str, dict[str, Any]]:
32
+ """Return the vendored ai.robots.txt entries, keyed by bot name."""
33
+ bots: dict[str, dict[str, Any]] = _read_data("ai_robots.json")
34
+ return bots
35
+
36
+
37
+ def ai_bot_names() -> list[str]:
38
+ """Return the AI bot names in use, after ``BOTS_AI_ALLOW`` and ``BOTS_AI_EXTRA``."""
39
+ allow = {name.lower() for name in bots_settings.AI_ALLOW}
40
+ names = [*ai_bots(), *bots_settings.AI_EXTRA]
41
+ return list(dict.fromkeys(name for name in names if name.lower() not in allow))
42
+
43
+
44
+ # Contact links name the operator, not the bot: "openai.com" would match "OpenAI".
45
+ URL_OR_EMAIL = re.compile(r"(?:https?://|www\.)[^\s;)]+|[\w.+-]+@[\w.-]+")
46
+
47
+
48
+ @lru_cache(maxsize=1)
49
+ def _get_matcher(
50
+ cache_size: int, allow: tuple[str, ...], extra: tuple[str, ...]
51
+ ) -> Callable[[str], str | None]:
52
+ """Build the matcher, rebuilt whenever one of the settings changes."""
53
+ # Upstream lists some names in two spellings, so the first one is reported.
54
+ canonical: dict[str, str] = {}
55
+ for name in ai_bot_names():
56
+ canonical.setdefault(name.lower(), name)
57
+ # Longest first, so a name wins over a shorter name it starts with.
58
+ alternation = "|".join(map(re.escape, sorted(canonical, key=len, reverse=True)))
59
+ # An empty alternation matches everything, so fall back to a never-matching regex.
60
+ regex = re.compile(rf"\b(?:{alternation})\b" if canonical else "(?!)", re.I)
61
+
62
+ def match(ua_string: str) -> str | None:
63
+ found = regex.search(URL_OR_EMAIL.sub(" ", ua_string))
64
+ return canonical[found.group().lower()] if found else None
65
+
66
+ if cache_size > 0:
67
+ return lru_cache(maxsize=cache_size)(match)
68
+ return match
69
+
70
+
71
+ def match_ai_bot(ua_string: str) -> str | None:
72
+ """Return the name of the AI bot the user-agent string matches, or ``None``."""
73
+ matcher = _get_matcher(
74
+ bots_settings.UA_CACHE_SIZE,
75
+ tuple(bots_settings.AI_ALLOW),
76
+ tuple(bots_settings.AI_EXTRA),
77
+ )
78
+ return matcher(ua_string)
79
+
80
+
81
+ def is_ai_bot(ua_string: str) -> bool:
82
+ """Whether the user-agent string matches an AI bot."""
83
+ return match_ai_bot(ua_string) is not None
84
+
85
+
86
+ def robots_rules() -> str:
87
+ """Return a robots.txt group that disallows every AI bot in ``ai_bot_names()``.
88
+
89
+ Returns an empty string when no names are left.
90
+ """
91
+ names = ai_bot_names()
92
+ if not names:
93
+ return ""
94
+ return "\n".join([*(f"User-agent: {name}" for name in names), "Disallow: /"])
@@ -0,0 +1,20 @@
1
+ from __future__ import annotations
2
+
3
+ from django.apps import AppConfig
4
+ from django.core.checks import Tags, register
5
+
6
+ from django_bots.checks import (
7
+ check_ai_block_view,
8
+ check_django_user_agents_installed,
9
+ check_user_agents_cache,
10
+ )
11
+
12
+
13
+ class DjangoBotsConfig(AppConfig):
14
+ name = "django_bots"
15
+ verbose_name = "Bots"
16
+
17
+ def ready(self) -> None:
18
+ register(check_user_agents_cache, Tags.compatibility)
19
+ register(check_django_user_agents_installed, Tags.compatibility)
20
+ register(check_ai_block_view)