arthexis 0.1.3__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 (73) hide show
  1. arthexis-0.1.3.dist-info/METADATA +126 -0
  2. arthexis-0.1.3.dist-info/RECORD +73 -0
  3. arthexis-0.1.3.dist-info/WHEEL +5 -0
  4. arthexis-0.1.3.dist-info/licenses/LICENSE +21 -0
  5. arthexis-0.1.3.dist-info/top_level.txt +5 -0
  6. config/__init__.py +6 -0
  7. config/active_app.py +15 -0
  8. config/asgi.py +29 -0
  9. config/auth_app.py +8 -0
  10. config/celery.py +19 -0
  11. config/context_processors.py +68 -0
  12. config/loadenv.py +11 -0
  13. config/logging.py +43 -0
  14. config/middleware.py +25 -0
  15. config/offline.py +47 -0
  16. config/settings.py +374 -0
  17. config/urls.py +91 -0
  18. config/wsgi.py +17 -0
  19. core/__init__.py +0 -0
  20. core/admin.py +830 -0
  21. core/apps.py +67 -0
  22. core/backends.py +82 -0
  23. core/entity.py +97 -0
  24. core/environment.py +43 -0
  25. core/fields.py +70 -0
  26. core/lcd_screen.py +77 -0
  27. core/middleware.py +34 -0
  28. core/models.py +1277 -0
  29. core/notifications.py +95 -0
  30. core/release.py +451 -0
  31. core/system.py +111 -0
  32. core/tasks.py +100 -0
  33. core/tests.py +483 -0
  34. core/urls.py +11 -0
  35. core/user_data.py +333 -0
  36. core/views.py +431 -0
  37. nodes/__init__.py +0 -0
  38. nodes/actions.py +72 -0
  39. nodes/admin.py +347 -0
  40. nodes/apps.py +76 -0
  41. nodes/lcd.py +151 -0
  42. nodes/models.py +577 -0
  43. nodes/tasks.py +50 -0
  44. nodes/tests.py +1072 -0
  45. nodes/urls.py +13 -0
  46. nodes/utils.py +62 -0
  47. nodes/views.py +262 -0
  48. ocpp/__init__.py +0 -0
  49. ocpp/admin.py +392 -0
  50. ocpp/apps.py +24 -0
  51. ocpp/consumers.py +267 -0
  52. ocpp/evcs.py +911 -0
  53. ocpp/models.py +300 -0
  54. ocpp/routing.py +9 -0
  55. ocpp/simulator.py +357 -0
  56. ocpp/store.py +175 -0
  57. ocpp/tasks.py +27 -0
  58. ocpp/test_export_import.py +129 -0
  59. ocpp/test_rfid.py +345 -0
  60. ocpp/tests.py +1229 -0
  61. ocpp/transactions_io.py +119 -0
  62. ocpp/urls.py +17 -0
  63. ocpp/views.py +359 -0
  64. pages/__init__.py +0 -0
  65. pages/admin.py +231 -0
  66. pages/apps.py +10 -0
  67. pages/checks.py +41 -0
  68. pages/context_processors.py +72 -0
  69. pages/models.py +224 -0
  70. pages/tests.py +628 -0
  71. pages/urls.py +17 -0
  72. pages/utils.py +13 -0
  73. pages/views.py +191 -0
nodes/urls.py ADDED
@@ -0,0 +1,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("last-message/", views.last_net_message, name="last-net-message"),
12
+ path("<slug:endpoint>/", views.public_node_endpoint, name="node-public-endpoint"),
13
+ ]
nodes/utils.py ADDED
@@ -0,0 +1,62 @@
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) -> Path:
18
+ """Capture a screenshot of ``url`` and save it to :data:`SCREENSHOT_DIR`."""
19
+ options = Options()
20
+ options.add_argument("-headless")
21
+ try:
22
+ with webdriver.Firefox(options=options) as browser:
23
+ browser.set_window_size(1280, 720)
24
+ SCREENSHOT_DIR.mkdir(parents=True, exist_ok=True)
25
+ filename = SCREENSHOT_DIR / f"{datetime.utcnow():%Y%m%d%H%M%S}.png"
26
+ try:
27
+ browser.get(url)
28
+ except WebDriverException as exc:
29
+ logger.error("Failed to load %s: %s", url, exc)
30
+ if not browser.save_screenshot(str(filename)):
31
+ raise RuntimeError("Screenshot capture failed")
32
+ return filename
33
+ except WebDriverException as exc:
34
+ logger.error("Failed to capture screenshot from %s: %s", url, exc)
35
+ raise RuntimeError(f"Screenshot capture failed: {exc}") from exc
36
+
37
+
38
+ def save_screenshot(path: Path, node=None, method: str = "", transaction_uuid=None):
39
+ """Save screenshot file info if not already recorded.
40
+
41
+ Returns the created :class:`ContentSample` or ``None`` if duplicate.
42
+ """
43
+
44
+ original = path
45
+ if not path.is_absolute():
46
+ path = settings.LOG_DIR / path
47
+ with path.open("rb") as fh:
48
+ digest = hashlib.sha256(fh.read()).hexdigest()
49
+ if ContentSample.objects.filter(hash=digest).exists():
50
+ logger.info("Duplicate screenshot content; record not created")
51
+ return None
52
+ stored_path = (original if not original.is_absolute() else path).as_posix()
53
+ data = {
54
+ "node": node,
55
+ "path": stored_path,
56
+ "method": method,
57
+ "hash": digest,
58
+ "kind": ContentSample.IMAGE,
59
+ }
60
+ if transaction_uuid is not None:
61
+ data["transaction_uuid"] = transaction_uuid
62
+ return ContentSample.objects.create(**data)
nodes/views.py ADDED
@@ -0,0 +1,262 @@
1
+ import json
2
+ import base64
3
+
4
+ from django.http import JsonResponse
5
+ from django.views.decorators.csrf import csrf_exempt
6
+ from django.shortcuts import get_object_or_404
7
+ from django.conf import settings
8
+ from pathlib import Path
9
+
10
+ from utils.api import api_login_required
11
+
12
+ from cryptography.hazmat.primitives import serialization, hashes
13
+ from cryptography.hazmat.primitives.asymmetric import padding
14
+
15
+ from .models import Node, NetMessage, NodeRole
16
+ from .utils import capture_screenshot, save_screenshot
17
+
18
+
19
+ @api_login_required
20
+ def node_list(request):
21
+ """Return a JSON list of all known nodes."""
22
+
23
+ nodes = list(
24
+ Node.objects.values(
25
+ "hostname",
26
+ "address",
27
+ "port",
28
+ "last_seen",
29
+ "has_lcd_screen",
30
+ )
31
+ )
32
+ return JsonResponse({"nodes": nodes})
33
+
34
+
35
+ @csrf_exempt
36
+ def node_info(request):
37
+ """Return information about the local node and sign ``token`` if provided."""
38
+
39
+ node = Node.get_local()
40
+ if node is None:
41
+ node, _ = Node.register_current()
42
+
43
+ token = request.GET.get("token", "")
44
+ data = {
45
+ "hostname": node.hostname,
46
+ "address": node.address,
47
+ "port": node.port,
48
+ "mac_address": node.mac_address,
49
+ "public_key": node.public_key,
50
+ "has_lcd_screen": node.has_lcd_screen,
51
+ }
52
+
53
+ if token:
54
+ try:
55
+ priv_path = (
56
+ Path(node.base_path or settings.BASE_DIR)
57
+ / "security"
58
+ / f"{node.public_endpoint}"
59
+ )
60
+ private_key = serialization.load_pem_private_key(
61
+ priv_path.read_bytes(), password=None
62
+ )
63
+ signature = private_key.sign(
64
+ token.encode(),
65
+ padding.PKCS1v15(),
66
+ hashes.SHA256(),
67
+ )
68
+ data["token_signature"] = base64.b64encode(signature).decode()
69
+ except Exception:
70
+ pass
71
+
72
+ response = JsonResponse(data)
73
+ response["Access-Control-Allow-Origin"] = "*"
74
+ return response
75
+
76
+
77
+ @csrf_exempt
78
+ @api_login_required
79
+ def register_node(request):
80
+ """Register or update a node from POSTed JSON data."""
81
+
82
+ if request.method != "POST":
83
+ return JsonResponse({"detail": "POST required"}, status=400)
84
+
85
+ try:
86
+ data = json.loads(request.body.decode())
87
+ except json.JSONDecodeError:
88
+ data = request.POST
89
+
90
+ hostname = data.get("hostname")
91
+ address = data.get("address")
92
+ port = data.get("port", 8000)
93
+ mac_address = data.get("mac_address")
94
+ public_key = data.get("public_key")
95
+ token = data.get("token")
96
+ signature = data.get("signature")
97
+ has_lcd_screen = data.get("has_lcd_screen")
98
+
99
+ if not hostname or not address or not mac_address:
100
+ return JsonResponse(
101
+ {"detail": "hostname, address and mac_address required"}, status=400
102
+ )
103
+
104
+ verified = False
105
+ if public_key and token and signature:
106
+ try:
107
+ pub = serialization.load_pem_public_key(public_key.encode())
108
+ pub.verify(
109
+ base64.b64decode(signature),
110
+ token.encode(),
111
+ padding.PKCS1v15(),
112
+ hashes.SHA256(),
113
+ )
114
+ verified = True
115
+ except Exception:
116
+ return JsonResponse({"detail": "invalid signature"}, status=403)
117
+
118
+ mac_address = mac_address.lower()
119
+ defaults = {
120
+ "hostname": hostname,
121
+ "address": address,
122
+ "port": port,
123
+ "has_lcd_screen": bool(has_lcd_screen),
124
+ }
125
+ if verified:
126
+ defaults["public_key"] = public_key
127
+
128
+ node, created = Node.objects.get_or_create(
129
+ mac_address=mac_address,
130
+ defaults=defaults,
131
+ )
132
+ if not created:
133
+ node.hostname = hostname
134
+ node.address = address
135
+ node.port = port
136
+ update_fields = ["hostname", "address", "port"]
137
+ if verified:
138
+ node.public_key = public_key
139
+ update_fields.append("public_key")
140
+ if has_lcd_screen is not None:
141
+ node.has_lcd_screen = bool(has_lcd_screen)
142
+ update_fields.append("has_lcd_screen")
143
+ node.save(update_fields=update_fields)
144
+ return JsonResponse(
145
+ {"id": node.id, "detail": f"Node already exists (id: {node.id})"}
146
+ )
147
+
148
+ return JsonResponse({"id": node.id})
149
+
150
+
151
+ @api_login_required
152
+ def capture(request):
153
+ """Capture a screenshot of the site's root URL and record it."""
154
+
155
+ url = request.build_absolute_uri("/")
156
+ try:
157
+ path = capture_screenshot(url)
158
+ except Exception as exc: # pragma: no cover - depends on selenium setup
159
+ return JsonResponse({"detail": str(exc)}, status=500)
160
+ node = Node.get_local()
161
+ screenshot = save_screenshot(path, node=node, method=request.method)
162
+ node_id = screenshot.node.id if screenshot and screenshot.node else None
163
+ return JsonResponse({"screenshot": str(path), "node": node_id})
164
+
165
+
166
+ @csrf_exempt
167
+ @api_login_required
168
+ def public_node_endpoint(request, endpoint):
169
+ """Public API endpoint for a node.
170
+
171
+ - ``GET`` returns information about the node.
172
+ - ``POST`` broadcasts the request body as a :class:`NetMessage`.
173
+ """
174
+
175
+ node = get_object_or_404(
176
+ Node, public_endpoint=endpoint, enable_public_api=True
177
+ )
178
+
179
+ if request.method == "GET":
180
+ data = {
181
+ "hostname": node.hostname,
182
+ "address": node.address,
183
+ "port": node.port,
184
+ "badge_color": node.badge_color,
185
+ "last_seen": node.last_seen,
186
+ }
187
+ return JsonResponse(data)
188
+
189
+ if request.method == "POST":
190
+ NetMessage.broadcast(
191
+ subject=request.method,
192
+ body=request.body.decode("utf-8") if request.body else "",
193
+ seen=[str(node.uuid)],
194
+ )
195
+ return JsonResponse({"status": "stored"})
196
+
197
+ return JsonResponse({"detail": "Method not allowed"}, status=405)
198
+
199
+
200
+ @csrf_exempt
201
+ def net_message(request):
202
+ """Receive a network message and continue propagation."""
203
+
204
+ if request.method != "POST":
205
+ return JsonResponse({"detail": "POST required"}, status=400)
206
+ try:
207
+ data = json.loads(request.body.decode())
208
+ except json.JSONDecodeError:
209
+ return JsonResponse({"detail": "invalid json"}, status=400)
210
+
211
+ signature = request.headers.get("X-Signature")
212
+ sender_id = data.get("sender")
213
+ if not signature or not sender_id:
214
+ return JsonResponse({"detail": "signature required"}, status=403)
215
+ node = Node.objects.filter(uuid=sender_id).first()
216
+ if not node or not node.public_key:
217
+ return JsonResponse({"detail": "unknown sender"}, status=403)
218
+ try:
219
+ public_key = serialization.load_pem_public_key(node.public_key.encode())
220
+ public_key.verify(
221
+ base64.b64decode(signature),
222
+ request.body,
223
+ padding.PKCS1v15(),
224
+ hashes.SHA256(),
225
+ )
226
+ except Exception:
227
+ return JsonResponse({"detail": "invalid signature"}, status=403)
228
+
229
+ msg_uuid = data.get("uuid")
230
+ subject = data.get("subject", "")
231
+ body = data.get("body", "")
232
+ reach_name = data.get("reach")
233
+ reach_role = None
234
+ if reach_name:
235
+ reach_role = NodeRole.objects.filter(name=reach_name).first()
236
+ seen = data.get("seen", [])
237
+ if not msg_uuid:
238
+ return JsonResponse({"detail": "uuid required"}, status=400)
239
+ msg, created = NetMessage.objects.get_or_create(
240
+ uuid=msg_uuid,
241
+ defaults={"subject": subject[:64], "body": body[:256], "reach": reach_role},
242
+ )
243
+ if not created:
244
+ msg.subject = subject[:64]
245
+ msg.body = body[:256]
246
+ update_fields = ["subject", "body"]
247
+ if reach_role and msg.reach_id != reach_role.id:
248
+ msg.reach = reach_role
249
+ update_fields.append("reach")
250
+ msg.save(update_fields=update_fields)
251
+ msg.propagate(seen=seen)
252
+ return JsonResponse({"status": "propagated", "complete": msg.complete})
253
+
254
+
255
+ @api_login_required
256
+ def last_net_message(request):
257
+ """Return the most recent :class:`NetMessage`."""
258
+
259
+ msg = NetMessage.objects.order_by("-created").first()
260
+ if not msg:
261
+ return JsonResponse({"subject": "", "body": ""})
262
+ return JsonResponse({"subject": msg.subject, "body": msg.body})
ocpp/__init__.py ADDED
File without changes