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/context.py
DELETED
|
@@ -1,229 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
import logging
|
|
5
|
-
from dataclasses import dataclass, field
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
from typing import Any, Dict, Iterable
|
|
8
|
-
|
|
9
|
-
import httpx
|
|
10
|
-
|
|
11
|
-
from .config import AvraeLSConfig, ContextProfile, VarSources
|
|
12
|
-
from .cvars import derive_character_cvars
|
|
13
|
-
|
|
14
|
-
log = logging.getLogger(__name__)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
@dataclass
|
|
18
|
-
class ContextData:
|
|
19
|
-
ctx: Dict[str, Any] = field(default_factory=dict)
|
|
20
|
-
combat: Dict[str, Any] = field(default_factory=dict)
|
|
21
|
-
character: Dict[str, Any] = field(default_factory=dict)
|
|
22
|
-
vars: VarSources = field(default_factory=VarSources)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
class ContextBuilder:
|
|
26
|
-
def __init__(self, config: AvraeLSConfig):
|
|
27
|
-
self._config = config
|
|
28
|
-
self._gvar_resolver = GVarResolver(config)
|
|
29
|
-
|
|
30
|
-
@property
|
|
31
|
-
def gvar_resolver(self) -> "GVarResolver":
|
|
32
|
-
return self._gvar_resolver
|
|
33
|
-
|
|
34
|
-
def build(self, profile_name: str | None = None) -> ContextData:
|
|
35
|
-
profile = self._select_profile(profile_name)
|
|
36
|
-
combat = self._ensure_me_combatant(profile)
|
|
37
|
-
merged_vars = self._merge_character_cvars(profile.character, self._load_var_files().merge(profile.vars))
|
|
38
|
-
self._gvar_resolver.seed(merged_vars.gvars)
|
|
39
|
-
return ContextData(
|
|
40
|
-
ctx=dict(profile.ctx),
|
|
41
|
-
combat=combat,
|
|
42
|
-
character=dict(profile.character),
|
|
43
|
-
vars=merged_vars,
|
|
44
|
-
)
|
|
45
|
-
|
|
46
|
-
def _select_profile(self, profile_name: str | None) -> ContextProfile:
|
|
47
|
-
if profile_name and profile_name in self._config.profiles:
|
|
48
|
-
return self._config.profiles[profile_name]
|
|
49
|
-
if self._config.default_profile in self._config.profiles:
|
|
50
|
-
return self._config.profiles[self._config.default_profile]
|
|
51
|
-
return next(iter(self._config.profiles.values()))
|
|
52
|
-
|
|
53
|
-
def _load_var_files(self) -> VarSources:
|
|
54
|
-
merged = VarSources()
|
|
55
|
-
for path in self._config.var_files:
|
|
56
|
-
data = _read_json_file(path)
|
|
57
|
-
if data is None:
|
|
58
|
-
continue
|
|
59
|
-
merged = merged.merge(VarSources.from_data(data))
|
|
60
|
-
return merged
|
|
61
|
-
|
|
62
|
-
def _merge_character_cvars(self, character: Dict[str, Any], vars: VarSources) -> VarSources:
|
|
63
|
-
merged = vars
|
|
64
|
-
char_cvars = character.get("cvars") or {}
|
|
65
|
-
if char_cvars:
|
|
66
|
-
merged = merged.merge(VarSources(cvars=dict(char_cvars)))
|
|
67
|
-
|
|
68
|
-
builtin_cvars = derive_character_cvars(character)
|
|
69
|
-
if builtin_cvars:
|
|
70
|
-
merged = merged.merge(VarSources(cvars=builtin_cvars))
|
|
71
|
-
return merged
|
|
72
|
-
|
|
73
|
-
def _ensure_me_combatant(self, profile: ContextProfile) -> Dict[str, Any]:
|
|
74
|
-
combat = dict(profile.combat or {})
|
|
75
|
-
combatants = list(combat.get("combatants") or [])
|
|
76
|
-
me = combat.get("me")
|
|
77
|
-
author_id = (profile.ctx.get("author") or {}).get("id")
|
|
78
|
-
|
|
79
|
-
def _matches_author(combatant: Dict[str, Any]) -> bool:
|
|
80
|
-
try:
|
|
81
|
-
return author_id is not None and str(combatant.get("controller")) == str(author_id)
|
|
82
|
-
except Exception:
|
|
83
|
-
return False
|
|
84
|
-
|
|
85
|
-
# Use an existing combatant controlled by the author if me is missing.
|
|
86
|
-
if me is None:
|
|
87
|
-
for existing in combatants:
|
|
88
|
-
if _matches_author(existing):
|
|
89
|
-
me = existing
|
|
90
|
-
break
|
|
91
|
-
|
|
92
|
-
# If still missing, synthesize a combatant from the character sheet.
|
|
93
|
-
if me is None and profile.character:
|
|
94
|
-
me = {
|
|
95
|
-
"name": profile.character.get("name", "Player"),
|
|
96
|
-
"id": "cmb_player",
|
|
97
|
-
"controller": author_id,
|
|
98
|
-
"group": None,
|
|
99
|
-
"race": profile.character.get("race"),
|
|
100
|
-
"monster_name": None,
|
|
101
|
-
"is_hidden": False,
|
|
102
|
-
"init": profile.character.get("stats", {}).get("dexterity", 10),
|
|
103
|
-
"initmod": 0,
|
|
104
|
-
"type": "combatant",
|
|
105
|
-
"note": "Mock combatant for preview",
|
|
106
|
-
"effects": [],
|
|
107
|
-
"stats": profile.character.get("stats") or {},
|
|
108
|
-
"levels": profile.character.get("levels") or profile.character.get("class_levels") or {},
|
|
109
|
-
"skills": profile.character.get("skills") or {},
|
|
110
|
-
"saves": profile.character.get("saves") or {},
|
|
111
|
-
"resistances": profile.character.get("resistances") or {},
|
|
112
|
-
"spellbook": profile.character.get("spellbook") or {},
|
|
113
|
-
"attacks": profile.character.get("attacks") or [],
|
|
114
|
-
"max_hp": profile.character.get("max_hp"),
|
|
115
|
-
"hp": profile.character.get("hp"),
|
|
116
|
-
"temp_hp": profile.character.get("temp_hp"),
|
|
117
|
-
"ac": profile.character.get("ac"),
|
|
118
|
-
"creature_type": profile.character.get("creature_type"),
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
if me is not None:
|
|
122
|
-
combat["me"] = me
|
|
123
|
-
if not any(c is me for c in combatants) and not any(_matches_author(c) for c in combatants):
|
|
124
|
-
combatants.insert(0, me)
|
|
125
|
-
combat["combatants"] = combatants
|
|
126
|
-
if "current" not in combat or combat.get("current") is None:
|
|
127
|
-
combat["current"] = me
|
|
128
|
-
else:
|
|
129
|
-
combat["combatants"] = combatants
|
|
130
|
-
|
|
131
|
-
return combat
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
class GVarResolver:
|
|
135
|
-
def __init__(self, config: AvraeLSConfig):
|
|
136
|
-
self._config = config
|
|
137
|
-
self._cache: Dict[str, Any] = {}
|
|
138
|
-
|
|
139
|
-
def reset(self, gvars: Dict[str, Any] | None = None) -> None:
|
|
140
|
-
self._cache = {}
|
|
141
|
-
if gvars:
|
|
142
|
-
self._cache.update({str(k): v for k, v in gvars.items()})
|
|
143
|
-
|
|
144
|
-
def seed(self, gvars: Dict[str, Any] | None = None) -> None:
|
|
145
|
-
"""Merge provided gvars into the cache without dropping fetched values."""
|
|
146
|
-
if not gvars:
|
|
147
|
-
return
|
|
148
|
-
for k, v in gvars.items():
|
|
149
|
-
self._cache[str(k)] = v
|
|
150
|
-
|
|
151
|
-
def get_local(self, key: str) -> Any:
|
|
152
|
-
return self._cache.get(str(key))
|
|
153
|
-
|
|
154
|
-
async def ensure(self, key: str) -> bool:
|
|
155
|
-
key = str(key)
|
|
156
|
-
if key in self._cache:
|
|
157
|
-
log.debug("GVAR ensure cache hit for %s", key)
|
|
158
|
-
return True
|
|
159
|
-
if not self._config.enable_gvar_fetch:
|
|
160
|
-
log.warning("GVAR fetch disabled; skipping %s", key)
|
|
161
|
-
return False
|
|
162
|
-
if not self._config.service.token:
|
|
163
|
-
log.debug("GVAR fetch skipped for %s: no token configured", key)
|
|
164
|
-
return False
|
|
165
|
-
|
|
166
|
-
base_url = self._config.service.base_url.rstrip("/")
|
|
167
|
-
url = f"{base_url}/customizations/gvars/{key}"
|
|
168
|
-
# Avrae service expects the JWT directly in Authorization (no Bearer prefix).
|
|
169
|
-
headers = {"Authorization": str(self._config.service.token)}
|
|
170
|
-
try:
|
|
171
|
-
log.debug("GVAR fetching %s from %s", key, url)
|
|
172
|
-
async with httpx.AsyncClient(timeout=5) as client:
|
|
173
|
-
resp = await client.get(url, headers=headers)
|
|
174
|
-
except Exception as exc:
|
|
175
|
-
log.error("GVAR fetch failed for %s: %s", key, exc)
|
|
176
|
-
return False
|
|
177
|
-
|
|
178
|
-
if resp.status_code != 200:
|
|
179
|
-
log.warning(
|
|
180
|
-
"GVAR fetch returned %s for %s (body: %s)",
|
|
181
|
-
resp.status_code,
|
|
182
|
-
key,
|
|
183
|
-
(resp.text or "").strip(),
|
|
184
|
-
)
|
|
185
|
-
return False
|
|
186
|
-
|
|
187
|
-
value: Any = None
|
|
188
|
-
try:
|
|
189
|
-
payload = resp.json()
|
|
190
|
-
except Exception:
|
|
191
|
-
payload = None
|
|
192
|
-
|
|
193
|
-
if isinstance(payload, dict) and "value" in payload:
|
|
194
|
-
value = payload["value"]
|
|
195
|
-
|
|
196
|
-
log.debug("GVAR fetch parsed value for %s (type=%s)", key, type(value).__name__)
|
|
197
|
-
|
|
198
|
-
if value is None:
|
|
199
|
-
log.error("GVAR %s payload missing value", key)
|
|
200
|
-
return False
|
|
201
|
-
self._cache[key] = value
|
|
202
|
-
return True
|
|
203
|
-
|
|
204
|
-
def snapshot(self) -> Dict[str, Any]:
|
|
205
|
-
return dict(self._cache)
|
|
206
|
-
|
|
207
|
-
async def refresh(self, seed: Dict[str, Any] | None = None, keys: Iterable[str] | None = None) -> Dict[str, Any]:
|
|
208
|
-
self.reset(seed)
|
|
209
|
-
if keys:
|
|
210
|
-
for key in keys:
|
|
211
|
-
await self.ensure(key)
|
|
212
|
-
return self.snapshot()
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
def _read_json_file(path: Path) -> Dict[str, Any] | None:
|
|
216
|
-
try:
|
|
217
|
-
text = path.read_text()
|
|
218
|
-
except FileNotFoundError:
|
|
219
|
-
log.debug("Var file not found: %s", path)
|
|
220
|
-
return None
|
|
221
|
-
except OSError as exc:
|
|
222
|
-
log.warning("Failed to read var file %s: %s", path, exc)
|
|
223
|
-
return None
|
|
224
|
-
|
|
225
|
-
try:
|
|
226
|
-
return json.loads(text)
|
|
227
|
-
except json.JSONDecodeError as exc:
|
|
228
|
-
log.warning("Failed to parse var file %s: %s", path, exc)
|
|
229
|
-
return None
|
avrae_ls/cvars.py
DELETED
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import math
|
|
4
|
-
from typing import Any, Dict, Mapping
|
|
5
|
-
|
|
6
|
-
ABILITY_KEYS = ["strength", "dexterity", "constitution", "intelligence", "wisdom", "charisma"]
|
|
7
|
-
SAVE_KEYS = {
|
|
8
|
-
"strength": "str",
|
|
9
|
-
"dexterity": "dex",
|
|
10
|
-
"constitution": "con",
|
|
11
|
-
"intelligence": "int",
|
|
12
|
-
"wisdom": "wis",
|
|
13
|
-
"charisma": "cha",
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def derive_character_cvars(character: Mapping[str, Any]) -> Dict[str, Any]:
|
|
18
|
-
"""Build the documented cvar table values from a character payload."""
|
|
19
|
-
stats = character.get("stats") or {}
|
|
20
|
-
saves = character.get("saves") or {}
|
|
21
|
-
levels = character.get("levels") or {}
|
|
22
|
-
spellbook = character.get("spellbook") or {}
|
|
23
|
-
csettings = character.get("csettings") or {}
|
|
24
|
-
|
|
25
|
-
cvars: dict[str, Any] = {}
|
|
26
|
-
|
|
27
|
-
for ability in ABILITY_KEYS:
|
|
28
|
-
score = _int_or_none(stats.get(ability))
|
|
29
|
-
save_val = _int_or_none(saves.get(SAVE_KEYS[ability]))
|
|
30
|
-
|
|
31
|
-
if score is not None:
|
|
32
|
-
cvars[ability] = score
|
|
33
|
-
cvars[f"{ability}Mod"] = math.floor((score - 10) / 2)
|
|
34
|
-
if save_val is not None:
|
|
35
|
-
cvars[f"{ability}Save"] = save_val
|
|
36
|
-
|
|
37
|
-
armor = _int_or_none(character.get("ac"))
|
|
38
|
-
if armor is not None:
|
|
39
|
-
cvars["armor"] = armor
|
|
40
|
-
|
|
41
|
-
description = character.get("description")
|
|
42
|
-
if description is not None:
|
|
43
|
-
cvars["description"] = description
|
|
44
|
-
|
|
45
|
-
image = character.get("image")
|
|
46
|
-
if image is not None:
|
|
47
|
-
cvars["image"] = image
|
|
48
|
-
|
|
49
|
-
name = character.get("name")
|
|
50
|
-
if name is not None:
|
|
51
|
-
cvars["name"] = name
|
|
52
|
-
|
|
53
|
-
max_hp = _int_or_none(character.get("max_hp"))
|
|
54
|
-
if max_hp is not None:
|
|
55
|
-
cvars["hp"] = max_hp
|
|
56
|
-
|
|
57
|
-
color = _color_hex(csettings.get("color"))
|
|
58
|
-
if color is not None:
|
|
59
|
-
cvars["color"] = color
|
|
60
|
-
|
|
61
|
-
prof = _int_or_none(stats.get("prof_bonus"))
|
|
62
|
-
if prof is not None:
|
|
63
|
-
cvars["proficiencyBonus"] = prof
|
|
64
|
-
|
|
65
|
-
spell_mod = _spell_mod(spellbook, prof)
|
|
66
|
-
if spell_mod is not None:
|
|
67
|
-
cvars["spell"] = spell_mod
|
|
68
|
-
|
|
69
|
-
total_level = _sum_ints(levels.values())
|
|
70
|
-
if total_level is not None:
|
|
71
|
-
cvars["level"] = total_level
|
|
72
|
-
|
|
73
|
-
for cls, lvl in levels.items():
|
|
74
|
-
lvl_int = _int_or_none(lvl)
|
|
75
|
-
if lvl_int is None:
|
|
76
|
-
continue
|
|
77
|
-
cvars[f"{str(cls).replace(' ', '')}Level"] = lvl_int
|
|
78
|
-
|
|
79
|
-
return cvars
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
def _int_or_none(value: Any) -> int | None:
|
|
83
|
-
if isinstance(value, dict) and "value" in value:
|
|
84
|
-
value = value.get("value")
|
|
85
|
-
try:
|
|
86
|
-
return int(value)
|
|
87
|
-
except (TypeError, ValueError):
|
|
88
|
-
return None
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
def _spell_mod(spellbook: Mapping[str, Any], prof_bonus: int | None) -> int | None:
|
|
92
|
-
if "spell_mod" in spellbook:
|
|
93
|
-
return _int_or_none(spellbook.get("spell_mod"))
|
|
94
|
-
if "sab" in spellbook and prof_bonus is not None:
|
|
95
|
-
sab = _int_or_none(spellbook.get("sab"))
|
|
96
|
-
if sab is not None:
|
|
97
|
-
return sab - prof_bonus
|
|
98
|
-
return None
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
def _sum_ints(values: Any) -> int | None:
|
|
102
|
-
try:
|
|
103
|
-
total = sum(int(v) for v in values)
|
|
104
|
-
except Exception:
|
|
105
|
-
return None
|
|
106
|
-
return total
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
def _color_hex(value: Any) -> str | None:
|
|
110
|
-
if value is None:
|
|
111
|
-
return None
|
|
112
|
-
try:
|
|
113
|
-
return hex(int(value))[2:]
|
|
114
|
-
except (TypeError, ValueError):
|
|
115
|
-
return None
|