snakeboot 0.1.2__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 (37) hide show
  1. snakeboot-0.1.2/LICENSE +21 -0
  2. snakeboot-0.1.2/PKG-INFO +144 -0
  3. snakeboot-0.1.2/README.md +111 -0
  4. snakeboot-0.1.2/pyproject.toml +246 -0
  5. snakeboot-0.1.2/pyproject.toml.orig +194 -0
  6. snakeboot-0.1.2/src/snakeboot/__init__.py +12 -0
  7. snakeboot-0.1.2/src/snakeboot/component_scan/__init__.py +27 -0
  8. snakeboot-0.1.2/src/snakeboot/component_scan/application_root.py +56 -0
  9. snakeboot-0.1.2/src/snakeboot/component_scan/component_scan.py +104 -0
  10. snakeboot-0.1.2/src/snakeboot/django/__init__.py +5 -0
  11. snakeboot-0.1.2/src/snakeboot/django/django_atomic_transaction_manager.py +24 -0
  12. snakeboot-0.1.2/src/snakeboot/events/__init__.py +33 -0
  13. snakeboot-0.1.2/src/snakeboot/events/application_event_listener.py +30 -0
  14. snakeboot-0.1.2/src/snakeboot/events/application_event_publisher.py +80 -0
  15. snakeboot-0.1.2/src/snakeboot/events/application_event_publisher_injectable.py +24 -0
  16. snakeboot-0.1.2/src/snakeboot/events/event_listener_markers.py +41 -0
  17. snakeboot-0.1.2/src/snakeboot/py.typed +0 -0
  18. snakeboot-0.1.2/src/snakeboot/rest/__init__.py +47 -0
  19. snakeboot-0.1.2/src/snakeboot/rest/authenticated_principal.py +15 -0
  20. snakeboot-0.1.2/src/snakeboot/rest/controller_endpoint_factory.py +72 -0
  21. snakeboot-0.1.2/src/snakeboot/rest/error_answer.py +14 -0
  22. snakeboot-0.1.2/src/snakeboot/rest/rest_controller.py +25 -0
  23. snakeboot-0.1.2/src/snakeboot/rest/rest_controller_advice.py +38 -0
  24. snakeboot-0.1.2/src/snakeboot/rest/rest_controller_errors.py +14 -0
  25. snakeboot-0.1.2/src/snakeboot/rest/rest_controller_mappings.py +64 -0
  26. snakeboot-0.1.2/src/snakeboot/rest/rest_controller_mounter.py +121 -0
  27. snakeboot-0.1.2/src/snakeboot/rest/rest_controller_scanner.py +42 -0
  28. snakeboot-0.1.2/src/snakeboot/rest/route_markers.py +45 -0
  29. snakeboot-0.1.2/src/snakeboot/testing/__init__.py +12 -0
  30. snakeboot-0.1.2/src/snakeboot/testing/declared_classes.py +33 -0
  31. snakeboot-0.1.2/src/snakeboot/testing/recording_transaction_manager.py +62 -0
  32. snakeboot-0.1.2/src/snakeboot/transaction/__init__.py +17 -0
  33. snakeboot-0.1.2/src/snakeboot/transaction/after_commit.py +13 -0
  34. snakeboot-0.1.2/src/snakeboot/transaction/transaction_manager.py +25 -0
  35. snakeboot-0.1.2/src/snakeboot/transaction/transaction_manager_not_installed_error.py +6 -0
  36. snakeboot-0.1.2/src/snakeboot/transaction/transaction_manager_registry.py +36 -0
  37. snakeboot-0.1.2/src/snakeboot/transaction/transactional.py +32 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Wouter Deketelaere
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,144 @@
1
+ Metadata-Version: 2.4
2
+ Name: snakeboot
3
+ Version: 0.1.2
4
+ Summary: Spring Boot ergonomics for hexagonal Python: @transactional, application events, a component scan and singleton REST controllers on Django Ninja, wired by wireup.
5
+ Keywords: spring-boot,hexagonal-architecture,ddd,django,django-ninja,wireup,dependency-injection,transactional
6
+ Author: Wouter Deketelaere
7
+ Author-email: Wouter Deketelaere <wouter.deketelaere@kdg.be>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Framework :: Django
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: Education
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
20
+ Classifier: Typing :: Typed
21
+ Requires-Dist: wireup>=2.12.0
22
+ Requires-Dist: django>=6.1.1 ; extra == 'django'
23
+ Requires-Dist: django>=6.1.1 ; extra == 'ninja'
24
+ Requires-Dist: django-ninja>=1.7.1 ; extra == 'ninja'
25
+ Requires-Python: >=3.13
26
+ Project-URL: Homepage, https://github.com/KdG-OCDI/snakeboot
27
+ Project-URL: Repository, https://github.com/KdG-OCDI/snakeboot
28
+ Project-URL: Issues, https://github.com/KdG-OCDI/snakeboot/issues
29
+ Project-URL: Changelog, https://github.com/KdG-OCDI/snakeboot/blob/main/CHANGELOG.md
30
+ Provides-Extra: django
31
+ Provides-Extra: ninja
32
+ Description-Content-Type: text/markdown
33
+
34
+ # snakeboot
35
+
36
+ Spring Boot ergonomics for hexagonal Python applications built with Django, Django Ninja and
37
+ [wireup](https://github.com/maldoinc/wireup).
38
+
39
+ snakeboot imitates the basics of Spring Boot, not Spring itself. Each part is a handful of small
40
+ classes you can read in an afternoon, and none of them knows anything about your domain.
41
+
42
+ | snakeboot | Spring Boot | Needs |
43
+ |---|---|---|
44
+ | `@transactional`, `after_commit` | `@Transactional`, `@TransactionalEventListener(AFTER_COMMIT)` | nothing |
45
+ | `@application_root`, `ComponentScan` | `@SpringBootApplication`, `@ComponentScan` | nothing |
46
+ | `ApplicationEventPublisher`, `@event_listener`, `@transactional_event_listener` | the same names | wireup |
47
+ | `@rest_controller`, `@route.get`, `@rest_controller_advice`, `@exception_handler` | `@RestController`, `@GetMapping`, `@RestControllerAdvice`, `@ExceptionHandler` | `snakeboot[ninja]` |
48
+ | `DjangoAtomicTransactionManager` | `PlatformTransactionManager` for JDBC | `snakeboot[django]` |
49
+ | `RecordingTransactionManager`, `declared_classes` | spring-test | nothing |
50
+
51
+ The transaction, component scan and testing packages import nothing but the standard library,
52
+ so the application layer of a hexagonal application can use `@transactional` without importing Django.
53
+
54
+ ## Install
55
+
56
+ ```bash
57
+ uv add "snakeboot[django,ninja]"
58
+ ```
59
+
60
+ ## A tour
61
+
62
+ **The application root.** The component scan starts in the package of the marked class, like
63
+ `@SpringBootApplication`. It skips Django's entry points (`wsgi.py`, `asgi.py`, migrations) and
64
+ adds the components snakeboot provides itself (the event publisher).
65
+
66
+ ```python
67
+ # myapp/myapp_application.py
68
+ @application_root
69
+ class MyApplication: ...
70
+
71
+
72
+ # settings.py: the setting wireup needs anyway
73
+ WIREUP = WireupSettings(injectables=[*ComponentScan.of(MyApplication).module_names()], auto_inject_views=False)
74
+ ```
75
+
76
+ **Transactions.** A use case declares its boundary; composition chooses the technology.
77
+
78
+ ```python
79
+ @injectable
80
+ class CancelRegistrationUseCase:
81
+ @transactional
82
+ def execute(self, request: CancelRegistrationRequestDTO) -> None:
83
+ workshop = self._workshops.get(...)
84
+ workshop.cancel_registration(...)
85
+ self._workshops.save(workshop)
86
+ self._events.publish_all(workshop.pull_events())
87
+
88
+
89
+ # composition, at start-up
90
+ TransactionManagerRegistry.install(DjangoAtomicTransactionManager())
91
+ ```
92
+
93
+ **Application events.** Listeners react to facts after the commit, so a rolled-back change never
94
+ reaches anyone, and a failing listener cannot undo a committed one.
95
+
96
+ ```python
97
+ @application_event_listener
98
+ class ParticipantNotificationListener:
99
+ def __init__(self, notifications: NotificationPort) -> None:
100
+ self._notifications = notifications
101
+
102
+ @transactional_event_listener(WaitlistedParticipantPromoted)
103
+ def notify_promoted(self, event: WaitlistedParticipantPromoted) -> None: ...
104
+ ```
105
+
106
+ **REST controllers.** Singleton classes with constructor injection, found by the component scan.
107
+ Two endpoints for the same URL, or two answers for one error, stop the start-up.
108
+
109
+ ```python
110
+ @rest_controller("/workshops", tags=["workshops"])
111
+ class WorkshopRestController:
112
+ def __init__(self, enrollment: EnrollmentFacadePort) -> None:
113
+ self._enrollment = enrollment
114
+
115
+ @route.get("/{workshop_id}", response=WorkshopViewDTO)
116
+ def get_workshop(self, request: HttpRequest, workshop_id: str) -> WorkshopViewDTO: ...
117
+
118
+
119
+ @rest_controller_advice
120
+ class EnrollmentRestErrorAdvice:
121
+ @exception_handler(WorkshopNotFoundError, status=404)
122
+ def answer_not_found(self, error: Exception) -> dict[str, str]:
123
+ return error_answer(error) # {"code": "workshop_not_found", "detail": "..."}
124
+
125
+
126
+ # urls.py
127
+ mounted = RestControllerMounter().mount(api, scan=settings.WIREUP.injectables or ())
128
+ ```
129
+
130
+ ## Design rules
131
+
132
+ * Imitate the basics, not all of Spring: no AOP, no profiles, no scheduling, no message brokers.
133
+ * Fail at start-up, not at the first request: a broken controller, listener or root stops the application.
134
+ * Each package knows as little as possible; `uv run lint-imports` enforces it.
135
+
136
+ ## Develop
137
+
138
+ ```bash
139
+ uv sync
140
+ uv run pre-commit install
141
+ uv run pytest # tests, including the import contracts
142
+ uv run ruff check # Clean Code rules
143
+ uv run mypy # strict
144
+ ```
@@ -0,0 +1,111 @@
1
+ # snakeboot
2
+
3
+ Spring Boot ergonomics for hexagonal Python applications built with Django, Django Ninja and
4
+ [wireup](https://github.com/maldoinc/wireup).
5
+
6
+ snakeboot imitates the basics of Spring Boot, not Spring itself. Each part is a handful of small
7
+ classes you can read in an afternoon, and none of them knows anything about your domain.
8
+
9
+ | snakeboot | Spring Boot | Needs |
10
+ |---|---|---|
11
+ | `@transactional`, `after_commit` | `@Transactional`, `@TransactionalEventListener(AFTER_COMMIT)` | nothing |
12
+ | `@application_root`, `ComponentScan` | `@SpringBootApplication`, `@ComponentScan` | nothing |
13
+ | `ApplicationEventPublisher`, `@event_listener`, `@transactional_event_listener` | the same names | wireup |
14
+ | `@rest_controller`, `@route.get`, `@rest_controller_advice`, `@exception_handler` | `@RestController`, `@GetMapping`, `@RestControllerAdvice`, `@ExceptionHandler` | `snakeboot[ninja]` |
15
+ | `DjangoAtomicTransactionManager` | `PlatformTransactionManager` for JDBC | `snakeboot[django]` |
16
+ | `RecordingTransactionManager`, `declared_classes` | spring-test | nothing |
17
+
18
+ The transaction, component scan and testing packages import nothing but the standard library,
19
+ so the application layer of a hexagonal application can use `@transactional` without importing Django.
20
+
21
+ ## Install
22
+
23
+ ```bash
24
+ uv add "snakeboot[django,ninja]"
25
+ ```
26
+
27
+ ## A tour
28
+
29
+ **The application root.** The component scan starts in the package of the marked class, like
30
+ `@SpringBootApplication`. It skips Django's entry points (`wsgi.py`, `asgi.py`, migrations) and
31
+ adds the components snakeboot provides itself (the event publisher).
32
+
33
+ ```python
34
+ # myapp/myapp_application.py
35
+ @application_root
36
+ class MyApplication: ...
37
+
38
+
39
+ # settings.py: the setting wireup needs anyway
40
+ WIREUP = WireupSettings(injectables=[*ComponentScan.of(MyApplication).module_names()], auto_inject_views=False)
41
+ ```
42
+
43
+ **Transactions.** A use case declares its boundary; composition chooses the technology.
44
+
45
+ ```python
46
+ @injectable
47
+ class CancelRegistrationUseCase:
48
+ @transactional
49
+ def execute(self, request: CancelRegistrationRequestDTO) -> None:
50
+ workshop = self._workshops.get(...)
51
+ workshop.cancel_registration(...)
52
+ self._workshops.save(workshop)
53
+ self._events.publish_all(workshop.pull_events())
54
+
55
+
56
+ # composition, at start-up
57
+ TransactionManagerRegistry.install(DjangoAtomicTransactionManager())
58
+ ```
59
+
60
+ **Application events.** Listeners react to facts after the commit, so a rolled-back change never
61
+ reaches anyone, and a failing listener cannot undo a committed one.
62
+
63
+ ```python
64
+ @application_event_listener
65
+ class ParticipantNotificationListener:
66
+ def __init__(self, notifications: NotificationPort) -> None:
67
+ self._notifications = notifications
68
+
69
+ @transactional_event_listener(WaitlistedParticipantPromoted)
70
+ def notify_promoted(self, event: WaitlistedParticipantPromoted) -> None: ...
71
+ ```
72
+
73
+ **REST controllers.** Singleton classes with constructor injection, found by the component scan.
74
+ Two endpoints for the same URL, or two answers for one error, stop the start-up.
75
+
76
+ ```python
77
+ @rest_controller("/workshops", tags=["workshops"])
78
+ class WorkshopRestController:
79
+ def __init__(self, enrollment: EnrollmentFacadePort) -> None:
80
+ self._enrollment = enrollment
81
+
82
+ @route.get("/{workshop_id}", response=WorkshopViewDTO)
83
+ def get_workshop(self, request: HttpRequest, workshop_id: str) -> WorkshopViewDTO: ...
84
+
85
+
86
+ @rest_controller_advice
87
+ class EnrollmentRestErrorAdvice:
88
+ @exception_handler(WorkshopNotFoundError, status=404)
89
+ def answer_not_found(self, error: Exception) -> dict[str, str]:
90
+ return error_answer(error) # {"code": "workshop_not_found", "detail": "..."}
91
+
92
+
93
+ # urls.py
94
+ mounted = RestControllerMounter().mount(api, scan=settings.WIREUP.injectables or ())
95
+ ```
96
+
97
+ ## Design rules
98
+
99
+ * Imitate the basics, not all of Spring: no AOP, no profiles, no scheduling, no message brokers.
100
+ * Fail at start-up, not at the first request: a broken controller, listener or root stops the application.
101
+ * Each package knows as little as possible; `uv run lint-imports` enforces it.
102
+
103
+ ## Develop
104
+
105
+ ```bash
106
+ uv sync
107
+ uv run pre-commit install
108
+ uv run pytest # tests, including the import contracts
109
+ uv run ruff check # Clean Code rules
110
+ uv run mypy # strict
111
+ ```
@@ -0,0 +1,246 @@
1
+ [project]
2
+ name = "snakeboot"
3
+ version = "0.1.2"
4
+ description = "Spring Boot ergonomics for hexagonal Python: @transactional, application events, a component scan and singleton REST controllers on Django Ninja, wired by wireup."
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ requires-python = ">=3.13"
9
+ dependencies = ["wireup>=2.12.0"]
10
+ keywords = [
11
+ "spring-boot",
12
+ "hexagonal-architecture",
13
+ "ddd",
14
+ "django",
15
+ "django-ninja",
16
+ "wireup",
17
+ "dependency-injection",
18
+ "transactional",
19
+ ]
20
+ classifiers = [
21
+ "Development Status :: 3 - Alpha",
22
+ "Framework :: Django",
23
+ "Intended Audience :: Developers",
24
+ "Intended Audience :: Education",
25
+ "Operating System :: OS Independent",
26
+ "Programming Language :: Python :: 3",
27
+ "Programming Language :: Python :: 3 :: Only",
28
+ "Programming Language :: Python :: 3.13",
29
+ "Programming Language :: Python :: 3.14",
30
+ "Topic :: Software Development :: Libraries :: Application Frameworks",
31
+ "Typing :: Typed",
32
+ ]
33
+
34
+ [[project.authors]]
35
+ name = "Wouter Deketelaere"
36
+ email = "wouter.deketelaere@kdg.be"
37
+
38
+ [project.urls]
39
+ Homepage = "https://github.com/KdG-OCDI/snakeboot"
40
+ Repository = "https://github.com/KdG-OCDI/snakeboot"
41
+ Issues = "https://github.com/KdG-OCDI/snakeboot/issues"
42
+ Changelog = "https://github.com/KdG-OCDI/snakeboot/blob/main/CHANGELOG.md"
43
+
44
+ [project.optional-dependencies]
45
+ django = ["django>=6.1.1"]
46
+ ninja = [
47
+ "django>=6.1.1",
48
+ "django-ninja>=1.7.1",
49
+ ]
50
+
51
+ [build-system]
52
+ requires = ["uv_build>=0.12.19,<0.13.0"]
53
+ build-backend = "uv_build"
54
+
55
+ [dependency-groups]
56
+ dev = [
57
+ "django>=6.1.1",
58
+ "django-ninja>=1.7.1",
59
+ "django-stubs>=6.1.2",
60
+ "import-linter>=2.15",
61
+ "mypy>=2.4.0",
62
+ "pre-commit>=4.7.0",
63
+ "pytest>=9.1.1",
64
+ "pytest-django>=4.14.0",
65
+ "ruff>=0.17.1",
66
+ ]
67
+
68
+ [tool.pytest.ini_options]
69
+ testpaths = ["tests"]
70
+ addopts = "-ra --strict-markers"
71
+ DJANGO_SETTINGS_MODULE = "tests.django_settings"
72
+ pythonpath = ["."]
73
+ django_find_project = false
74
+
75
+ [tool.importlinter]
76
+ root_package = "snakeboot"
77
+ include_external_packages = true
78
+
79
+ [[tool.importlinter.contracts]]
80
+ name = "transaction knows no framework and no other part of snakeboot"
81
+ type = "forbidden"
82
+ source_modules = ["snakeboot.transaction"]
83
+ forbidden_modules = [
84
+ "django",
85
+ "ninja",
86
+ "wireup",
87
+ "snakeboot.component_scan",
88
+ "snakeboot.events",
89
+ "snakeboot.rest",
90
+ "snakeboot.django",
91
+ "snakeboot.testing",
92
+ ]
93
+
94
+ [[tool.importlinter.contracts]]
95
+ name = "component_scan is standard library only, so settings can use it before Django starts"
96
+ type = "forbidden"
97
+ source_modules = ["snakeboot.component_scan"]
98
+ forbidden_modules = [
99
+ "django",
100
+ "ninja",
101
+ "wireup",
102
+ "snakeboot.transaction",
103
+ "snakeboot.events",
104
+ "snakeboot.rest",
105
+ "snakeboot.django",
106
+ "snakeboot.testing",
107
+ ]
108
+
109
+ [[tool.importlinter.contracts]]
110
+ name = "events knows wireup and transaction, but no web or ORM framework"
111
+ type = "forbidden"
112
+ source_modules = ["snakeboot.events"]
113
+ forbidden_modules = [
114
+ "django",
115
+ "ninja",
116
+ "pydantic",
117
+ "snakeboot.component_scan",
118
+ "snakeboot.rest",
119
+ "snakeboot.django",
120
+ "snakeboot.testing",
121
+ ]
122
+
123
+ [[tool.importlinter.contracts]]
124
+ name = "rest knows Ninja and wireup, but not the transaction and event libraries"
125
+ type = "forbidden"
126
+ source_modules = ["snakeboot.rest"]
127
+ forbidden_modules = [
128
+ "snakeboot.transaction",
129
+ "snakeboot.events",
130
+ "snakeboot.component_scan",
131
+ "snakeboot.django",
132
+ "snakeboot.testing",
133
+ ]
134
+
135
+ [[tool.importlinter.contracts]]
136
+ name = "django adapts transaction to Django, nothing else"
137
+ type = "forbidden"
138
+ source_modules = ["snakeboot.django"]
139
+ forbidden_modules = [
140
+ "ninja",
141
+ "wireup",
142
+ "snakeboot.component_scan",
143
+ "snakeboot.events",
144
+ "snakeboot.rest",
145
+ "snakeboot.testing",
146
+ ]
147
+
148
+ [[tool.importlinter.contracts]]
149
+ name = "testing helpers run without a framework"
150
+ type = "forbidden"
151
+ source_modules = ["snakeboot.testing"]
152
+ forbidden_modules = [
153
+ "django",
154
+ "ninja",
155
+ "wireup",
156
+ "snakeboot.rest",
157
+ "snakeboot.events",
158
+ "snakeboot.django",
159
+ ]
160
+
161
+ [tool.ruff]
162
+ line-length = 120
163
+ target-version = "py313"
164
+ src = [
165
+ "src",
166
+ "tests",
167
+ ]
168
+
169
+ [tool.ruff.lint]
170
+ select = [
171
+ "E",
172
+ "W",
173
+ "F",
174
+ "I",
175
+ "N",
176
+ "UP",
177
+ "B",
178
+ "A",
179
+ "C4",
180
+ "SIM",
181
+ "RET",
182
+ "PIE",
183
+ "PERF",
184
+ "C90",
185
+ "PL",
186
+ "FBT",
187
+ "ARG",
188
+ "ERA",
189
+ "T20",
190
+ "BLE",
191
+ "DJ",
192
+ "PT",
193
+ "D101",
194
+ "D102",
195
+ "D103",
196
+ "D106",
197
+ "RUF",
198
+ ]
199
+
200
+ [tool.ruff.lint.mccabe]
201
+ max-complexity = 8
202
+
203
+ [tool.ruff.lint.pylint]
204
+ max-args = 5
205
+ max-positional-args = 3
206
+ max-branches = 8
207
+ max-returns = 4
208
+ max-statements = 25
209
+
210
+ [tool.ruff.lint.pydocstyle]
211
+ ignore-decorators = ["typing.override"]
212
+
213
+ [tool.ruff.lint.per-file-ignores]
214
+ "tests/**" = [
215
+ "D",
216
+ "PLR2004",
217
+ "PLR0913",
218
+ "PLR0917",
219
+ "ARG",
220
+ "FBT",
221
+ ]
222
+ "**/models.py" = [
223
+ "D106",
224
+ "RUF012",
225
+ ]
226
+
227
+ [tool.mypy]
228
+ python_version = "3.13"
229
+ strict = true
230
+ files = [
231
+ "src",
232
+ "tests",
233
+ ]
234
+ mypy_path = "src"
235
+ explicit_package_bases = true
236
+ enable_error_code = [
237
+ "explicit-override",
238
+ "ignore-without-code",
239
+ "redundant-expr",
240
+ "truthy-bool",
241
+ "possibly-undefined",
242
+ ]
243
+ plugins = ["mypy_django_plugin.main"]
244
+
245
+ [tool.django-stubs]
246
+ django_settings_module = "tests.django_settings"