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
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("last-message/", views.last_net_message, name="last-net-message"),
12
- path("<slug:endpoint>/", views.public_node_endpoint, name="node-public-endpoint"),
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
- from django.conf import settings
7
- from selenium import webdriver
8
- from selenium.webdriver.firefox.options import Options
9
- from selenium.common.exceptions import WebDriverException
10
-
11
- from .models import ContentSample
12
-
13
- SCREENSHOT_DIR = settings.LOG_DIR / "screenshots"
14
- logger = logging.getLogger(__name__)
15
-
16
-
17
- def capture_screenshot(url: str, cookies=None) -> Path:
18
- """Capture a screenshot of ``url`` and save it to :data:`SCREENSHOT_DIR`.
19
-
20
- ``cookies`` can be an iterable of Selenium cookie mappings which will be
21
- applied after the initial navigation and before the screenshot is taken.
22
- """
23
- options = Options()
24
- options.add_argument("-headless")
25
- try:
26
- with webdriver.Firefox(options=options) as browser:
27
- browser.set_window_size(1280, 720)
28
- SCREENSHOT_DIR.mkdir(parents=True, exist_ok=True)
29
- filename = SCREENSHOT_DIR / f"{datetime.utcnow():%Y%m%d%H%M%S}.png"
30
- try:
31
- browser.get(url)
32
- except WebDriverException as exc:
33
- logger.error("Failed to load %s: %s", url, exc)
34
- if cookies:
35
- for cookie in cookies:
36
- try:
37
- browser.add_cookie(cookie)
38
- except WebDriverException as exc:
39
- logger.error("Failed to apply cookie for %s: %s", url, exc)
40
- browser.get(url)
41
- if not browser.save_screenshot(str(filename)):
42
- raise RuntimeError("Screenshot capture failed")
43
- return filename
44
- except WebDriverException as exc:
45
- logger.error("Failed to capture screenshot from %s: %s", url, exc)
46
- raise RuntimeError(f"Screenshot capture failed: {exc}") from exc
47
-
48
-
49
- def save_screenshot(path: Path, node=None, method: str = "", transaction_uuid=None):
50
- """Save screenshot file info if not already recorded.
51
-
52
- Returns the created :class:`ContentSample` or ``None`` if duplicate.
53
- """
54
-
55
- original = path
56
- if not path.is_absolute():
57
- path = settings.LOG_DIR / path
58
- with path.open("rb") as fh:
59
- digest = hashlib.sha256(fh.read()).hexdigest()
60
- if ContentSample.objects.filter(hash=digest).exists():
61
- logger.info("Duplicate screenshot content; record not created")
62
- return None
63
- stored_path = (original if not original.is_absolute() else path).as_posix()
64
- data = {
65
- "node": node,
66
- "path": stored_path,
67
- "method": method,
68
- "hash": digest,
69
- "kind": ContentSample.IMAGE,
70
- }
71
- if transaction_uuid is not None:
72
- data["transaction_uuid"] = transaction_uuid
73
- return ContentSample.objects.create(**data)
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