arthexis 0.1.13__py3-none-any.whl → 0.1.15__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.
Files changed (108) hide show
  1. {arthexis-0.1.13.dist-info → arthexis-0.1.15.dist-info}/METADATA +224 -221
  2. arthexis-0.1.15.dist-info/RECORD +110 -0
  3. {arthexis-0.1.13.dist-info → arthexis-0.1.15.dist-info}/licenses/LICENSE +674 -674
  4. config/__init__.py +5 -5
  5. config/active_app.py +15 -15
  6. config/asgi.py +43 -43
  7. config/auth_app.py +7 -7
  8. config/celery.py +32 -32
  9. config/context_processors.py +67 -69
  10. config/horologia_app.py +7 -7
  11. config/loadenv.py +11 -11
  12. config/logging.py +59 -48
  13. config/middleware.py +25 -25
  14. config/offline.py +49 -49
  15. config/settings.py +691 -682
  16. config/settings_helpers.py +109 -109
  17. config/urls.py +171 -166
  18. config/wsgi.py +17 -17
  19. core/admin.py +3795 -2809
  20. core/admin_history.py +50 -50
  21. core/admindocs.py +151 -151
  22. core/apps.py +356 -272
  23. core/auto_upgrade.py +57 -57
  24. core/backends.py +265 -236
  25. core/changelog.py +342 -0
  26. core/entity.py +149 -133
  27. core/environment.py +61 -61
  28. core/fields.py +168 -168
  29. core/form_fields.py +75 -75
  30. core/github_helper.py +188 -25
  31. core/github_issues.py +178 -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 +85 -85
  37. core/middleware.py +91 -91
  38. core/models.py +3637 -2795
  39. core/notifications.py +105 -105
  40. core/public_wifi.py +267 -227
  41. core/reference_utils.py +108 -108
  42. core/release.py +840 -368
  43. core/rfid_import_export.py +113 -0
  44. core/sigil_builder.py +149 -149
  45. core/sigil_context.py +20 -20
  46. core/sigil_resolver.py +315 -315
  47. core/system.py +952 -493
  48. core/tasks.py +408 -394
  49. core/temp_passwords.py +181 -181
  50. core/test_system_info.py +186 -139
  51. core/tests.py +2168 -1521
  52. core/tests_liveupdate.py +17 -17
  53. core/urls.py +11 -11
  54. core/user_data.py +641 -633
  55. core/views.py +2201 -1417
  56. core/widgets.py +213 -94
  57. core/workgroup_urls.py +17 -17
  58. core/workgroup_views.py +94 -94
  59. nodes/admin.py +1720 -1161
  60. nodes/apps.py +87 -85
  61. nodes/backends.py +160 -160
  62. nodes/dns.py +203 -203
  63. nodes/feature_checks.py +133 -133
  64. nodes/lcd.py +165 -165
  65. nodes/models.py +1764 -1597
  66. nodes/reports.py +411 -411
  67. nodes/rfid_sync.py +195 -0
  68. nodes/signals.py +18 -0
  69. nodes/tasks.py +46 -46
  70. nodes/tests.py +3830 -3116
  71. nodes/urls.py +15 -14
  72. nodes/utils.py +121 -105
  73. nodes/views.py +683 -619
  74. ocpp/admin.py +948 -948
  75. ocpp/apps.py +25 -25
  76. ocpp/consumers.py +1565 -1459
  77. ocpp/evcs.py +844 -844
  78. ocpp/evcs_discovery.py +158 -158
  79. ocpp/models.py +917 -917
  80. ocpp/reference_utils.py +42 -42
  81. ocpp/routing.py +11 -11
  82. ocpp/simulator.py +745 -745
  83. ocpp/status_display.py +26 -26
  84. ocpp/store.py +601 -541
  85. ocpp/tasks.py +31 -31
  86. ocpp/test_export_import.py +130 -130
  87. ocpp/test_rfid.py +913 -702
  88. ocpp/tests.py +4445 -4094
  89. ocpp/transactions_io.py +189 -189
  90. ocpp/urls.py +50 -50
  91. ocpp/views.py +1479 -1251
  92. pages/admin.py +769 -539
  93. pages/apps.py +10 -10
  94. pages/checks.py +40 -40
  95. pages/context_processors.py +127 -119
  96. pages/defaults.py +13 -13
  97. pages/forms.py +198 -198
  98. pages/middleware.py +209 -153
  99. pages/models.py +643 -426
  100. pages/tasks.py +74 -0
  101. pages/tests.py +3025 -2200
  102. pages/urls.py +26 -25
  103. pages/utils.py +23 -12
  104. pages/views.py +1176 -1128
  105. arthexis-0.1.13.dist-info/RECORD +0 -105
  106. nodes/actions.py +0 -70
  107. {arthexis-0.1.13.dist-info → arthexis-0.1.15.dist-info}/WHEEL +0 -0
  108. {arthexis-0.1.13.dist-info → arthexis-0.1.15.dist-info}/top_level.txt +0 -0
core/public_wifi.py CHANGED
@@ -1,227 +1,267 @@
1
- """Utilities for managing public Wi-Fi access control."""
2
-
3
- from __future__ import annotations
4
-
5
- import logging
6
- import re
7
- import shutil
8
- import subprocess
9
- from pathlib import Path
10
- from typing import Iterable
11
-
12
- from django.conf import settings
13
- from django.utils import timezone
14
-
15
- logger = logging.getLogger(__name__)
16
-
17
- _MAC_RE = re.compile(r"(?P<mac>([0-9a-f]{2}:){5}[0-9a-f]{2})", re.IGNORECASE)
18
-
19
-
20
- def _lock_dir() -> Path:
21
- return Path(settings.BASE_DIR) / "locks"
22
-
23
-
24
- def _mode_lock() -> Path:
25
- return _lock_dir() / "public_wifi_mode.lck"
26
-
27
-
28
- def _allowlist_path() -> Path:
29
- return _lock_dir() / "public_wifi_allow.list"
30
-
31
-
32
- def _normalize_mac(mac: str) -> str:
33
- return mac.strip().lower()
34
-
35
-
36
- def resolve_mac_address(ip_address: str | None) -> str | None:
37
- """Attempt to resolve the MAC address for ``ip_address``.
38
-
39
- The lookup prefers ``ip neigh`` and falls back to ``arp`` when available.
40
- Returns ``None`` when the MAC cannot be determined.
41
- """
42
-
43
- if not ip_address:
44
- return None
45
-
46
- commands: Iterable[list[str]] = []
47
- ip_cmd = shutil.which("ip")
48
- if ip_cmd:
49
- commands = [[ip_cmd, "neigh", "show", ip_address]]
50
- arp_cmd = shutil.which("arp")
51
- if arp_cmd:
52
- commands = list(commands) + [[arp_cmd, "-n", ip_address]]
53
- for command in commands:
54
- try:
55
- result = subprocess.run(
56
- command,
57
- capture_output=True,
58
- text=True,
59
- check=False,
60
- timeout=1,
61
- )
62
- except Exception: # pragma: no cover - defensive
63
- continue
64
- if result.returncode != 0:
65
- continue
66
- match = _MAC_RE.search(result.stdout)
67
- if match:
68
- mac = match.group("mac")
69
- return _normalize_mac(mac)
70
- return None
71
-
72
-
73
- def _iptables_available() -> bool:
74
- return shutil.which("iptables") is not None
75
-
76
-
77
- def _run_iptables(args: list[str]) -> None:
78
- try:
79
- subprocess.run(["iptables", *args], check=False, timeout=2)
80
- except Exception: # pragma: no cover - defensive
81
- logger.exception("iptables command failed: %s", " ".join(args))
82
-
83
-
84
- def _load_allowlist() -> set[str]:
85
- path = _allowlist_path()
86
- if not path.exists():
87
- return set()
88
- try:
89
- content = path.read_text().splitlines()
90
- except OSError: # pragma: no cover - defensive
91
- logger.exception("Unable to read public Wi-Fi allow list")
92
- return set()
93
- return {line.strip().lower() for line in content if line.strip()}
94
-
95
-
96
- def _save_allowlist(macs: Iterable[str]) -> None:
97
- path = _allowlist_path()
98
- path.parent.mkdir(parents=True, exist_ok=True)
99
- lines = sorted({m.lower() for m in macs if m})
100
- try:
101
- path.write_text("\n".join(lines) + ("\n" if lines else ""))
102
- except OSError: # pragma: no cover - defensive
103
- logger.exception("Unable to write public Wi-Fi allow list")
104
-
105
-
106
- def allow_mac(mac: str) -> None:
107
- mac = _normalize_mac(mac)
108
- if not mac:
109
- return
110
- allowlist = _load_allowlist()
111
- if mac not in allowlist:
112
- allowlist.add(mac)
113
- _save_allowlist(allowlist)
114
- if _iptables_available():
115
- check_args = [
116
- "-C",
117
- "FORWARD",
118
- "-i",
119
- "wlan0",
120
- "-m",
121
- "mac",
122
- "--mac-source",
123
- mac,
124
- "-j",
125
- "ACCEPT",
126
- ]
127
- try:
128
- result = subprocess.run(
129
- ["iptables", *check_args],
130
- capture_output=True,
131
- text=True,
132
- check=False,
133
- timeout=2,
134
- )
135
- exists = result.returncode == 0
136
- except Exception: # pragma: no cover - defensive
137
- logger.exception("iptables check failed for %s", mac)
138
- exists = True
139
- if not exists:
140
- _run_iptables(
141
- [
142
- "-I",
143
- "FORWARD",
144
- "1",
145
- "-i",
146
- "wlan0",
147
- "-m",
148
- "mac",
149
- "--mac-source",
150
- mac,
151
- "-j",
152
- "ACCEPT",
153
- ]
154
- )
155
-
156
-
157
- def revoke_mac(mac: str) -> None:
158
- mac = _normalize_mac(mac)
159
- if not mac:
160
- return
161
- allowlist = _load_allowlist()
162
- if mac in allowlist:
163
- allowlist.remove(mac)
164
- _save_allowlist(allowlist)
165
- if _iptables_available():
166
- while True:
167
- try:
168
- result = subprocess.run(
169
- [
170
- "iptables",
171
- "-D",
172
- "FORWARD",
173
- "-i",
174
- "wlan0",
175
- "-m",
176
- "mac",
177
- "--mac-source",
178
- mac,
179
- "-j",
180
- "ACCEPT",
181
- ],
182
- capture_output=True,
183
- text=True,
184
- check=False,
185
- timeout=2,
186
- )
187
- except Exception: # pragma: no cover - defensive
188
- logger.exception("iptables delete failed for %s", mac)
189
- break
190
- if result.returncode != 0:
191
- break
192
-
193
-
194
- def public_mode_lock_exists() -> bool:
195
- return _mode_lock().exists()
196
-
197
-
198
- def grant_public_access(user, mac: str):
199
- from core.models import PublicWifiAccess
200
-
201
- mac = _normalize_mac(mac)
202
- if not mac:
203
- return None
204
- access, created = PublicWifiAccess.objects.get_or_create(
205
- user=user,
206
- mac_address=mac,
207
- defaults={"revoked_on": None},
208
- )
209
- if access.revoked_on is not None:
210
- access.revoked_on = None
211
- access.save(update_fields=["revoked_on", "updated_on"])
212
- allow_mac(mac)
213
- return access
214
-
215
-
216
- def revoke_public_access(access) -> None:
217
- if access.revoked_on is None:
218
- access.revoked_on = timezone.now()
219
- access.save(update_fields=["revoked_on", "updated_on"])
220
- revoke_mac(access.mac_address)
221
-
222
-
223
- def revoke_public_access_for_user(user) -> None:
224
- from core.models import PublicWifiAccess
225
-
226
- for access in PublicWifiAccess.objects.filter(user=user, revoked_on__isnull=True):
227
- revoke_public_access(access)
1
+ """Utilities for managing public Wi-Fi access control."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import re
7
+ import shutil
8
+ import subprocess
9
+ from pathlib import Path
10
+ from typing import Iterable
11
+
12
+ from django.conf import settings
13
+ from django.utils import timezone
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ _MAC_RE = re.compile(r"(?P<mac>([0-9a-f]{2}:){5}[0-9a-f]{2})", re.IGNORECASE)
18
+
19
+
20
+ def _lock_dir() -> Path:
21
+ return Path(settings.BASE_DIR) / "locks"
22
+
23
+
24
+ def _mode_lock() -> Path:
25
+ return _lock_dir() / "public_wifi_mode.lck"
26
+
27
+
28
+ def _allowlist_path() -> Path:
29
+ return _lock_dir() / "public_wifi_allow.list"
30
+
31
+
32
+ def _normalize_mac(mac: str) -> str:
33
+ return mac.strip().lower()
34
+
35
+
36
+ def resolve_mac_address(ip_address: str | None) -> str | None:
37
+ """Attempt to resolve the MAC address for ``ip_address``.
38
+
39
+ The lookup prefers ``ip neigh`` and falls back to ``arp`` when available.
40
+ Returns ``None`` when the MAC cannot be determined.
41
+ """
42
+
43
+ if not ip_address:
44
+ return None
45
+
46
+ commands: Iterable[list[str]] = []
47
+ ip_cmd = shutil.which("ip")
48
+ if ip_cmd:
49
+ commands = [[ip_cmd, "neigh", "show", ip_address]]
50
+ arp_cmd = shutil.which("arp")
51
+ if arp_cmd:
52
+ commands = list(commands) + [[arp_cmd, "-n", ip_address]]
53
+ for command in commands:
54
+ try:
55
+ result = subprocess.run(
56
+ command,
57
+ capture_output=True,
58
+ text=True,
59
+ check=False,
60
+ timeout=1,
61
+ )
62
+ except Exception: # pragma: no cover - defensive
63
+ continue
64
+ if result.returncode != 0:
65
+ continue
66
+ match = _MAC_RE.search(result.stdout)
67
+ if match:
68
+ mac = match.group("mac")
69
+ return _normalize_mac(mac)
70
+ return None
71
+
72
+
73
+ def _iptables_available() -> bool:
74
+ return shutil.which("iptables") is not None
75
+
76
+
77
+ def _run_iptables(args: list[str]) -> None:
78
+ try:
79
+ subprocess.run(["iptables", *args], check=False, timeout=2)
80
+ except Exception: # pragma: no cover - defensive
81
+ logger.exception("iptables command failed: %s", " ".join(args))
82
+
83
+
84
+ def _ensure_wlan0_drop_rule() -> None:
85
+ if not _iptables_available():
86
+ return
87
+ check_args = [
88
+ "-C",
89
+ "FORWARD",
90
+ "-i",
91
+ "wlan0",
92
+ "-o",
93
+ "wlan1",
94
+ "-j",
95
+ "DROP",
96
+ ]
97
+ try:
98
+ result = subprocess.run(
99
+ ["iptables", *check_args],
100
+ capture_output=True,
101
+ text=True,
102
+ check=False,
103
+ timeout=2,
104
+ )
105
+ except Exception: # pragma: no cover - defensive
106
+ logger.exception("iptables check failed for wlan0 drop rule")
107
+ result = None
108
+ if result is None or result.returncode != 0:
109
+ _run_iptables(
110
+ [
111
+ "-A",
112
+ "FORWARD",
113
+ "-i",
114
+ "wlan0",
115
+ "-o",
116
+ "wlan1",
117
+ "-j",
118
+ "DROP",
119
+ ]
120
+ )
121
+
122
+
123
+ def _load_allowlist() -> set[str]:
124
+ path = _allowlist_path()
125
+ if not path.exists():
126
+ return set()
127
+ try:
128
+ content = path.read_text().splitlines()
129
+ except OSError: # pragma: no cover - defensive
130
+ logger.exception("Unable to read public Wi-Fi allow list")
131
+ return set()
132
+ return {line.strip().lower() for line in content if line.strip()}
133
+
134
+
135
+ def _save_allowlist(macs: Iterable[str]) -> None:
136
+ path = _allowlist_path()
137
+ path.parent.mkdir(parents=True, exist_ok=True)
138
+ lines = sorted({m.lower() for m in macs if m})
139
+ try:
140
+ path.write_text("\n".join(lines) + ("\n" if lines else ""))
141
+ except OSError: # pragma: no cover - defensive
142
+ logger.exception("Unable to write public Wi-Fi allow list")
143
+
144
+
145
+ def allow_mac(mac: str) -> None:
146
+ mac = _normalize_mac(mac)
147
+ if not mac:
148
+ return
149
+ allowlist = _load_allowlist()
150
+ if mac not in allowlist:
151
+ allowlist.add(mac)
152
+ _save_allowlist(allowlist)
153
+ if _iptables_available():
154
+ _ensure_wlan0_drop_rule()
155
+ check_args = [
156
+ "-C",
157
+ "FORWARD",
158
+ "-i",
159
+ "wlan0",
160
+ "-m",
161
+ "mac",
162
+ "--mac-source",
163
+ mac,
164
+ "-j",
165
+ "ACCEPT",
166
+ ]
167
+ try:
168
+ result = subprocess.run(
169
+ ["iptables", *check_args],
170
+ capture_output=True,
171
+ text=True,
172
+ check=False,
173
+ timeout=2,
174
+ )
175
+ exists = result.returncode == 0
176
+ except Exception: # pragma: no cover - defensive
177
+ logger.exception("iptables check failed for %s", mac)
178
+ exists = True
179
+ if not exists:
180
+ _run_iptables(
181
+ [
182
+ "-I",
183
+ "FORWARD",
184
+ "1",
185
+ "-i",
186
+ "wlan0",
187
+ "-m",
188
+ "mac",
189
+ "--mac-source",
190
+ mac,
191
+ "-j",
192
+ "ACCEPT",
193
+ ]
194
+ )
195
+
196
+
197
+ def revoke_mac(mac: str) -> None:
198
+ mac = _normalize_mac(mac)
199
+ if not mac:
200
+ return
201
+ allowlist = _load_allowlist()
202
+ if mac in allowlist:
203
+ allowlist.remove(mac)
204
+ _save_allowlist(allowlist)
205
+ if _iptables_available():
206
+ while True:
207
+ try:
208
+ result = subprocess.run(
209
+ [
210
+ "iptables",
211
+ "-D",
212
+ "FORWARD",
213
+ "-i",
214
+ "wlan0",
215
+ "-m",
216
+ "mac",
217
+ "--mac-source",
218
+ mac,
219
+ "-j",
220
+ "ACCEPT",
221
+ ],
222
+ capture_output=True,
223
+ text=True,
224
+ check=False,
225
+ timeout=2,
226
+ )
227
+ except Exception: # pragma: no cover - defensive
228
+ logger.exception("iptables delete failed for %s", mac)
229
+ break
230
+ if result.returncode != 0:
231
+ break
232
+
233
+
234
+ def public_mode_lock_exists() -> bool:
235
+ return _mode_lock().exists()
236
+
237
+
238
+ def grant_public_access(user, mac: str):
239
+ from core.models import PublicWifiAccess
240
+
241
+ mac = _normalize_mac(mac)
242
+ if not mac:
243
+ return None
244
+ access, created = PublicWifiAccess.objects.get_or_create(
245
+ user=user,
246
+ mac_address=mac,
247
+ defaults={"revoked_on": None},
248
+ )
249
+ if access.revoked_on is not None:
250
+ access.revoked_on = None
251
+ access.save(update_fields=["revoked_on", "updated_on"])
252
+ allow_mac(mac)
253
+ return access
254
+
255
+
256
+ def revoke_public_access(access) -> None:
257
+ if access.revoked_on is None:
258
+ access.revoked_on = timezone.now()
259
+ access.save(update_fields=["revoked_on", "updated_on"])
260
+ revoke_mac(access.mac_address)
261
+
262
+
263
+ def revoke_public_access_for_user(user) -> None:
264
+ from core.models import PublicWifiAccess
265
+
266
+ for access in PublicWifiAccess.objects.filter(user=user, revoked_on__isnull=True):
267
+ revoke_public_access(access)