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.
- {arthexis-0.1.8.dist-info → arthexis-0.1.10.dist-info}/METADATA +87 -6
- arthexis-0.1.10.dist-info/RECORD +95 -0
- arthexis-0.1.10.dist-info/licenses/LICENSE +674 -0
- config/__init__.py +0 -1
- config/auth_app.py +0 -1
- config/celery.py +1 -2
- config/context_processors.py +1 -1
- config/offline.py +2 -0
- config/settings.py +352 -37
- config/urls.py +71 -6
- core/admin.py +1601 -200
- core/admin_history.py +50 -0
- core/admindocs.py +108 -1
- core/apps.py +161 -3
- core/auto_upgrade.py +57 -0
- core/backends.py +123 -7
- core/entity.py +62 -48
- core/fields.py +98 -0
- core/github_helper.py +25 -0
- core/github_issues.py +172 -0
- core/lcd_screen.py +1 -0
- core/liveupdate.py +25 -0
- core/log_paths.py +100 -0
- core/mailer.py +83 -0
- core/middleware.py +57 -0
- core/models.py +1279 -267
- core/notifications.py +11 -1
- core/public_wifi.py +227 -0
- core/reference_utils.py +97 -0
- core/release.py +27 -20
- core/sigil_builder.py +144 -0
- core/sigil_context.py +20 -0
- core/sigil_resolver.py +284 -0
- core/system.py +162 -29
- core/tasks.py +269 -27
- core/test_system_info.py +59 -1
- core/tests.py +644 -73
- core/tests_liveupdate.py +17 -0
- core/urls.py +2 -2
- core/user_data.py +425 -168
- core/views.py +627 -59
- core/widgets.py +51 -0
- core/workgroup_urls.py +7 -3
- core/workgroup_views.py +43 -6
- nodes/actions.py +0 -2
- nodes/admin.py +168 -285
- nodes/apps.py +9 -15
- nodes/backends.py +145 -0
- nodes/lcd.py +24 -10
- nodes/models.py +579 -179
- nodes/tasks.py +1 -5
- nodes/tests.py +894 -130
- nodes/utils.py +13 -2
- nodes/views.py +204 -28
- ocpp/admin.py +212 -63
- ocpp/apps.py +1 -1
- ocpp/consumers.py +642 -68
- ocpp/evcs.py +30 -10
- ocpp/models.py +452 -70
- ocpp/simulator.py +75 -11
- ocpp/store.py +288 -30
- ocpp/tasks.py +11 -7
- ocpp/test_export_import.py +8 -7
- ocpp/test_rfid.py +211 -16
- ocpp/tests.py +1576 -137
- ocpp/transactions_io.py +68 -22
- ocpp/urls.py +35 -2
- ocpp/views.py +701 -123
- pages/admin.py +173 -13
- pages/checks.py +0 -1
- pages/context_processors.py +39 -6
- pages/forms.py +131 -0
- pages/middleware.py +153 -0
- pages/models.py +37 -9
- pages/tests.py +1182 -42
- pages/urls.py +4 -0
- pages/utils.py +0 -1
- pages/views.py +844 -51
- arthexis-0.1.8.dist-info/RECORD +0 -80
- arthexis-0.1.8.dist-info/licenses/LICENSE +0 -21
- config/workgroup_app.py +0 -7
- core/checks.py +0 -29
- {arthexis-0.1.8.dist-info → arthexis-0.1.10.dist-info}/WHEEL +0 -0
- {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(
|
|
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(
|
|
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(
|
|
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) ->
|
|
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
|
-
|
|
105
|
+
output = subprocess.check_output(["i2cdetect", "-y", "1"], text=True)
|
|
100
106
|
except Exception: # pragma: no cover - depends on environment
|
|
101
107
|
return []
|
|
102
|
-
|
|
103
|
-
|
|
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(
|
|
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
|
|