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