reactpy_django 5.0.0__py3-none-any.whl

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 (118) hide show
  1. js/node_modules/@pyscript/core/src/stdlib/pyscript/__init__.py +61 -0
  2. js/node_modules/@pyscript/core/src/stdlib/pyscript/display.py +177 -0
  3. js/node_modules/@pyscript/core/src/stdlib/pyscript/event_handling.py +76 -0
  4. js/node_modules/@pyscript/core/src/stdlib/pyscript/fetch.py +87 -0
  5. js/node_modules/@pyscript/core/src/stdlib/pyscript/ffi.py +18 -0
  6. js/node_modules/@pyscript/core/src/stdlib/pyscript/flatted.py +148 -0
  7. js/node_modules/@pyscript/core/src/stdlib/pyscript/magic_js.py +79 -0
  8. js/node_modules/@pyscript/core/src/stdlib/pyscript/storage.py +60 -0
  9. js/node_modules/@pyscript/core/src/stdlib/pyscript/util.py +33 -0
  10. js/node_modules/@pyscript/core/src/stdlib/pyscript/web.py +1176 -0
  11. js/node_modules/@pyscript/core/src/stdlib/pyscript/websocket.py +71 -0
  12. js/node_modules/@pyscript/core/src/stdlib/pyscript/workers.py +43 -0
  13. js/node_modules/flatted/python/flatted.py +149 -0
  14. js/node_modules/flatted/python/test.py +63 -0
  15. js/node_modules/polyscript/python/mip.py +288 -0
  16. reactpy_django/__init__.py +32 -0
  17. reactpy_django/apps.py +11 -0
  18. reactpy_django/checks.py +558 -0
  19. reactpy_django/clean.py +141 -0
  20. reactpy_django/components.py +284 -0
  21. reactpy_django/config.py +128 -0
  22. reactpy_django/database.py +31 -0
  23. reactpy_django/decorators.py +65 -0
  24. reactpy_django/exceptions.py +34 -0
  25. reactpy_django/hooks.py +469 -0
  26. reactpy_django/html.py +3 -0
  27. reactpy_django/http/__init__.py +0 -0
  28. reactpy_django/http/urls.py +18 -0
  29. reactpy_django/http/views.py +62 -0
  30. reactpy_django/management/__init__.py +0 -0
  31. reactpy_django/management/commands/__init__.py +0 -0
  32. reactpy_django/management/commands/clean_reactpy.py +37 -0
  33. reactpy_django/migrations/0001_initial.py +25 -0
  34. reactpy_django/migrations/0002_rename_created_at_componentparams_last_accessed.py +17 -0
  35. reactpy_django/migrations/0003_componentsession_delete_componentparams.py +28 -0
  36. reactpy_django/migrations/0004_config.py +27 -0
  37. reactpy_django/migrations/0005_alter_componentsession_last_accessed.py +17 -0
  38. reactpy_django/migrations/0006_userdatamodel.py +28 -0
  39. reactpy_django/migrations/__init__.py +0 -0
  40. reactpy_django/models.py +48 -0
  41. reactpy_django/py.typed +1 -0
  42. reactpy_django/pyscript/__init__.py +0 -0
  43. reactpy_django/pyscript/component_template.py +26 -0
  44. reactpy_django/pyscript/layout_handler.py +139 -0
  45. reactpy_django/router/__init__.py +5 -0
  46. reactpy_django/router/converters.py +8 -0
  47. reactpy_django/router/resolvers.py +22 -0
  48. reactpy_django/static/reactpy_django/client.js +1 -0
  49. reactpy_django/static/reactpy_django/morphdom/morphdom-esm.js +769 -0
  50. reactpy_django/static/reactpy_django/morphdom/morphdom-factory.js +705 -0
  51. reactpy_django/static/reactpy_django/morphdom/morphdom-umd.js +777 -0
  52. reactpy_django/static/reactpy_django/morphdom/morphdom-umd.min.js +1 -0
  53. reactpy_django/static/reactpy_django/morphdom/morphdom.js +771 -0
  54. reactpy_django/static/reactpy_django/pyscript/codemirror-BEtcgaoQ.js +2 -0
  55. reactpy_django/static/reactpy_django/pyscript/codemirror-BEtcgaoQ.js.map +1 -0
  56. reactpy_django/static/reactpy_django/pyscript/codemirror_commands-DDxffOmd.js +2 -0
  57. reactpy_django/static/reactpy_django/pyscript/codemirror_commands-DDxffOmd.js.map +1 -0
  58. reactpy_django/static/reactpy_django/pyscript/codemirror_lang-python-CnWnFqxD.js +2 -0
  59. reactpy_django/static/reactpy_django/pyscript/codemirror_lang-python-CnWnFqxD.js.map +1 -0
  60. reactpy_django/static/reactpy_django/pyscript/codemirror_language-CjmvX4ix.js +2 -0
  61. reactpy_django/static/reactpy_django/pyscript/codemirror_language-CjmvX4ix.js.map +1 -0
  62. reactpy_django/static/reactpy_django/pyscript/codemirror_state-D1qTXrff.js +2 -0
  63. reactpy_django/static/reactpy_django/pyscript/codemirror_state-D1qTXrff.js.map +1 -0
  64. reactpy_django/static/reactpy_django/pyscript/codemirror_view-DVb8uYMr.js +2 -0
  65. reactpy_django/static/reactpy_django/pyscript/codemirror_view-DVb8uYMr.js.map +1 -0
  66. reactpy_django/static/reactpy_django/pyscript/core-CjO3FOKB.js +2 -0
  67. reactpy_django/static/reactpy_django/pyscript/core-CjO3FOKB.js.map +1 -0
  68. reactpy_django/static/reactpy_django/pyscript/core.css +1 -0
  69. reactpy_django/static/reactpy_django/pyscript/core.js +2 -0
  70. reactpy_django/static/reactpy_django/pyscript/core.js.map +1 -0
  71. reactpy_django/static/reactpy_django/pyscript/deprecations-manager-pFtn19mE.js +2 -0
  72. reactpy_django/static/reactpy_django/pyscript/deprecations-manager-pFtn19mE.js.map +1 -0
  73. reactpy_django/static/reactpy_django/pyscript/error-tq-z48YI.js +2 -0
  74. reactpy_django/static/reactpy_django/pyscript/error-tq-z48YI.js.map +1 -0
  75. reactpy_django/static/reactpy_django/pyscript/index-S1Do43bx.js +2 -0
  76. reactpy_django/static/reactpy_django/pyscript/index-S1Do43bx.js.map +1 -0
  77. reactpy_django/static/reactpy_django/pyscript/mpy-DovD7Qjy.js +2 -0
  78. reactpy_django/static/reactpy_django/pyscript/mpy-DovD7Qjy.js.map +1 -0
  79. reactpy_django/static/reactpy_django/pyscript/py-BUsUWVJg.js +2 -0
  80. reactpy_django/static/reactpy_django/pyscript/py-BUsUWVJg.js.map +1 -0
  81. reactpy_django/static/reactpy_django/pyscript/py-editor-CeySmmer.js +2 -0
  82. reactpy_django/static/reactpy_django/pyscript/py-editor-CeySmmer.js.map +1 -0
  83. reactpy_django/static/reactpy_django/pyscript/py-terminal-CH_wV7wQ.js +2 -0
  84. reactpy_django/static/reactpy_django/pyscript/py-terminal-CH_wV7wQ.js.map +1 -0
  85. reactpy_django/static/reactpy_django/pyscript/storage.js +2 -0
  86. reactpy_django/static/reactpy_django/pyscript/storage.js.map +1 -0
  87. reactpy_django/static/reactpy_django/pyscript/toml-CvAfdf9_.js +3 -0
  88. reactpy_django/static/reactpy_django/pyscript/toml-CvAfdf9_.js.map +1 -0
  89. reactpy_django/static/reactpy_django/pyscript/toml-DiUM0_qs.js +3 -0
  90. reactpy_django/static/reactpy_django/pyscript/toml-DiUM0_qs.js.map +1 -0
  91. reactpy_django/static/reactpy_django/pyscript/xterm-BY7uk_OU.js +2 -0
  92. reactpy_django/static/reactpy_django/pyscript/xterm-BY7uk_OU.js.map +1 -0
  93. reactpy_django/static/reactpy_django/pyscript/xterm-readline-CZfBw7ic.js +2 -0
  94. reactpy_django/static/reactpy_django/pyscript/xterm-readline-CZfBw7ic.js.map +1 -0
  95. reactpy_django/static/reactpy_django/pyscript/xterm.css +7 -0
  96. reactpy_django/static/reactpy_django/pyscript/xterm_addon-fit--gyF3PcZ.js +2 -0
  97. reactpy_django/static/reactpy_django/pyscript/xterm_addon-fit--gyF3PcZ.js.map +1 -0
  98. reactpy_django/static/reactpy_django/pyscript/xterm_addon-web-links-Cnej-nJ6.js +2 -0
  99. reactpy_django/static/reactpy_django/pyscript/xterm_addon-web-links-Cnej-nJ6.js.map +1 -0
  100. reactpy_django/static/reactpy_django/pyscript/zip-DrwYHuF9.js +2 -0
  101. reactpy_django/static/reactpy_django/pyscript/zip-DrwYHuF9.js.map +1 -0
  102. reactpy_django/static/reactpy_django/pyscript-custom.css +3 -0
  103. reactpy_django/static/reactpy_django/pyscript-hide-debug.css +3 -0
  104. reactpy_django/templates/reactpy/component.html +27 -0
  105. reactpy_django/templates/reactpy/pyscript_component.html +2 -0
  106. reactpy_django/templates/reactpy/pyscript_setup.html +8 -0
  107. reactpy_django/templatetags/__init__.py +0 -0
  108. reactpy_django/templatetags/reactpy.py +250 -0
  109. reactpy_django/types.py +81 -0
  110. reactpy_django/utils.py +560 -0
  111. reactpy_django/websocket/__init__.py +0 -0
  112. reactpy_django/websocket/consumer.py +227 -0
  113. reactpy_django/websocket/paths.py +15 -0
  114. reactpy_django-5.0.0.dist-info/LICENSE.md +9 -0
  115. reactpy_django-5.0.0.dist-info/METADATA +152 -0
  116. reactpy_django-5.0.0.dist-info/RECORD +118 -0
  117. reactpy_django-5.0.0.dist-info/WHEEL +5 -0
  118. reactpy_django-5.0.0.dist-info/top_level.txt +2 -0
@@ -0,0 +1,558 @@
1
+ import contextlib
2
+ import math
3
+ import sys
4
+
5
+ from django.contrib.staticfiles.finders import find
6
+ from django.core.checks import Error, Tags, Warning, register
7
+ from django.template import loader
8
+ from django.urls import NoReverseMatch
9
+
10
+
11
+ @register(Tags.compatibility)
12
+ def reactpy_warnings(app_configs, **kwargs):
13
+ from django.conf import settings
14
+ from django.urls import reverse
15
+
16
+ from reactpy_django import config
17
+ from reactpy_django.config import REACTPY_FAILED_COMPONENTS
18
+
19
+ warnings = []
20
+ INSTALLED_APPS: list[str] = getattr(settings, "INSTALLED_APPS", [])
21
+
22
+ # Check if REACTPY_DATABASE is not an in-memory database.
23
+ if (
24
+ getattr(settings, "DATABASES", {})
25
+ .get(getattr(settings, "REACTPY_DATABASE", "default"), {})
26
+ .get("NAME", None)
27
+ == ":memory:"
28
+ ):
29
+ warnings.append(
30
+ Warning(
31
+ "Using ReactPy with an in-memory database can cause unexpected "
32
+ "behaviors.",
33
+ hint="Configure settings.py:DATABASES[REACTPY_DATABASE], to use a "
34
+ "multiprocessing and thread safe database.",
35
+ id="reactpy_django.W001",
36
+ )
37
+ )
38
+
39
+ # Check if ReactPy URLs are reachable
40
+ try:
41
+ reverse("reactpy:web_modules", kwargs={"file": "example"})
42
+ reverse("reactpy:view_to_iframe", kwargs={"dotted_path": "example"})
43
+ except Exception:
44
+ warnings.append(
45
+ Warning(
46
+ "ReactPy URLs have not been registered.",
47
+ hint="""Add 'path("reactpy/", include("reactpy_django.http.urls"))' """
48
+ "to your application's urlpatterns. If this application does not need "
49
+ "to render ReactPy components, you add this warning to SILENCED_SYSTEM_CHECKS.",
50
+ id="reactpy_django.W002",
51
+ )
52
+ )
53
+
54
+ # Warn if REACTPY_BACKHAUL_THREAD is set to True with Daphne
55
+ if (
56
+ sys.argv[0].endswith("daphne")
57
+ or ("runserver" in sys.argv and "daphne" in INSTALLED_APPS)
58
+ ) and getattr(settings, "REACTPY_BACKHAUL_THREAD", False):
59
+ warnings.append(
60
+ Warning(
61
+ "Unstable configuration detected. REACTPY_BACKHAUL_THREAD is enabled "
62
+ "and you running with Daphne.",
63
+ hint="Set settings.py:REACTPY_BACKHAUL_THREAD to False or use a different web server.",
64
+ id="reactpy_django.W003",
65
+ )
66
+ )
67
+
68
+ # Check if reactpy_django/client.js is available
69
+ if not find("reactpy_django/client.js"):
70
+ warnings.append(
71
+ Warning(
72
+ "ReactPy client.js could not be found within Django static files!",
73
+ hint="Check all static files related Django settings and INSTALLED_APPS.",
74
+ id="reactpy_django.W004",
75
+ )
76
+ )
77
+
78
+ # Check if any components failed to be registered
79
+ if REACTPY_FAILED_COMPONENTS:
80
+ warnings.append(
81
+ Warning(
82
+ "ReactPy failed to register the following components:\n\t+ "
83
+ + "\n\t+ ".join(REACTPY_FAILED_COMPONENTS),
84
+ hint="Check if these paths are valid, or if an exception is being "
85
+ "raised during import.",
86
+ id="reactpy_django.W005",
87
+ )
88
+ )
89
+
90
+ # Check if the reactpy/component.html template exists
91
+ try:
92
+ loader.get_template("reactpy/component.html")
93
+ except Exception:
94
+ warnings.append(
95
+ Warning(
96
+ "ReactPy HTML templates could not be found!",
97
+ hint="Check your settings.py:TEMPLATES configuration and make sure "
98
+ "ReactPy-Django is installed properly.",
99
+ id="reactpy_django.W006",
100
+ )
101
+ )
102
+
103
+ # DELETED W007: Check if REACTPY_WEBSOCKET_URL doesn't end with a slash
104
+ # DELETED W008: Check if REACTPY_WEBSOCKET_URL doesn't start with an alphanumeric character
105
+ # DELETED W009: Check if deprecated value REACTPY_WEBSOCKET_URL exists
106
+
107
+ # Check if REACTPY_URL_PREFIX is being used properly in our HTTP URLs
108
+ with contextlib.suppress(NoReverseMatch):
109
+ full_path = reverse("reactpy:web_modules", kwargs={"file": "example"}).strip(
110
+ "/"
111
+ )
112
+ reactpy_http_prefix = f'{full_path[: full_path.find("web_module/")].strip("/")}'
113
+ if reactpy_http_prefix != config.REACTPY_URL_PREFIX:
114
+ warnings.append(
115
+ Warning(
116
+ "HTTP paths are not prefixed with REACTPY_URL_PREFIX. "
117
+ "Some ReactPy features may not work as expected.",
118
+ hint="Use one of the following solutions.\n"
119
+ "\t1) Utilize REACTPY_URL_PREFIX within your urls.py:\n"
120
+ f'\t path("{config.REACTPY_URL_PREFIX}/", include("reactpy_django.http.urls"))\n'
121
+ "\t2) Modify settings.py:REACTPY_URL_PREFIX to match your existing HTTP path:\n"
122
+ f'\t REACTPY_URL_PREFIX = "{reactpy_http_prefix}/"\n'
123
+ "\t3) If you not rendering components by this ASGI application, then remove "
124
+ "ReactPy HTTP and websocket routing. This is common for configurations that "
125
+ "rely entirely on `host` configuration in your template tag.",
126
+ id="reactpy_django.W010",
127
+ )
128
+ )
129
+
130
+ # Check if REACTPY_URL_PREFIX is empty
131
+ if not getattr(settings, "REACTPY_URL_PREFIX", "reactpy/"):
132
+ warnings.append(
133
+ Warning(
134
+ "REACTPY_URL_PREFIX should not be empty!",
135
+ hint="Change your REACTPY_URL_PREFIX to be written in the following format: '/example_url/'",
136
+ id="reactpy_django.W011",
137
+ )
138
+ )
139
+
140
+ # Check if `daphne` is not in installed apps when using `runserver`
141
+ if "runserver" in sys.argv and "daphne" not in getattr(
142
+ settings, "INSTALLED_APPS", []
143
+ ):
144
+ warnings.append(
145
+ Warning(
146
+ "You have not configured the `runserver` command to use ASGI. "
147
+ "ReactPy will work properly in this configuration.",
148
+ hint="Add daphne to settings.py:INSTALLED_APPS.",
149
+ id="reactpy_django.W012",
150
+ )
151
+ )
152
+
153
+ # DELETED W013: Check if deprecated value REACTPY_RECONNECT_MAX exists
154
+
155
+ # Check if REACTPY_RECONNECT_INTERVAL is set to a large value
156
+ if (
157
+ isinstance(config.REACTPY_RECONNECT_INTERVAL, int)
158
+ and config.REACTPY_RECONNECT_INTERVAL > 30000
159
+ ):
160
+ warnings.append(
161
+ Warning(
162
+ "REACTPY_RECONNECT_INTERVAL is set to >30 seconds. Are you sure this is intentional? "
163
+ "This may cause unexpected delays between reconnection.",
164
+ hint="Check your value for REACTPY_RECONNECT_INTERVAL or suppress this warning.",
165
+ id="reactpy_django.W014",
166
+ )
167
+ )
168
+
169
+ # Check if REACTPY_RECONNECT_MAX_RETRIES is set to a large value
170
+ if (
171
+ isinstance(config.REACTPY_RECONNECT_MAX_RETRIES, int)
172
+ and config.REACTPY_RECONNECT_MAX_RETRIES > 5000
173
+ ):
174
+ warnings.append(
175
+ Warning(
176
+ "REACTPY_RECONNECT_MAX_RETRIES is set to a very large value "
177
+ f"{config.REACTPY_RECONNECT_MAX_RETRIES}. Are you sure this is intentional? "
178
+ "This may leave your clients attempting reconnections for a long time.",
179
+ hint="Check your value for REACTPY_RECONNECT_MAX_RETRIES or suppress this warning.",
180
+ id="reactpy_django.W015",
181
+ )
182
+ )
183
+
184
+ # Check if the REACTPY_RECONNECT_BACKOFF_MULTIPLIER is set to a large value
185
+ if (
186
+ isinstance(config.REACTPY_RECONNECT_BACKOFF_MULTIPLIER, (int, float))
187
+ and config.REACTPY_RECONNECT_BACKOFF_MULTIPLIER > 100
188
+ ):
189
+ warnings.append(
190
+ Warning(
191
+ "REACTPY_RECONNECT_BACKOFF_MULTIPLIER is set to a very large value. Are you sure this is intentional?",
192
+ hint="Check your value for REACTPY_RECONNECT_BACKOFF_MULTIPLIER or suppress this warning.",
193
+ id="reactpy_django.W016",
194
+ )
195
+ )
196
+
197
+ # Check if REACTPY_RECONNECT_MAX_INTERVAL is reachable
198
+ if (
199
+ isinstance(config.REACTPY_RECONNECT_MAX_INTERVAL, int)
200
+ and isinstance(config.REACTPY_RECONNECT_INTERVAL, int)
201
+ and isinstance(config.REACTPY_RECONNECT_MAX_RETRIES, int)
202
+ and isinstance(config.REACTPY_RECONNECT_BACKOFF_MULTIPLIER, (int, float))
203
+ and config.REACTPY_RECONNECT_INTERVAL > 0
204
+ and config.REACTPY_RECONNECT_MAX_INTERVAL > 0
205
+ and config.REACTPY_RECONNECT_MAX_RETRIES > 0
206
+ and config.REACTPY_RECONNECT_BACKOFF_MULTIPLIER > 1
207
+ and (
208
+ config.REACTPY_RECONNECT_BACKOFF_MULTIPLIER
209
+ ** config.REACTPY_RECONNECT_MAX_RETRIES
210
+ )
211
+ * config.REACTPY_RECONNECT_INTERVAL
212
+ < config.REACTPY_RECONNECT_MAX_INTERVAL
213
+ ):
214
+ max_value = math.floor(
215
+ (
216
+ config.REACTPY_RECONNECT_BACKOFF_MULTIPLIER
217
+ ** config.REACTPY_RECONNECT_MAX_RETRIES
218
+ )
219
+ * config.REACTPY_RECONNECT_INTERVAL
220
+ )
221
+ warnings.append(
222
+ Warning(
223
+ "Your current ReactPy configuration can never reach REACTPY_RECONNECT_MAX_INTERVAL. At most you will reach "
224
+ f"{max_value} miliseconds, which is less than {config.REACTPY_RECONNECT_MAX_INTERVAL} (REACTPY_RECONNECT_MAX_INTERVAL).",
225
+ hint="Check your ReactPy REACTPY_RECONNECT_* settings.",
226
+ id="reactpy_django.W017",
227
+ )
228
+ )
229
+
230
+ # Check if 'reactpy_django' is in the correct position in INSTALLED_APPS
231
+ position_to_beat = 0
232
+ for app in INSTALLED_APPS:
233
+ if app.startswith("django.contrib."):
234
+ position_to_beat = INSTALLED_APPS.index(app)
235
+ if (
236
+ "reactpy_django" in INSTALLED_APPS
237
+ and INSTALLED_APPS.index("reactpy_django") < position_to_beat
238
+ ):
239
+ warnings.append(
240
+ Warning(
241
+ "The position of 'reactpy_django' in INSTALLED_APPS is suspicious.",
242
+ hint="Move 'reactpy_django' below all 'django.contrib.*' apps, or suppress this warning.",
243
+ id="reactpy_django.W018",
244
+ )
245
+ )
246
+
247
+ # Check if REACTPY_CLEAN_SESSION is not a valid property
248
+ if getattr(settings, "REACTPY_CLEAN_SESSION", None):
249
+ warnings.append(
250
+ Warning(
251
+ "REACTPY_CLEAN_SESSION is not a valid property value.",
252
+ hint="Did you mean to use REACTPY_CLEAN_SESSIONS instead?",
253
+ id="reactpy_django.W019",
254
+ )
255
+ )
256
+
257
+ return warnings
258
+
259
+
260
+ @register(Tags.compatibility)
261
+ def reactpy_errors(app_configs, **kwargs):
262
+ from django.conf import settings
263
+
264
+ from reactpy_django import config
265
+
266
+ errors = []
267
+
268
+ # Make sure ASGI is enabled
269
+ if not getattr(settings, "ASGI_APPLICATION", None):
270
+ errors.append(
271
+ Error(
272
+ "ASGI_APPLICATION is not defined, but ReactPy requires ASGI.",
273
+ hint="Add ASGI_APPLICATION to settings.py.",
274
+ id="reactpy_django.E001",
275
+ )
276
+ )
277
+
278
+ # DATABASE_ROUTERS is properly configured when REACTPY_DATABASE is defined
279
+ if getattr(
280
+ settings, "REACTPY_DATABASE", None
281
+ ) and "reactpy_django.database.Router" not in getattr(
282
+ settings, "DATABASE_ROUTERS", []
283
+ ):
284
+ errors.append(
285
+ Error(
286
+ "ReactPy database has been changed but the database router is "
287
+ "not configured.",
288
+ hint="Set settings.py:DATABASE_ROUTERS to "
289
+ "['reactpy_django.database.Router', ...]",
290
+ id="reactpy_django.E002",
291
+ )
292
+ )
293
+
294
+ # Check if REACTPY_URL_PREFIX is a valid data type
295
+ if not isinstance(getattr(settings, "REACTPY_URL_PREFIX", ""), str):
296
+ errors.append(
297
+ Error(
298
+ "Invalid type for REACTPY_URL_PREFIX.",
299
+ hint="REACTPY_URL_PREFIX should be a string.",
300
+ obj=settings.REACTPY_URL_PREFIX,
301
+ id="reactpy_django.E003",
302
+ )
303
+ )
304
+
305
+ # Check if REACTPY_SESSION_MAX_AGE is a valid data type
306
+ if not isinstance(getattr(settings, "REACTPY_SESSION_MAX_AGE", 0), int):
307
+ errors.append(
308
+ Error(
309
+ "Invalid type for REACTPY_SESSION_MAX_AGE.",
310
+ hint="REACTPY_SESSION_MAX_AGE should be an integer.",
311
+ obj=settings.REACTPY_SESSION_MAX_AGE,
312
+ id="reactpy_django.E004",
313
+ )
314
+ )
315
+
316
+ # Check if REACTPY_CACHE is a valid data type
317
+ if not isinstance(getattr(settings, "REACTPY_CACHE", ""), str):
318
+ errors.append(
319
+ Error(
320
+ "Invalid type for REACTPY_CACHE.",
321
+ hint="REACTPY_CACHE should be a string.",
322
+ obj=settings.REACTPY_CACHE,
323
+ id="reactpy_django.E005",
324
+ )
325
+ )
326
+
327
+ # Check if REACTPY_DATABASE is a valid data type
328
+ if not isinstance(getattr(settings, "REACTPY_DATABASE", ""), str):
329
+ errors.append(
330
+ Error(
331
+ "Invalid type for REACTPY_DATABASE.",
332
+ hint="REACTPY_DATABASE should be a string.",
333
+ obj=settings.REACTPY_DATABASE,
334
+ id="reactpy_django.E006",
335
+ )
336
+ )
337
+
338
+ # Check if REACTPY_DEFAULT_QUERY_POSTPROCESSOR is a valid data type
339
+ if not isinstance(
340
+ getattr(settings, "REACTPY_DEFAULT_QUERY_POSTPROCESSOR", ""), (str, type(None))
341
+ ):
342
+ errors.append(
343
+ Error(
344
+ "Invalid type for REACTPY_DEFAULT_QUERY_POSTPROCESSOR.",
345
+ hint="REACTPY_DEFAULT_QUERY_POSTPROCESSOR should be a string or None.",
346
+ obj=settings.REACTPY_DEFAULT_QUERY_POSTPROCESSOR,
347
+ id="reactpy_django.E007",
348
+ )
349
+ )
350
+
351
+ # Check if REACTPY_AUTH_BACKEND is a valid data type
352
+ if not isinstance(getattr(settings, "REACTPY_AUTH_BACKEND", ""), str):
353
+ errors.append(
354
+ Error(
355
+ "Invalid type for REACTPY_AUTH_BACKEND.",
356
+ hint="REACTPY_AUTH_BACKEND should be a string.",
357
+ obj=settings.REACTPY_AUTH_BACKEND,
358
+ id="reactpy_django.E008",
359
+ )
360
+ )
361
+
362
+ # DELETED E009: Check if `channels` is in INSTALLED_APPS
363
+
364
+ # Check if REACTPY_DEFAULT_HOSTS is a valid data type
365
+ if not isinstance(getattr(settings, "REACTPY_DEFAULT_HOSTS", []), list):
366
+ errors.append(
367
+ Error(
368
+ "Invalid type for REACTPY_DEFAULT_HOSTS.",
369
+ hint="REACTPY_DEFAULT_HOSTS should be a list.",
370
+ obj=settings.REACTPY_DEFAULT_HOSTS,
371
+ id="reactpy_django.E010",
372
+ )
373
+ )
374
+
375
+ # Check of all values in the REACTPY_DEFAULT_HOSTS are strings
376
+ if isinstance(getattr(settings, "REACTPY_DEFAULT_HOSTS", None), list):
377
+ for host in settings.REACTPY_DEFAULT_HOSTS:
378
+ if not isinstance(host, str):
379
+ errors.append(
380
+ Error(
381
+ f"Invalid type {type(host)} within REACTPY_DEFAULT_HOSTS.",
382
+ hint="REACTPY_DEFAULT_HOSTS should be a list of strings.",
383
+ obj=settings.REACTPY_DEFAULT_HOSTS,
384
+ id="reactpy_django.E011",
385
+ )
386
+ )
387
+ break
388
+
389
+ # Check if REACTPY_RECONNECT_INTERVAL is a valid data type
390
+ if not isinstance(config.REACTPY_RECONNECT_INTERVAL, int):
391
+ errors.append(
392
+ Error(
393
+ "Invalid type for REACTPY_RECONNECT_INTERVAL.",
394
+ hint="REACTPY_RECONNECT_INTERVAL should be an integer.",
395
+ id="reactpy_django.E012",
396
+ )
397
+ )
398
+
399
+ # Check if REACTPY_RECONNECT_INTERVAL is a positive integer
400
+ if (
401
+ isinstance(config.REACTPY_RECONNECT_INTERVAL, int)
402
+ and config.REACTPY_RECONNECT_INTERVAL < 0
403
+ ):
404
+ errors.append(
405
+ Error(
406
+ "Invalid value for REACTPY_RECONNECT_INTERVAL.",
407
+ hint="REACTPY_RECONNECT_INTERVAL should be a positive integer.",
408
+ id="reactpy_django.E013",
409
+ )
410
+ )
411
+
412
+ # Check if REACTPY_RECONNECT_MAX_INTERVAL is a valid data type
413
+ if not isinstance(config.REACTPY_RECONNECT_MAX_INTERVAL, int):
414
+ errors.append(
415
+ Error(
416
+ "Invalid type for REACTPY_RECONNECT_MAX_INTERVAL.",
417
+ hint="REACTPY_RECONNECT_MAX_INTERVAL should be an integer.",
418
+ id="reactpy_django.E014",
419
+ )
420
+ )
421
+
422
+ # Check if REACTPY_RECONNECT_MAX_INTERVAL is a positive integer
423
+ if (
424
+ isinstance(config.REACTPY_RECONNECT_MAX_INTERVAL, int)
425
+ and config.REACTPY_RECONNECT_MAX_INTERVAL < 0
426
+ ):
427
+ errors.append(
428
+ Error(
429
+ "Invalid value for REACTPY_RECONNECT_MAX_INTERVAL.",
430
+ hint="REACTPY_RECONNECT_MAX_INTERVAL should be a positive integer.",
431
+ id="reactpy_django.E015",
432
+ )
433
+ )
434
+
435
+ # Check if REACTPY_RECONNECT_MAX_INTERVAL is greater than REACTPY_RECONNECT_INTERVAL
436
+ if (
437
+ isinstance(config.REACTPY_RECONNECT_MAX_INTERVAL, int)
438
+ and isinstance(config.REACTPY_RECONNECT_INTERVAL, int)
439
+ and config.REACTPY_RECONNECT_MAX_INTERVAL < config.REACTPY_RECONNECT_INTERVAL
440
+ ):
441
+ errors.append(
442
+ Error(
443
+ "REACTPY_RECONNECT_MAX_INTERVAL is less than REACTPY_RECONNECT_INTERVAL.",
444
+ hint="REACTPY_RECONNECT_MAX_INTERVAL should be greater than or equal to REACTPY_RECONNECT_INTERVAL.",
445
+ id="reactpy_django.E016",
446
+ )
447
+ )
448
+
449
+ # Check if REACTPY_RECONNECT_MAX_RETRIES is a valid data type
450
+ if not isinstance(config.REACTPY_RECONNECT_MAX_RETRIES, int):
451
+ errors.append(
452
+ Error(
453
+ "Invalid type for REACTPY_RECONNECT_MAX_RETRIES.",
454
+ hint="REACTPY_RECONNECT_MAX_RETRIES should be an integer.",
455
+ id="reactpy_django.E017",
456
+ )
457
+ )
458
+
459
+ # Check if REACTPY_RECONNECT_MAX_RETRIES is a positive integer
460
+ if (
461
+ isinstance(config.REACTPY_RECONNECT_MAX_RETRIES, int)
462
+ and config.REACTPY_RECONNECT_MAX_RETRIES < 0
463
+ ):
464
+ errors.append(
465
+ Error(
466
+ "Invalid value for REACTPY_RECONNECT_MAX_RETRIES.",
467
+ hint="REACTPY_RECONNECT_MAX_RETRIES should be a positive integer.",
468
+ id="reactpy_django.E018",
469
+ )
470
+ )
471
+
472
+ # Check if REACTPY_RECONNECT_BACKOFF_MULTIPLIER is a valid data type
473
+ if not isinstance(config.REACTPY_RECONNECT_BACKOFF_MULTIPLIER, (int, float)):
474
+ errors.append(
475
+ Error(
476
+ "Invalid type for REACTPY_RECONNECT_BACKOFF_MULTIPLIER.",
477
+ hint="REACTPY_RECONNECT_BACKOFF_MULTIPLIER should be an integer or float.",
478
+ id="reactpy_django.E019",
479
+ )
480
+ )
481
+
482
+ # Check if REACTPY_RECONNECT_BACKOFF_MULTIPLIER is greater than or equal to 1
483
+ if (
484
+ isinstance(config.REACTPY_RECONNECT_BACKOFF_MULTIPLIER, (int, float))
485
+ and config.REACTPY_RECONNECT_BACKOFF_MULTIPLIER < 1
486
+ ):
487
+ errors.append(
488
+ Error(
489
+ "Invalid value for REACTPY_RECONNECT_BACKOFF_MULTIPLIER.",
490
+ hint="REACTPY_RECONNECT_BACKOFF_MULTIPLIER should be greater than or equal to 1.",
491
+ id="reactpy_django.E020",
492
+ )
493
+ )
494
+
495
+ # Check if REACTPY_PRERENDER is a valid data type
496
+ if not isinstance(config.REACTPY_PRERENDER, bool):
497
+ errors.append(
498
+ Error(
499
+ "Invalid type for REACTPY_PRERENDER.",
500
+ hint="REACTPY_PRERENDER should be a boolean.",
501
+ id="reactpy_django.E021",
502
+ )
503
+ )
504
+
505
+ # Check if REACTPY_AUTO_RELOGIN is a valid data type
506
+ if not isinstance(config.REACTPY_AUTO_RELOGIN, bool):
507
+ errors.append(
508
+ Error(
509
+ "Invalid type for REACTPY_AUTO_RELOGIN.",
510
+ hint="REACTPY_AUTO_RELOGIN should be a boolean.",
511
+ id="reactpy_django.E022",
512
+ )
513
+ )
514
+
515
+ # Check if REACTPY_CLEAN_INTERVAL is a valid data type
516
+ if not isinstance(config.REACTPY_CLEAN_INTERVAL, (int, type(None))):
517
+ errors.append(
518
+ Error(
519
+ "Invalid type for REACTPY_CLEAN_INTERVAL.",
520
+ hint="REACTPY_CLEAN_INTERVAL should be an integer or None.",
521
+ id="reactpy_django.E023",
522
+ )
523
+ )
524
+
525
+ # Check if REACTPY_CLEAN_INTERVAL is a positive integer
526
+ if (
527
+ isinstance(config.REACTPY_CLEAN_INTERVAL, int)
528
+ and config.REACTPY_CLEAN_INTERVAL < 0
529
+ ):
530
+ errors.append(
531
+ Error(
532
+ "Invalid value for REACTPY_CLEAN_INTERVAL.",
533
+ hint="REACTPY_CLEAN_INTERVAL should be a positive integer or None.",
534
+ id="reactpy_django.E024",
535
+ )
536
+ )
537
+
538
+ # Check if REACTPY_CLEAN_SESSIONS is a valid data type
539
+ if not isinstance(config.REACTPY_CLEAN_SESSIONS, bool):
540
+ errors.append(
541
+ Error(
542
+ "Invalid type for REACTPY_CLEAN_SESSIONS.",
543
+ hint="REACTPY_CLEAN_SESSIONS should be a boolean.",
544
+ id="reactpy_django.E025",
545
+ )
546
+ )
547
+
548
+ # Check if REACTPY_CLEAN_USER_DATA is a valid data type
549
+ if not isinstance(config.REACTPY_CLEAN_USER_DATA, bool):
550
+ errors.append(
551
+ Error(
552
+ "Invalid type for REACTPY_CLEAN_USER_DATA.",
553
+ hint="REACTPY_CLEAN_USER_DATA should be a boolean.",
554
+ id="reactpy_django.E026",
555
+ )
556
+ )
557
+
558
+ return errors
@@ -0,0 +1,141 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from datetime import datetime, timedelta
5
+ from typing import TYPE_CHECKING, Literal
6
+
7
+ from django.contrib.auth import get_user_model
8
+ from django.utils import timezone
9
+
10
+ _logger = logging.getLogger(__name__)
11
+
12
+ if TYPE_CHECKING:
13
+ from reactpy_django.models import Config
14
+
15
+ CLEAN_NEEDED_BY: datetime = datetime(
16
+ year=1, month=1, day=1, tzinfo=timezone.now().tzinfo
17
+ )
18
+
19
+
20
+ def clean(
21
+ *args: Literal["all", "sessions", "user_data"],
22
+ immediate: bool = False,
23
+ verbosity: int = 1,
24
+ ):
25
+ from reactpy_django.config import (
26
+ REACTPY_CLEAN_SESSIONS,
27
+ REACTPY_CLEAN_USER_DATA,
28
+ )
29
+ from reactpy_django.models import Config
30
+
31
+ config = Config.load()
32
+ if immediate or is_clean_needed(config):
33
+ config.cleaned_at = timezone.now()
34
+ config.save()
35
+ sessions = REACTPY_CLEAN_SESSIONS
36
+ user_data = REACTPY_CLEAN_USER_DATA
37
+
38
+ if args:
39
+ sessions = any(value in args for value in {"sessions", "all"})
40
+ user_data = any(value in args for value in {"user_data", "all"})
41
+
42
+ if sessions:
43
+ clean_sessions(verbosity)
44
+ if user_data:
45
+ clean_user_data(verbosity)
46
+
47
+
48
+ def clean_sessions(verbosity: int = 1):
49
+ """Deletes expired component sessions from the database.
50
+ As a performance optimization, this is only run once every REACTPY_SESSION_MAX_AGE seconds.
51
+ """
52
+ from reactpy_django.config import REACTPY_DEBUG_MODE, REACTPY_SESSION_MAX_AGE
53
+ from reactpy_django.models import ComponentSession
54
+
55
+ if verbosity >= 2:
56
+ print("Cleaning ReactPy component sessions...")
57
+
58
+ start_time = timezone.now()
59
+ expiration_date = timezone.now() - timedelta(seconds=REACTPY_SESSION_MAX_AGE)
60
+ session_objects = ComponentSession.objects.filter(
61
+ last_accessed__lte=expiration_date
62
+ )
63
+
64
+ if verbosity >= 2:
65
+ print(f"Deleting {session_objects.count()} expired component sessions...")
66
+
67
+ session_objects.delete()
68
+
69
+ if REACTPY_DEBUG_MODE or verbosity >= 2:
70
+ inspect_clean_duration(start_time, "component sessions", verbosity)
71
+
72
+
73
+ def clean_user_data(verbosity: int = 1):
74
+ """Delete any user data that is not associated with an existing `User`.
75
+ This is a safety measure to ensure that we don't have any orphaned data in the database.
76
+
77
+ Our `UserDataModel` is supposed to automatically get deleted on every `User` delete signal.
78
+ However, we can't use Django to enforce this relationship since ReactPy can be configured to
79
+ use any database.
80
+ """
81
+ from reactpy_django.config import REACTPY_DEBUG_MODE
82
+ from reactpy_django.models import UserDataModel
83
+
84
+ if verbosity >= 2:
85
+ print("Cleaning ReactPy user data...")
86
+
87
+ start_time = timezone.now()
88
+ user_model = get_user_model()
89
+ all_users = user_model.objects.all()
90
+ all_user_pks = all_users.values_list(user_model._meta.pk.name, flat=True)
91
+
92
+ # Django doesn't support using QuerySets as an argument with cross-database relations.
93
+ if user_model.objects.db != UserDataModel.objects.db:
94
+ all_user_pks = list(all_user_pks) # type: ignore
95
+
96
+ user_data_objects = UserDataModel.objects.exclude(user_pk__in=all_user_pks)
97
+
98
+ if verbosity >= 2:
99
+ print(
100
+ f"Deleting {user_data_objects.count()} user data objects not associated with an existing user..."
101
+ )
102
+
103
+ user_data_objects.delete()
104
+
105
+ if REACTPY_DEBUG_MODE or verbosity >= 2:
106
+ inspect_clean_duration(start_time, "user data", verbosity)
107
+
108
+
109
+ def is_clean_needed(config: Config | None = None) -> bool:
110
+ """Check if a clean is needed. This function avoids unnecessary database reads by caching the
111
+ CLEAN_NEEDED_BY date."""
112
+ from reactpy_django.config import REACTPY_CLEAN_INTERVAL
113
+ from reactpy_django.models import Config
114
+
115
+ global CLEAN_NEEDED_BY
116
+
117
+ if REACTPY_CLEAN_INTERVAL is None:
118
+ return False
119
+
120
+ if timezone.now() >= CLEAN_NEEDED_BY:
121
+ config = config or Config.load()
122
+ CLEAN_NEEDED_BY = config.cleaned_at + timedelta(seconds=REACTPY_CLEAN_INTERVAL)
123
+
124
+ return timezone.now() >= CLEAN_NEEDED_BY
125
+
126
+
127
+ def inspect_clean_duration(start_time: datetime, task_name: str, verbosity: int):
128
+ clean_duration = timezone.now() - start_time
129
+
130
+ if verbosity >= 3:
131
+ print(
132
+ f"Cleaned ReactPy {task_name} in {clean_duration.total_seconds()} seconds."
133
+ )
134
+
135
+ if clean_duration.total_seconds() > 1:
136
+ _logger.warning(
137
+ "ReactPy has taken %s seconds to clean %s. "
138
+ "This may indicate a performance issue with your system, cache, or database.",
139
+ clean_duration.total_seconds(),
140
+ task_name,
141
+ )