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.
- arthexis-0.1.26.dist-info/METADATA +272 -0
- arthexis-0.1.26.dist-info/RECORD +111 -0
- {arthexis-0.1.9.dist-info → arthexis-0.1.26.dist-info}/licenses/LICENSE +674 -674
- config/__init__.py +5 -5
- config/active_app.py +15 -15
- config/asgi.py +29 -29
- config/auth_app.py +7 -7
- config/celery.py +32 -25
- config/context_processors.py +67 -68
- config/horologia_app.py +7 -7
- config/loadenv.py +11 -11
- config/logging.py +59 -48
- config/middleware.py +71 -25
- config/offline.py +49 -49
- config/settings.py +676 -492
- config/settings_helpers.py +109 -0
- config/urls.py +228 -159
- config/wsgi.py +17 -17
- core/admin.py +4052 -2066
- core/admin_history.py +50 -50
- core/admindocs.py +192 -151
- core/apps.py +350 -223
- core/auto_upgrade.py +72 -0
- core/backends.py +311 -124
- core/changelog.py +403 -0
- core/entity.py +149 -133
- core/environment.py +60 -43
- core/fields.py +168 -75
- core/form_fields.py +75 -0
- core/github_helper.py +188 -25
- core/github_issues.py +183 -172
- core/github_repos.py +72 -0
- core/lcd_screen.py +78 -78
- core/liveupdate.py +25 -25
- core/log_paths.py +114 -100
- core/mailer.py +89 -83
- core/middleware.py +91 -91
- core/models.py +5041 -2195
- core/notifications.py +105 -105
- core/public_wifi.py +267 -227
- core/reference_utils.py +107 -0
- core/release.py +940 -346
- core/rfid_import_export.py +113 -0
- core/sigil_builder.py +149 -131
- core/sigil_context.py +20 -20
- core/sigil_resolver.py +250 -284
- core/system.py +1425 -230
- core/tasks.py +538 -199
- core/temp_passwords.py +181 -0
- core/test_system_info.py +202 -43
- core/tests.py +2673 -1069
- core/tests_liveupdate.py +17 -17
- core/urls.py +11 -11
- core/user_data.py +681 -495
- core/views.py +2484 -789
- core/widgets.py +213 -51
- nodes/admin.py +2236 -445
- nodes/apps.py +98 -70
- nodes/backends.py +160 -53
- nodes/dns.py +203 -0
- nodes/feature_checks.py +133 -0
- nodes/lcd.py +165 -165
- nodes/models.py +2375 -870
- nodes/reports.py +411 -0
- nodes/rfid_sync.py +210 -0
- nodes/signals.py +18 -0
- nodes/tasks.py +141 -46
- nodes/tests.py +5045 -1489
- nodes/urls.py +29 -13
- nodes/utils.py +172 -73
- nodes/views.py +1768 -304
- ocpp/admin.py +1775 -481
- ocpp/apps.py +25 -25
- ocpp/consumers.py +1843 -630
- ocpp/evcs.py +844 -928
- ocpp/evcs_discovery.py +158 -0
- ocpp/models.py +1417 -640
- ocpp/network.py +398 -0
- ocpp/reference_utils.py +42 -0
- ocpp/routing.py +11 -9
- ocpp/simulator.py +745 -368
- ocpp/status_display.py +26 -0
- ocpp/store.py +603 -403
- ocpp/tasks.py +479 -31
- ocpp/test_export_import.py +131 -130
- ocpp/test_rfid.py +1072 -540
- ocpp/tests.py +5494 -2296
- ocpp/transactions_io.py +197 -165
- ocpp/urls.py +50 -50
- ocpp/views.py +2024 -912
- pages/admin.py +1123 -396
- pages/apps.py +45 -10
- pages/checks.py +40 -40
- pages/context_processors.py +151 -85
- pages/defaults.py +13 -0
- pages/forms.py +221 -0
- pages/middleware.py +213 -153
- pages/models.py +720 -252
- pages/module_defaults.py +156 -0
- pages/site_config.py +137 -0
- pages/tasks.py +74 -0
- pages/tests.py +4009 -1389
- pages/urls.py +38 -20
- pages/utils.py +93 -12
- pages/views.py +1736 -762
- arthexis-0.1.9.dist-info/METADATA +0 -168
- arthexis-0.1.9.dist-info/RECORD +0 -92
- core/workgroup_urls.py +0 -17
- core/workgroup_views.py +0 -94
- nodes/actions.py +0 -70
- {arthexis-0.1.9.dist-info → arthexis-0.1.26.dist-info}/WHEEL +0 -0
- {arthexis-0.1.9.dist-info → arthexis-0.1.26.dist-info}/top_level.txt +0 -0
nodes/urls.py
CHANGED
|
@@ -1,13 +1,29 @@
|
|
|
1
|
-
from django.urls import path
|
|
2
|
-
|
|
3
|
-
from . import views
|
|
4
|
-
|
|
5
|
-
urlpatterns = [
|
|
6
|
-
path("info/", views.node_info, name="node-info"),
|
|
7
|
-
path("list/", views.node_list, name="node-list"),
|
|
8
|
-
path("register/", views.register_node, name="register-node"),
|
|
9
|
-
path("screenshot/", views.capture, name="node-screenshot"),
|
|
10
|
-
path("net-message/", views.net_message, name="net-message"),
|
|
11
|
-
path("
|
|
12
|
-
path("
|
|
13
|
-
|
|
1
|
+
from django.urls import path
|
|
2
|
+
|
|
3
|
+
from . import views
|
|
4
|
+
|
|
5
|
+
urlpatterns = [
|
|
6
|
+
path("info/", views.node_info, name="node-info"),
|
|
7
|
+
path("list/", views.node_list, name="node-list"),
|
|
8
|
+
path("register/", views.register_node, name="register-node"),
|
|
9
|
+
path("screenshot/", views.capture, name="node-screenshot"),
|
|
10
|
+
path("net-message/", views.net_message, name="net-message"),
|
|
11
|
+
path("net-message/pull/", views.net_message_pull, name="net-message-pull"),
|
|
12
|
+
path("rfid/export/", views.export_rfids, name="node-rfid-export"),
|
|
13
|
+
path("rfid/import/", views.import_rfids, name="node-rfid-import"),
|
|
14
|
+
path("network/chargers/", views.network_chargers, name="node-network-chargers"),
|
|
15
|
+
path(
|
|
16
|
+
"network/chargers/forward/",
|
|
17
|
+
views.forward_chargers,
|
|
18
|
+
name="node-network-forward-chargers",
|
|
19
|
+
),
|
|
20
|
+
path(
|
|
21
|
+
"network/chargers/action/",
|
|
22
|
+
views.network_charger_action,
|
|
23
|
+
name="node-network-charger-action",
|
|
24
|
+
),
|
|
25
|
+
path("proxy/session/", views.proxy_session, name="node-proxy-session"),
|
|
26
|
+
path("proxy/login/<str:token>/", views.proxy_login, name="node-proxy-login"),
|
|
27
|
+
path("proxy/execute/", views.proxy_execute, name="node-proxy-execute"),
|
|
28
|
+
path("<slug:endpoint>/", views.public_node_endpoint, name="node-public-endpoint"),
|
|
29
|
+
]
|
nodes/utils.py
CHANGED
|
@@ -1,73 +1,172 @@
|
|
|
1
|
-
from datetime import datetime
|
|
2
|
-
from pathlib import Path
|
|
3
|
-
import hashlib
|
|
4
|
-
import logging
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
from
|
|
10
|
-
|
|
11
|
-
from .
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
import hashlib
|
|
4
|
+
import logging
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
import uuid
|
|
8
|
+
|
|
9
|
+
from django.conf import settings
|
|
10
|
+
from selenium import webdriver
|
|
11
|
+
from selenium.webdriver.firefox.options import Options
|
|
12
|
+
from selenium.common.exceptions import WebDriverException
|
|
13
|
+
|
|
14
|
+
try: # pragma: no cover - optional dependency may be missing
|
|
15
|
+
from geckodriver_autoinstaller import install as install_geckodriver
|
|
16
|
+
except Exception: # pragma: no cover - fallback when installer is unavailable
|
|
17
|
+
install_geckodriver = None
|
|
18
|
+
|
|
19
|
+
from .classifiers import run_default_classifiers, suppress_default_classifiers
|
|
20
|
+
from .models import ContentSample
|
|
21
|
+
|
|
22
|
+
SCREENSHOT_DIR = settings.LOG_DIR / "screenshots"
|
|
23
|
+
CAMERA_DIR = settings.LOG_DIR / "camera"
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
_FIREFOX_BINARY_CANDIDATES = ("firefox", "firefox-esr", "firefox-bin")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _find_firefox_binary() -> str | None:
|
|
30
|
+
"""Return the first available Firefox binary path or ``None``."""
|
|
31
|
+
|
|
32
|
+
for candidate in _FIREFOX_BINARY_CANDIDATES:
|
|
33
|
+
path = shutil.which(candidate)
|
|
34
|
+
if path:
|
|
35
|
+
return path
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _ensure_geckodriver() -> None:
|
|
40
|
+
"""Install geckodriver on demand when possible."""
|
|
41
|
+
|
|
42
|
+
if install_geckodriver is None: # pragma: no cover - dependency not installed
|
|
43
|
+
return
|
|
44
|
+
try:
|
|
45
|
+
install_geckodriver()
|
|
46
|
+
except Exception as exc: # pragma: no cover - external failures are rare in tests
|
|
47
|
+
logger.warning("Unable to ensure geckodriver availability: %s", exc)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def capture_screenshot(url: str, cookies=None) -> Path:
|
|
51
|
+
"""Capture a screenshot of ``url`` and save it to :data:`SCREENSHOT_DIR`.
|
|
52
|
+
|
|
53
|
+
``cookies`` can be an iterable of Selenium cookie mappings which will be
|
|
54
|
+
applied after the initial navigation and before the screenshot is taken.
|
|
55
|
+
"""
|
|
56
|
+
firefox_binary = _find_firefox_binary()
|
|
57
|
+
if not firefox_binary:
|
|
58
|
+
raise RuntimeError(
|
|
59
|
+
"Screenshot capture failed: Firefox is not installed. Install Firefox to enable screenshot capture."
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
options = Options()
|
|
63
|
+
options.binary_location = firefox_binary
|
|
64
|
+
options.add_argument("-headless")
|
|
65
|
+
_ensure_geckodriver()
|
|
66
|
+
try:
|
|
67
|
+
with webdriver.Firefox(options=options) as browser:
|
|
68
|
+
browser.set_window_size(1280, 720)
|
|
69
|
+
SCREENSHOT_DIR.mkdir(parents=True, exist_ok=True)
|
|
70
|
+
filename = SCREENSHOT_DIR / f"{datetime.utcnow():%Y%m%d%H%M%S}.png"
|
|
71
|
+
try:
|
|
72
|
+
browser.get(url)
|
|
73
|
+
except WebDriverException as exc:
|
|
74
|
+
logger.error("Failed to load %s: %s", url, exc)
|
|
75
|
+
if cookies:
|
|
76
|
+
for cookie in cookies:
|
|
77
|
+
try:
|
|
78
|
+
browser.add_cookie(cookie)
|
|
79
|
+
except WebDriverException as exc:
|
|
80
|
+
logger.error("Failed to apply cookie for %s: %s", url, exc)
|
|
81
|
+
browser.get(url)
|
|
82
|
+
if not browser.save_screenshot(str(filename)):
|
|
83
|
+
raise RuntimeError("Screenshot capture failed")
|
|
84
|
+
return filename
|
|
85
|
+
except WebDriverException as exc:
|
|
86
|
+
logger.error("Failed to capture screenshot from %s: %s", url, exc)
|
|
87
|
+
message = str(exc)
|
|
88
|
+
if "Unable to obtain driver for firefox" in message:
|
|
89
|
+
message = (
|
|
90
|
+
"Firefox WebDriver is unavailable. Install geckodriver or configure the GECKODRIVER environment variable so Selenium can locate it."
|
|
91
|
+
)
|
|
92
|
+
raise RuntimeError(f"Screenshot capture failed: {message}") from exc
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def capture_rpi_snapshot(timeout: int = 10) -> Path:
|
|
96
|
+
"""Capture a snapshot using the Raspberry Pi camera stack."""
|
|
97
|
+
|
|
98
|
+
tool_path = shutil.which("rpicam-still")
|
|
99
|
+
if not tool_path:
|
|
100
|
+
raise RuntimeError("rpicam-still is not available")
|
|
101
|
+
CAMERA_DIR.mkdir(parents=True, exist_ok=True)
|
|
102
|
+
timestamp = datetime.utcnow()
|
|
103
|
+
unique_suffix = uuid.uuid4().hex
|
|
104
|
+
filename = CAMERA_DIR / f"{timestamp:%Y%m%d%H%M%S}-{unique_suffix}.jpg"
|
|
105
|
+
try:
|
|
106
|
+
result = subprocess.run(
|
|
107
|
+
[tool_path, "-o", str(filename), "-t", "1"],
|
|
108
|
+
capture_output=True,
|
|
109
|
+
text=True,
|
|
110
|
+
check=False,
|
|
111
|
+
timeout=timeout,
|
|
112
|
+
)
|
|
113
|
+
except Exception as exc: # pragma: no cover - depends on camera stack
|
|
114
|
+
logger.error("Failed to invoke %s: %s", tool_path, exc)
|
|
115
|
+
raise RuntimeError(f"Snapshot capture failed: {exc}") from exc
|
|
116
|
+
if result.returncode != 0:
|
|
117
|
+
error = (result.stderr or result.stdout or "Snapshot capture failed").strip()
|
|
118
|
+
logger.error("rpicam-still exited with %s: %s", result.returncode, error)
|
|
119
|
+
raise RuntimeError(error)
|
|
120
|
+
if not filename.exists():
|
|
121
|
+
logger.error("Snapshot file %s was not created", filename)
|
|
122
|
+
raise RuntimeError("Snapshot capture failed")
|
|
123
|
+
return filename
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def save_screenshot(
|
|
127
|
+
path: Path,
|
|
128
|
+
node=None,
|
|
129
|
+
method: str = "",
|
|
130
|
+
transaction_uuid=None,
|
|
131
|
+
*,
|
|
132
|
+
content: str | None = None,
|
|
133
|
+
user=None,
|
|
134
|
+
link_duplicates: bool = False,
|
|
135
|
+
):
|
|
136
|
+
"""Save screenshot file info if not already recorded.
|
|
137
|
+
|
|
138
|
+
Returns the created :class:`ContentSample`. If ``link_duplicates`` is ``True``
|
|
139
|
+
and a sample with identical content already exists, the existing record is
|
|
140
|
+
returned instead of ``None``.
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
original = path
|
|
144
|
+
if not path.is_absolute():
|
|
145
|
+
path = settings.LOG_DIR / path
|
|
146
|
+
with path.open("rb") as fh:
|
|
147
|
+
digest = hashlib.sha256(fh.read()).hexdigest()
|
|
148
|
+
existing = ContentSample.objects.filter(hash=digest).first()
|
|
149
|
+
if existing:
|
|
150
|
+
if link_duplicates:
|
|
151
|
+
logger.info("Duplicate screenshot content; reusing existing sample")
|
|
152
|
+
return existing
|
|
153
|
+
logger.info("Duplicate screenshot content; record not created")
|
|
154
|
+
return None
|
|
155
|
+
stored_path = (original if not original.is_absolute() else path).as_posix()
|
|
156
|
+
data = {
|
|
157
|
+
"node": node,
|
|
158
|
+
"path": stored_path,
|
|
159
|
+
"method": method,
|
|
160
|
+
"hash": digest,
|
|
161
|
+
"kind": ContentSample.IMAGE,
|
|
162
|
+
}
|
|
163
|
+
if transaction_uuid is not None:
|
|
164
|
+
data["transaction_uuid"] = transaction_uuid
|
|
165
|
+
if content is not None:
|
|
166
|
+
data["content"] = content
|
|
167
|
+
if user is not None:
|
|
168
|
+
data["user"] = user
|
|
169
|
+
with suppress_default_classifiers():
|
|
170
|
+
sample = ContentSample.objects.create(**data)
|
|
171
|
+
run_default_classifiers(sample)
|
|
172
|
+
return sample
|