arthexis 0.1.8__py3-none-any.whl → 0.1.10__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 (84) hide show
  1. {arthexis-0.1.8.dist-info → arthexis-0.1.10.dist-info}/METADATA +87 -6
  2. arthexis-0.1.10.dist-info/RECORD +95 -0
  3. arthexis-0.1.10.dist-info/licenses/LICENSE +674 -0
  4. config/__init__.py +0 -1
  5. config/auth_app.py +0 -1
  6. config/celery.py +1 -2
  7. config/context_processors.py +1 -1
  8. config/offline.py +2 -0
  9. config/settings.py +352 -37
  10. config/urls.py +71 -6
  11. core/admin.py +1601 -200
  12. core/admin_history.py +50 -0
  13. core/admindocs.py +108 -1
  14. core/apps.py +161 -3
  15. core/auto_upgrade.py +57 -0
  16. core/backends.py +123 -7
  17. core/entity.py +62 -48
  18. core/fields.py +98 -0
  19. core/github_helper.py +25 -0
  20. core/github_issues.py +172 -0
  21. core/lcd_screen.py +1 -0
  22. core/liveupdate.py +25 -0
  23. core/log_paths.py +100 -0
  24. core/mailer.py +83 -0
  25. core/middleware.py +57 -0
  26. core/models.py +1279 -267
  27. core/notifications.py +11 -1
  28. core/public_wifi.py +227 -0
  29. core/reference_utils.py +97 -0
  30. core/release.py +27 -20
  31. core/sigil_builder.py +144 -0
  32. core/sigil_context.py +20 -0
  33. core/sigil_resolver.py +284 -0
  34. core/system.py +162 -29
  35. core/tasks.py +269 -27
  36. core/test_system_info.py +59 -1
  37. core/tests.py +644 -73
  38. core/tests_liveupdate.py +17 -0
  39. core/urls.py +2 -2
  40. core/user_data.py +425 -168
  41. core/views.py +627 -59
  42. core/widgets.py +51 -0
  43. core/workgroup_urls.py +7 -3
  44. core/workgroup_views.py +43 -6
  45. nodes/actions.py +0 -2
  46. nodes/admin.py +168 -285
  47. nodes/apps.py +9 -15
  48. nodes/backends.py +145 -0
  49. nodes/lcd.py +24 -10
  50. nodes/models.py +579 -179
  51. nodes/tasks.py +1 -5
  52. nodes/tests.py +894 -130
  53. nodes/utils.py +13 -2
  54. nodes/views.py +204 -28
  55. ocpp/admin.py +212 -63
  56. ocpp/apps.py +1 -1
  57. ocpp/consumers.py +642 -68
  58. ocpp/evcs.py +30 -10
  59. ocpp/models.py +452 -70
  60. ocpp/simulator.py +75 -11
  61. ocpp/store.py +288 -30
  62. ocpp/tasks.py +11 -7
  63. ocpp/test_export_import.py +8 -7
  64. ocpp/test_rfid.py +211 -16
  65. ocpp/tests.py +1576 -137
  66. ocpp/transactions_io.py +68 -22
  67. ocpp/urls.py +35 -2
  68. ocpp/views.py +701 -123
  69. pages/admin.py +173 -13
  70. pages/checks.py +0 -1
  71. pages/context_processors.py +39 -6
  72. pages/forms.py +131 -0
  73. pages/middleware.py +153 -0
  74. pages/models.py +37 -9
  75. pages/tests.py +1182 -42
  76. pages/urls.py +4 -0
  77. pages/utils.py +0 -1
  78. pages/views.py +844 -51
  79. arthexis-0.1.8.dist-info/RECORD +0 -80
  80. arthexis-0.1.8.dist-info/licenses/LICENSE +0 -21
  81. config/workgroup_app.py +0 -7
  82. core/checks.py +0 -29
  83. {arthexis-0.1.8.dist-info → arthexis-0.1.10.dist-info}/WHEEL +0 -0
  84. {arthexis-0.1.8.dist-info → arthexis-0.1.10.dist-info}/top_level.txt +0 -0
nodes/backends.py ADDED
@@ -0,0 +1,145 @@
1
+ from __future__ import annotations
2
+
3
+ import random
4
+ from collections import defaultdict
5
+
6
+ from django.core.mail.backends.base import BaseEmailBackend
7
+ from django.core.mail import get_connection
8
+ from django.conf import settings
9
+ from django.db.models import Q
10
+
11
+ from .models import EmailOutbox
12
+
13
+
14
+ class OutboxEmailBackend(BaseEmailBackend):
15
+ """Email backend that selects an :class:`EmailOutbox` automatically.
16
+
17
+ If a matching outbox exists for the message's ``from_email`` (matching
18
+ either ``from_email`` or ``username``), that outbox's SMTP credentials are
19
+ used. ``EmailOutbox`` associations to ``node``, ``user`` and ``group`` are
20
+ also considered and preferred when multiple criteria match. When no
21
+ outboxes are configured, the system falls back to Django's default SMTP
22
+ settings.
23
+ """
24
+
25
+ def _resolve_identifier(self, message, attr: str):
26
+ value = getattr(message, attr, None)
27
+ if value is None:
28
+ value = getattr(message, f"{attr}_id", None)
29
+ if value is None:
30
+ return None
31
+ return getattr(value, "pk", value)
32
+
33
+ def _select_outbox(
34
+ self, message
35
+ ) -> tuple[EmailOutbox | None, list[EmailOutbox]]:
36
+ from_email = getattr(message, "from_email", None)
37
+ node_id = self._resolve_identifier(message, "node")
38
+ user_id = self._resolve_identifier(message, "user")
39
+ group_id = self._resolve_identifier(message, "group")
40
+
41
+ match_sets: list[tuple[str, list[EmailOutbox]]] = []
42
+
43
+ if from_email:
44
+ email_matches = list(
45
+ EmailOutbox.objects.filter(
46
+ Q(from_email__iexact=from_email) | Q(username__iexact=from_email)
47
+ )
48
+ )
49
+ if email_matches:
50
+ match_sets.append(("from_email", email_matches))
51
+
52
+ if node_id:
53
+ node_matches = list(EmailOutbox.objects.filter(node_id=node_id))
54
+ if node_matches:
55
+ match_sets.append(("node", node_matches))
56
+
57
+ if user_id:
58
+ user_matches = list(EmailOutbox.objects.filter(user_id=user_id))
59
+ if user_matches:
60
+ match_sets.append(("user", user_matches))
61
+
62
+ if group_id:
63
+ group_matches = list(EmailOutbox.objects.filter(group_id=group_id))
64
+ if group_matches:
65
+ match_sets.append(("group", group_matches))
66
+
67
+ if not match_sets:
68
+ return EmailOutbox.objects.first(), []
69
+
70
+ candidates: dict[int, EmailOutbox] = {}
71
+ scores: defaultdict[int, int] = defaultdict(int)
72
+
73
+ for _, matches in match_sets:
74
+ for outbox in matches:
75
+ candidates[outbox.pk] = outbox
76
+ scores[outbox.pk] += 1
77
+
78
+ if not candidates:
79
+ return EmailOutbox.objects.first(), []
80
+
81
+ selected: EmailOutbox | None = None
82
+ fallbacks: list[EmailOutbox] = []
83
+
84
+ for score in sorted(set(scores.values()), reverse=True):
85
+ group = [candidates[pk] for pk, value in scores.items() if value == score]
86
+ if len(group) > 1:
87
+ random.shuffle(group)
88
+ if selected is None:
89
+ selected = group[0]
90
+ fallbacks.extend(group[1:])
91
+ else:
92
+ fallbacks.extend(group)
93
+
94
+ return selected, fallbacks
95
+
96
+ def send_messages(self, email_messages):
97
+ sent = 0
98
+ for message in email_messages:
99
+ original_from_email = message.from_email
100
+ outbox, fallbacks = self._select_outbox(message)
101
+ tried_outboxes = []
102
+ if outbox:
103
+ tried_outboxes.append(outbox)
104
+ tried_outboxes.extend(fallbacks)
105
+
106
+ last_error: Exception | None = None
107
+
108
+ if tried_outboxes:
109
+ for candidate in tried_outboxes:
110
+ connection = candidate.get_connection()
111
+ message.from_email = (
112
+ original_from_email
113
+ or candidate.from_email
114
+ or settings.DEFAULT_FROM_EMAIL
115
+ )
116
+ try:
117
+ sent += connection.send_messages([message]) or 0
118
+ last_error = None
119
+ break
120
+ except Exception as exc: # pragma: no cover - retry on error
121
+ last_error = exc
122
+ finally:
123
+ try:
124
+ connection.close()
125
+ except Exception: # pragma: no cover - close errors shouldn't fail send
126
+ pass
127
+ if last_error is not None:
128
+ message.from_email = original_from_email
129
+ raise last_error
130
+ else:
131
+ connection = get_connection(
132
+ "django.core.mail.backends.smtp.EmailBackend"
133
+ )
134
+ if not message.from_email:
135
+ message.from_email = settings.DEFAULT_FROM_EMAIL
136
+ try:
137
+ sent += connection.send_messages([message]) or 0
138
+ finally:
139
+ try:
140
+ connection.close()
141
+ except Exception: # pragma: no cover - close errors shouldn't fail send
142
+ pass
143
+
144
+ message.from_email = original_from_email
145
+ return sent
nodes/lcd.py CHANGED
@@ -11,7 +11,6 @@ from __future__ import annotations
11
11
  import subprocess
12
12
  import time
13
13
  from dataclasses import dataclass
14
- from typing import List
15
14
 
16
15
  try: # pragma: no cover - hardware dependent
17
16
  import smbus # type: ignore
@@ -21,6 +20,12 @@ except Exception: # pragma: no cover - missing dependency
21
20
  except Exception: # pragma: no cover - missing dependency
22
21
  smbus = None # type: ignore
23
22
 
23
+ SMBUS_HINT = (
24
+ "smbus module not found. Enable the I2C interface and install the dependencies.\n"
25
+ "For Debian/Ubuntu run: sudo apt-get install i2c-tools python3-smbus\n"
26
+ "Within the virtualenv: pip install smbus2"
27
+ )
28
+
24
29
 
25
30
  class LCDUnavailableError(RuntimeError):
26
31
  """Raised when the LCD cannot be initialised."""
@@ -32,9 +37,11 @@ class _BusWrapper:
32
37
 
33
38
  channel: int
34
39
 
35
- def write_byte(self, addr: int, data: int) -> None: # pragma: no cover - thin wrapper
40
+ def write_byte(
41
+ self, addr: int, data: int
42
+ ) -> None: # pragma: no cover - thin wrapper
36
43
  if smbus is None:
37
- raise LCDUnavailableError("smbus not available")
44
+ raise LCDUnavailableError(SMBUS_HINT)
38
45
  bus = smbus.SMBus(self.channel)
39
46
  bus.write_byte(addr, data)
40
47
  bus.close()
@@ -45,7 +52,7 @@ class CharLCD1602:
45
52
 
46
53
  def __init__(self, bus: _BusWrapper | None = None) -> None:
47
54
  if smbus is None: # pragma: no cover - hardware dependent
48
- raise LCDUnavailableError("smbus not available")
55
+ raise LCDUnavailableError(SMBUS_HINT)
49
56
  self.bus = bus or _BusWrapper(1)
50
57
  self.BLEN = 1
51
58
  self.PCF8574_address = 0x27
@@ -85,7 +92,7 @@ class CharLCD1602:
85
92
  # Allow the LCD controller to catch up between data writes.
86
93
  time.sleep(0.001)
87
94
 
88
- def i2c_scan(self) -> List[str]: # pragma: no cover - requires hardware
95
+ def i2c_scan(self) -> list[str]: # pragma: no cover - requires hardware
89
96
  """Return a list of detected I2C addresses.
90
97
 
91
98
  The implementation relies on the external ``i2cdetect`` command. On
@@ -94,13 +101,18 @@ class CharLCD1602:
94
101
  list so callers can fall back to a sensible default address.
95
102
  """
96
103
 
97
- cmd = "i2cdetect -y 1 | awk 'NR>1 {$1=\"\"; print}'"
98
104
  try:
99
- out = subprocess.check_output(cmd, shell=True).decode()
105
+ output = subprocess.check_output(["i2cdetect", "-y", "1"], text=True)
100
106
  except Exception: # pragma: no cover - depends on environment
101
107
  return []
102
- out = out.replace("\n", "").replace(" --", "")
103
- return [tok for tok in out.split(" ") if tok]
108
+
109
+ addresses: list[str] = []
110
+ for line in output.splitlines()[1:]:
111
+ parts = line.split()
112
+ for token in parts[1:]:
113
+ if token != "--":
114
+ addresses.append(token)
115
+ return addresses
104
116
 
105
117
  def init_lcd(self, addr: int | None = None, bl: int = 1) -> None:
106
118
  self.BLEN = 1 if bl else 0
@@ -138,7 +150,9 @@ class CharLCD1602:
138
150
  """Re-run the initialisation sequence to recover the display."""
139
151
  self.init_lcd(addr=self.LCD_ADDR, bl=self.BLEN)
140
152
 
141
- def set_backlight(self, on: bool = True) -> None: # pragma: no cover - hardware dependent
153
+ def set_backlight(
154
+ self, on: bool = True
155
+ ) -> None: # pragma: no cover - hardware dependent
142
156
  self.BLEN = 1 if on else 0
143
157
  self._write_word(self.LCD_ADDR, 0x00)
144
158