arthexis 0.1.16__py3-none-any.whl → 0.1.26__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.

Potentially problematic release.


This version of arthexis might be problematic. Click here for more details.

Files changed (63) hide show
  1. {arthexis-0.1.16.dist-info → arthexis-0.1.26.dist-info}/METADATA +84 -35
  2. arthexis-0.1.26.dist-info/RECORD +111 -0
  3. config/asgi.py +1 -15
  4. config/middleware.py +47 -1
  5. config/settings.py +15 -30
  6. config/urls.py +53 -1
  7. core/admin.py +540 -450
  8. core/apps.py +0 -6
  9. core/auto_upgrade.py +19 -4
  10. core/backends.py +13 -3
  11. core/changelog.py +66 -5
  12. core/environment.py +4 -5
  13. core/models.py +1566 -203
  14. core/notifications.py +1 -1
  15. core/reference_utils.py +10 -11
  16. core/release.py +55 -7
  17. core/sigil_builder.py +2 -2
  18. core/sigil_resolver.py +1 -66
  19. core/system.py +268 -2
  20. core/tasks.py +174 -48
  21. core/tests.py +314 -16
  22. core/user_data.py +42 -2
  23. core/views.py +278 -183
  24. nodes/admin.py +557 -65
  25. nodes/apps.py +11 -0
  26. nodes/models.py +658 -113
  27. nodes/rfid_sync.py +1 -1
  28. nodes/tasks.py +97 -2
  29. nodes/tests.py +1212 -116
  30. nodes/urls.py +15 -1
  31. nodes/utils.py +51 -3
  32. nodes/views.py +1239 -154
  33. ocpp/admin.py +979 -152
  34. ocpp/consumers.py +268 -28
  35. ocpp/models.py +488 -3
  36. ocpp/network.py +398 -0
  37. ocpp/store.py +6 -4
  38. ocpp/tasks.py +296 -2
  39. ocpp/test_export_import.py +1 -0
  40. ocpp/test_rfid.py +121 -4
  41. ocpp/tests.py +950 -11
  42. ocpp/transactions_io.py +9 -1
  43. ocpp/urls.py +3 -3
  44. ocpp/views.py +596 -51
  45. pages/admin.py +262 -30
  46. pages/apps.py +35 -0
  47. pages/context_processors.py +26 -21
  48. pages/defaults.py +1 -1
  49. pages/forms.py +31 -8
  50. pages/middleware.py +6 -2
  51. pages/models.py +77 -2
  52. pages/module_defaults.py +5 -5
  53. pages/site_config.py +137 -0
  54. pages/tests.py +885 -109
  55. pages/urls.py +13 -2
  56. pages/utils.py +70 -0
  57. pages/views.py +558 -55
  58. arthexis-0.1.16.dist-info/RECORD +0 -111
  59. core/workgroup_urls.py +0 -17
  60. core/workgroup_views.py +0 -94
  61. {arthexis-0.1.16.dist-info → arthexis-0.1.26.dist-info}/WHEEL +0 -0
  62. {arthexis-0.1.16.dist-info → arthexis-0.1.26.dist-info}/licenses/LICENSE +0 -0
  63. {arthexis-0.1.16.dist-info → arthexis-0.1.26.dist-info}/top_level.txt +0 -0
core/tasks.py CHANGED
@@ -2,24 +2,51 @@ from __future__ import annotations
2
2
 
3
3
  import logging
4
4
  import shutil
5
+ import re
5
6
  import subprocess
6
7
  from pathlib import Path
7
8
  import urllib.error
8
9
  import urllib.request
9
10
 
10
11
  from celery import shared_task
11
- from django.conf import settings
12
- from django.contrib.auth import get_user_model
13
- from core import mailer
14
12
  from core import github_issues
13
+ from django.db import DatabaseError
15
14
  from django.utils import timezone
16
15
 
17
- from nodes.models import NetMessage
18
-
19
16
 
20
17
  AUTO_UPGRADE_HEALTH_DELAY_SECONDS = 30
21
18
  AUTO_UPGRADE_SKIP_LOCK_NAME = "auto_upgrade_skip_revisions.lck"
22
19
 
20
+ SEVERITY_NORMAL = "normal"
21
+ SEVERITY_LOW = "low"
22
+ SEVERITY_CRITICAL = "critical"
23
+
24
+ _PackageReleaseModel = None
25
+
26
+
27
+ def _get_package_release_model():
28
+ """Return the :class:`core.models.PackageRelease` model when available."""
29
+
30
+ global _PackageReleaseModel
31
+
32
+ if _PackageReleaseModel is not None:
33
+ return _PackageReleaseModel
34
+
35
+ try:
36
+ from core.models import PackageRelease # noqa: WPS433 - runtime import
37
+ except Exception: # pragma: no cover - app registry not ready
38
+ return None
39
+
40
+ _PackageReleaseModel = PackageRelease
41
+ return PackageRelease
42
+
43
+
44
+ model = _get_package_release_model()
45
+ if model is not None: # pragma: no branch - runtime constant setup
46
+ SEVERITY_NORMAL = model.Severity.NORMAL
47
+ SEVERITY_LOW = model.Severity.LOW
48
+ SEVERITY_CRITICAL = model.Severity.CRITICAL
49
+
23
50
 
24
51
  logger = logging.getLogger(__name__)
25
52
 
@@ -30,23 +57,6 @@ def heartbeat() -> None:
30
57
  logger.info("Heartbeat task executed")
31
58
 
32
59
 
33
- @shared_task
34
- def birthday_greetings() -> None:
35
- """Send birthday greetings to users via Net Message and email."""
36
- User = get_user_model()
37
- today = timezone.localdate()
38
- for user in User.objects.filter(birthday=today):
39
- NetMessage.broadcast("Happy bday!", user.username)
40
- if user.email:
41
- mailer.send(
42
- "Happy bday!",
43
- f"Happy bday! {user.username}",
44
- [user.email],
45
- settings.DEFAULT_FROM_EMAIL,
46
- fail_silently=True,
47
- )
48
-
49
-
50
60
  def _auto_upgrade_log_path(base_dir: Path) -> Path:
51
61
  """Return the log file used for auto-upgrade events."""
52
62
 
@@ -67,6 +77,65 @@ def _append_auto_upgrade_log(base_dir: Path, message: str) -> None:
67
77
  logger.warning("Failed to append auto-upgrade log entry: %s", message)
68
78
 
69
79
 
80
+ def _resolve_release_severity(version: str | None) -> str:
81
+ """Return the stored severity for *version*, defaulting to normal."""
82
+
83
+ if not version:
84
+ return SEVERITY_NORMAL
85
+
86
+ model = _get_package_release_model()
87
+ if model is None:
88
+ return SEVERITY_NORMAL
89
+
90
+ try:
91
+ queryset = model.objects.filter(version=version)
92
+ release = (
93
+ queryset.filter(package__is_active=True).first() or queryset.first()
94
+ )
95
+ except DatabaseError: # pragma: no cover - depends on DB availability
96
+ return SEVERITY_NORMAL
97
+
98
+ if not release:
99
+ return SEVERITY_NORMAL
100
+
101
+ severity = getattr(release, "severity", None)
102
+ if not severity:
103
+ return SEVERITY_NORMAL
104
+ return severity
105
+
106
+
107
+ def _read_local_version(base_dir: Path) -> str | None:
108
+ """Return the local VERSION file contents when readable."""
109
+
110
+ version_path = base_dir / "VERSION"
111
+ if not version_path.exists():
112
+ return None
113
+ try:
114
+ return version_path.read_text().strip()
115
+ except OSError: # pragma: no cover - filesystem error
116
+ return None
117
+
118
+
119
+ def _read_remote_version(base_dir: Path, branch: str) -> str | None:
120
+ """Return the VERSION file from ``origin/<branch>`` when available."""
121
+
122
+ try:
123
+ return (
124
+ subprocess.check_output(
125
+ [
126
+ "git",
127
+ "show",
128
+ f"origin/{branch}:VERSION",
129
+ ],
130
+ cwd=base_dir,
131
+ )
132
+ .decode()
133
+ .strip()
134
+ )
135
+ except subprocess.CalledProcessError: # pragma: no cover - git failure
136
+ return None
137
+
138
+
70
139
  def _skip_lock_path(base_dir: Path) -> Path:
71
140
  return base_dir / "locks" / AUTO_UPGRADE_SKIP_LOCK_NAME
72
141
 
@@ -124,6 +193,21 @@ def _resolve_service_url(base_dir: Path) -> str:
124
193
  return f"http://127.0.0.1:{port}/"
125
194
 
126
195
 
196
+ def _parse_major_minor(version: str) -> tuple[int, int] | None:
197
+ match = re.match(r"^\s*(\d+)\.(\d+)", version)
198
+ if not match:
199
+ return None
200
+ return int(match.group(1)), int(match.group(2))
201
+
202
+
203
+ def _shares_stable_series(local: str, remote: str) -> bool:
204
+ local_parts = _parse_major_minor(local)
205
+ remote_parts = _parse_major_minor(remote)
206
+ if not local_parts or not remote_parts:
207
+ return False
208
+ return local_parts == remote_parts
209
+
210
+
127
211
  @shared_task
128
212
  def check_github_updates() -> None:
129
213
  """Check the GitHub repo for updates and upgrade if needed."""
@@ -179,48 +263,71 @@ def check_github_updates() -> None:
179
263
  startup()
180
264
  return
181
265
 
266
+ remote_version = _read_remote_version(base_dir, branch)
267
+ local_version = _read_local_version(base_dir)
268
+ remote_severity = _resolve_release_severity(remote_version)
269
+
182
270
  upgrade_stamp = timezone.now().strftime("@ %Y%m%d %H:%M")
183
271
 
184
272
  upgrade_was_applied = False
185
273
 
186
274
  if mode == "latest":
187
- local = (
275
+ local_revision = (
188
276
  subprocess.check_output(["git", "rev-parse", branch], cwd=base_dir)
189
277
  .decode()
190
278
  .strip()
191
279
  )
192
- if local == remote_revision:
280
+ if local_revision == remote_revision:
193
281
  if startup:
194
282
  startup()
195
283
  return
284
+
285
+ if (
286
+ remote_version
287
+ and local_version
288
+ and remote_version != local_version
289
+ and remote_severity == SEVERITY_LOW
290
+ and _shares_stable_series(local_version, remote_version)
291
+ ):
292
+ _append_auto_upgrade_log(
293
+ base_dir,
294
+ f"Skipping auto-upgrade for low severity patch {remote_version}",
295
+ )
296
+ if startup:
297
+ startup()
298
+ return
299
+
196
300
  if notify:
197
301
  notify("Upgrading...", upgrade_stamp)
198
302
  args = ["./upgrade.sh", "--latest", "--no-restart"]
199
303
  upgrade_was_applied = True
200
304
  else:
201
- local = "0"
202
- version_file = base_dir / "VERSION"
203
- if version_file.exists():
204
- local = version_file.read_text().strip()
205
- remote = (
206
- subprocess.check_output(
207
- [
208
- "git",
209
- "show",
210
- f"origin/{branch}:VERSION",
211
- ],
212
- cwd=base_dir,
213
- )
214
- .decode()
215
- .strip()
216
- )
217
- if local == remote:
305
+ local_value = local_version or "0"
306
+ remote_value = remote_version or local_value
307
+
308
+ if local_value == remote_value:
218
309
  if startup:
219
310
  startup()
220
311
  return
312
+
313
+ if (
314
+ mode == "stable"
315
+ and local_version
316
+ and remote_version
317
+ and remote_version != local_version
318
+ and _shares_stable_series(local_version, remote_version)
319
+ and remote_severity != SEVERITY_CRITICAL
320
+ ):
321
+ if startup:
322
+ startup()
323
+ return
324
+
221
325
  if notify:
222
326
  notify("Upgrading...", upgrade_stamp)
223
- args = ["./upgrade.sh", "--no-restart"]
327
+ if mode == "stable":
328
+ args = ["./upgrade.sh", "--stable", "--no-restart"]
329
+ else:
330
+ args = ["./upgrade.sh", "--no-restart"]
224
331
  upgrade_was_applied = True
225
332
 
226
333
  with log_file.open("a") as fh:
@@ -230,12 +337,6 @@ def check_github_updates() -> None:
230
337
 
231
338
  subprocess.run(args, cwd=base_dir, check=True)
232
339
 
233
- if shutil.which("gway"):
234
- try:
235
- subprocess.run(["gway", "upgrade"], check=True)
236
- except subprocess.CalledProcessError:
237
- logger.warning("gway upgrade failed; continuing anyway", exc_info=True)
238
-
239
340
  service_file = base_dir / "locks/service.lck"
240
341
  if service_file.exists():
241
342
  service = service_file.read_text().strip()
@@ -410,3 +511,28 @@ def run_client_report_schedule(schedule_id: int) -> None:
410
511
  except Exception:
411
512
  logger.exception("ClientReportSchedule %s failed", schedule_id)
412
513
  raise
514
+
515
+
516
+ @shared_task
517
+ def ensure_recurring_client_reports() -> None:
518
+ """Ensure scheduled consumer reports run for the current period."""
519
+
520
+ from core.models import ClientReportSchedule
521
+
522
+ reference = timezone.localdate()
523
+ schedules = ClientReportSchedule.objects.filter(
524
+ periodicity__in=[
525
+ ClientReportSchedule.PERIODICITY_DAILY,
526
+ ClientReportSchedule.PERIODICITY_WEEKLY,
527
+ ClientReportSchedule.PERIODICITY_MONTHLY,
528
+ ]
529
+ ).prefetch_related("chargers")
530
+
531
+ for schedule in schedules:
532
+ try:
533
+ schedule.generate_missing_reports(reference=reference)
534
+ except Exception:
535
+ logger.exception(
536
+ "Automatic consumer report generation failed for schedule %s",
537
+ schedule.pk,
538
+ )