glitchlings 0.10.2__cp312-cp312-macosx_11_0_universal2.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 glitchlings might be problematic. Click here for more details.

Files changed (83) hide show
  1. glitchlings/__init__.py +99 -0
  2. glitchlings/__main__.py +8 -0
  3. glitchlings/_zoo_rust/__init__.py +12 -0
  4. glitchlings/_zoo_rust.cpython-312-darwin.so +0 -0
  5. glitchlings/assets/__init__.py +180 -0
  6. glitchlings/assets/apostrofae_pairs.json +32 -0
  7. glitchlings/assets/ekkokin_homophones.json +2014 -0
  8. glitchlings/assets/hokey_assets.json +193 -0
  9. glitchlings/assets/lexemes/academic.json +1049 -0
  10. glitchlings/assets/lexemes/colors.json +1333 -0
  11. glitchlings/assets/lexemes/corporate.json +716 -0
  12. glitchlings/assets/lexemes/cyberpunk.json +22 -0
  13. glitchlings/assets/lexemes/lovecraftian.json +23 -0
  14. glitchlings/assets/lexemes/synonyms.json +3354 -0
  15. glitchlings/assets/mim1c_homoglyphs.json.gz.b64 +1064 -0
  16. glitchlings/assets/ocr_confusions.tsv +30 -0
  17. glitchlings/assets/pipeline_assets.json +29 -0
  18. glitchlings/attack/__init__.py +147 -0
  19. glitchlings/attack/analysis.py +1321 -0
  20. glitchlings/attack/core.py +493 -0
  21. glitchlings/attack/core_execution.py +367 -0
  22. glitchlings/attack/core_planning.py +612 -0
  23. glitchlings/attack/encode.py +114 -0
  24. glitchlings/attack/metrics.py +218 -0
  25. glitchlings/attack/metrics_dispatch.py +70 -0
  26. glitchlings/attack/tokenization.py +227 -0
  27. glitchlings/auggie.py +284 -0
  28. glitchlings/compat/__init__.py +9 -0
  29. glitchlings/compat/loaders.py +355 -0
  30. glitchlings/compat/types.py +41 -0
  31. glitchlings/conf/__init__.py +41 -0
  32. glitchlings/conf/loaders.py +331 -0
  33. glitchlings/conf/schema.py +156 -0
  34. glitchlings/conf/types.py +72 -0
  35. glitchlings/config.toml +2 -0
  36. glitchlings/constants.py +59 -0
  37. glitchlings/dev/__init__.py +3 -0
  38. glitchlings/dev/docs.py +45 -0
  39. glitchlings/dlc/__init__.py +19 -0
  40. glitchlings/dlc/_shared.py +296 -0
  41. glitchlings/dlc/gutenberg.py +400 -0
  42. glitchlings/dlc/huggingface.py +68 -0
  43. glitchlings/dlc/prime.py +215 -0
  44. glitchlings/dlc/pytorch.py +98 -0
  45. glitchlings/dlc/pytorch_lightning.py +173 -0
  46. glitchlings/internal/__init__.py +16 -0
  47. glitchlings/internal/rust.py +159 -0
  48. glitchlings/internal/rust_ffi.py +490 -0
  49. glitchlings/main.py +426 -0
  50. glitchlings/protocols.py +91 -0
  51. glitchlings/runtime_config.py +24 -0
  52. glitchlings/util/__init__.py +27 -0
  53. glitchlings/util/adapters.py +65 -0
  54. glitchlings/util/keyboards.py +356 -0
  55. glitchlings/util/transcripts.py +108 -0
  56. glitchlings/zoo/__init__.py +161 -0
  57. glitchlings/zoo/assets/__init__.py +29 -0
  58. glitchlings/zoo/core.py +678 -0
  59. glitchlings/zoo/core_execution.py +154 -0
  60. glitchlings/zoo/core_planning.py +451 -0
  61. glitchlings/zoo/corrupt_dispatch.py +295 -0
  62. glitchlings/zoo/hokey.py +139 -0
  63. glitchlings/zoo/jargoyle.py +243 -0
  64. glitchlings/zoo/mim1c.py +148 -0
  65. glitchlings/zoo/pedant/__init__.py +109 -0
  66. glitchlings/zoo/pedant/core.py +105 -0
  67. glitchlings/zoo/pedant/forms.py +74 -0
  68. glitchlings/zoo/pedant/stones.py +74 -0
  69. glitchlings/zoo/redactyl.py +97 -0
  70. glitchlings/zoo/rng.py +259 -0
  71. glitchlings/zoo/rushmore.py +416 -0
  72. glitchlings/zoo/scannequin.py +66 -0
  73. glitchlings/zoo/transforms.py +346 -0
  74. glitchlings/zoo/typogre.py +128 -0
  75. glitchlings/zoo/validation.py +477 -0
  76. glitchlings/zoo/wherewolf.py +120 -0
  77. glitchlings/zoo/zeedub.py +93 -0
  78. glitchlings-0.10.2.dist-info/METADATA +337 -0
  79. glitchlings-0.10.2.dist-info/RECORD +83 -0
  80. glitchlings-0.10.2.dist-info/WHEEL +5 -0
  81. glitchlings-0.10.2.dist-info/entry_points.txt +3 -0
  82. glitchlings-0.10.2.dist-info/licenses/LICENSE +201 -0
  83. glitchlings-0.10.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,356 @@
1
+ """Keyboard layout neighbor maps for typo simulation.
2
+
3
+ This module centralizes keyboard layout data that was previously stored
4
+ directly in :mod:`glitchlings.util.__init__`. It defines adjacency maps
5
+ for various keyboard layouts used by typo-generating glitchlings.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from collections.abc import Iterable
11
+
12
+ __all__ = [
13
+ "KeyboardLayouts",
14
+ "KeyNeighbors",
15
+ "KEYNEIGHBORS",
16
+ "ShiftMap",
17
+ "ShiftMaps",
18
+ "SHIFT_MAPS",
19
+ "KeyNeighborMap",
20
+ "build_keyboard_neighbor_map",
21
+ ]
22
+
23
+ # Type alias for keyboard neighbor maps
24
+ KeyNeighborMap = dict[str, list[str]]
25
+
26
+
27
+ def build_keyboard_neighbor_map(rows: Iterable[str]) -> KeyNeighborMap:
28
+ """Derive 8-neighbour adjacency lists from keyboard layout rows.
29
+
30
+ Each row represents a keyboard row with characters positioned by index.
31
+ Spaces are treated as empty positions. Characters are normalized to lowercase.
32
+
33
+ Args:
34
+ rows: Iterable of strings representing keyboard rows, with
35
+ characters positioned to reflect their physical layout.
36
+
37
+ Returns:
38
+ Dictionary mapping each lowercase character to its adjacent characters.
39
+
40
+ Example:
41
+ >>> rows = ["qwerty", " asdfg"] # 'a' offset by 1
42
+ >>> neighbors = build_keyboard_neighbor_map(rows)
43
+ >>> neighbors['s'] # adjacent to q, w, e, a, d on QWERTY
44
+ ['q', 'w', 'e', 'a', 'd']
45
+ """
46
+ grid: dict[tuple[int, int], str] = {}
47
+ for y, row in enumerate(rows):
48
+ for x, char in enumerate(row):
49
+ if char == " ":
50
+ continue
51
+ grid[(x, y)] = char.lower()
52
+
53
+ neighbors: KeyNeighborMap = {}
54
+ for (x, y), char in grid.items():
55
+ seen: list[str] = []
56
+ for dy in (-1, 0, 1):
57
+ for dx in (-1, 0, 1):
58
+ if dx == 0 and dy == 0:
59
+ continue
60
+ candidate = grid.get((x + dx, y + dy))
61
+ if candidate is None:
62
+ continue
63
+ seen.append(candidate)
64
+ # Preserve encounter order but drop duplicates for determinism
65
+ deduped = list(dict.fromkeys(seen))
66
+ neighbors[char] = deduped
67
+
68
+ return neighbors
69
+
70
+
71
+ KeyboardLayouts = dict[str, KeyNeighborMap]
72
+ ShiftMap = dict[str, str]
73
+ ShiftMaps = dict[str, ShiftMap]
74
+
75
+
76
+ _KEYNEIGHBORS: KeyboardLayouts = {
77
+ "CURATOR_QWERTY": {
78
+ "a": [*"qwsz"],
79
+ "b": [*"vghn "],
80
+ "c": [*"xdfv "],
81
+ "d": [*"serfcx"],
82
+ "e": [*"wsdrf34"],
83
+ "f": [*"drtgvc"],
84
+ "g": [*"ftyhbv"],
85
+ "h": [*"gyujnb"],
86
+ "i": [*"ujko89"],
87
+ "j": [*"huikmn"],
88
+ "k": [*"jilom,"],
89
+ "l": [*"kop;.,"],
90
+ "m": [*"njk, "],
91
+ "n": [*"bhjm "],
92
+ "o": [*"iklp90"],
93
+ "p": [*"o0-[;l"],
94
+ "q": [*"was 12"],
95
+ "r": [*"edft45"],
96
+ "s": [*"awedxz"],
97
+ "t": [*"r56ygf"],
98
+ "u": [*"y78ijh"],
99
+ "v": [*"cfgb "],
100
+ "w": [*"q23esa"],
101
+ "x": [*"zsdc "],
102
+ "y": [*"t67uhg"],
103
+ "z": [*"asx"],
104
+ }
105
+ }
106
+
107
+
108
+ def _register_layout(name: str, rows: Iterable[str]) -> None:
109
+ _KEYNEIGHBORS[name] = build_keyboard_neighbor_map(rows)
110
+
111
+
112
+ _register_layout(
113
+ "DVORAK",
114
+ (
115
+ "`1234567890[]\\",
116
+ " ',.pyfgcrl/=\\",
117
+ " aoeuidhtns-",
118
+ " ;qjkxbmwvz",
119
+ ),
120
+ )
121
+
122
+ _register_layout(
123
+ "COLEMAK",
124
+ (
125
+ "`1234567890-=",
126
+ " qwfpgjluy;[]\\",
127
+ " arstdhneio'",
128
+ " zxcvbkm,./",
129
+ ),
130
+ )
131
+
132
+ _register_layout(
133
+ "QWERTY",
134
+ (
135
+ "`1234567890-=",
136
+ " qwertyuiop[]\\",
137
+ " asdfghjkl;'",
138
+ " zxcvbnm,./",
139
+ ),
140
+ )
141
+
142
+ _register_layout(
143
+ "AZERTY",
144
+ (
145
+ "²&é\"'(-è_çà)=",
146
+ " azertyuiop^$",
147
+ " qsdfghjklmù*",
148
+ " <wxcvbn,;:!",
149
+ ),
150
+ )
151
+
152
+ _register_layout(
153
+ "QWERTZ",
154
+ (
155
+ "^1234567890ß´",
156
+ " qwertzuiopü+",
157
+ " asdfghjklöä#",
158
+ " yxcvbnm,.-",
159
+ ),
160
+ )
161
+
162
+ _register_layout(
163
+ "SPANISH_QWERTY",
164
+ (
165
+ "º1234567890'¡",
166
+ " qwertyuiop´+",
167
+ " asdfghjklñ´",
168
+ " <zxcvbnm,.-",
169
+ ),
170
+ )
171
+
172
+ _register_layout(
173
+ "SWEDISH_QWERTY",
174
+ (
175
+ "§1234567890+´",
176
+ " qwertyuiopå¨",
177
+ " asdfghjklöä'",
178
+ " <zxcvbnm,.-",
179
+ ),
180
+ )
181
+
182
+
183
+ class KeyNeighbors:
184
+ """Attribute-based access to keyboard layout neighbor maps."""
185
+
186
+ def __init__(self) -> None:
187
+ for layout_name, layout in _KEYNEIGHBORS.items():
188
+ setattr(self, layout_name, layout)
189
+
190
+
191
+ KEYNEIGHBORS: KeyNeighbors = KeyNeighbors()
192
+
193
+
194
+ def _uppercase_keys(layout: str) -> ShiftMap:
195
+ mapping: ShiftMap = {}
196
+ for key in _KEYNEIGHBORS.get(layout, {}):
197
+ if key.isalpha():
198
+ mapping[key] = key.upper()
199
+ return mapping
200
+
201
+
202
+ def _with_letters(base: ShiftMap, layout: str) -> ShiftMap:
203
+ mapping = dict(base)
204
+ mapping.update(_uppercase_keys(layout))
205
+ return mapping
206
+
207
+
208
+ def _qwerty_symbols() -> ShiftMap:
209
+ return {
210
+ "`": "~",
211
+ "1": "!",
212
+ "2": "@",
213
+ "3": "#",
214
+ "4": "$",
215
+ "5": "%",
216
+ "6": "^",
217
+ "7": "&",
218
+ "8": "*",
219
+ "9": "(",
220
+ "0": ")",
221
+ "-": "_",
222
+ "=": "+",
223
+ "[": "{",
224
+ "]": "}",
225
+ "\\": "|",
226
+ ";": ":",
227
+ "'": '"',
228
+ ",": "<",
229
+ ".": ">",
230
+ "/": "?",
231
+ }
232
+
233
+
234
+ def _azerty_symbols() -> ShiftMap:
235
+ return {
236
+ "&": "1",
237
+ "\u00e9": "2",
238
+ '"': "3",
239
+ "'": "4",
240
+ "(": "5",
241
+ "-": "6",
242
+ "\u00e8": "7",
243
+ "_": "8",
244
+ "\u00e7": "9",
245
+ "\u00e0": "0",
246
+ ")": "\u00b0",
247
+ "=": "+",
248
+ "^": "\u00a8",
249
+ "$": "\u00a3",
250
+ "*": "\u00b5",
251
+ "\u00f9": "%",
252
+ "<": ">",
253
+ ",": "?",
254
+ ";": ".",
255
+ ":": "/",
256
+ "!": "\u00a7",
257
+ }
258
+
259
+
260
+ def _qwertz_symbols() -> ShiftMap:
261
+ return {
262
+ "^": "\u00b0",
263
+ "1": "!",
264
+ "2": '"',
265
+ "3": "\u00a7",
266
+ "4": "$",
267
+ "5": "%",
268
+ "6": "&",
269
+ "7": "/",
270
+ "8": "(",
271
+ "9": ")",
272
+ "0": "=",
273
+ "\u00df": "?",
274
+ "\u00b4": "`",
275
+ "+": "*",
276
+ "#": "'",
277
+ "-": "_",
278
+ ",": ";",
279
+ ".": ":",
280
+ "\u00e4": "\u00c4",
281
+ "\u00f6": "\u00d6",
282
+ "\u00fc": "\u00dc",
283
+ }
284
+
285
+
286
+ def _spanish_symbols() -> ShiftMap:
287
+ return {
288
+ "\u00ba": "\u00aa",
289
+ "1": "!",
290
+ "2": '"',
291
+ "3": "\u00b7",
292
+ "4": "$",
293
+ "5": "%",
294
+ "6": "&",
295
+ "7": "/",
296
+ "8": "(",
297
+ "9": ")",
298
+ "0": "=",
299
+ "'": "?",
300
+ "\u00a1": "\u00bf",
301
+ "+": "*",
302
+ "\u00b4": "\u00a8",
303
+ "-": "_",
304
+ ",": ";",
305
+ ".": ":",
306
+ "<": ">",
307
+ "\u00f1": "\u00d1",
308
+ }
309
+
310
+
311
+ def _swedish_symbols() -> ShiftMap:
312
+ return {
313
+ "\u00a7": "\u00bd",
314
+ "1": "!",
315
+ "2": '"',
316
+ "3": "#",
317
+ "4": "\u00a4",
318
+ "5": "%",
319
+ "6": "&",
320
+ "7": "/",
321
+ "8": "(",
322
+ "9": ")",
323
+ "0": "=",
324
+ "+": "?",
325
+ "\u00b4": "\u00a8",
326
+ "-": "_",
327
+ ",": ";",
328
+ ".": ":",
329
+ "<": ">",
330
+ "\u00e5": "\u00c5",
331
+ "\u00e4": "\u00c4",
332
+ "\u00f6": "\u00d6",
333
+ }
334
+
335
+
336
+ _SHIFT_MAPS: ShiftMaps = {
337
+ "CURATOR_QWERTY": _with_letters(_qwerty_symbols(), "CURATOR_QWERTY"),
338
+ "QWERTY": _with_letters(_qwerty_symbols(), "QWERTY"),
339
+ "COLEMAK": _with_letters(_qwerty_symbols(), "COLEMAK"),
340
+ "DVORAK": _with_letters(_qwerty_symbols(), "DVORAK"),
341
+ "AZERTY": _with_letters(_azerty_symbols(), "AZERTY"),
342
+ "QWERTZ": _with_letters(_qwertz_symbols(), "QWERTZ"),
343
+ "SPANISH_QWERTY": _with_letters(_spanish_symbols(), "SPANISH_QWERTY"),
344
+ "SWEDISH_QWERTY": _with_letters(_swedish_symbols(), "SWEDISH_QWERTY"),
345
+ }
346
+
347
+
348
+ class ShiftMapsAccessor:
349
+ """Attribute-based access to per-layout shift maps."""
350
+
351
+ def __init__(self) -> None:
352
+ for layout_name, mapping in _SHIFT_MAPS.items():
353
+ setattr(self, layout_name, mapping)
354
+
355
+
356
+ SHIFT_MAPS: ShiftMapsAccessor = ShiftMapsAccessor()
@@ -0,0 +1,108 @@
1
+ """Shared transcript type helpers used across attack and DLC modules."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Literal, Sequence, TypeGuard, Union
6
+
7
+ TranscriptTurn = dict[str, Any]
8
+ Transcript = list[TranscriptTurn]
9
+
10
+ # Type alias for transcript target specifications.
11
+ # - "last": corrupt only the last turn (default behavior)
12
+ # - "all": corrupt all turns
13
+ # - "assistant": corrupt only turns with role="assistant"
14
+ # - "user": corrupt only turns with role="user"
15
+ # - int: corrupt a specific index (negative indexing supported)
16
+ # - Sequence[int]: corrupt specific indices
17
+ TranscriptTarget = Union[Literal["last", "all", "assistant", "user"], int, Sequence[int]]
18
+
19
+
20
+ def is_transcript(
21
+ value: Any,
22
+ *,
23
+ allow_empty: bool = True,
24
+ require_all_content: bool = False,
25
+ ) -> TypeGuard[Transcript]:
26
+ """Return True when ``value`` appears to be a chat transcript mapping list."""
27
+ if not isinstance(value, list):
28
+ return False
29
+
30
+ if not value:
31
+ return allow_empty
32
+
33
+ if not all(isinstance(turn, dict) for turn in value):
34
+ return False
35
+
36
+ if require_all_content:
37
+ return all("content" in turn for turn in value)
38
+
39
+ return "content" in value[-1]
40
+
41
+
42
+ def resolve_transcript_indices(
43
+ transcript: Transcript,
44
+ target: TranscriptTarget,
45
+ ) -> list[int]:
46
+ """Resolve a transcript target specification to concrete indices.
47
+
48
+ Args:
49
+ transcript: The transcript to resolve indices for.
50
+ target: The target specification indicating which turns to corrupt.
51
+
52
+ Returns:
53
+ A list of valid indices into the transcript, sorted in ascending order.
54
+
55
+ Raises:
56
+ ValueError: If the target specification is invalid or references
57
+ indices outside the transcript bounds.
58
+ """
59
+ if not transcript:
60
+ return []
61
+
62
+ length = len(transcript)
63
+
64
+ if target == "last":
65
+ return [length - 1]
66
+
67
+ if target == "all":
68
+ return list(range(length))
69
+
70
+ if target == "assistant":
71
+ return [i for i, turn in enumerate(transcript) if turn.get("role") == "assistant"]
72
+
73
+ if target == "user":
74
+ return [i for i, turn in enumerate(transcript) if turn.get("role") == "user"]
75
+
76
+ if isinstance(target, int):
77
+ # Normalize negative indices
78
+ normalized = target if target >= 0 else length + target
79
+ if not 0 <= normalized < length:
80
+ raise ValueError(f"Transcript index {target} out of bounds for length {length}")
81
+ return [normalized]
82
+
83
+ # Handle sequence of indices
84
+ if isinstance(target, Sequence) and not isinstance(target, str):
85
+ indices: list[int] = []
86
+ for idx in target:
87
+ if not isinstance(idx, int):
88
+ raise ValueError(f"Transcript indices must be integers, got {type(idx).__name__}")
89
+ normalized = idx if idx >= 0 else length + idx
90
+ if not 0 <= normalized < length:
91
+ raise ValueError(f"Transcript index {idx} out of bounds for length {length}")
92
+ indices.append(normalized)
93
+ # Deduplicate and sort
94
+ return sorted(set(indices))
95
+
96
+ raise ValueError(
97
+ f"Invalid transcript target: {target!r}. "
98
+ "Expected 'last', 'all', 'assistant', 'user', int, or sequence of ints."
99
+ )
100
+
101
+
102
+ __all__ = [
103
+ "Transcript",
104
+ "TranscriptTarget",
105
+ "TranscriptTurn",
106
+ "is_transcript",
107
+ "resolve_transcript_indices",
108
+ ]
@@ -0,0 +1,161 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from typing import Any
5
+
6
+ from .core import Gaggle, Glitchling, plan_glitchlings
7
+ from .hokey import Hokey, hokey
8
+ from .jargoyle import Jargoyle, jargoyle
9
+ from .mim1c import Mim1c, mim1c
10
+ from .pedant import Pedant, pedant
11
+ from .redactyl import Redactyl, redactyl
12
+ from .rushmore import Rushmore, RushmoreMode, rushmore
13
+ from .scannequin import Scannequin, scannequin
14
+ from .typogre import Typogre, typogre
15
+ from .wherewolf import Wherewolf, wherewolf
16
+ from .zeedub import Zeedub, zeedub
17
+
18
+ __all__ = [
19
+ "Typogre",
20
+ "typogre",
21
+ "Mim1c",
22
+ "mim1c",
23
+ "Jargoyle",
24
+ "jargoyle",
25
+ "Wherewolf",
26
+ "wherewolf",
27
+ "Hokey",
28
+ "hokey",
29
+ "Rushmore",
30
+ "RushmoreMode",
31
+ "rushmore",
32
+ "Redactyl",
33
+ "redactyl",
34
+ "Scannequin",
35
+ "scannequin",
36
+ "Zeedub",
37
+ "zeedub",
38
+ "Pedant",
39
+ "pedant",
40
+ "Glitchling",
41
+ "Gaggle",
42
+ "plan_glitchlings",
43
+ "summon",
44
+ "BUILTIN_GLITCHLINGS",
45
+ "DEFAULT_GLITCHLING_NAMES",
46
+ "parse_glitchling_spec",
47
+ "get_glitchling_class",
48
+ ]
49
+
50
+ _BUILTIN_GLITCHLING_LIST: list[Glitchling] = [
51
+ typogre,
52
+ hokey,
53
+ mim1c,
54
+ wherewolf,
55
+ pedant,
56
+ jargoyle,
57
+ rushmore,
58
+ redactyl,
59
+ scannequin,
60
+ zeedub,
61
+ ]
62
+
63
+ BUILTIN_GLITCHLINGS: dict[str, Glitchling] = {
64
+ glitchling.name.lower(): glitchling for glitchling in _BUILTIN_GLITCHLING_LIST
65
+ }
66
+
67
+ _BUILTIN_GLITCHLING_TYPES: dict[str, type[Glitchling]] = {
68
+ typogre.name.lower(): Typogre,
69
+ wherewolf.name.lower(): Wherewolf,
70
+ hokey.name.lower(): Hokey,
71
+ mim1c.name.lower(): Mim1c,
72
+ pedant.name.lower(): Pedant,
73
+ jargoyle.name.lower(): Jargoyle,
74
+ rushmore.name.lower(): Rushmore,
75
+ redactyl.name.lower(): Redactyl,
76
+ scannequin.name.lower(): Scannequin,
77
+ zeedub.name.lower(): Zeedub,
78
+ }
79
+
80
+ DEFAULT_GLITCHLING_NAMES: list[str] = ["typogre", "scannequin"]
81
+
82
+
83
+ def parse_glitchling_spec(specification: str) -> Glitchling:
84
+ """Return a glitchling instance configured according to ``specification``."""
85
+ text = specification.strip()
86
+ if not text:
87
+ raise ValueError("Glitchling specification cannot be empty.")
88
+
89
+ if "(" not in text:
90
+ glitchling = BUILTIN_GLITCHLINGS.get(text.lower())
91
+ if glitchling is None:
92
+ raise ValueError(f"Glitchling '{text}' not found.")
93
+ return glitchling
94
+
95
+ if not text.endswith(")"):
96
+ raise ValueError(f"Invalid parameter syntax for glitchling '{text}'.")
97
+
98
+ name_part, arg_source = text[:-1].split("(", 1)
99
+ name = name_part.strip()
100
+ if not name:
101
+ raise ValueError(f"Invalid glitchling specification '{text}'.")
102
+
103
+ lower_name = name.lower()
104
+ glitchling_type = _BUILTIN_GLITCHLING_TYPES.get(lower_name)
105
+ if glitchling_type is None:
106
+ raise ValueError(f"Glitchling '{name}' not found.")
107
+
108
+ try:
109
+ call_expr = ast.parse(f"_({arg_source})", mode="eval").body
110
+ except SyntaxError as exc:
111
+ raise ValueError(f"Invalid parameter syntax for glitchling '{name}': {exc.msg}") from exc
112
+
113
+ if not isinstance(call_expr, ast.Call) or call_expr.args:
114
+ raise ValueError(f"Glitchling '{name}' parameters must be provided as keyword arguments.")
115
+
116
+ kwargs: dict[str, Any] = {}
117
+ for keyword in call_expr.keywords:
118
+ if keyword.arg is None:
119
+ raise ValueError(
120
+ f"Glitchling '{name}' does not support unpacking arbitrary keyword arguments."
121
+ )
122
+ try:
123
+ kwargs[keyword.arg] = ast.literal_eval(keyword.value)
124
+ except (ValueError, SyntaxError) as exc:
125
+ raise ValueError(
126
+ f"Failed to parse value for parameter '{keyword.arg}' on glitchling '{name}': {exc}"
127
+ ) from exc
128
+
129
+ try:
130
+ return glitchling_type(**kwargs)
131
+ except TypeError as exc:
132
+ raise ValueError(f"Failed to instantiate glitchling '{name}': {exc}") from exc
133
+
134
+
135
+ def get_glitchling_class(name: str) -> type[Glitchling]:
136
+ """Look up the glitchling class registered under ``name``."""
137
+ key = name.strip().lower()
138
+ if not key:
139
+ raise ValueError("Glitchling name cannot be empty.")
140
+
141
+ glitchling_type = _BUILTIN_GLITCHLING_TYPES.get(key)
142
+ if glitchling_type is None:
143
+ raise ValueError(f"Glitchling '{name}' not found.")
144
+
145
+ return glitchling_type
146
+
147
+
148
+ def summon(glitchlings: list[str | Glitchling], seed: int = 151) -> Gaggle:
149
+ """Summon glitchlings by name (using defaults) or instance (to change parameters)."""
150
+ summoned: list[Glitchling] = []
151
+ for entry in glitchlings:
152
+ if isinstance(entry, Glitchling):
153
+ summoned.append(entry)
154
+ continue
155
+
156
+ try:
157
+ summoned.append(parse_glitchling_spec(entry))
158
+ except ValueError as exc:
159
+ raise ValueError(str(exc)) from exc
160
+
161
+ return Gaggle(summoned, seed=seed)
@@ -0,0 +1,29 @@
1
+ """Compatibility shim for the relocated asset helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from glitchlings.assets import (
6
+ PIPELINE_ASSET_SPECS,
7
+ PIPELINE_ASSETS,
8
+ AssetKind,
9
+ PipelineAsset,
10
+ hash_asset,
11
+ load_homophone_groups,
12
+ load_json,
13
+ open_binary,
14
+ open_text,
15
+ read_text,
16
+ )
17
+
18
+ __all__ = [
19
+ "AssetKind",
20
+ "PipelineAsset",
21
+ "PIPELINE_ASSETS",
22
+ "PIPELINE_ASSET_SPECS",
23
+ "hash_asset",
24
+ "load_homophone_groups",
25
+ "load_json",
26
+ "open_binary",
27
+ "open_text",
28
+ "read_text",
29
+ ]