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/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)