reactpy_django 3.8.1__py2.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 (44) hide show
  1. js/node_modules/flatted/python/flatted.py +149 -0
  2. js/node_modules/flatted/python/test.py +63 -0
  3. reactpy_django/__init__.py +28 -0
  4. reactpy_django/apps.py +11 -0
  5. reactpy_django/checks.py +542 -0
  6. reactpy_django/clean.py +141 -0
  7. reactpy_django/components.py +286 -0
  8. reactpy_django/config.py +135 -0
  9. reactpy_django/database.py +31 -0
  10. reactpy_django/decorators.py +101 -0
  11. reactpy_django/exceptions.py +34 -0
  12. reactpy_django/hooks.py +496 -0
  13. reactpy_django/http/__init__.py +0 -0
  14. reactpy_django/http/urls.py +18 -0
  15. reactpy_django/http/views.py +62 -0
  16. reactpy_django/management/__init__.py +0 -0
  17. reactpy_django/management/commands/__init__.py +0 -0
  18. reactpy_django/management/commands/clean_reactpy.py +37 -0
  19. reactpy_django/migrations/0001_initial.py +25 -0
  20. reactpy_django/migrations/0002_rename_created_at_componentparams_last_accessed.py +17 -0
  21. reactpy_django/migrations/0003_componentsession_delete_componentparams.py +28 -0
  22. reactpy_django/migrations/0004_config.py +27 -0
  23. reactpy_django/migrations/0005_alter_componentsession_last_accessed.py +17 -0
  24. reactpy_django/migrations/0006_userdatamodel.py +28 -0
  25. reactpy_django/migrations/__init__.py +0 -0
  26. reactpy_django/models.py +48 -0
  27. reactpy_django/py.typed +1 -0
  28. reactpy_django/router/__init__.py +5 -0
  29. reactpy_django/router/converters.py +7 -0
  30. reactpy_django/router/resolvers.py +58 -0
  31. reactpy_django/static/reactpy_django/client.js +1630 -0
  32. reactpy_django/templates/reactpy/component.html +27 -0
  33. reactpy_django/templatetags/__init__.py +0 -0
  34. reactpy_django/templatetags/reactpy.py +221 -0
  35. reactpy_django/types.py +121 -0
  36. reactpy_django/utils.py +384 -0
  37. reactpy_django/websocket/__init__.py +0 -0
  38. reactpy_django/websocket/consumer.py +218 -0
  39. reactpy_django/websocket/paths.py +17 -0
  40. reactpy_django-3.8.1.dist-info/LICENSE.md +9 -0
  41. reactpy_django-3.8.1.dist-info/METADATA +152 -0
  42. reactpy_django-3.8.1.dist-info/RECORD +44 -0
  43. reactpy_django-3.8.1.dist-info/WHEEL +6 -0
  44. reactpy_django-3.8.1.dist-info/top_level.txt +2 -0
@@ -0,0 +1,542 @@
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
+ # 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
+ # ReactPy URLs exist
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
+
106
+ # Removed REACTPY_WEBSOCKET_URL setting
107
+ if getattr(settings, "REACTPY_WEBSOCKET_URL", None):
108
+ warnings.append(
109
+ Warning(
110
+ "REACTPY_WEBSOCKET_URL has been removed.",
111
+ hint="Use REACTPY_URL_PREFIX instead.",
112
+ id="reactpy_django.W009",
113
+ )
114
+ )
115
+
116
+ # Check if REACTPY_URL_PREFIX is being used properly in our HTTP URLs
117
+ with contextlib.suppress(NoReverseMatch):
118
+ full_path = reverse("reactpy:web_modules", kwargs={"file": "example"}).strip(
119
+ "/"
120
+ )
121
+ reactpy_http_prefix = f'{full_path[: full_path.find("web_module/")].strip("/")}'
122
+ if reactpy_http_prefix != config.REACTPY_URL_PREFIX:
123
+ warnings.append(
124
+ Warning(
125
+ "HTTP paths are not prefixed with REACTPY_URL_PREFIX. "
126
+ "Some ReactPy features may not work as expected.",
127
+ hint="Use one of the following solutions.\n"
128
+ "\t1) Utilize REACTPY_URL_PREFIX within your urls.py:\n"
129
+ f'\t path("{config.REACTPY_URL_PREFIX}/", include("reactpy_django.http.urls"))\n'
130
+ "\t2) Modify settings.py:REACTPY_URL_PREFIX to match your existing HTTP path:\n"
131
+ f'\t REACTPY_URL_PREFIX = "{reactpy_http_prefix}/"\n'
132
+ "\t3) If you not rendering components by this ASGI application, then remove "
133
+ "ReactPy HTTP and websocket routing. This is common for configurations that "
134
+ "rely entirely on `host` configuration in your template tag.",
135
+ id="reactpy_django.W010",
136
+ )
137
+ )
138
+
139
+ # Check if REACTPY_URL_PREFIX is empty
140
+ if not getattr(settings, "REACTPY_URL_PREFIX", "reactpy/"):
141
+ warnings.append(
142
+ Warning(
143
+ "REACTPY_URL_PREFIX should not be empty!",
144
+ hint="Change your REACTPY_URL_PREFIX to be written in the following format: '/example_url/'",
145
+ id="reactpy_django.W011",
146
+ )
147
+ )
148
+
149
+ # Check if `daphne` is not in installed apps when using `runserver`
150
+ if "runserver" in sys.argv and "daphne" not in getattr(
151
+ settings, "INSTALLED_APPS", []
152
+ ):
153
+ warnings.append(
154
+ Warning(
155
+ "You have not configured runserver to use ASGI.",
156
+ hint="Add daphne to settings.py:INSTALLED_APPS.",
157
+ id="reactpy_django.W012",
158
+ )
159
+ )
160
+
161
+ # Removed REACTPY_RECONNECT_MAX setting
162
+ if getattr(settings, "REACTPY_RECONNECT_MAX", None):
163
+ warnings.append(
164
+ Warning(
165
+ "REACTPY_RECONNECT_MAX has been removed.",
166
+ hint="See the docs for the new REACTPY_RECONNECT_* settings.",
167
+ id="reactpy_django.W013",
168
+ )
169
+ )
170
+
171
+ if (
172
+ isinstance(config.REACTPY_RECONNECT_INTERVAL, int)
173
+ and config.REACTPY_RECONNECT_INTERVAL > 30000
174
+ ):
175
+ warnings.append(
176
+ Warning(
177
+ "REACTPY_RECONNECT_INTERVAL is set to >30 seconds. Are you sure this is intentional? "
178
+ "This may cause unexpected delays between reconnection.",
179
+ hint="Check your value for REACTPY_RECONNECT_INTERVAL or suppress this warning.",
180
+ id="reactpy_django.W014",
181
+ )
182
+ )
183
+
184
+ if (
185
+ isinstance(config.REACTPY_RECONNECT_MAX_RETRIES, int)
186
+ and config.REACTPY_RECONNECT_MAX_RETRIES > 5000
187
+ ):
188
+ warnings.append(
189
+ Warning(
190
+ "REACTPY_RECONNECT_MAX_RETRIES is set to a very large value. Are you sure this is intentional? "
191
+ "This may leave your clients attempting reconnections for a long time.",
192
+ hint="Check your value for REACTPY_RECONNECT_MAX_RETRIES or suppress this warning.",
193
+ id="reactpy_django.W015",
194
+ )
195
+ )
196
+
197
+ # Check if the value is too large (greater than 50)
198
+ if (
199
+ isinstance(config.REACTPY_RECONNECT_BACKOFF_MULTIPLIER, (int, float))
200
+ and config.REACTPY_RECONNECT_BACKOFF_MULTIPLIER > 100
201
+ ):
202
+ warnings.append(
203
+ Warning(
204
+ "REACTPY_RECONNECT_BACKOFF_MULTIPLIER is set to a very large value. Are you sure this is intentional?",
205
+ hint="Check your value for REACTPY_RECONNECT_BACKOFF_MULTIPLIER or suppress this warning.",
206
+ id="reactpy_django.W016",
207
+ )
208
+ )
209
+
210
+ if (
211
+ isinstance(config.REACTPY_RECONNECT_MAX_INTERVAL, int)
212
+ and isinstance(config.REACTPY_RECONNECT_INTERVAL, int)
213
+ and isinstance(config.REACTPY_RECONNECT_MAX_RETRIES, int)
214
+ and isinstance(config.REACTPY_RECONNECT_BACKOFF_MULTIPLIER, (int, float))
215
+ and config.REACTPY_RECONNECT_INTERVAL > 0
216
+ and config.REACTPY_RECONNECT_MAX_INTERVAL > 0
217
+ and config.REACTPY_RECONNECT_MAX_RETRIES > 0
218
+ and config.REACTPY_RECONNECT_BACKOFF_MULTIPLIER > 1
219
+ and (
220
+ config.REACTPY_RECONNECT_BACKOFF_MULTIPLIER
221
+ ** config.REACTPY_RECONNECT_MAX_RETRIES
222
+ )
223
+ * config.REACTPY_RECONNECT_INTERVAL
224
+ < config.REACTPY_RECONNECT_MAX_INTERVAL
225
+ ):
226
+ max_value = math.floor(
227
+ (
228
+ config.REACTPY_RECONNECT_BACKOFF_MULTIPLIER
229
+ ** config.REACTPY_RECONNECT_MAX_RETRIES
230
+ )
231
+ * config.REACTPY_RECONNECT_INTERVAL
232
+ )
233
+ warnings.append(
234
+ Warning(
235
+ "Your current ReactPy configuration can never reach REACTPY_RECONNECT_MAX_INTERVAL. At most you will reach "
236
+ f"{max_value} miliseconds, which is less than {config.REACTPY_RECONNECT_MAX_INTERVAL} (REACTPY_RECONNECT_MAX_INTERVAL).",
237
+ hint="Check your ReactPy REACTPY_RECONNECT_* settings.",
238
+ id="reactpy_django.W017",
239
+ )
240
+ )
241
+
242
+ position_to_beat = 0
243
+ for app in INSTALLED_APPS:
244
+ if app.startswith("django.contrib."):
245
+ position_to_beat = INSTALLED_APPS.index(app)
246
+ if (
247
+ "reactpy_django" in INSTALLED_APPS
248
+ and INSTALLED_APPS.index("reactpy_django") < position_to_beat
249
+ ):
250
+ warnings.append(
251
+ Warning(
252
+ "The position of 'reactpy_django' in INSTALLED_APPS is suspicious.",
253
+ hint="Move 'reactpy_django' below all 'django.contrib.*' apps, or suppress this warning.",
254
+ id="reactpy_django.W018",
255
+ )
256
+ )
257
+
258
+ if getattr(settings, "REACTPY_CLEAN_SESSION", None):
259
+ warnings.append(
260
+ Warning(
261
+ "REACTPY_CLEAN_SESSION is not a valid property value.",
262
+ hint="Did you mean to use REACTPY_CLEAN_SESSIONS instead?",
263
+ id="reactpy_django.W019",
264
+ )
265
+ )
266
+
267
+ return warnings
268
+
269
+
270
+ @register(Tags.compatibility)
271
+ def reactpy_errors(app_configs, **kwargs):
272
+ from django.conf import settings
273
+
274
+ from reactpy_django import config
275
+
276
+ errors = []
277
+
278
+ # Make sure ASGI is enabled
279
+ if not getattr(settings, "ASGI_APPLICATION", None):
280
+ errors.append(
281
+ Error(
282
+ "ASGI_APPLICATION is not defined, but ReactPy requires ASGI.",
283
+ hint="Add ASGI_APPLICATION to settings.py.",
284
+ id="reactpy_django.E001",
285
+ )
286
+ )
287
+
288
+ # DATABASE_ROUTERS is properly configured when REACTPY_DATABASE is defined
289
+ if getattr(
290
+ settings, "REACTPY_DATABASE", None
291
+ ) and "reactpy_django.database.Router" not in getattr(
292
+ settings, "DATABASE_ROUTERS", []
293
+ ):
294
+ errors.append(
295
+ Error(
296
+ "ReactPy database has been changed but the database router is "
297
+ "not configured.",
298
+ hint="Set settings.py:DATABASE_ROUTERS to "
299
+ "['reactpy_django.database.Router', ...]",
300
+ id="reactpy_django.E002",
301
+ )
302
+ )
303
+
304
+ # All settings in reactpy_django.conf are the correct data type
305
+ if not isinstance(getattr(settings, "REACTPY_URL_PREFIX", ""), str):
306
+ errors.append(
307
+ Error(
308
+ "Invalid type for REACTPY_URL_PREFIX.",
309
+ hint="REACTPY_URL_PREFIX should be a string.",
310
+ obj=settings.REACTPY_URL_PREFIX,
311
+ id="reactpy_django.E003",
312
+ )
313
+ )
314
+ if not isinstance(getattr(settings, "REACTPY_SESSION_MAX_AGE", 0), int):
315
+ errors.append(
316
+ Error(
317
+ "Invalid type for REACTPY_SESSION_MAX_AGE.",
318
+ hint="REACTPY_SESSION_MAX_AGE should be an integer.",
319
+ obj=settings.REACTPY_SESSION_MAX_AGE,
320
+ id="reactpy_django.E004",
321
+ )
322
+ )
323
+ if not isinstance(getattr(settings, "REACTPY_CACHE", ""), str):
324
+ errors.append(
325
+ Error(
326
+ "Invalid type for REACTPY_CACHE.",
327
+ hint="REACTPY_CACHE should be a string.",
328
+ obj=settings.REACTPY_CACHE,
329
+ id="reactpy_django.E005",
330
+ )
331
+ )
332
+ if not isinstance(getattr(settings, "REACTPY_DATABASE", ""), str):
333
+ errors.append(
334
+ Error(
335
+ "Invalid type for REACTPY_DATABASE.",
336
+ hint="REACTPY_DATABASE should be a string.",
337
+ obj=settings.REACTPY_DATABASE,
338
+ id="reactpy_django.E006",
339
+ )
340
+ )
341
+ if not isinstance(
342
+ getattr(settings, "REACTPY_DEFAULT_QUERY_POSTPROCESSOR", ""), (str, type(None))
343
+ ):
344
+ errors.append(
345
+ Error(
346
+ "Invalid type for REACTPY_DEFAULT_QUERY_POSTPROCESSOR.",
347
+ hint="REACTPY_DEFAULT_QUERY_POSTPROCESSOR should be a string or None.",
348
+ obj=settings.REACTPY_DEFAULT_QUERY_POSTPROCESSOR,
349
+ id="reactpy_django.E007",
350
+ )
351
+ )
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
+ if not isinstance(getattr(settings, "REACTPY_DEFAULT_HOSTS", []), list):
365
+ errors.append(
366
+ Error(
367
+ "Invalid type for REACTPY_DEFAULT_HOSTS.",
368
+ hint="REACTPY_DEFAULT_HOSTS should be a list.",
369
+ obj=settings.REACTPY_DEFAULT_HOSTS,
370
+ id="reactpy_django.E010",
371
+ )
372
+ )
373
+
374
+ # Check of all values in the list are strings
375
+ if isinstance(getattr(settings, "REACTPY_DEFAULT_HOSTS", None), list):
376
+ for host in settings.REACTPY_DEFAULT_HOSTS:
377
+ if not isinstance(host, str):
378
+ errors.append(
379
+ Error(
380
+ f"Invalid type {type(host)} within REACTPY_DEFAULT_HOSTS.",
381
+ hint="REACTPY_DEFAULT_HOSTS should be a list of strings.",
382
+ obj=settings.REACTPY_DEFAULT_HOSTS,
383
+ id="reactpy_django.E011",
384
+ )
385
+ )
386
+ break
387
+
388
+ if not isinstance(config.REACTPY_RECONNECT_INTERVAL, int):
389
+ errors.append(
390
+ Error(
391
+ "Invalid type for REACTPY_RECONNECT_INTERVAL.",
392
+ hint="REACTPY_RECONNECT_INTERVAL should be an integer.",
393
+ id="reactpy_django.E012",
394
+ )
395
+ )
396
+
397
+ if (
398
+ isinstance(config.REACTPY_RECONNECT_INTERVAL, int)
399
+ and config.REACTPY_RECONNECT_INTERVAL < 0
400
+ ):
401
+ errors.append(
402
+ Error(
403
+ "Invalid value for REACTPY_RECONNECT_INTERVAL.",
404
+ hint="REACTPY_RECONNECT_INTERVAL should be a positive integer.",
405
+ id="reactpy_django.E013",
406
+ )
407
+ )
408
+
409
+ if not isinstance(config.REACTPY_RECONNECT_MAX_INTERVAL, int):
410
+ errors.append(
411
+ Error(
412
+ "Invalid type for REACTPY_RECONNECT_MAX_INTERVAL.",
413
+ hint="REACTPY_RECONNECT_MAX_INTERVAL should be an integer.",
414
+ id="reactpy_django.E014",
415
+ )
416
+ )
417
+
418
+ if (
419
+ isinstance(config.REACTPY_RECONNECT_MAX_INTERVAL, int)
420
+ and config.REACTPY_RECONNECT_MAX_INTERVAL < 0
421
+ ):
422
+ errors.append(
423
+ Error(
424
+ "Invalid value for REACTPY_RECONNECT_MAX_INTERVAL.",
425
+ hint="REACTPY_RECONNECT_MAX_INTERVAL should be a positive integer.",
426
+ id="reactpy_django.E015",
427
+ )
428
+ )
429
+
430
+ if (
431
+ isinstance(config.REACTPY_RECONNECT_MAX_INTERVAL, int)
432
+ and isinstance(config.REACTPY_RECONNECT_INTERVAL, int)
433
+ and config.REACTPY_RECONNECT_MAX_INTERVAL < config.REACTPY_RECONNECT_INTERVAL
434
+ ):
435
+ errors.append(
436
+ Error(
437
+ "REACTPY_RECONNECT_MAX_INTERVAL is less than REACTPY_RECONNECT_INTERVAL.",
438
+ hint="REACTPY_RECONNECT_MAX_INTERVAL should be greater than or equal to REACTPY_RECONNECT_INTERVAL.",
439
+ id="reactpy_django.E016",
440
+ )
441
+ )
442
+
443
+ if not isinstance(config.REACTPY_RECONNECT_MAX_RETRIES, int):
444
+ errors.append(
445
+ Error(
446
+ "Invalid type for REACTPY_RECONNECT_MAX_RETRIES.",
447
+ hint="REACTPY_RECONNECT_MAX_RETRIES should be an integer.",
448
+ id="reactpy_django.E017",
449
+ )
450
+ )
451
+
452
+ if (
453
+ isinstance(config.REACTPY_RECONNECT_MAX_RETRIES, int)
454
+ and config.REACTPY_RECONNECT_MAX_RETRIES < 0
455
+ ):
456
+ errors.append(
457
+ Error(
458
+ "Invalid value for REACTPY_RECONNECT_MAX_RETRIES.",
459
+ hint="REACTPY_RECONNECT_MAX_RETRIES should be a positive integer.",
460
+ id="reactpy_django.E018",
461
+ )
462
+ )
463
+
464
+ if not isinstance(config.REACTPY_RECONNECT_BACKOFF_MULTIPLIER, (int, float)):
465
+ errors.append(
466
+ Error(
467
+ "Invalid type for REACTPY_RECONNECT_BACKOFF_MULTIPLIER.",
468
+ hint="REACTPY_RECONNECT_BACKOFF_MULTIPLIER should be an integer or float.",
469
+ id="reactpy_django.E019",
470
+ )
471
+ )
472
+
473
+ if (
474
+ isinstance(config.REACTPY_RECONNECT_BACKOFF_MULTIPLIER, (int, float))
475
+ and config.REACTPY_RECONNECT_BACKOFF_MULTIPLIER < 1
476
+ ):
477
+ errors.append(
478
+ Error(
479
+ "Invalid value for REACTPY_RECONNECT_BACKOFF_MULTIPLIER.",
480
+ hint="REACTPY_RECONNECT_BACKOFF_MULTIPLIER should be greater than or equal to 1.",
481
+ id="reactpy_django.E020",
482
+ )
483
+ )
484
+
485
+ if not isinstance(config.REACTPY_PRERENDER, bool):
486
+ errors.append(
487
+ Error(
488
+ "Invalid type for REACTPY_PRERENDER.",
489
+ hint="REACTPY_PRERENDER should be a boolean.",
490
+ id="reactpy_django.E021",
491
+ )
492
+ )
493
+
494
+ if not isinstance(config.REACTPY_AUTO_RELOGIN, bool):
495
+ errors.append(
496
+ Error(
497
+ "Invalid type for REACTPY_AUTO_RELOGIN.",
498
+ hint="REACTPY_AUTO_RELOGIN should be a boolean.",
499
+ id="reactpy_django.E022",
500
+ )
501
+ )
502
+
503
+ if not isinstance(config.REACTPY_CLEAN_INTERVAL, (int, type(None))):
504
+ errors.append(
505
+ Error(
506
+ "Invalid type for REACTPY_CLEAN_INTERVAL.",
507
+ hint="REACTPY_CLEAN_INTERVAL should be an integer or None.",
508
+ id="reactpy_django.E023",
509
+ )
510
+ )
511
+
512
+ if (
513
+ isinstance(config.REACTPY_CLEAN_INTERVAL, int)
514
+ and config.REACTPY_CLEAN_INTERVAL < 0
515
+ ):
516
+ errors.append(
517
+ Error(
518
+ "Invalid value for REACTPY_CLEAN_INTERVAL.",
519
+ hint="REACTPY_CLEAN_INTERVAL should be a positive integer or None.",
520
+ id="reactpy_django.E024",
521
+ )
522
+ )
523
+
524
+ if not isinstance(config.REACTPY_CLEAN_SESSIONS, bool):
525
+ errors.append(
526
+ Error(
527
+ "Invalid type for REACTPY_CLEAN_SESSIONS.",
528
+ hint="REACTPY_CLEAN_SESSIONS should be a boolean.",
529
+ id="reactpy_django.E025",
530
+ )
531
+ )
532
+
533
+ if not isinstance(config.REACTPY_CLEAN_USER_DATA, bool):
534
+ errors.append(
535
+ Error(
536
+ "Invalid type for REACTPY_CLEAN_USER_DATA.",
537
+ hint="REACTPY_CLEAN_USER_DATA should be a boolean.",
538
+ id="reactpy_django.E026",
539
+ )
540
+ )
541
+
542
+ 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) # type: ignore
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
+ )