arthexis 0.1.12__py3-none-any.whl → 0.1.14__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 (107) hide show
  1. {arthexis-0.1.12.dist-info → arthexis-0.1.14.dist-info}/METADATA +222 -221
  2. arthexis-0.1.14.dist-info/RECORD +109 -0
  3. {arthexis-0.1.12.dist-info → arthexis-0.1.14.dist-info}/licenses/LICENSE +674 -674
  4. config/__init__.py +5 -5
  5. config/active_app.py +15 -15
  6. config/asgi.py +43 -29
  7. config/auth_app.py +7 -7
  8. config/celery.py +32 -25
  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 -716
  16. config/settings_helpers.py +109 -0
  17. config/urls.py +171 -166
  18. config/wsgi.py +17 -17
  19. core/admin.py +3771 -2772
  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 +133 -133
  27. core/environment.py +61 -61
  28. core/fields.py +168 -168
  29. core/form_fields.py +75 -0
  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 +100 -100
  36. core/mailer.py +85 -85
  37. core/middleware.py +91 -91
  38. core/models.py +3609 -2672
  39. core/notifications.py +105 -105
  40. core/public_wifi.py +267 -227
  41. core/reference_utils.py +108 -108
  42. core/release.py +721 -350
  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 +752 -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 +2095 -1511
  52. core/tests_liveupdate.py +17 -17
  53. core/urls.py +11 -11
  54. core/user_data.py +641 -633
  55. core/views.py +2175 -1382
  56. core/widgets.py +213 -51
  57. core/workgroup_urls.py +17 -17
  58. core/workgroup_views.py +94 -94
  59. nodes/admin.py +1720 -898
  60. nodes/apps.py +87 -70
  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 +1737 -1416
  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 +3810 -2497
  71. nodes/urls.py +15 -13
  72. nodes/utils.py +121 -105
  73. nodes/views.py +683 -451
  74. ocpp/admin.py +948 -804
  75. ocpp/apps.py +25 -25
  76. ocpp/consumers.py +1565 -1342
  77. ocpp/evcs.py +844 -931
  78. ocpp/evcs_discovery.py +158 -158
  79. ocpp/models.py +917 -915
  80. ocpp/reference_utils.py +42 -42
  81. ocpp/routing.py +11 -9
  82. ocpp/simulator.py +745 -724
  83. ocpp/status_display.py +26 -0
  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 -3485
  89. ocpp/transactions_io.py +189 -179
  90. ocpp/urls.py +50 -50
  91. ocpp/views.py +1479 -1151
  92. pages/admin.py +708 -536
  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 -169
  98. pages/middleware.py +205 -153
  99. pages/models.py +607 -426
  100. pages/tests.py +2612 -2083
  101. pages/urls.py +25 -25
  102. pages/utils.py +12 -12
  103. pages/views.py +1165 -1120
  104. arthexis-0.1.12.dist-info/RECORD +0 -102
  105. nodes/actions.py +0 -70
  106. {arthexis-0.1.12.dist-info → arthexis-0.1.14.dist-info}/WHEEL +0 -0
  107. {arthexis-0.1.12.dist-info → arthexis-0.1.14.dist-info}/top_level.txt +0 -0
core/widgets.py CHANGED
@@ -1,51 +1,213 @@
1
- from django import forms
2
- import json
3
-
4
-
5
- class CopyColorWidget(forms.TextInput):
6
- input_type = "color"
7
- template_name = "widgets/copy_color.html"
8
-
9
- class Media:
10
- js = ["core/copy_color.js"]
11
-
12
-
13
- class CodeEditorWidget(forms.Textarea):
14
- """Simple code editor widget for editing recipes."""
15
-
16
- def __init__(self, attrs=None):
17
- default_attrs = {"class": "code-editor"}
18
- if attrs:
19
- default_attrs.update(attrs)
20
- super().__init__(attrs=default_attrs)
21
-
22
- class Media:
23
- css = {"all": ["core/code_editor.css"]}
24
- js = ["core/code_editor.js"]
25
-
26
-
27
- class OdooProductWidget(forms.Select):
28
- """Widget for selecting an Odoo product."""
29
-
30
- template_name = "widgets/odoo_product.html"
31
-
32
- class Media:
33
- js = ["core/odoo_product.js"]
34
-
35
- def get_context(self, name, value, attrs):
36
- attrs = attrs or {}
37
- if isinstance(value, dict):
38
- attrs["data-current-id"] = str(value.get("id", ""))
39
- value = json.dumps(value)
40
- elif not value:
41
- value = ""
42
- return super().get_context(name, value, attrs)
43
-
44
- def value_from_datadict(self, data, files, name):
45
- raw = data.get(name)
46
- if not raw:
47
- return {}
48
- try:
49
- return json.loads(raw)
50
- except Exception:
51
- return {}
1
+ from collections import OrderedDict
2
+ from typing import Any
3
+
4
+ from django import forms
5
+ from django.forms.widgets import ClearableFileInput
6
+ import json
7
+
8
+
9
+ class CopyColorWidget(forms.TextInput):
10
+ input_type = "color"
11
+ template_name = "widgets/copy_color.html"
12
+
13
+ class Media:
14
+ js = ["core/copy_color.js"]
15
+
16
+
17
+ class CodeEditorWidget(forms.Textarea):
18
+ """Simple code editor widget for editing recipes."""
19
+
20
+ def __init__(self, attrs=None):
21
+ default_attrs = {"class": "code-editor"}
22
+ if attrs:
23
+ default_attrs.update(attrs)
24
+ super().__init__(attrs=default_attrs)
25
+
26
+ class Media:
27
+ css = {"all": ["core/code_editor.css"]}
28
+ js = ["core/code_editor.js"]
29
+
30
+
31
+ class OdooProductWidget(forms.Select):
32
+ """Widget for selecting an Odoo product."""
33
+
34
+ template_name = "widgets/odoo_product.html"
35
+
36
+ class Media:
37
+ js = ["core/odoo_product.js"]
38
+
39
+ def get_context(self, name, value, attrs):
40
+ attrs = attrs or {}
41
+ if isinstance(value, dict):
42
+ attrs["data-current-id"] = str(value.get("id", ""))
43
+ value = json.dumps(value)
44
+ elif not value:
45
+ value = ""
46
+ return super().get_context(name, value, attrs)
47
+
48
+ def value_from_datadict(self, data, files, name):
49
+ raw = data.get(name)
50
+ if not raw:
51
+ return {}
52
+ try:
53
+ return json.loads(raw)
54
+ except Exception:
55
+ return {}
56
+
57
+
58
+ class AdminBase64FileWidget(ClearableFileInput):
59
+ """Clearable file input that exposes base64 data for downloads."""
60
+
61
+ template_name = "widgets/admin_base64_file.html"
62
+
63
+ def __init__(
64
+ self,
65
+ *,
66
+ download_name: str | None = None,
67
+ content_type: str = "application/octet-stream",
68
+ **kwargs,
69
+ ) -> None:
70
+ self.download_name = download_name
71
+ self.content_type = content_type
72
+ super().__init__(**kwargs)
73
+
74
+ def is_initial(self, value):
75
+ if isinstance(value, str):
76
+ return bool(value)
77
+ return super().is_initial(value)
78
+
79
+ def format_value(self, value):
80
+ if isinstance(value, str):
81
+ return value
82
+ return super().format_value(value)
83
+
84
+ def get_context(self, name, value, attrs):
85
+ if isinstance(value, str):
86
+ base64_value = value.strip()
87
+ rendered_value = None
88
+ else:
89
+ base64_value = None
90
+ rendered_value = value
91
+ context = super().get_context(name, rendered_value, attrs)
92
+ widget_context = context["widget"]
93
+ widget_context["is_initial"] = bool(base64_value)
94
+ widget_context["base64_value"] = base64_value
95
+ widget_context["download_name"] = self.download_name or f"{name}.bin"
96
+ widget_context["content_type"] = self.content_type
97
+ return context
98
+
99
+
100
+ class RFIDDataWidget(forms.Textarea):
101
+ """Render RFID block dumps as a readable grid while keeping raw JSON editable."""
102
+
103
+ template_name = "admin/core/widgets/rfid_data_widget.html"
104
+
105
+ def __init__(self, attrs: dict[str, Any] | None = None) -> None:
106
+ default_attrs = {
107
+ "class": "vLargeTextField rfid-data-widget__input",
108
+ "rows": 8,
109
+ }
110
+ if attrs:
111
+ default_attrs.update(attrs)
112
+ super().__init__(attrs=default_attrs)
113
+
114
+ class Media:
115
+ css = {"all": ["core/rfid_data_widget.css"]}
116
+ js = ["core/rfid_data_widget.js"]
117
+
118
+ def format_value(self, value): # noqa: D401 - inherits docs
119
+ if value in ({}, []):
120
+ return "[]"
121
+ if value is None:
122
+ return "[]"
123
+ if isinstance(value, str):
124
+ return value
125
+ try:
126
+ return json.dumps(value, indent=2, sort_keys=True)
127
+ except (TypeError, ValueError):
128
+ try:
129
+ return json.dumps(list(value), indent=2, sort_keys=True)
130
+ except Exception:
131
+ return "[]"
132
+
133
+ def get_context(self, name, value, attrs):
134
+ context = super().get_context(name, value, attrs)
135
+ parsed_entries = self._parse_entries(value)
136
+ context["widget"]["sectors"] = self._build_sectors(parsed_entries)
137
+ context["widget"]["has_parse_error"] = bool(
138
+ context["widget"].get("value") and not parsed_entries
139
+ )
140
+ context["widget"]["byte_headers"] = [f"{index:02X}" for index in range(16)]
141
+ return context
142
+
143
+ def _parse_entries(self, value: Any) -> list[dict[str, Any]]:
144
+ if isinstance(value, str):
145
+ try:
146
+ value = json.loads(value)
147
+ except Exception:
148
+ return []
149
+ if not isinstance(value, list):
150
+ return []
151
+
152
+ entries: list[dict[str, Any]] = []
153
+ for entry in value:
154
+ if not isinstance(entry, dict):
155
+ continue
156
+ block = entry.get("block")
157
+ data = entry.get("data")
158
+ if not isinstance(block, int) or not isinstance(data, (list, tuple)):
159
+ continue
160
+
161
+ bytes_: list[str] = []
162
+ raw_bytes: list[int] = []
163
+ valid = True
164
+ for raw in list(data)[:16]:
165
+ try:
166
+ byte_value = int(raw)
167
+ except (TypeError, ValueError):
168
+ valid = False
169
+ break
170
+ byte_value = max(0, min(255, byte_value))
171
+ raw_bytes.append(byte_value)
172
+ bytes_.append(f"{byte_value:02X}")
173
+ if not valid:
174
+ continue
175
+
176
+ if len(bytes_) < 16:
177
+ bytes_.extend(["--"] * (16 - len(bytes_)))
178
+ raw_bytes.extend([0] * (16 - len(raw_bytes)))
179
+
180
+ text_chars: list[str] = []
181
+ for byte_value in raw_bytes:
182
+ if 32 <= byte_value <= 126:
183
+ text_chars.append(chr(byte_value))
184
+ else:
185
+ text_chars.append("·")
186
+ text_value = "".join(text_chars)
187
+
188
+ entries.append(
189
+ {
190
+ "block": block,
191
+ "sector": block // 4,
192
+ "offset": block % 4,
193
+ "key": entry.get("key"),
194
+ "bytes": bytes_,
195
+ "is_trailer": block % 4 == 3,
196
+ "text_value": text_value,
197
+ }
198
+ )
199
+
200
+ return sorted(entries, key=lambda item: item["block"])
201
+
202
+ def _build_sectors(self, entries: list[dict[str, Any]]):
203
+ sectors: OrderedDict[int, dict[str, Any]] = OrderedDict()
204
+ for entry in entries:
205
+ sector_key = entry["sector"]
206
+ sector = sectors.setdefault(
207
+ sector_key,
208
+ {"sector": sector_key, "blocks": []},
209
+ )
210
+ sector["blocks"].append(entry)
211
+ for sector in sectors.values():
212
+ sector["blocks"].sort(key=lambda block: block["offset"])
213
+ return list(sectors.values())
core/workgroup_urls.py CHANGED
@@ -1,17 +1,17 @@
1
- """URL routes for assistant profile endpoints."""
2
-
3
- from django.urls import path
4
-
5
- from . import workgroup_views as views
6
-
7
- app_name = "workgroup"
8
-
9
- urlpatterns = [
10
- path(
11
- "assistant-profiles/<int:user_id>/",
12
- views.issue_key,
13
- name="assistantprofile-issue",
14
- ),
15
- path("assistant/test/", views.assistant_test, name="assistant-test"),
16
- path("chat/", views.chat, name="chat"),
17
- ]
1
+ """URL routes for assistant profile endpoints."""
2
+
3
+ from django.urls import path
4
+
5
+ from . import workgroup_views as views
6
+
7
+ app_name = "workgroup"
8
+
9
+ urlpatterns = [
10
+ path(
11
+ "assistant-profiles/<int:user_id>/",
12
+ views.issue_key,
13
+ name="assistantprofile-issue",
14
+ ),
15
+ path("assistant/test/", views.assistant_test, name="assistant-test"),
16
+ path("chat/", views.chat, name="chat"),
17
+ ]
core/workgroup_views.py CHANGED
@@ -1,94 +1,94 @@
1
- """REST endpoints for AssistantProfile issuance and authentication."""
2
-
3
- from __future__ import annotations
4
-
5
- from functools import wraps
6
-
7
- from django.apps import apps
8
- from django.contrib.auth import get_user_model
9
- from django.forms.models import model_to_dict
10
- from django.http import HttpResponse, JsonResponse
11
- from django.views.decorators.csrf import csrf_exempt
12
- from django.views.decorators.http import require_GET, require_POST
13
-
14
- from .models import AssistantProfile, hash_key
15
-
16
-
17
- @csrf_exempt
18
- @require_POST
19
- def issue_key(request, user_id: int) -> JsonResponse:
20
- """Issue a new ``user_key`` for ``user_id``.
21
-
22
- The response reveals the plain key once. Store only the hash server-side.
23
- """
24
-
25
- user = get_user_model().objects.get(pk=user_id)
26
- profile, key = AssistantProfile.issue_key(user)
27
- return JsonResponse({"user_id": user_id, "user_key": key})
28
-
29
-
30
- def authenticate(view_func):
31
- """View decorator that validates the ``Authorization`` header."""
32
-
33
- @wraps(view_func)
34
- def wrapper(request, *args, **kwargs):
35
- header = request.META.get("HTTP_AUTHORIZATION", "")
36
- if not header.startswith("Bearer "):
37
- return HttpResponse(status=401)
38
-
39
- key_hash = hash_key(header.split(" ", 1)[1])
40
- try:
41
- profile = AssistantProfile.objects.get(
42
- user_key_hash=key_hash, is_active=True
43
- )
44
- except AssistantProfile.DoesNotExist:
45
- return HttpResponse(status=401)
46
-
47
- profile.touch()
48
- request.assistant_profile = profile
49
- request.chat_profile = profile
50
- return view_func(request, *args, **kwargs)
51
-
52
- return wrapper
53
-
54
-
55
- @require_GET
56
- @authenticate
57
- def assistant_test(request):
58
- """Return a simple greeting to confirm authentication."""
59
-
60
- profile = getattr(request, "assistant_profile", None)
61
- user_id = profile.user_id if profile else None
62
- return JsonResponse({"message": f"Hello from user {user_id}"})
63
-
64
-
65
- @require_GET
66
- @authenticate
67
- def chat(request):
68
- """Return serialized data from any model.
69
-
70
- Clients must provide ``model`` as ``app_label.ModelName`` and may include a
71
- ``pk`` to fetch a specific record. When ``pk`` is omitted, the view returns
72
- up to 100 records.
73
- """
74
-
75
- model_label = request.GET.get("model")
76
- if not model_label:
77
- return JsonResponse({"error": "model parameter required"}, status=400)
78
- try:
79
- model = apps.get_model(model_label)
80
- except LookupError:
81
- return JsonResponse({"error": "unknown model"}, status=400)
82
-
83
- qs = model.objects.all()
84
- pk = request.GET.get("pk")
85
- if pk is not None:
86
- try:
87
- obj = qs.get(pk=pk)
88
- except model.DoesNotExist:
89
- return JsonResponse({"error": "object not found"}, status=404)
90
- data = model_to_dict(obj)
91
- else:
92
- data = [model_to_dict(o) for o in qs[:100]]
93
-
94
- return JsonResponse({"data": data})
1
+ """REST endpoints for AssistantProfile issuance and authentication."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from functools import wraps
6
+
7
+ from django.apps import apps
8
+ from django.contrib.auth import get_user_model
9
+ from django.forms.models import model_to_dict
10
+ from django.http import HttpResponse, JsonResponse
11
+ from django.views.decorators.csrf import csrf_exempt
12
+ from django.views.decorators.http import require_GET, require_POST
13
+
14
+ from .models import AssistantProfile, hash_key
15
+
16
+
17
+ @csrf_exempt
18
+ @require_POST
19
+ def issue_key(request, user_id: int) -> JsonResponse:
20
+ """Issue a new ``user_key`` for ``user_id``.
21
+
22
+ The response reveals the plain key once. Store only the hash server-side.
23
+ """
24
+
25
+ user = get_user_model().objects.get(pk=user_id)
26
+ profile, key = AssistantProfile.issue_key(user)
27
+ return JsonResponse({"user_id": user_id, "user_key": key})
28
+
29
+
30
+ def authenticate(view_func):
31
+ """View decorator that validates the ``Authorization`` header."""
32
+
33
+ @wraps(view_func)
34
+ def wrapper(request, *args, **kwargs):
35
+ header = request.META.get("HTTP_AUTHORIZATION", "")
36
+ if not header.startswith("Bearer "):
37
+ return HttpResponse(status=401)
38
+
39
+ key_hash = hash_key(header.split(" ", 1)[1])
40
+ try:
41
+ profile = AssistantProfile.objects.get(
42
+ user_key_hash=key_hash, is_active=True
43
+ )
44
+ except AssistantProfile.DoesNotExist:
45
+ return HttpResponse(status=401)
46
+
47
+ profile.touch()
48
+ request.assistant_profile = profile
49
+ request.chat_profile = profile
50
+ return view_func(request, *args, **kwargs)
51
+
52
+ return wrapper
53
+
54
+
55
+ @require_GET
56
+ @authenticate
57
+ def assistant_test(request):
58
+ """Return a simple greeting to confirm authentication."""
59
+
60
+ profile = getattr(request, "assistant_profile", None)
61
+ user_id = profile.user_id if profile else None
62
+ return JsonResponse({"message": f"Hello from user {user_id}"})
63
+
64
+
65
+ @require_GET
66
+ @authenticate
67
+ def chat(request):
68
+ """Return serialized data from any model.
69
+
70
+ Clients must provide ``model`` as ``app_label.ModelName`` and may include a
71
+ ``pk`` to fetch a specific record. When ``pk`` is omitted, the view returns
72
+ up to 100 records.
73
+ """
74
+
75
+ model_label = request.GET.get("model")
76
+ if not model_label:
77
+ return JsonResponse({"error": "model parameter required"}, status=400)
78
+ try:
79
+ model = apps.get_model(model_label)
80
+ except LookupError:
81
+ return JsonResponse({"error": "unknown model"}, status=400)
82
+
83
+ qs = model.objects.all()
84
+ pk = request.GET.get("pk")
85
+ if pk is not None:
86
+ try:
87
+ obj = qs.get(pk=pk)
88
+ except model.DoesNotExist:
89
+ return JsonResponse({"error": "object not found"}, status=404)
90
+ data = model_to_dict(obj)
91
+ else:
92
+ data = [model_to_dict(o) for o in qs[:100]]
93
+
94
+ return JsonResponse({"data": data})