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
core/sigil_resolver.py CHANGED
@@ -1,284 +1,250 @@
1
- import logging
2
- import os
3
- import shutil
4
- import subprocess
5
- from functools import lru_cache
6
- from typing import Optional
7
-
8
- from django.apps import apps
9
- from django.conf import settings
10
- from django.core import serializers
11
- from django.db import models
12
-
13
- from .sigil_context import get_context
14
-
15
- logger = logging.getLogger("core.entity")
16
-
17
-
18
- def _is_wizard_mode() -> bool:
19
- """Return ``True`` when the application is running in wizard mode."""
20
-
21
- flag = getattr(settings, "WIZARD_MODE", False)
22
- if isinstance(flag, str):
23
- return flag.lower() in {"1", "true", "yes", "on"}
24
- return bool(flag)
25
-
26
-
27
- def _first_instance(model: type[models.Model]) -> Optional[models.Model]:
28
- qs = model.objects
29
- ordering = list(getattr(model._meta, "ordering", []))
30
- if ordering:
31
- qs = qs.order_by(*ordering)
32
- else:
33
- qs = qs.order_by("?")
34
- return qs.first()
35
-
36
-
37
- @lru_cache(maxsize=1)
38
- def _find_gway_command() -> Optional[str]:
39
- path = shutil.which("gway")
40
- if path:
41
- return path
42
- for candidate in ("~/.local/bin/gway", "/usr/local/bin/gway"):
43
- expanded = os.path.expanduser(candidate)
44
- if os.path.isfile(expanded) and os.access(expanded, os.X_OK):
45
- return expanded
46
- return None
47
-
48
-
49
- def _resolve_with_gway(sigil: str) -> Optional[str]:
50
- command = _find_gway_command()
51
- if not command:
52
- return None
53
- timeout = 60 if _is_wizard_mode() else 1
54
- try:
55
- result = subprocess.run(
56
- [command, "-e", sigil],
57
- check=False,
58
- stdout=subprocess.PIPE,
59
- stderr=subprocess.PIPE,
60
- text=True,
61
- timeout=timeout,
62
- )
63
- except subprocess.TimeoutExpired:
64
- logger.warning(
65
- "gway timed out after %s seconds while resolving sigil %s",
66
- timeout,
67
- sigil,
68
- )
69
- return None
70
- except Exception:
71
- logger.exception("Failed executing gway for sigil %s", sigil)
72
- return None
73
- if result.returncode != 0:
74
- logger.warning(
75
- "gway exited with status %s while resolving sigil %s",
76
- result.returncode,
77
- sigil,
78
- )
79
- return None
80
- return result.stdout.strip()
81
-
82
-
83
- def _failed_resolution(token: str) -> str:
84
- sigil = f"[{token}]"
85
- resolved = _resolve_with_gway(sigil)
86
- if resolved is not None:
87
- return resolved
88
- return sigil
89
-
90
-
91
- def _resolve_token(token: str, current: Optional[models.Model] = None) -> str:
92
- original_token = token
93
- i = 0
94
- n = len(token)
95
- root_name = ""
96
- while i < n and token[i] not in ":=.":
97
- root_name += token[i]
98
- i += 1
99
- if not root_name:
100
- return _failed_resolution(original_token)
101
- filter_field = None
102
- if i < n and token[i] == ":":
103
- i += 1
104
- field = ""
105
- while i < n and token[i] != "=":
106
- field += token[i]
107
- i += 1
108
- if i == n:
109
- return _failed_resolution(original_token)
110
- filter_field = field.replace("-", "_")
111
- instance_id = None
112
- if i < n and token[i] == "=":
113
- i += 1
114
- start = i
115
- depth = 0
116
- while i < n:
117
- ch = token[i]
118
- if ch == "[":
119
- depth += 1
120
- elif ch == "]" and depth:
121
- depth -= 1
122
- elif ch == "." and depth == 0:
123
- break
124
- i += 1
125
- instance_id = token[start:i]
126
- key = None
127
- if i < n and token[i] == ".":
128
- i += 1
129
- start = i
130
- while i < n and token[i] != "=":
131
- i += 1
132
- key = token[start:i]
133
- param = None
134
- if i < n and token[i] == "=":
135
- param = token[i + 1 :]
136
- normalized_root = root_name.replace("-", "_")
137
- lookup_root = normalized_root.upper()
138
- raw_key = key
139
- normalized_key = None
140
- key_upper = None
141
- key_lower = None
142
- if key:
143
- normalized_key = key.replace("-", "_")
144
- key_upper = normalized_key.upper()
145
- key_lower = normalized_key.lower()
146
- if param:
147
- param = resolve_sigils(param, current)
148
- if instance_id:
149
- instance_id = resolve_sigils(instance_id, current)
150
- SigilRoot = apps.get_model("core", "SigilRoot")
151
- try:
152
- root = SigilRoot.objects.get(prefix__iexact=lookup_root)
153
- if root.context_type == SigilRoot.Context.CONFIG:
154
- if not normalized_key:
155
- return ""
156
- if root.prefix.upper() == "ENV":
157
- candidates = []
158
- if raw_key:
159
- candidates.append(raw_key.replace("-", "_"))
160
- if normalized_key:
161
- candidates.append(normalized_key)
162
- if key_upper:
163
- candidates.append(key_upper)
164
- if key_lower:
165
- candidates.append(key_lower)
166
- seen_candidates: set[str] = set()
167
- for candidate in candidates:
168
- if not candidate or candidate in seen_candidates:
169
- continue
170
- seen_candidates.add(candidate)
171
- val = os.environ.get(candidate)
172
- if val is not None:
173
- return val
174
- logger.warning(
175
- "Missing environment variable for sigil [ENV.%s]",
176
- key_upper or normalized_key or raw_key or "",
177
- )
178
- return _failed_resolution(original_token)
179
- if root.prefix.upper() == "SYS":
180
- for candidate in [normalized_key, key_upper, key_lower]:
181
- if not candidate:
182
- continue
183
- sentinel = object()
184
- value = getattr(settings, candidate, sentinel)
185
- if value is not sentinel:
186
- return str(value)
187
- fallback = _resolve_with_gway(f"[{original_token}]")
188
- if fallback is not None:
189
- return fallback
190
- return ""
191
- elif root.context_type == SigilRoot.Context.ENTITY:
192
- model = root.content_type.model_class() if root.content_type else None
193
- instance = None
194
- if model:
195
- if instance_id:
196
- try:
197
- if filter_field:
198
- field_name = filter_field.lower()
199
- try:
200
- field_obj = model._meta.get_field(field_name)
201
- except Exception:
202
- field_obj = None
203
- lookup: dict[str, str] = {}
204
- if field_obj and isinstance(field_obj, models.CharField):
205
- lookup = {f"{field_name}__iexact": instance_id}
206
- else:
207
- lookup = {field_name: instance_id}
208
- instance = model.objects.filter(**lookup).first()
209
- else:
210
- instance = model.objects.filter(pk=instance_id).first()
211
- except Exception:
212
- instance = None
213
- if instance is None and not filter_field:
214
- for field in model._meta.fields:
215
- if field.unique and isinstance(field, models.CharField):
216
- instance = model.objects.filter(
217
- **{f"{field.name}__iexact": instance_id}
218
- ).first()
219
- if instance:
220
- break
221
- elif current and isinstance(current, model):
222
- instance = current
223
- else:
224
- ctx = get_context()
225
- inst_pk = ctx.get(model)
226
- if inst_pk is not None:
227
- instance = model.objects.filter(pk=inst_pk).first()
228
- if instance is None:
229
- instance = _first_instance(model)
230
- if instance:
231
- if normalized_key:
232
- field = next(
233
- (
234
- f
235
- for f in model._meta.fields
236
- if f.name.lower() == (key_lower or "")
237
- ),
238
- None,
239
- )
240
- if field:
241
- val = getattr(instance, field.attname)
242
- return "" if val is None else str(val)
243
- return _failed_resolution(original_token)
244
- return serializers.serialize("json", [instance])
245
- return _failed_resolution(original_token)
246
- except SigilRoot.DoesNotExist:
247
- logger.warning("Unknown sigil root [%s]", lookup_root)
248
- except Exception:
249
- logger.exception(
250
- "Error resolving sigil [%s.%s]",
251
- lookup_root,
252
- key_upper or normalized_key or raw_key,
253
- )
254
- return _failed_resolution(original_token)
255
-
256
-
257
- def resolve_sigils(text: str, current: Optional[models.Model] = None) -> str:
258
- result = ""
259
- i = 0
260
- while i < len(text):
261
- if text[i] == "[":
262
- depth = 1
263
- j = i + 1
264
- while j < len(text) and depth:
265
- if text[j] == "[":
266
- depth += 1
267
- elif text[j] == "]":
268
- depth -= 1
269
- j += 1
270
- if depth:
271
- result += text[i]
272
- i += 1
273
- continue
274
- token = text[i + 1 : j - 1]
275
- result += _resolve_token(token, current)
276
- i = j
277
- else:
278
- result += text[i]
279
- i += 1
280
- return result
281
-
282
-
283
- def resolve_sigil(sigil: str, current: Optional[models.Model] = None) -> str:
284
- return resolve_sigils(sigil, current)
1
+ import logging
2
+ import os
3
+ from typing import Optional
4
+
5
+ from django.apps import apps
6
+ from django.conf import settings
7
+ from django.core import serializers
8
+ from django.db import models
9
+
10
+ from .sigil_context import get_context
11
+ from .system import get_system_sigil_values, resolve_system_namespace_value
12
+
13
+ logger = logging.getLogger("core.entity")
14
+
15
+
16
+ def _first_instance(model: type[models.Model]) -> Optional[models.Model]:
17
+ qs = model.objects
18
+ ordering = list(getattr(model._meta, "ordering", []))
19
+ if ordering:
20
+ qs = qs.order_by(*ordering)
21
+ else:
22
+ qs = qs.order_by("?")
23
+ return qs.first()
24
+
25
+
26
+ def _failed_resolution(token: str) -> str:
27
+ return f"[{token}]"
28
+
29
+
30
+ def _resolve_token(token: str, current: Optional[models.Model] = None) -> str:
31
+ original_token = token
32
+ i = 0
33
+ n = len(token)
34
+ root_name = ""
35
+ while i < n and token[i] not in ":=.":
36
+ root_name += token[i]
37
+ i += 1
38
+ if not root_name:
39
+ return _failed_resolution(original_token)
40
+ filter_field = None
41
+ if i < n and token[i] == ":":
42
+ i += 1
43
+ field = ""
44
+ while i < n and token[i] != "=":
45
+ field += token[i]
46
+ i += 1
47
+ if i == n:
48
+ return _failed_resolution(original_token)
49
+ filter_field = field.replace("-", "_")
50
+ instance_id = None
51
+ if i < n and token[i] == "=":
52
+ i += 1
53
+ start = i
54
+ depth = 0
55
+ while i < n:
56
+ ch = token[i]
57
+ if ch == "[":
58
+ depth += 1
59
+ elif ch == "]" and depth:
60
+ depth -= 1
61
+ elif ch == "." and depth == 0:
62
+ break
63
+ i += 1
64
+ instance_id = token[start:i]
65
+ key = None
66
+ if i < n and token[i] == ".":
67
+ i += 1
68
+ start = i
69
+ while i < n and token[i] != "=":
70
+ i += 1
71
+ key = token[start:i]
72
+ param = None
73
+ if i < n and token[i] == "=":
74
+ param = token[i + 1 :]
75
+ normalized_root = root_name.replace("-", "_")
76
+ lookup_root = normalized_root.upper()
77
+ raw_key = key
78
+ normalized_key = None
79
+ key_upper = None
80
+ key_lower = None
81
+ if key:
82
+ normalized_key = key.replace("-", "_")
83
+ key_upper = normalized_key.upper()
84
+ key_lower = normalized_key.lower()
85
+ if param:
86
+ param = resolve_sigils(param, current)
87
+ if instance_id:
88
+ instance_id = resolve_sigils(instance_id, current)
89
+ SigilRoot = apps.get_model("core", "SigilRoot")
90
+ try:
91
+ root = SigilRoot.objects.get(prefix__iexact=lookup_root)
92
+ except SigilRoot.DoesNotExist:
93
+ logger.warning("Unknown sigil root [%s]", lookup_root)
94
+ return _failed_resolution(original_token)
95
+ except Exception:
96
+ logger.exception(
97
+ "Error resolving sigil [%s.%s]",
98
+ lookup_root,
99
+ key_upper or normalized_key or raw_key,
100
+ )
101
+ return _failed_resolution(original_token)
102
+
103
+ try:
104
+ if root.context_type == SigilRoot.Context.CONFIG:
105
+ if not normalized_key:
106
+ return ""
107
+ if root.prefix.upper() == "ENV":
108
+ candidates = []
109
+ if raw_key:
110
+ candidates.append(raw_key.replace("-", "_"))
111
+ if normalized_key:
112
+ candidates.append(normalized_key)
113
+ if key_upper:
114
+ candidates.append(key_upper)
115
+ if key_lower:
116
+ candidates.append(key_lower)
117
+ seen_candidates: set[str] = set()
118
+ for candidate in candidates:
119
+ if not candidate or candidate in seen_candidates:
120
+ continue
121
+ seen_candidates.add(candidate)
122
+ val = os.environ.get(candidate)
123
+ if val is not None:
124
+ return val
125
+ logger.warning(
126
+ "Missing environment variable for sigil [ENV.%s]",
127
+ key_upper or normalized_key or raw_key or "",
128
+ )
129
+ return _failed_resolution(original_token)
130
+ if root.prefix.upper() == "CONF":
131
+ for candidate in [normalized_key, key_upper, key_lower]:
132
+ if not candidate:
133
+ continue
134
+ sentinel = object()
135
+ value = getattr(settings, candidate, sentinel)
136
+ if value is not sentinel:
137
+ return str(value)
138
+ return ""
139
+ if root.prefix.upper() == "SYS":
140
+ values = get_system_sigil_values()
141
+ candidates = {
142
+ key_upper,
143
+ normalized_key.upper() if normalized_key else None,
144
+ (raw_key or "").upper(),
145
+ }
146
+ for candidate in candidates:
147
+ if not candidate:
148
+ continue
149
+ if candidate in values:
150
+ return values[candidate]
151
+ resolved = resolve_system_namespace_value(candidate)
152
+ if resolved is not None:
153
+ return resolved
154
+ logger.warning(
155
+ "Missing system information for sigil [SYS.%s]",
156
+ key_upper or normalized_key or raw_key or "",
157
+ )
158
+ return _failed_resolution(original_token)
159
+ elif root.context_type == SigilRoot.Context.ENTITY:
160
+ model = root.content_type.model_class() if root.content_type else None
161
+ instance = None
162
+ if model:
163
+ if instance_id:
164
+ try:
165
+ if filter_field:
166
+ field_name = filter_field.lower()
167
+ try:
168
+ field_obj = model._meta.get_field(field_name)
169
+ except Exception:
170
+ field_obj = None
171
+ lookup: dict[str, str] = {}
172
+ if field_obj and isinstance(field_obj, models.CharField):
173
+ lookup = {f"{field_name}__iexact": instance_id}
174
+ else:
175
+ lookup = {field_name: instance_id}
176
+ instance = model.objects.filter(**lookup).first()
177
+ else:
178
+ instance = model.objects.filter(pk=instance_id).first()
179
+ except Exception:
180
+ instance = None
181
+ if instance is None and not filter_field:
182
+ for field in model._meta.fields:
183
+ if field.unique and isinstance(field, models.CharField):
184
+ instance = model.objects.filter(
185
+ **{f"{field.name}__iexact": instance_id}
186
+ ).first()
187
+ if instance:
188
+ break
189
+ elif current and isinstance(current, model):
190
+ instance = current
191
+ else:
192
+ ctx = get_context()
193
+ inst_pk = ctx.get(model)
194
+ if inst_pk is not None:
195
+ instance = model.objects.filter(pk=inst_pk).first()
196
+ if instance is None:
197
+ instance = _first_instance(model)
198
+ if instance:
199
+ if normalized_key:
200
+ field = next(
201
+ (
202
+ f
203
+ for f in model._meta.fields
204
+ if f.name.lower() == (key_lower or "")
205
+ ),
206
+ None,
207
+ )
208
+ if field:
209
+ val = getattr(instance, field.attname)
210
+ return "" if val is None else str(val)
211
+ return _failed_resolution(original_token)
212
+ return serializers.serialize("json", [instance])
213
+ return _failed_resolution(original_token)
214
+ except Exception:
215
+ logger.exception(
216
+ "Error resolving sigil [%s.%s]",
217
+ lookup_root,
218
+ key_upper or normalized_key or raw_key,
219
+ )
220
+ return _failed_resolution(original_token)
221
+
222
+
223
+ def resolve_sigils(text: str, current: Optional[models.Model] = None) -> str:
224
+ result = ""
225
+ i = 0
226
+ while i < len(text):
227
+ if text[i] == "[":
228
+ depth = 1
229
+ j = i + 1
230
+ while j < len(text) and depth:
231
+ if text[j] == "[":
232
+ depth += 1
233
+ elif text[j] == "]":
234
+ depth -= 1
235
+ j += 1
236
+ if depth:
237
+ result += text[i]
238
+ i += 1
239
+ continue
240
+ token = text[i + 1 : j - 1]
241
+ result += _resolve_token(token, current)
242
+ i = j
243
+ else:
244
+ result += text[i]
245
+ i += 1
246
+ return result
247
+
248
+
249
+ def resolve_sigil(sigil: str, current: Optional[models.Model] = None) -> str:
250
+ return resolve_sigils(sigil, current)