arthexis 0.1.16__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 (63) hide show
  1. {arthexis-0.1.16.dist-info → arthexis-0.1.26.dist-info}/METADATA +84 -35
  2. arthexis-0.1.26.dist-info/RECORD +111 -0
  3. config/asgi.py +1 -15
  4. config/middleware.py +47 -1
  5. config/settings.py +15 -30
  6. config/urls.py +53 -1
  7. core/admin.py +540 -450
  8. core/apps.py +0 -6
  9. core/auto_upgrade.py +19 -4
  10. core/backends.py +13 -3
  11. core/changelog.py +66 -5
  12. core/environment.py +4 -5
  13. core/models.py +1566 -203
  14. core/notifications.py +1 -1
  15. core/reference_utils.py +10 -11
  16. core/release.py +55 -7
  17. core/sigil_builder.py +2 -2
  18. core/sigil_resolver.py +1 -66
  19. core/system.py +268 -2
  20. core/tasks.py +174 -48
  21. core/tests.py +314 -16
  22. core/user_data.py +42 -2
  23. core/views.py +278 -183
  24. nodes/admin.py +557 -65
  25. nodes/apps.py +11 -0
  26. nodes/models.py +658 -113
  27. nodes/rfid_sync.py +1 -1
  28. nodes/tasks.py +97 -2
  29. nodes/tests.py +1212 -116
  30. nodes/urls.py +15 -1
  31. nodes/utils.py +51 -3
  32. nodes/views.py +1239 -154
  33. ocpp/admin.py +979 -152
  34. ocpp/consumers.py +268 -28
  35. ocpp/models.py +488 -3
  36. ocpp/network.py +398 -0
  37. ocpp/store.py +6 -4
  38. ocpp/tasks.py +296 -2
  39. ocpp/test_export_import.py +1 -0
  40. ocpp/test_rfid.py +121 -4
  41. ocpp/tests.py +950 -11
  42. ocpp/transactions_io.py +9 -1
  43. ocpp/urls.py +3 -3
  44. ocpp/views.py +596 -51
  45. pages/admin.py +262 -30
  46. pages/apps.py +35 -0
  47. pages/context_processors.py +26 -21
  48. pages/defaults.py +1 -1
  49. pages/forms.py +31 -8
  50. pages/middleware.py +6 -2
  51. pages/models.py +77 -2
  52. pages/module_defaults.py +5 -5
  53. pages/site_config.py +137 -0
  54. pages/tests.py +885 -109
  55. pages/urls.py +13 -2
  56. pages/utils.py +70 -0
  57. pages/views.py +558 -55
  58. arthexis-0.1.16.dist-info/RECORD +0 -111
  59. core/workgroup_urls.py +0 -17
  60. core/workgroup_views.py +0 -94
  61. {arthexis-0.1.16.dist-info → arthexis-0.1.26.dist-info}/WHEEL +0 -0
  62. {arthexis-0.1.16.dist-info → arthexis-0.1.26.dist-info}/licenses/LICENSE +0 -0
  63. {arthexis-0.1.16.dist-info → arthexis-0.1.26.dist-info}/top_level.txt +0 -0
nodes/urls.py CHANGED
@@ -8,8 +8,22 @@ urlpatterns = [
8
8
  path("register/", views.register_node, name="register-node"),
9
9
  path("screenshot/", views.capture, name="node-screenshot"),
10
10
  path("net-message/", views.net_message, name="net-message"),
11
- path("last-message/", views.last_net_message, name="last-net-message"),
11
+ path("net-message/pull/", views.net_message_pull, name="net-message-pull"),
12
12
  path("rfid/export/", views.export_rfids, name="node-rfid-export"),
13
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"),
14
28
  path("<slug:endpoint>/", views.public_node_endpoint, name="node-public-endpoint"),
15
29
  ]
nodes/utils.py CHANGED
@@ -11,6 +11,11 @@ from selenium import webdriver
11
11
  from selenium.webdriver.firefox.options import Options
12
12
  from selenium.common.exceptions import WebDriverException
13
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
+
14
19
  from .classifiers import run_default_classifiers, suppress_default_classifiers
15
20
  from .models import ContentSample
16
21
 
@@ -18,6 +23,29 @@ SCREENSHOT_DIR = settings.LOG_DIR / "screenshots"
18
23
  CAMERA_DIR = settings.LOG_DIR / "camera"
19
24
  logger = logging.getLogger(__name__)
20
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
+
21
49
 
22
50
  def capture_screenshot(url: str, cookies=None) -> Path:
23
51
  """Capture a screenshot of ``url`` and save it to :data:`SCREENSHOT_DIR`.
@@ -25,8 +53,16 @@ def capture_screenshot(url: str, cookies=None) -> Path:
25
53
  ``cookies`` can be an iterable of Selenium cookie mappings which will be
26
54
  applied after the initial navigation and before the screenshot is taken.
27
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
+
28
62
  options = Options()
63
+ options.binary_location = firefox_binary
29
64
  options.add_argument("-headless")
65
+ _ensure_geckodriver()
30
66
  try:
31
67
  with webdriver.Firefox(options=options) as browser:
32
68
  browser.set_window_size(1280, 720)
@@ -48,7 +84,12 @@ def capture_screenshot(url: str, cookies=None) -> Path:
48
84
  return filename
49
85
  except WebDriverException as exc:
50
86
  logger.error("Failed to capture screenshot from %s: %s", url, exc)
51
- raise RuntimeError(f"Screenshot capture failed: {exc}") from 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
52
93
 
53
94
 
54
95
  def capture_rpi_snapshot(timeout: int = 10) -> Path:
@@ -90,10 +131,13 @@ def save_screenshot(
90
131
  *,
91
132
  content: str | None = None,
92
133
  user=None,
134
+ link_duplicates: bool = False,
93
135
  ):
94
136
  """Save screenshot file info if not already recorded.
95
137
 
96
- Returns the created :class:`ContentSample` or ``None`` if duplicate.
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``.
97
141
  """
98
142
 
99
143
  original = path
@@ -101,7 +145,11 @@ def save_screenshot(
101
145
  path = settings.LOG_DIR / path
102
146
  with path.open("rb") as fh:
103
147
  digest = hashlib.sha256(fh.read()).hexdigest()
104
- if ContentSample.objects.filter(hash=digest).exists():
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
105
153
  logger.info("Duplicate screenshot content; record not created")
106
154
  return None
107
155
  stored_path = (original if not original.is_absolute() else path).as_posix()