avrae-ls 0.4.1__py3-none-any.whl → 0.5.0__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.
- {avrae_ls-0.4.1.dist-info → avrae_ls-0.5.0.dist-info}/METADATA +31 -11
- avrae_ls-0.5.0.dist-info/RECORD +6 -0
- {avrae_ls-0.4.1.dist-info → avrae_ls-0.5.0.dist-info}/WHEEL +1 -2
- avrae_ls/__init__.py +0 -3
- avrae_ls/__main__.py +0 -108
- avrae_ls/alias_preview.py +0 -346
- avrae_ls/api.py +0 -2014
- avrae_ls/argparser.py +0 -430
- avrae_ls/argument_parsing.py +0 -67
- avrae_ls/code_actions.py +0 -282
- avrae_ls/codes.py +0 -3
- avrae_ls/completions.py +0 -1391
- avrae_ls/config.py +0 -496
- avrae_ls/context.py +0 -229
- avrae_ls/cvars.py +0 -115
- avrae_ls/diagnostics.py +0 -751
- avrae_ls/dice.py +0 -33
- avrae_ls/parser.py +0 -44
- avrae_ls/runtime.py +0 -661
- avrae_ls/server.py +0 -399
- avrae_ls/signature_help.py +0 -252
- avrae_ls/symbols.py +0 -266
- avrae_ls-0.4.1.dist-info/RECORD +0 -34
- avrae_ls-0.4.1.dist-info/top_level.txt +0 -2
- draconic/__init__.py +0 -4
- draconic/exceptions.py +0 -157
- draconic/helpers.py +0 -236
- draconic/interpreter.py +0 -1091
- draconic/string.py +0 -100
- draconic/types.py +0 -364
- draconic/utils.py +0 -78
- draconic/versions.py +0 -4
- {avrae_ls-0.4.1.dist-info → avrae_ls-0.5.0.dist-info}/entry_points.txt +0 -0
- {avrae_ls-0.4.1.dist-info → avrae_ls-0.5.0.dist-info}/licenses/LICENSE +0 -0
avrae_ls/runtime.py
DELETED
|
@@ -1,661 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import ast
|
|
4
|
-
import io
|
|
5
|
-
import json
|
|
6
|
-
import logging
|
|
7
|
-
import math
|
|
8
|
-
import random
|
|
9
|
-
import re
|
|
10
|
-
import time
|
|
11
|
-
from types import SimpleNamespace
|
|
12
|
-
try: # optional dependency
|
|
13
|
-
import yaml
|
|
14
|
-
except ImportError: # pragma: no cover - fallback when PyYAML is absent
|
|
15
|
-
yaml = None # type: ignore
|
|
16
|
-
from dataclasses import dataclass
|
|
17
|
-
from typing import Any, Dict, Set, Callable
|
|
18
|
-
|
|
19
|
-
import d20
|
|
20
|
-
import draconic
|
|
21
|
-
import httpx
|
|
22
|
-
from draconic.interpreter import _Break, _Continue, _Return
|
|
23
|
-
|
|
24
|
-
from .context import ContextData, GVarResolver
|
|
25
|
-
from .config import AvraeServiceConfig, VarSources
|
|
26
|
-
from .api import AliasContextAPI, CharacterAPI, SimpleCombat, SimpleRollResult
|
|
27
|
-
from . import argparser as avrae_argparser
|
|
28
|
-
# Minimal stand-in for Avrae's AliasException
|
|
29
|
-
class AliasException(Exception):
|
|
30
|
-
def __init__(self, msg, pm_user):
|
|
31
|
-
super().__init__(msg)
|
|
32
|
-
self.pm_user = pm_user
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
try:
|
|
36
|
-
from avrae.aliasing.errors import FunctionRequiresCharacter # type: ignore
|
|
37
|
-
except Exception: # pragma: no cover - fallback when avrae is unavailable
|
|
38
|
-
class FunctionRequiresCharacter(Exception):
|
|
39
|
-
def __init__(self, msg: str | None = None):
|
|
40
|
-
super().__init__(msg or "This alias requires an active character.")
|
|
41
|
-
|
|
42
|
-
log = logging.getLogger(__name__)
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
class MockNamespace:
|
|
46
|
-
"""A minimal attribute-friendly namespace used for ctx/combat/character."""
|
|
47
|
-
|
|
48
|
-
def __init__(self, data: Dict[str, Any] | None = None):
|
|
49
|
-
self._data = data or {}
|
|
50
|
-
|
|
51
|
-
def __getattr__(self, item: str) -> Any:
|
|
52
|
-
return self._data.get(item)
|
|
53
|
-
|
|
54
|
-
def __getitem__(self, item: str) -> Any:
|
|
55
|
-
return self._data.get(item)
|
|
56
|
-
|
|
57
|
-
def __repr__(self) -> str: # pragma: no cover - debugging helper
|
|
58
|
-
return f"MockNamespace({self._data})"
|
|
59
|
-
|
|
60
|
-
def to_dict(self) -> Dict[str, Any]:
|
|
61
|
-
return dict(self._data)
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
@dataclass
|
|
65
|
-
class ExecutionResult:
|
|
66
|
-
stdout: str
|
|
67
|
-
value: Any = None
|
|
68
|
-
error: BaseException | None = None
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
def _roll_dice(dice: str) -> int:
|
|
72
|
-
roller = d20.Roller()
|
|
73
|
-
try:
|
|
74
|
-
result = roller.roll(str(dice))
|
|
75
|
-
except d20.RollError:
|
|
76
|
-
return 0
|
|
77
|
-
return result.total
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
def _vroll_dice(dice: str, multiply: int = 1, add: int = 0) -> SimpleRollResult | None:
|
|
81
|
-
roller = d20.Roller()
|
|
82
|
-
try:
|
|
83
|
-
dice_ast = roller.parse(str(dice))
|
|
84
|
-
except d20.RollError:
|
|
85
|
-
return None
|
|
86
|
-
|
|
87
|
-
if multiply != 1 or add != 0:
|
|
88
|
-
def _scale(node):
|
|
89
|
-
if isinstance(node, d20.ast.Dice):
|
|
90
|
-
node.num = (node.num * multiply) + add
|
|
91
|
-
return node
|
|
92
|
-
|
|
93
|
-
dice_ast = d20.utils.tree_map(_scale, dice_ast)
|
|
94
|
-
|
|
95
|
-
try:
|
|
96
|
-
rolled = roller.roll(dice_ast)
|
|
97
|
-
except d20.RollError:
|
|
98
|
-
return None
|
|
99
|
-
return SimpleRollResult(rolled)
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
@dataclass
|
|
103
|
-
class _CoinsArgs:
|
|
104
|
-
pp: int = 0
|
|
105
|
-
gp: int = 0
|
|
106
|
-
ep: int = 0
|
|
107
|
-
sp: int = 0
|
|
108
|
-
cp: int = 0
|
|
109
|
-
explicit: bool = False
|
|
110
|
-
|
|
111
|
-
@property
|
|
112
|
-
def total(self) -> float:
|
|
113
|
-
return (self.pp * 10) + self.gp + (self.ep * 0.5) + (self.sp * 0.1) + (self.cp * 0.01)
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
def _parse_coin_args(args: str) -> _CoinsArgs:
|
|
117
|
-
cleaned = str(args).replace(",", "")
|
|
118
|
-
try:
|
|
119
|
-
return _parse_coin_args_float(float(cleaned))
|
|
120
|
-
except ValueError:
|
|
121
|
-
return _parse_coin_args_re(cleaned)
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
def _parse_coin_args_float(coins: float) -> _CoinsArgs:
|
|
125
|
-
total_copper = int(round(coins * 100, 1))
|
|
126
|
-
if coins < 0:
|
|
127
|
-
return _CoinsArgs(cp=total_copper)
|
|
128
|
-
return _CoinsArgs(
|
|
129
|
-
gp=total_copper // 100,
|
|
130
|
-
sp=(total_copper % 100) // 10,
|
|
131
|
-
cp=total_copper % 10,
|
|
132
|
-
)
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
def _parse_coin_args_re(args: str) -> _CoinsArgs:
|
|
136
|
-
is_valid = re.fullmatch(r"(([+-]?\d+)\s*([pgesc]p)\s*)+", args, re.IGNORECASE)
|
|
137
|
-
if not is_valid:
|
|
138
|
-
raise avrae_argparser.InvalidArgument(
|
|
139
|
-
"Coins must be a number or a currency string, e.g. `+101.2` or `10cp +101gp -2sp`."
|
|
140
|
-
)
|
|
141
|
-
|
|
142
|
-
out = _CoinsArgs(explicit=True)
|
|
143
|
-
for coin_match in re.finditer(r"(?P<amount>[+-]?\d+)\s*(?P<currency>[pgesc]p)", args, re.IGNORECASE):
|
|
144
|
-
amount = int(coin_match["amount"])
|
|
145
|
-
currency = coin_match["currency"].lower()
|
|
146
|
-
|
|
147
|
-
if currency == "pp":
|
|
148
|
-
out.pp += amount
|
|
149
|
-
elif currency == "gp":
|
|
150
|
-
out.gp += amount
|
|
151
|
-
elif currency == "ep":
|
|
152
|
-
out.ep += amount
|
|
153
|
-
elif currency == "sp":
|
|
154
|
-
out.sp += amount
|
|
155
|
-
else:
|
|
156
|
-
out.cp += amount
|
|
157
|
-
|
|
158
|
-
return out
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
def _parse_coins(args: str, include_total: bool = True):
|
|
162
|
-
try:
|
|
163
|
-
from avrae.aliasing.api.functions import parse_coins as avrae_parse_coins
|
|
164
|
-
except Exception:
|
|
165
|
-
avrae_parse_coins = None
|
|
166
|
-
|
|
167
|
-
if avrae_parse_coins:
|
|
168
|
-
try:
|
|
169
|
-
return avrae_parse_coins(str(args), include_total=include_total)
|
|
170
|
-
except Exception:
|
|
171
|
-
pass
|
|
172
|
-
|
|
173
|
-
coin_args = _parse_coin_args(str(args))
|
|
174
|
-
parsed = {
|
|
175
|
-
"pp": coin_args.pp,
|
|
176
|
-
"gp": coin_args.gp,
|
|
177
|
-
"ep": coin_args.ep,
|
|
178
|
-
"sp": coin_args.sp,
|
|
179
|
-
"cp": coin_args.cp,
|
|
180
|
-
}
|
|
181
|
-
if include_total:
|
|
182
|
-
parsed["total"] = coin_args.total
|
|
183
|
-
return parsed
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
def _default_builtins() -> Dict[str, Any]:
|
|
187
|
-
return {
|
|
188
|
-
"len": len,
|
|
189
|
-
"min": min,
|
|
190
|
-
"max": max,
|
|
191
|
-
"sum": sum,
|
|
192
|
-
"any": any,
|
|
193
|
-
"all": all,
|
|
194
|
-
"abs": abs,
|
|
195
|
-
"range": range,
|
|
196
|
-
"enumerate": enumerate,
|
|
197
|
-
"int": int,
|
|
198
|
-
"float": float,
|
|
199
|
-
"str": str,
|
|
200
|
-
"round": round,
|
|
201
|
-
"ceil": math.ceil,
|
|
202
|
-
"floor": math.floor,
|
|
203
|
-
"sqrt": math.sqrt,
|
|
204
|
-
"time": time.time,
|
|
205
|
-
"roll": _roll_dice,
|
|
206
|
-
"vroll": _vroll_dice,
|
|
207
|
-
"rand": random.random,
|
|
208
|
-
"randint": random.randrange,
|
|
209
|
-
"randchoice": random.choice,
|
|
210
|
-
"randchoices": random.choices,
|
|
211
|
-
"typeof": lambda inst: type(inst).__name__,
|
|
212
|
-
"parse_coins": _parse_coins,
|
|
213
|
-
"load_json": lambda s: json.loads(str(s)),
|
|
214
|
-
"dump_json": lambda obj: json.dumps(obj),
|
|
215
|
-
"load_yaml": lambda s: yaml.safe_load(str(s)) if yaml else None,
|
|
216
|
-
"dump_yaml": (
|
|
217
|
-
(lambda obj, indent=2: yaml.safe_dump(obj, indent=indent, sort_keys=False)) if yaml else (lambda obj, indent=2: str(obj))
|
|
218
|
-
),
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
class MockExecutor:
|
|
223
|
-
def __init__(self, service_config: AvraeServiceConfig | None = None):
|
|
224
|
-
self._base_builtins = _default_builtins()
|
|
225
|
-
self._service_config = service_config or AvraeServiceConfig()
|
|
226
|
-
|
|
227
|
-
def available_names(self, ctx_data: ContextData) -> Set[str]:
|
|
228
|
-
builtin_names = set(self._base_builtins.keys())
|
|
229
|
-
runtime_names = {
|
|
230
|
-
"ctx",
|
|
231
|
-
"combat",
|
|
232
|
-
"character",
|
|
233
|
-
"roll",
|
|
234
|
-
"vroll",
|
|
235
|
-
"rand",
|
|
236
|
-
"randint",
|
|
237
|
-
"randchoice",
|
|
238
|
-
"randchoices",
|
|
239
|
-
"typeof",
|
|
240
|
-
"parse_coins",
|
|
241
|
-
"load_json",
|
|
242
|
-
"dump_json",
|
|
243
|
-
"load_yaml",
|
|
244
|
-
"dump_yaml",
|
|
245
|
-
"get_gvar",
|
|
246
|
-
"get_svar",
|
|
247
|
-
"get_cvar",
|
|
248
|
-
"get_uvar",
|
|
249
|
-
"get_uvars",
|
|
250
|
-
"set_uvar",
|
|
251
|
-
"set_uvar_nx",
|
|
252
|
-
"delete_uvar",
|
|
253
|
-
"uvar_exists",
|
|
254
|
-
"print",
|
|
255
|
-
"argparse",
|
|
256
|
-
"err",
|
|
257
|
-
"exists",
|
|
258
|
-
"get",
|
|
259
|
-
"using",
|
|
260
|
-
"signature",
|
|
261
|
-
"verify_signature",
|
|
262
|
-
}
|
|
263
|
-
variable_names = set(ctx_data.vars.to_initial_names().keys())
|
|
264
|
-
return builtin_names | runtime_names | variable_names
|
|
265
|
-
|
|
266
|
-
async def run(
|
|
267
|
-
self,
|
|
268
|
-
code: str,
|
|
269
|
-
ctx_data: ContextData,
|
|
270
|
-
gvar_resolver: GVarResolver | None = None,
|
|
271
|
-
) -> ExecutionResult:
|
|
272
|
-
buffer = io.StringIO()
|
|
273
|
-
resolver = gvar_resolver
|
|
274
|
-
interpreter_ref: dict[str, draconic.DraconicInterpreter | None] = {"interpreter": None}
|
|
275
|
-
runtime_character: CharacterAPI | None = None
|
|
276
|
-
|
|
277
|
-
def _character_provider() -> CharacterAPI:
|
|
278
|
-
nonlocal runtime_character
|
|
279
|
-
interp = interpreter_ref["interpreter"]
|
|
280
|
-
if not ctx_data.character:
|
|
281
|
-
raise FunctionRequiresCharacter()
|
|
282
|
-
if runtime_character is None and interp is not None:
|
|
283
|
-
runtime_character = _RuntimeCharacter(ctx_data.character, ctx_data.vars, interp)
|
|
284
|
-
if runtime_character is None:
|
|
285
|
-
runtime_character = CharacterAPI(ctx_data.character)
|
|
286
|
-
return runtime_character # type: ignore[return-value]
|
|
287
|
-
|
|
288
|
-
import_cache: dict[str, SimpleNamespace] = {}
|
|
289
|
-
import_stack: list[str] = []
|
|
290
|
-
builtins = self._build_builtins(
|
|
291
|
-
ctx_data,
|
|
292
|
-
resolver,
|
|
293
|
-
buffer,
|
|
294
|
-
character_provider=_character_provider,
|
|
295
|
-
interpreter_ref=interpreter_ref,
|
|
296
|
-
import_cache=import_cache,
|
|
297
|
-
import_stack=import_stack,
|
|
298
|
-
)
|
|
299
|
-
interpreter = draconic.DraconicInterpreter(
|
|
300
|
-
builtins=builtins,
|
|
301
|
-
initial_names=ctx_data.vars.to_initial_names(),
|
|
302
|
-
)
|
|
303
|
-
interpreter_ref["interpreter"] = interpreter
|
|
304
|
-
|
|
305
|
-
value = None
|
|
306
|
-
error: BaseException | None = None
|
|
307
|
-
code_to_run = code
|
|
308
|
-
try:
|
|
309
|
-
parsed = interpreter.parse(code_to_run)
|
|
310
|
-
except BaseException:
|
|
311
|
-
wrapped, _ = _wrap_draconic(code_to_run)
|
|
312
|
-
code_to_run = wrapped
|
|
313
|
-
try:
|
|
314
|
-
parsed = interpreter.parse(code_to_run)
|
|
315
|
-
except BaseException as exc:
|
|
316
|
-
error = exc
|
|
317
|
-
log.debug("Mock execution error: %s", exc, exc_info=exc)
|
|
318
|
-
return ExecutionResult(stdout=buffer.getvalue(), value=value, error=error)
|
|
319
|
-
|
|
320
|
-
if resolver:
|
|
321
|
-
await _ensure_literal_gvars(code_to_run, resolver)
|
|
322
|
-
|
|
323
|
-
try:
|
|
324
|
-
interpreter._preflight()
|
|
325
|
-
value = self._exec_with_value(interpreter, parsed)
|
|
326
|
-
except BaseException as exc: # draconic raises BaseException subclasses
|
|
327
|
-
error = exc
|
|
328
|
-
log.debug("Mock execution error: %s", exc, exc_info=exc)
|
|
329
|
-
return ExecutionResult(stdout=buffer.getvalue(), value=value, error=error)
|
|
330
|
-
|
|
331
|
-
def _build_builtins(
|
|
332
|
-
self,
|
|
333
|
-
ctx_data: ContextData,
|
|
334
|
-
resolver: GVarResolver | None,
|
|
335
|
-
buffer: io.StringIO,
|
|
336
|
-
character_provider: Callable[[], CharacterAPI] | None = None,
|
|
337
|
-
interpreter_ref: Dict[str, draconic.DraconicInterpreter | None] | None = None,
|
|
338
|
-
import_cache: Dict[str, SimpleNamespace] | None = None,
|
|
339
|
-
import_stack: list[str] | None = None,
|
|
340
|
-
) -> Dict[str, Any]:
|
|
341
|
-
builtins = dict(self._base_builtins)
|
|
342
|
-
var_store = ctx_data.vars
|
|
343
|
-
interpreter_ref = interpreter_ref or {"interpreter": None}
|
|
344
|
-
import_cache = import_cache or {}
|
|
345
|
-
import_stack = import_stack or []
|
|
346
|
-
service_cfg = self._service_config
|
|
347
|
-
verify_cache_sig: str | None = None
|
|
348
|
-
verify_cache_result: Dict[str, Any] | None = None
|
|
349
|
-
verify_cache_error: ValueError | None = None
|
|
350
|
-
|
|
351
|
-
def _print(*args, sep=" ", end="\n"):
|
|
352
|
-
buffer.write(sep.join(map(str, args)) + end)
|
|
353
|
-
|
|
354
|
-
def _get_gvar(address: str):
|
|
355
|
-
if resolver is None:
|
|
356
|
-
return None
|
|
357
|
-
return resolver.get_local(address)
|
|
358
|
-
|
|
359
|
-
def _get_svar(name: str, default=None):
|
|
360
|
-
return var_store.svars.get(str(name), default)
|
|
361
|
-
|
|
362
|
-
def _get_cvar(name: str, default=None):
|
|
363
|
-
val = var_store.cvars.get(str(name), default)
|
|
364
|
-
return str(val) if val is not None else default
|
|
365
|
-
|
|
366
|
-
def _get_uvar(name: str, default=None):
|
|
367
|
-
val = var_store.uvars.get(str(name), default)
|
|
368
|
-
return str(val) if val is not None else default
|
|
369
|
-
|
|
370
|
-
def _get_uvars():
|
|
371
|
-
return {k: (str(v) if v is not None else v) for k, v in var_store.uvars.items()}
|
|
372
|
-
|
|
373
|
-
def _set_uvar(name: str, value: Any):
|
|
374
|
-
str_val = str(value) if value is not None else None
|
|
375
|
-
var_store.uvars[str(name)] = str_val
|
|
376
|
-
return str_val
|
|
377
|
-
|
|
378
|
-
def _set_uvar_nx(name: str, value: Any):
|
|
379
|
-
key = str(name)
|
|
380
|
-
if key not in var_store.uvars:
|
|
381
|
-
var_store.uvars[key] = str(value) if value is not None else None
|
|
382
|
-
return var_store.uvars[key]
|
|
383
|
-
|
|
384
|
-
def _delete_uvar(name: str):
|
|
385
|
-
return var_store.uvars.pop(str(name), None)
|
|
386
|
-
|
|
387
|
-
def _uvar_exists(name: str) -> bool:
|
|
388
|
-
return str(name) in var_store.uvars
|
|
389
|
-
|
|
390
|
-
def _resolve_name(key: str) -> tuple[bool, Any]:
|
|
391
|
-
key = str(key)
|
|
392
|
-
interp = interpreter_ref.get("interpreter")
|
|
393
|
-
if interp is not None:
|
|
394
|
-
names = getattr(interp, "_names", {})
|
|
395
|
-
if key in names:
|
|
396
|
-
return True, names[key]
|
|
397
|
-
|
|
398
|
-
if key in var_store.cvars:
|
|
399
|
-
return True, var_store.cvars[key]
|
|
400
|
-
|
|
401
|
-
if key in var_store.uvars:
|
|
402
|
-
return True, var_store.uvars[key]
|
|
403
|
-
|
|
404
|
-
return False, None
|
|
405
|
-
|
|
406
|
-
def _exists(name: str) -> bool:
|
|
407
|
-
found, _ = _resolve_name(name)
|
|
408
|
-
return found
|
|
409
|
-
|
|
410
|
-
def _get(name: str, default=None):
|
|
411
|
-
found, value = _resolve_name(name)
|
|
412
|
-
return value if found else default
|
|
413
|
-
|
|
414
|
-
def _using(**imports):
|
|
415
|
-
interp = interpreter_ref.get("interpreter")
|
|
416
|
-
if interp is None:
|
|
417
|
-
return None
|
|
418
|
-
user_ns = getattr(interp, "_names", {})
|
|
419
|
-
|
|
420
|
-
def _load_module(addr: str) -> SimpleNamespace:
|
|
421
|
-
if addr in import_cache:
|
|
422
|
-
return import_cache[addr]
|
|
423
|
-
if resolver is None:
|
|
424
|
-
raise ModuleNotFoundError(f"No gvar named {addr!r}")
|
|
425
|
-
mod_contents = resolver.get_local(addr)
|
|
426
|
-
if mod_contents is None:
|
|
427
|
-
raise ModuleNotFoundError(f"No gvar named {addr!r}")
|
|
428
|
-
|
|
429
|
-
old_names = getattr(interp, "_names", {})
|
|
430
|
-
depth_increased = False
|
|
431
|
-
try:
|
|
432
|
-
interp._names = {}
|
|
433
|
-
interp._depth += 1
|
|
434
|
-
depth_increased = True
|
|
435
|
-
if interp._depth > interp._config.max_recursion_depth:
|
|
436
|
-
raise RecursionError("Maximum recursion depth exceeded")
|
|
437
|
-
interp.execute_module(str(mod_contents), module_name=addr)
|
|
438
|
-
mod_ns = SimpleNamespace(**getattr(interp, "_names", {}))
|
|
439
|
-
import_cache[addr] = mod_ns
|
|
440
|
-
return mod_ns
|
|
441
|
-
finally:
|
|
442
|
-
if depth_increased:
|
|
443
|
-
interp._depth -= 1
|
|
444
|
-
interp._names = old_names
|
|
445
|
-
|
|
446
|
-
for ns, addr in imports.items():
|
|
447
|
-
addr_str = str(addr)
|
|
448
|
-
if addr_str in import_stack:
|
|
449
|
-
circle = " imports\n".join(import_stack)
|
|
450
|
-
raise ImportError(f"Circular import detected!\n{circle} imports\n{addr_str}")
|
|
451
|
-
import_stack.append(addr_str)
|
|
452
|
-
try:
|
|
453
|
-
mod_ns = _load_module(addr_str)
|
|
454
|
-
finally:
|
|
455
|
-
import_stack.pop()
|
|
456
|
-
name = str(ns)
|
|
457
|
-
if name in interp.builtins:
|
|
458
|
-
raise ValueError(f"{name} is already builtin (no shadow assignments).")
|
|
459
|
-
user_ns[name] = mod_ns
|
|
460
|
-
|
|
461
|
-
interp._names = user_ns
|
|
462
|
-
return None
|
|
463
|
-
|
|
464
|
-
def _signature(data=0):
|
|
465
|
-
try:
|
|
466
|
-
data = int(data)
|
|
467
|
-
except ValueError:
|
|
468
|
-
raise TypeError(f"Data {data} could not be converted to integer.")
|
|
469
|
-
return f"mock-signature:{int(data)}"
|
|
470
|
-
|
|
471
|
-
def _verify_signature(sig):
|
|
472
|
-
nonlocal verify_cache_sig, verify_cache_result, verify_cache_error
|
|
473
|
-
sig_str = str(sig)
|
|
474
|
-
if sig_str == verify_cache_sig:
|
|
475
|
-
if verify_cache_error:
|
|
476
|
-
raise verify_cache_error
|
|
477
|
-
return verify_cache_result
|
|
478
|
-
|
|
479
|
-
verify_cache_sig = sig_str
|
|
480
|
-
verify_cache_error = None
|
|
481
|
-
verify_cache_result = None
|
|
482
|
-
timeout = float(service_cfg.verify_timeout if service_cfg else AvraeServiceConfig.verify_timeout)
|
|
483
|
-
retries = int(service_cfg.verify_retries if service_cfg else AvraeServiceConfig.verify_retries)
|
|
484
|
-
retries = max(0, retries)
|
|
485
|
-
|
|
486
|
-
def _call_verify_api(signature: str) -> Dict[str, Any]:
|
|
487
|
-
base_url = (service_cfg.base_url if service_cfg else AvraeServiceConfig.base_url).rstrip("/")
|
|
488
|
-
url = f"{base_url}/bot/signature/verify"
|
|
489
|
-
headers = {"Content-Type": "application/json"}
|
|
490
|
-
if service_cfg and service_cfg.token:
|
|
491
|
-
headers["Authorization"] = str(service_cfg.token)
|
|
492
|
-
last_exc: Exception | None = None
|
|
493
|
-
for attempt in range(retries + 1):
|
|
494
|
-
try:
|
|
495
|
-
resp = httpx.post(url, json={"signature": signature}, headers=headers, timeout=timeout)
|
|
496
|
-
break
|
|
497
|
-
except Exception as exc:
|
|
498
|
-
last_exc = exc
|
|
499
|
-
if attempt >= retries:
|
|
500
|
-
raise ValueError(f"Failed to verify signature: {exc}") from exc
|
|
501
|
-
continue
|
|
502
|
-
else: # pragma: no cover - defensive
|
|
503
|
-
raise ValueError(f"Failed to verify signature: {last_exc}") from last_exc
|
|
504
|
-
|
|
505
|
-
try:
|
|
506
|
-
payload = resp.json()
|
|
507
|
-
except Exception as exc:
|
|
508
|
-
raise ValueError("Failed to verify signature: invalid response body") from exc
|
|
509
|
-
|
|
510
|
-
if resp.status_code != 200:
|
|
511
|
-
message = None
|
|
512
|
-
if isinstance(payload, dict):
|
|
513
|
-
message = payload.get("error") or payload.get("message")
|
|
514
|
-
detail = f"{message} (HTTP {resp.status_code})" if message else f"HTTP {resp.status_code}"
|
|
515
|
-
raise ValueError(f"Failed to verify signature: {detail}")
|
|
516
|
-
|
|
517
|
-
if not isinstance(payload, dict):
|
|
518
|
-
raise ValueError("Failed to verify signature: invalid response")
|
|
519
|
-
if payload.get("success") is not True:
|
|
520
|
-
message = payload.get("error")
|
|
521
|
-
raise ValueError(f"Failed to verify signature: {message or 'unsuccessful response'}")
|
|
522
|
-
|
|
523
|
-
data = payload.get("data")
|
|
524
|
-
if not isinstance(data, dict):
|
|
525
|
-
raise ValueError("Failed to verify signature: malformed response")
|
|
526
|
-
return data
|
|
527
|
-
|
|
528
|
-
try:
|
|
529
|
-
verify_cache_result = _call_verify_api(sig_str)
|
|
530
|
-
except ValueError as exc:
|
|
531
|
-
verify_cache_error = exc
|
|
532
|
-
raise
|
|
533
|
-
return verify_cache_result
|
|
534
|
-
|
|
535
|
-
def _argparse(args, character=None, splitter=avrae_argparser.argsplit, parse_ephem=True):
|
|
536
|
-
return avrae_argparser.argparse(args, character=character, splitter=splitter, parse_ephem=parse_ephem)
|
|
537
|
-
|
|
538
|
-
def _err(reason, pm_user: bool = False):
|
|
539
|
-
raise AliasException(str(reason), pm_user)
|
|
540
|
-
|
|
541
|
-
ns_ctx = AliasContextAPI(ctx_data.ctx)
|
|
542
|
-
ns_combat = SimpleCombat(ctx_data.combat) if ctx_data.combat else None
|
|
543
|
-
if character_provider:
|
|
544
|
-
character_fn = character_provider
|
|
545
|
-
else:
|
|
546
|
-
ns_character = CharacterAPI(ctx_data.character) if ctx_data.character else None
|
|
547
|
-
|
|
548
|
-
def character_fn():
|
|
549
|
-
if ns_character is None:
|
|
550
|
-
raise FunctionRequiresCharacter()
|
|
551
|
-
return ns_character
|
|
552
|
-
|
|
553
|
-
builtins.update(
|
|
554
|
-
print=_print,
|
|
555
|
-
roll=_roll_dice,
|
|
556
|
-
vroll=_vroll_dice,
|
|
557
|
-
ctx=ns_ctx,
|
|
558
|
-
combat=lambda: ns_combat,
|
|
559
|
-
character=lambda: character_fn(),
|
|
560
|
-
get_gvar=_get_gvar,
|
|
561
|
-
get_svar=_get_svar,
|
|
562
|
-
get_cvar=_get_cvar,
|
|
563
|
-
get_uvar=_get_uvar,
|
|
564
|
-
get_uvars=_get_uvars,
|
|
565
|
-
set_uvar=_set_uvar,
|
|
566
|
-
set_uvar_nx=_set_uvar_nx,
|
|
567
|
-
delete_uvar=_delete_uvar,
|
|
568
|
-
uvar_exists=_uvar_exists,
|
|
569
|
-
argparse=_argparse,
|
|
570
|
-
err=_err,
|
|
571
|
-
exists=_exists,
|
|
572
|
-
get=_get,
|
|
573
|
-
using=_using,
|
|
574
|
-
signature=_signature,
|
|
575
|
-
verify_signature=_verify_signature,
|
|
576
|
-
)
|
|
577
|
-
return builtins
|
|
578
|
-
|
|
579
|
-
def _exec_with_value(self, interpreter: draconic.DraconicInterpreter, body) -> Any:
|
|
580
|
-
last_val = None
|
|
581
|
-
for expression in body:
|
|
582
|
-
retval = interpreter._eval(expression) # type: ignore[attr-defined]
|
|
583
|
-
if isinstance(retval, (_Break, _Continue)):
|
|
584
|
-
raise draconic.DraconicSyntaxError.from_node(retval.node, msg="Loop control outside loop", expr=interpreter._expr) # type: ignore[attr-defined]
|
|
585
|
-
if isinstance(retval, _Return):
|
|
586
|
-
return retval.value
|
|
587
|
-
last_val = retval
|
|
588
|
-
return last_val
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
class _RuntimeCharacter(CharacterAPI):
|
|
592
|
-
"""Character wrapper that keeps mock runtime bindings in sync with cvar mutations."""
|
|
593
|
-
|
|
594
|
-
def __init__(self, data: Dict[str, Any], var_store: VarSources, interpreter: draconic.DraconicInterpreter):
|
|
595
|
-
super().__init__(data)
|
|
596
|
-
self._var_store = var_store
|
|
597
|
-
self._interpreter = interpreter
|
|
598
|
-
|
|
599
|
-
def set_cvar(self, name: str, val: Any) -> Any:
|
|
600
|
-
bound_val = super().set_cvar(name, val)
|
|
601
|
-
key = str(name)
|
|
602
|
-
self._var_store.cvars[key] = bound_val
|
|
603
|
-
try:
|
|
604
|
-
# Mirror Avrae behavior: new cvars are available as locals immediately.
|
|
605
|
-
self._interpreter._names[key] = bound_val # type: ignore[attr-defined]
|
|
606
|
-
except Exception:
|
|
607
|
-
pass
|
|
608
|
-
return bound_val
|
|
609
|
-
|
|
610
|
-
def set_cvar_nx(self, name: str, val: Any) -> Any:
|
|
611
|
-
key = str(name)
|
|
612
|
-
if key in self._var_store.cvars:
|
|
613
|
-
return self._var_store.cvars[key]
|
|
614
|
-
return self.set_cvar(key, val)
|
|
615
|
-
|
|
616
|
-
# delete_cvar intentionally does not unbind runtime names, matching Avrae's docs.
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
def _wrap_draconic(code: str) -> tuple[str, int]:
|
|
620
|
-
indented = "\n".join(f" {line}" for line in code.splitlines())
|
|
621
|
-
wrapped = f"def __alias_main__():\n{indented}\n__alias_main__()"
|
|
622
|
-
return wrapped, 1
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
async def _ensure_literal_gvars(code: str, resolver: GVarResolver) -> None:
|
|
626
|
-
for key in _literal_gvars(code):
|
|
627
|
-
try:
|
|
628
|
-
await resolver.ensure(key)
|
|
629
|
-
except Exception as exc: # pragma: no cover - defensive
|
|
630
|
-
log.debug("Failed to prefetch gvar %s: %s", key, exc)
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
def _literal_gvars(code: str) -> Set[str]:
|
|
634
|
-
try:
|
|
635
|
-
tree = ast.parse(code)
|
|
636
|
-
except SyntaxError:
|
|
637
|
-
wrapped, _ = _wrap_draconic(code)
|
|
638
|
-
try:
|
|
639
|
-
tree = ast.parse(wrapped)
|
|
640
|
-
except SyntaxError:
|
|
641
|
-
return set()
|
|
642
|
-
|
|
643
|
-
gvars: set[str] = set()
|
|
644
|
-
for node in ast.walk(tree):
|
|
645
|
-
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name):
|
|
646
|
-
if node.func.id == "get_gvar":
|
|
647
|
-
if not node.args:
|
|
648
|
-
continue
|
|
649
|
-
arg = node.args[0]
|
|
650
|
-
if isinstance(arg, ast.Constant) and isinstance(arg.value, str):
|
|
651
|
-
gvars.add(arg.value)
|
|
652
|
-
elif isinstance(arg, ast.Str):
|
|
653
|
-
gvars.add(arg.s)
|
|
654
|
-
elif node.func.id == "using":
|
|
655
|
-
for kw in node.keywords:
|
|
656
|
-
val = kw.value
|
|
657
|
-
if isinstance(val, ast.Constant) and isinstance(val.value, str):
|
|
658
|
-
gvars.add(val.value)
|
|
659
|
-
elif isinstance(val, ast.Str):
|
|
660
|
-
gvars.add(val.s)
|
|
661
|
-
return gvars
|