arthexis 0.1.9__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 (112) hide show
  1. arthexis-0.1.26.dist-info/METADATA +272 -0
  2. arthexis-0.1.26.dist-info/RECORD +111 -0
  3. {arthexis-0.1.9.dist-info → arthexis-0.1.26.dist-info}/licenses/LICENSE +674 -674
  4. config/__init__.py +5 -5
  5. config/active_app.py +15 -15
  6. config/asgi.py +29 -29
  7. config/auth_app.py +7 -7
  8. config/celery.py +32 -25
  9. config/context_processors.py +67 -68
  10. config/horologia_app.py +7 -7
  11. config/loadenv.py +11 -11
  12. config/logging.py +59 -48
  13. config/middleware.py +71 -25
  14. config/offline.py +49 -49
  15. config/settings.py +676 -492
  16. config/settings_helpers.py +109 -0
  17. config/urls.py +228 -159
  18. config/wsgi.py +17 -17
  19. core/admin.py +4052 -2066
  20. core/admin_history.py +50 -50
  21. core/admindocs.py +192 -151
  22. core/apps.py +350 -223
  23. core/auto_upgrade.py +72 -0
  24. core/backends.py +311 -124
  25. core/changelog.py +403 -0
  26. core/entity.py +149 -133
  27. core/environment.py +60 -43
  28. core/fields.py +168 -75
  29. core/form_fields.py +75 -0
  30. core/github_helper.py +188 -25
  31. core/github_issues.py +183 -172
  32. core/github_repos.py +72 -0
  33. core/lcd_screen.py +78 -78
  34. core/liveupdate.py +25 -25
  35. core/log_paths.py +114 -100
  36. core/mailer.py +89 -83
  37. core/middleware.py +91 -91
  38. core/models.py +5041 -2195
  39. core/notifications.py +105 -105
  40. core/public_wifi.py +267 -227
  41. core/reference_utils.py +107 -0
  42. core/release.py +940 -346
  43. core/rfid_import_export.py +113 -0
  44. core/sigil_builder.py +149 -131
  45. core/sigil_context.py +20 -20
  46. core/sigil_resolver.py +250 -284
  47. core/system.py +1425 -230
  48. core/tasks.py +538 -199
  49. core/temp_passwords.py +181 -0
  50. core/test_system_info.py +202 -43
  51. core/tests.py +2673 -1069
  52. core/tests_liveupdate.py +17 -17
  53. core/urls.py +11 -11
  54. core/user_data.py +681 -495
  55. core/views.py +2484 -789
  56. core/widgets.py +213 -51
  57. nodes/admin.py +2236 -445
  58. nodes/apps.py +98 -70
  59. nodes/backends.py +160 -53
  60. nodes/dns.py +203 -0
  61. nodes/feature_checks.py +133 -0
  62. nodes/lcd.py +165 -165
  63. nodes/models.py +2375 -870
  64. nodes/reports.py +411 -0
  65. nodes/rfid_sync.py +210 -0
  66. nodes/signals.py +18 -0
  67. nodes/tasks.py +141 -46
  68. nodes/tests.py +5045 -1489
  69. nodes/urls.py +29 -13
  70. nodes/utils.py +172 -73
  71. nodes/views.py +1768 -304
  72. ocpp/admin.py +1775 -481
  73. ocpp/apps.py +25 -25
  74. ocpp/consumers.py +1843 -630
  75. ocpp/evcs.py +844 -928
  76. ocpp/evcs_discovery.py +158 -0
  77. ocpp/models.py +1417 -640
  78. ocpp/network.py +398 -0
  79. ocpp/reference_utils.py +42 -0
  80. ocpp/routing.py +11 -9
  81. ocpp/simulator.py +745 -368
  82. ocpp/status_display.py +26 -0
  83. ocpp/store.py +603 -403
  84. ocpp/tasks.py +479 -31
  85. ocpp/test_export_import.py +131 -130
  86. ocpp/test_rfid.py +1072 -540
  87. ocpp/tests.py +5494 -2296
  88. ocpp/transactions_io.py +197 -165
  89. ocpp/urls.py +50 -50
  90. ocpp/views.py +2024 -912
  91. pages/admin.py +1123 -396
  92. pages/apps.py +45 -10
  93. pages/checks.py +40 -40
  94. pages/context_processors.py +151 -85
  95. pages/defaults.py +13 -0
  96. pages/forms.py +221 -0
  97. pages/middleware.py +213 -153
  98. pages/models.py +720 -252
  99. pages/module_defaults.py +156 -0
  100. pages/site_config.py +137 -0
  101. pages/tasks.py +74 -0
  102. pages/tests.py +4009 -1389
  103. pages/urls.py +38 -20
  104. pages/utils.py +93 -12
  105. pages/views.py +1736 -762
  106. arthexis-0.1.9.dist-info/METADATA +0 -168
  107. arthexis-0.1.9.dist-info/RECORD +0 -92
  108. core/workgroup_urls.py +0 -17
  109. core/workgroup_views.py +0 -94
  110. nodes/actions.py +0 -70
  111. {arthexis-0.1.9.dist-info → arthexis-0.1.26.dist-info}/WHEEL +0 -0
  112. {arthexis-0.1.9.dist-info → arthexis-0.1.26.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,133 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any, Callable, Dict, Iterable, Optional
5
+
6
+ from django.contrib import messages
7
+
8
+ if False: # pragma: no cover - typing imports only
9
+ from .models import Node, NodeFeature
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class FeatureCheckResult:
14
+ """Outcome of a feature validation."""
15
+
16
+ success: bool
17
+ message: str
18
+ level: int = messages.INFO
19
+
20
+
21
+ FeatureCheck = Callable[["NodeFeature", Optional["Node"]], Any]
22
+
23
+
24
+ class FeatureCheckRegistry:
25
+ """Registry for feature validation callbacks."""
26
+
27
+ def __init__(self) -> None:
28
+ self._checks: Dict[str, FeatureCheck] = {}
29
+ self._default_check: Optional[FeatureCheck] = None
30
+
31
+ def register(self, slug: str) -> Callable[[FeatureCheck], FeatureCheck]:
32
+ """Register ``func`` as the validator for ``slug``."""
33
+
34
+ def decorator(func: FeatureCheck) -> FeatureCheck:
35
+ self._checks[slug] = func
36
+ return func
37
+
38
+ return decorator
39
+
40
+ def register_default(self, func: FeatureCheck) -> FeatureCheck:
41
+ """Register ``func`` as the fallback validator."""
42
+
43
+ self._default_check = func
44
+ return func
45
+
46
+ def get(self, slug: str) -> Optional[FeatureCheck]:
47
+ return self._checks.get(slug)
48
+
49
+ def items(self) -> Iterable[tuple[str, FeatureCheck]]:
50
+ return self._checks.items()
51
+
52
+ def run(
53
+ self, feature: "NodeFeature", *, node: Optional["Node"] = None
54
+ ) -> Optional[FeatureCheckResult]:
55
+ check = self._checks.get(feature.slug)
56
+ if check is None:
57
+ check = self._default_check
58
+ if check is None:
59
+ return None
60
+ result = check(feature, node)
61
+ return self._normalize_result(feature, result)
62
+
63
+ def _normalize_result(
64
+ self, feature: "NodeFeature", result: Any
65
+ ) -> FeatureCheckResult:
66
+ if isinstance(result, FeatureCheckResult):
67
+ return result
68
+ if result is None:
69
+ return FeatureCheckResult(
70
+ True,
71
+ f"{feature.display} check completed successfully.",
72
+ messages.SUCCESS,
73
+ )
74
+ if isinstance(result, tuple) and len(result) >= 2:
75
+ success, message, *rest = result
76
+ level = rest[0] if rest else (
77
+ messages.SUCCESS if success else messages.ERROR
78
+ )
79
+ return FeatureCheckResult(bool(success), str(message), int(level))
80
+ if isinstance(result, bool):
81
+ message = (
82
+ f"{feature.display} check {'passed' if result else 'failed'}."
83
+ )
84
+ level = messages.SUCCESS if result else messages.ERROR
85
+ return FeatureCheckResult(result, message, level)
86
+ raise TypeError(
87
+ f"Unsupported feature check result type: {type(result)!r}"
88
+ )
89
+
90
+
91
+ feature_checks = FeatureCheckRegistry()
92
+
93
+
94
+ @feature_checks.register_default
95
+ def _default_feature_check(
96
+ feature: "NodeFeature", node: Optional["Node"]
97
+ ) -> FeatureCheckResult:
98
+ from .models import Node
99
+
100
+ target: Optional["Node"] = node or Node.get_local()
101
+ if target is None:
102
+ return FeatureCheckResult(
103
+ False,
104
+ f"No local node is registered; cannot verify {feature.display}.",
105
+ messages.WARNING,
106
+ )
107
+ try:
108
+ enabled = feature.is_enabled
109
+ except Exception as exc: # pragma: no cover - defensive
110
+ return FeatureCheckResult(
111
+ False,
112
+ f"{feature.display} check failed: {exc}",
113
+ messages.ERROR,
114
+ )
115
+ if enabled:
116
+ return FeatureCheckResult(
117
+ True,
118
+ f"{feature.display} is enabled on {target.hostname}.",
119
+ messages.SUCCESS,
120
+ )
121
+ return FeatureCheckResult(
122
+ False,
123
+ f"{feature.display} is not enabled on {target.hostname}.",
124
+ messages.WARNING,
125
+ )
126
+
127
+
128
+ __all__ = [
129
+ "FeatureCheck",
130
+ "FeatureCheckRegistry",
131
+ "FeatureCheckResult",
132
+ "feature_checks",
133
+ ]
nodes/lcd.py CHANGED
@@ -1,165 +1,165 @@
1
- """Minimal driver for PCF8574/PCF8574A I2C LCD1602 displays.
2
-
3
- The implementation is adapted from the example provided in the
4
- instructions. It is intentionally lightweight and only implements the
5
- operations required for this project: initialisation, clearing the
6
- screen and writing text to a specific position.
7
- """
8
-
9
- from __future__ import annotations
10
-
11
- import subprocess
12
- import time
13
- from dataclasses import dataclass
14
-
15
- try: # pragma: no cover - hardware dependent
16
- import smbus # type: ignore
17
- except Exception: # pragma: no cover - missing dependency
18
- try: # pragma: no cover - hardware dependent
19
- import smbus2 as smbus # type: ignore
20
- except Exception: # pragma: no cover - missing dependency
21
- smbus = None # type: ignore
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
-
29
-
30
- class LCDUnavailableError(RuntimeError):
31
- """Raised when the LCD cannot be initialised."""
32
-
33
-
34
- @dataclass
35
- class _BusWrapper:
36
- """Wrapper around :class:`smbus.SMBus` to allow mocking in tests."""
37
-
38
- channel: int
39
-
40
- def write_byte(
41
- self, addr: int, data: int
42
- ) -> None: # pragma: no cover - thin wrapper
43
- if smbus is None:
44
- raise LCDUnavailableError(SMBUS_HINT)
45
- bus = smbus.SMBus(self.channel)
46
- bus.write_byte(addr, data)
47
- bus.close()
48
-
49
-
50
- class CharLCD1602:
51
- """Minimal driver for PCF8574/PCF8574A I2C backpack (LCD1602)."""
52
-
53
- def __init__(self, bus: _BusWrapper | None = None) -> None:
54
- if smbus is None: # pragma: no cover - hardware dependent
55
- raise LCDUnavailableError(SMBUS_HINT)
56
- self.bus = bus or _BusWrapper(1)
57
- self.BLEN = 1
58
- self.PCF8574_address = 0x27
59
- self.PCF8574A_address = 0x3F
60
- self.LCD_ADDR = self.PCF8574_address
61
-
62
- def _write_word(self, addr: int, data: int) -> None:
63
- if self.BLEN:
64
- data |= 0x08
65
- else:
66
- data &= 0xF7
67
- self.bus.write_byte(addr, data)
68
-
69
- def _pulse_enable(self, data: int) -> None:
70
- self._write_word(self.LCD_ADDR, data | 0x04)
71
- time.sleep(0.0005)
72
- self._write_word(self.LCD_ADDR, data & ~0x04)
73
- time.sleep(0.0001)
74
-
75
- def send_command(self, cmd: int) -> None:
76
- high = cmd & 0xF0
77
- low = (cmd << 4) & 0xF0
78
- self._write_word(self.LCD_ADDR, high)
79
- self._pulse_enable(high)
80
- self._write_word(self.LCD_ADDR, low)
81
- self._pulse_enable(low)
82
- # Give the LCD time to process the command to avoid garbled output.
83
- time.sleep(0.001)
84
-
85
- def send_data(self, data: int) -> None:
86
- high = (data & 0xF0) | 0x01
87
- low = ((data << 4) & 0xF0) | 0x01
88
- self._write_word(self.LCD_ADDR, high)
89
- self._pulse_enable(high)
90
- self._write_word(self.LCD_ADDR, low)
91
- self._pulse_enable(low)
92
- # Allow the LCD controller to catch up between data writes.
93
- time.sleep(0.001)
94
-
95
- def i2c_scan(self) -> list[str]: # pragma: no cover - requires hardware
96
- """Return a list of detected I2C addresses.
97
-
98
- The implementation relies on the external ``i2cdetect`` command. On
99
- systems where ``i2c-tools`` is not installed or the command cannot be
100
- executed (e.g. insufficient permissions), the function returns an empty
101
- list so callers can fall back to a sensible default address.
102
- """
103
-
104
- try:
105
- output = subprocess.check_output(["i2cdetect", "-y", "1"], text=True)
106
- except Exception: # pragma: no cover - depends on environment
107
- return []
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
116
-
117
- def init_lcd(self, addr: int | None = None, bl: int = 1) -> None:
118
- self.BLEN = 1 if bl else 0
119
- if addr is None:
120
- try:
121
- found = self.i2c_scan()
122
- except Exception: # pragma: no cover - i2c detection issues
123
- found = []
124
- if "3f" in found or "3F" in found:
125
- self.LCD_ADDR = self.PCF8574A_address
126
- else:
127
- # Default to the common PCF8574 address (0x27) when detection
128
- # fails or returns no recognised addresses. This mirrors the
129
- # behaviour prior to introducing automatic address detection and
130
- # prevents the display from remaining uninitialised on systems
131
- # without ``i2c-tools``.
132
- self.LCD_ADDR = self.PCF8574_address
133
- else:
134
- self.LCD_ADDR = addr
135
-
136
- time.sleep(0.05)
137
- self.send_command(0x33)
138
- self.send_command(0x32)
139
- self.send_command(0x28)
140
- self.send_command(0x0C)
141
- self.send_command(0x06)
142
- self.clear()
143
- self._write_word(self.LCD_ADDR, 0x00)
144
-
145
- def clear(self) -> None:
146
- self.send_command(0x01)
147
- time.sleep(0.002)
148
-
149
- def reset(self) -> None:
150
- """Re-run the initialisation sequence to recover the display."""
151
- self.init_lcd(addr=self.LCD_ADDR, bl=self.BLEN)
152
-
153
- def set_backlight(
154
- self, on: bool = True
155
- ) -> None: # pragma: no cover - hardware dependent
156
- self.BLEN = 1 if on else 0
157
- self._write_word(self.LCD_ADDR, 0x00)
158
-
159
- def write(self, x: int, y: int, s: str) -> None:
160
- x = max(0, min(15, int(x)))
161
- y = 0 if int(y) <= 0 else 1
162
- addr = 0x80 + 0x40 * y + x
163
- self.send_command(addr)
164
- for ch in str(s):
165
- self.send_data(ord(ch))
1
+ """Minimal driver for PCF8574/PCF8574A I2C LCD1602 displays.
2
+
3
+ The implementation is adapted from the example provided in the
4
+ instructions. It is intentionally lightweight and only implements the
5
+ operations required for this project: initialisation, clearing the
6
+ screen and writing text to a specific position.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import subprocess
12
+ import time
13
+ from dataclasses import dataclass
14
+
15
+ try: # pragma: no cover - hardware dependent
16
+ import smbus # type: ignore
17
+ except Exception: # pragma: no cover - missing dependency
18
+ try: # pragma: no cover - hardware dependent
19
+ import smbus2 as smbus # type: ignore
20
+ except Exception: # pragma: no cover - missing dependency
21
+ smbus = None # type: ignore
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
+
29
+
30
+ class LCDUnavailableError(RuntimeError):
31
+ """Raised when the LCD cannot be initialised."""
32
+
33
+
34
+ @dataclass
35
+ class _BusWrapper:
36
+ """Wrapper around :class:`smbus.SMBus` to allow mocking in tests."""
37
+
38
+ channel: int
39
+
40
+ def write_byte(
41
+ self, addr: int, data: int
42
+ ) -> None: # pragma: no cover - thin wrapper
43
+ if smbus is None:
44
+ raise LCDUnavailableError(SMBUS_HINT)
45
+ bus = smbus.SMBus(self.channel)
46
+ bus.write_byte(addr, data)
47
+ bus.close()
48
+
49
+
50
+ class CharLCD1602:
51
+ """Minimal driver for PCF8574/PCF8574A I2C backpack (LCD1602)."""
52
+
53
+ def __init__(self, bus: _BusWrapper | None = None) -> None:
54
+ if smbus is None: # pragma: no cover - hardware dependent
55
+ raise LCDUnavailableError(SMBUS_HINT)
56
+ self.bus = bus or _BusWrapper(1)
57
+ self.BLEN = 1
58
+ self.PCF8574_address = 0x27
59
+ self.PCF8574A_address = 0x3F
60
+ self.LCD_ADDR = self.PCF8574_address
61
+
62
+ def _write_word(self, addr: int, data: int) -> None:
63
+ if self.BLEN:
64
+ data |= 0x08
65
+ else:
66
+ data &= 0xF7
67
+ self.bus.write_byte(addr, data)
68
+
69
+ def _pulse_enable(self, data: int) -> None:
70
+ self._write_word(self.LCD_ADDR, data | 0x04)
71
+ time.sleep(0.0005)
72
+ self._write_word(self.LCD_ADDR, data & ~0x04)
73
+ time.sleep(0.0001)
74
+
75
+ def send_command(self, cmd: int) -> None:
76
+ high = cmd & 0xF0
77
+ low = (cmd << 4) & 0xF0
78
+ self._write_word(self.LCD_ADDR, high)
79
+ self._pulse_enable(high)
80
+ self._write_word(self.LCD_ADDR, low)
81
+ self._pulse_enable(low)
82
+ # Give the LCD time to process the command to avoid garbled output.
83
+ time.sleep(0.001)
84
+
85
+ def send_data(self, data: int) -> None:
86
+ high = (data & 0xF0) | 0x01
87
+ low = ((data << 4) & 0xF0) | 0x01
88
+ self._write_word(self.LCD_ADDR, high)
89
+ self._pulse_enable(high)
90
+ self._write_word(self.LCD_ADDR, low)
91
+ self._pulse_enable(low)
92
+ # Allow the LCD controller to catch up between data writes.
93
+ time.sleep(0.001)
94
+
95
+ def i2c_scan(self) -> list[str]: # pragma: no cover - requires hardware
96
+ """Return a list of detected I2C addresses.
97
+
98
+ The implementation relies on the external ``i2cdetect`` command. On
99
+ systems where ``i2c-tools`` is not installed or the command cannot be
100
+ executed (e.g. insufficient permissions), the function returns an empty
101
+ list so callers can fall back to a sensible default address.
102
+ """
103
+
104
+ try:
105
+ output = subprocess.check_output(["i2cdetect", "-y", "1"], text=True)
106
+ except Exception: # pragma: no cover - depends on environment
107
+ return []
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
116
+
117
+ def init_lcd(self, addr: int | None = None, bl: int = 1) -> None:
118
+ self.BLEN = 1 if bl else 0
119
+ if addr is None:
120
+ try:
121
+ found = self.i2c_scan()
122
+ except Exception: # pragma: no cover - i2c detection issues
123
+ found = []
124
+ if "3f" in found or "3F" in found:
125
+ self.LCD_ADDR = self.PCF8574A_address
126
+ else:
127
+ # Default to the common PCF8574 address (0x27) when detection
128
+ # fails or returns no recognised addresses. This mirrors the
129
+ # behaviour prior to introducing automatic address detection and
130
+ # prevents the display from remaining uninitialised on systems
131
+ # without ``i2c-tools``.
132
+ self.LCD_ADDR = self.PCF8574_address
133
+ else:
134
+ self.LCD_ADDR = addr
135
+
136
+ time.sleep(0.05)
137
+ self.send_command(0x33)
138
+ self.send_command(0x32)
139
+ self.send_command(0x28)
140
+ self.send_command(0x0C)
141
+ self.send_command(0x06)
142
+ self.clear()
143
+ self._write_word(self.LCD_ADDR, 0x00)
144
+
145
+ def clear(self) -> None:
146
+ self.send_command(0x01)
147
+ time.sleep(0.002)
148
+
149
+ def reset(self) -> None:
150
+ """Re-run the initialisation sequence to recover the display."""
151
+ self.init_lcd(addr=self.LCD_ADDR, bl=self.BLEN)
152
+
153
+ def set_backlight(
154
+ self, on: bool = True
155
+ ) -> None: # pragma: no cover - hardware dependent
156
+ self.BLEN = 1 if on else 0
157
+ self._write_word(self.LCD_ADDR, 0x00)
158
+
159
+ def write(self, x: int, y: int, s: str) -> None:
160
+ x = max(0, min(15, int(x)))
161
+ y = 0 if int(y) <= 0 else 1
162
+ addr = 0x80 + 0x40 * y + x
163
+ self.send_command(addr)
164
+ for ch in str(s):
165
+ self.send_data(ord(ch))