arthexis 0.1.8__py3-none-any.whl → 0.1.9__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 (81) hide show
  1. {arthexis-0.1.8.dist-info → arthexis-0.1.9.dist-info}/METADATA +42 -4
  2. arthexis-0.1.9.dist-info/RECORD +92 -0
  3. arthexis-0.1.9.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 +133 -16
  10. config/urls.py +65 -6
  11. core/admin.py +1226 -191
  12. core/admin_history.py +50 -0
  13. core/admindocs.py +108 -1
  14. core/apps.py +158 -3
  15. core/backends.py +46 -4
  16. core/entity.py +62 -48
  17. core/fields.py +6 -1
  18. core/github_helper.py +25 -0
  19. core/github_issues.py +172 -0
  20. core/lcd_screen.py +1 -0
  21. core/liveupdate.py +25 -0
  22. core/log_paths.py +100 -0
  23. core/mailer.py +83 -0
  24. core/middleware.py +57 -0
  25. core/models.py +1071 -264
  26. core/notifications.py +11 -1
  27. core/public_wifi.py +227 -0
  28. core/release.py +27 -20
  29. core/sigil_builder.py +131 -0
  30. core/sigil_context.py +20 -0
  31. core/sigil_resolver.py +284 -0
  32. core/system.py +129 -10
  33. core/tasks.py +118 -19
  34. core/test_system_info.py +22 -0
  35. core/tests.py +358 -63
  36. core/tests_liveupdate.py +17 -0
  37. core/urls.py +2 -2
  38. core/user_data.py +329 -167
  39. core/views.py +383 -57
  40. core/widgets.py +51 -0
  41. core/workgroup_urls.py +7 -3
  42. core/workgroup_views.py +43 -6
  43. nodes/actions.py +0 -2
  44. nodes/admin.py +159 -284
  45. nodes/apps.py +9 -15
  46. nodes/backends.py +53 -0
  47. nodes/lcd.py +24 -10
  48. nodes/models.py +375 -178
  49. nodes/tasks.py +1 -5
  50. nodes/tests.py +524 -129
  51. nodes/utils.py +13 -2
  52. nodes/views.py +66 -23
  53. ocpp/admin.py +150 -61
  54. ocpp/apps.py +1 -1
  55. ocpp/consumers.py +432 -69
  56. ocpp/evcs.py +25 -8
  57. ocpp/models.py +408 -68
  58. ocpp/simulator.py +13 -6
  59. ocpp/store.py +258 -30
  60. ocpp/tasks.py +11 -7
  61. ocpp/test_export_import.py +8 -7
  62. ocpp/test_rfid.py +211 -16
  63. ocpp/tests.py +1198 -135
  64. ocpp/transactions_io.py +68 -22
  65. ocpp/urls.py +35 -2
  66. ocpp/views.py +654 -101
  67. pages/admin.py +173 -13
  68. pages/checks.py +0 -1
  69. pages/context_processors.py +19 -6
  70. pages/middleware.py +153 -0
  71. pages/models.py +37 -9
  72. pages/tests.py +759 -40
  73. pages/urls.py +3 -0
  74. pages/utils.py +0 -1
  75. pages/views.py +576 -25
  76. arthexis-0.1.8.dist-info/RECORD +0 -80
  77. arthexis-0.1.8.dist-info/licenses/LICENSE +0 -21
  78. config/workgroup_app.py +0 -7
  79. core/checks.py +0 -29
  80. {arthexis-0.1.8.dist-info → arthexis-0.1.9.dist-info}/WHEEL +0 -0
  81. {arthexis-0.1.8.dist-info → arthexis-0.1.9.dist-info}/top_level.txt +0 -0
nodes/apps.py CHANGED
@@ -7,7 +7,6 @@ from pathlib import Path
7
7
 
8
8
  from django.apps import AppConfig
9
9
  from django.conf import settings
10
- from django.core.signals import request_started
11
10
  from django.db import connections
12
11
  from django.db.utils import OperationalError
13
12
  from utils import revision
@@ -17,13 +16,9 @@ logger = logging.getLogger(__name__)
17
16
 
18
17
 
19
18
  def _startup_notification() -> None:
20
- """Queue a notification with host:port and version on a background thread."""
19
+ """Queue a Net Message with ``hostname:port`` and version info."""
21
20
 
22
21
  host = socket.gethostname()
23
- try:
24
- address = socket.gethostbyname(host)
25
- except socket.gaierror:
26
- address = host
27
22
 
28
23
  port = os.environ.get("PORT", "8000")
29
24
 
@@ -35,9 +30,9 @@ def _startup_notification() -> None:
35
30
  revision_value = revision.get_revision()
36
31
  rev_short = revision_value[-6:] if revision_value else ""
37
32
 
38
- body = f"v{version}"
33
+ body = version
39
34
  if rev_short:
40
- body += f" r{rev_short}"
35
+ body = f"{body} r{rev_short}" if body else f"r{rev_short}"
41
36
 
42
37
  def _worker() -> None: # pragma: no cover - background thread
43
38
  # Allow the LCD a moment to become ready and retry a few times
@@ -45,7 +40,7 @@ def _startup_notification() -> None:
45
40
  try:
46
41
  from nodes.models import NetMessage
47
42
 
48
- NetMessage.broadcast(subject=f"{address}:{port}", body=body)
43
+ NetMessage.broadcast(subject=f"{host}:{port}", body=body)
49
44
  break
50
45
  except Exception:
51
46
  time.sleep(1)
@@ -54,9 +49,8 @@ def _startup_notification() -> None:
54
49
 
55
50
 
56
51
  def _trigger_startup_notification(**_: object) -> None:
57
- """Send the startup notification once a request has started."""
52
+ """Attempt to send the startup notification in the background."""
58
53
 
59
- request_started.disconnect(_trigger_startup_notification, dispatch_uid="nodes-startup")
60
54
  try:
61
55
  connections["default"].ensure_connection()
62
56
  except OperationalError:
@@ -68,9 +62,9 @@ def _trigger_startup_notification(**_: object) -> None:
68
62
  class NodesConfig(AppConfig):
69
63
  default_auto_field = "django.db.models.BigAutoField"
70
64
  name = "nodes"
71
- verbose_name = "2. Infrastructure"
65
+ verbose_name = "4. Infrastructure"
72
66
 
73
67
  def ready(self): # pragma: no cover - exercised on app start
74
- request_started.connect(
75
- _trigger_startup_notification, dispatch_uid="nodes-startup"
76
- )
68
+ from django.db.models.signals import post_migrate
69
+
70
+ post_migrate.connect(_trigger_startup_notification, sender=self)
nodes/backends.py ADDED
@@ -0,0 +1,53 @@
1
+ from __future__ import annotations
2
+
3
+ from django.core.mail.backends.base import BaseEmailBackend
4
+ from django.core.mail import get_connection
5
+ from django.conf import settings
6
+ from django.db.models import Q
7
+
8
+ from .models import EmailOutbox
9
+
10
+
11
+ class OutboxEmailBackend(BaseEmailBackend):
12
+ """Email backend that selects an :class:`EmailOutbox` automatically.
13
+
14
+ If a matching outbox exists for the message's ``from_email`` (matching
15
+ either ``from_email`` or ``username``), that outbox's SMTP credentials are
16
+ used. Otherwise, the first available outbox is used. When no outboxes are
17
+ configured, the system falls back to Django's default SMTP settings.
18
+ """
19
+
20
+ def _select_outbox(self, from_email: str | None) -> EmailOutbox | None:
21
+ if from_email:
22
+ return (
23
+ EmailOutbox.objects.filter(
24
+ Q(from_email__iexact=from_email) | Q(username__iexact=from_email)
25
+ ).first()
26
+ or EmailOutbox.objects.first()
27
+ )
28
+ return EmailOutbox.objects.first()
29
+
30
+ def send_messages(self, email_messages):
31
+ sent = 0
32
+ for message in email_messages:
33
+ outbox = self._select_outbox(message.from_email)
34
+ if outbox:
35
+ connection = outbox.get_connection()
36
+ if not message.from_email:
37
+ message.from_email = (
38
+ outbox.from_email or settings.DEFAULT_FROM_EMAIL
39
+ )
40
+ else:
41
+ connection = get_connection(
42
+ "django.core.mail.backends.smtp.EmailBackend"
43
+ )
44
+ if not message.from_email:
45
+ message.from_email = settings.DEFAULT_FROM_EMAIL
46
+ try:
47
+ sent += connection.send_messages([message]) or 0
48
+ finally:
49
+ try:
50
+ connection.close()
51
+ except Exception: # pragma: no cover - close errors shouldn't fail send
52
+ pass
53
+ 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