AdvancedTagScript 3.2.4__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.
Files changed (48) hide show
  1. TagScriptEngine/__init__.py +227 -0
  2. TagScriptEngine/_warnings.py +88 -0
  3. TagScriptEngine/adapter/__init__.py +50 -0
  4. TagScriptEngine/adapter/discordadapters.py +596 -0
  5. TagScriptEngine/adapter/functionadapter.py +23 -0
  6. TagScriptEngine/adapter/intadapter.py +22 -0
  7. TagScriptEngine/adapter/objectadapter.py +35 -0
  8. TagScriptEngine/adapter/redbotadapters.py +161 -0
  9. TagScriptEngine/adapter/stringadapter.py +47 -0
  10. TagScriptEngine/block/__init__.py +130 -0
  11. TagScriptEngine/block/allowedmentions.py +60 -0
  12. TagScriptEngine/block/assign.py +43 -0
  13. TagScriptEngine/block/breakblock.py +41 -0
  14. TagScriptEngine/block/case.py +63 -0
  15. TagScriptEngine/block/command.py +141 -0
  16. TagScriptEngine/block/comment.py +29 -0
  17. TagScriptEngine/block/control.py +149 -0
  18. TagScriptEngine/block/cooldown.py +95 -0
  19. TagScriptEngine/block/count.py +68 -0
  20. TagScriptEngine/block/embedblock.py +306 -0
  21. TagScriptEngine/block/fiftyfifty.py +34 -0
  22. TagScriptEngine/block/helpers.py +164 -0
  23. TagScriptEngine/block/loosevariablegetter.py +40 -0
  24. TagScriptEngine/block/mathblock.py +164 -0
  25. TagScriptEngine/block/randomblock.py +51 -0
  26. TagScriptEngine/block/range.py +56 -0
  27. TagScriptEngine/block/redirect.py +42 -0
  28. TagScriptEngine/block/replaceblock.py +110 -0
  29. TagScriptEngine/block/require_blacklist.py +79 -0
  30. TagScriptEngine/block/shortcutredirect.py +23 -0
  31. TagScriptEngine/block/stopblock.py +38 -0
  32. TagScriptEngine/block/strf.py +70 -0
  33. TagScriptEngine/block/strictvariablegetter.py +38 -0
  34. TagScriptEngine/block/substr.py +25 -0
  35. TagScriptEngine/block/urlencodeblock.py +41 -0
  36. TagScriptEngine/exceptions.py +105 -0
  37. TagScriptEngine/interface/__init__.py +14 -0
  38. TagScriptEngine/interface/adapter.py +75 -0
  39. TagScriptEngine/interface/block.py +124 -0
  40. TagScriptEngine/interpreter.py +502 -0
  41. TagScriptEngine/py.typed +0 -0
  42. TagScriptEngine/utils.py +71 -0
  43. TagScriptEngine/verb.py +160 -0
  44. advancedtagscript-3.2.4.dist-info/METADATA +99 -0
  45. advancedtagscript-3.2.4.dist-info/RECORD +48 -0
  46. advancedtagscript-3.2.4.dist-info/WHEEL +5 -0
  47. advancedtagscript-3.2.4.dist-info/licenses/LICENSE +1 -0
  48. advancedtagscript-3.2.4.dist-info/top_level.txt +1 -0
@@ -0,0 +1,502 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from itertools import islice
5
+ from typing import Any, Dict, List, Optional, Protocol, Tuple, cast
6
+ from typing_extensions import TypeAlias
7
+
8
+ from .exceptions import (
9
+ ProcessError,
10
+ StopError,
11
+ TagScriptError,
12
+ WorkloadExceededError,
13
+ )
14
+ from .interface import Adapter, Block
15
+ from .utils import maybe_await
16
+ from .verb import Verb
17
+
18
+ __all__: Tuple[str, ...] = (
19
+ "Interpreter",
20
+ "AsyncInterpreter",
21
+ "Context",
22
+ "Response",
23
+ "Node",
24
+ "build_node_tree",
25
+ )
26
+
27
+ log: logging.Logger = logging.getLogger(__name__)
28
+
29
+ AdapterDict: TypeAlias = Dict[str, Adapter]
30
+ AnyDict: TypeAlias = Dict[str, Any]
31
+
32
+
33
+ class _Node(Protocol):
34
+ def __init__(self, coordinates: Tuple[int, int], verb: Optional[Verb] = None) -> None: ...
35
+
36
+ def __str__(self) -> str: ...
37
+
38
+ def __repr__(self) -> str: ...
39
+
40
+
41
+ class Node(_Node):
42
+ """
43
+ A low-level object representing a bracketed block.
44
+
45
+ Attributes
46
+ ----------
47
+ coordinates: Tuple[int, int]
48
+ The start and end position of the bracketed text block.
49
+ verb: Optional[Verb]
50
+ The determined Verb for this node.
51
+ output:
52
+ The `Block` processed output for this node.
53
+ """
54
+
55
+ __slots__: Tuple[str, ...] = ("output", "verb", "coordinates")
56
+
57
+ def __init__(self, coordinates: Tuple[int, int], verb: Optional[Verb] = None) -> None:
58
+ self.output: Optional[str] = None
59
+ self.verb: Optional[Verb] = verb
60
+ self.coordinates: Tuple[int, int] = coordinates
61
+
62
+ def __str__(self) -> str:
63
+ return str(self.verb) + " at " + str(self.coordinates)
64
+
65
+ def __repr__(self) -> str:
66
+ return f"<Node verb={self.verb!r} coordinates={self.coordinates!r} output={self.output!r}>"
67
+
68
+
69
+ def build_node_tree(message: str) -> List[Node]:
70
+ """
71
+ Function that finds all possible nodes in a string.
72
+
73
+ Returns
74
+ -------
75
+ List[Node]
76
+ A list of all possible text bracket blocks.
77
+ """
78
+ nodes: List[Node] = []
79
+ previous: str = r""
80
+ starts: List[int] = []
81
+ for idx, char in enumerate(message):
82
+ if char == "{" and previous != r"\\":
83
+ starts.append(idx)
84
+ if char == "}" and previous != r"\\":
85
+ if not starts:
86
+ continue
87
+ coords: Tuple[int, int] = (starts.pop(), idx)
88
+ node: Node = Node(coords)
89
+ nodes.append(node)
90
+ previous: str = char
91
+ return nodes
92
+
93
+
94
+ class _Response(Protocol):
95
+ def __init__(
96
+ self,
97
+ *,
98
+ variables: Optional[AdapterDict] = None,
99
+ extra_kwargs: Optional[AnyDict] = None,
100
+ ) -> None: ...
101
+
102
+ def __repr__(self) -> str: ...
103
+
104
+
105
+ class Response(_Response):
106
+ """
107
+ An object containing information on a completed TagScript process.
108
+
109
+ Attributes
110
+ ----------
111
+ body: str
112
+ The cleaned message with all verbs interpreted.
113
+ actions: Dict[str, Any]
114
+ A dictionary that blocks can access and modify to define post-processing actions.
115
+ variables: Dict[str, Adapter]
116
+ A dictionary of variables that blocks such as the `LooseVariableGetterBlock` can access.
117
+ extra_kwargs: Dict[str, Any]
118
+ A dictionary of extra keyword arguments that blocks can use to define their own behavior.
119
+ """
120
+
121
+ __slots__: Tuple[str, ...] = ("body", "actions", "variables", "extra_kwargs")
122
+
123
+ def __init__(
124
+ self,
125
+ *,
126
+ variables: Optional[AdapterDict] = None,
127
+ extra_kwargs: Optional[AnyDict] = None,
128
+ ) -> None:
129
+ self.body: Optional[str] = None
130
+ self.actions: AnyDict = {}
131
+ self.variables: AdapterDict = variables if variables is not None else {}
132
+ self.extra_kwargs: AnyDict = extra_kwargs if extra_kwargs is not None else {}
133
+
134
+ def __repr__(self) -> str:
135
+ return (
136
+ f"<Response body={self.body!r} actions={self.actions!r} variables={self.variables!r}>"
137
+ )
138
+
139
+
140
+ class _Context(Protocol):
141
+ def __init__(self, verb: Verb, res: Response, interpreter: Interpreter, og: str) -> None: ...
142
+
143
+ def __repr__(self) -> str: ...
144
+
145
+
146
+ class Context(_Context):
147
+ """
148
+ An object containing data on the TagScript block processed by the interpreter.
149
+ This class is passed to adapters and blocks during processing.
150
+
151
+ Attributes
152
+ ----------
153
+ verb: Verb
154
+ The Verb object representing a TagScript block.
155
+ original_message: str
156
+ The original message passed to the interpreter.
157
+ interpreter: Interpreter
158
+ The interpreter processing the TagScript.
159
+ """
160
+
161
+ __slots__: Tuple[str, ...] = ("verb", "original_message", "interpreter", "response")
162
+
163
+ def __init__(self, verb: Verb, res: Response, interpreter: Interpreter, og: str) -> None:
164
+ self.verb: Verb = verb
165
+ self.original_message: str = og
166
+ self.interpreter: Interpreter = interpreter
167
+ self.response: Response = res
168
+
169
+ def __repr__(self) -> str:
170
+ return f"<Context verb={self.verb!r}>"
171
+
172
+
173
+ class _Interpreter(Protocol):
174
+ def __init__(self, blocks: List[Block]) -> None: ...
175
+
176
+ def __repr__(self) -> str: ...
177
+
178
+ def _get_context(
179
+ self,
180
+ node: Node,
181
+ final: str,
182
+ *,
183
+ response: Response,
184
+ original_message: str,
185
+ verb_limit: int,
186
+ dot_parameter: bool,
187
+ ) -> Context: ...
188
+
189
+ def _get_acceptors(self, ctx: Context) -> List[Block]: ...
190
+
191
+ def _process_blocks(self, ctx: Context, node: Node) -> Optional[str]: ...
192
+
193
+ @staticmethod
194
+ def _check_workload(charlimit: int, total_work: int, output: str) -> Optional[int]: ...
195
+
196
+ @staticmethod
197
+ def _text_deform(start: int, end: int, final: str, output: str) -> Tuple[str, int]: ...
198
+
199
+ @staticmethod
200
+ def _translate_nodes(
201
+ node_ordered_list: List[Node], index: int, start: int, differential: int
202
+ ) -> None: ...
203
+
204
+ def _solve(
205
+ self,
206
+ message: str,
207
+ node_ordered_list: List[Node],
208
+ response: Response,
209
+ *,
210
+ charlimit: int,
211
+ verb_limit: int = 6000,
212
+ dot_parameter: bool,
213
+ ) -> str: ...
214
+
215
+ @staticmethod
216
+ def _return_response(response: Response, output: str) -> Response: ...
217
+
218
+ def process(
219
+ self,
220
+ message: str,
221
+ seed_variables: Optional[AdapterDict] = None,
222
+ *,
223
+ charlimit: Optional[int] = None,
224
+ dot_parameter: bool = False,
225
+ **kwargs: Any,
226
+ ) -> Response: ...
227
+
228
+
229
+ class Interpreter(_Interpreter):
230
+ """
231
+ The TagScript interpreter.
232
+
233
+ Attributes
234
+ ----------
235
+ blocks: List[Block]
236
+ A list of blocks to be used for TagScript processing.
237
+ """
238
+
239
+ __slots__ = ("blocks",)
240
+
241
+ def __init__(self, blocks: List[Block]) -> None:
242
+ self.blocks: List[Block] = blocks
243
+
244
+ def __repr__(self) -> str:
245
+ return f"<{type(self).__name__} blocks={self.blocks!r}>"
246
+
247
+ def _get_context(
248
+ self,
249
+ node: Node,
250
+ final: str,
251
+ *,
252
+ response: Response,
253
+ original_message: str,
254
+ verb_limit: int,
255
+ dot_parameter: bool,
256
+ ) -> Context:
257
+ # Get the updated verb string from coordinates and make the context
258
+ start, end = node.coordinates
259
+ node.verb = Verb(final[start : end + 1], limit=verb_limit, dot_parameter=dot_parameter)
260
+ return Context(node.verb, response, self, original_message)
261
+
262
+ def _get_acceptors(self, ctx: Context) -> List[Block]:
263
+ acceptors = [b for b in self.blocks if b.will_accept(ctx)]
264
+ log.debug("%r acceptors: %r", ctx, acceptors)
265
+ return acceptors
266
+
267
+ def _process_blocks(self, ctx: Context, node: Node) -> Optional[str]:
268
+ acceptors = self._get_acceptors(ctx)
269
+ for b in acceptors:
270
+ value = b.process(ctx)
271
+ if value is not None: # Value found? We're done here.
272
+ value = str(value)
273
+ node.output = value
274
+ return value
275
+
276
+ @staticmethod
277
+ def _check_workload(charlimit: int, total_work: int, output: str) -> Optional[int]:
278
+ if not charlimit:
279
+ return
280
+ total_work += len(output)
281
+ if total_work > charlimit:
282
+ raise WorkloadExceededError(
283
+ "The TSE interpreter had its workload exceeded. The total characters "
284
+ f"attempted were {total_work}/{charlimit}"
285
+ )
286
+ return total_work
287
+
288
+ @staticmethod
289
+ def _text_deform(start: int, end: int, final: str, output: str) -> Tuple[str, int]:
290
+ message_slice_len = (end + 1) - start
291
+ replacement_len = len(output)
292
+ differential = (
293
+ replacement_len - message_slice_len
294
+ ) # The change in size of `final` after the change is applied
295
+ final = final[:start] + output + final[end + 1 :]
296
+ return final, differential
297
+
298
+ @staticmethod
299
+ def _translate_nodes(
300
+ node_ordered_list: List[Node], index: int, start: int, differential: int
301
+ ) -> None:
302
+ for future_n in islice(node_ordered_list, index + 1, None):
303
+ new_start = None
304
+ new_end = None
305
+ if future_n.coordinates[0] > start:
306
+ new_start = future_n.coordinates[0] + differential
307
+ else:
308
+ new_start = future_n.coordinates[0]
309
+
310
+ if future_n.coordinates[1] > start:
311
+ new_end = future_n.coordinates[1] + differential
312
+ else:
313
+ new_end = future_n.coordinates[1]
314
+ future_n.coordinates = (new_start, new_end)
315
+
316
+ def _solve(
317
+ self,
318
+ message: str,
319
+ node_ordered_list: List[Node],
320
+ response: Response,
321
+ *,
322
+ charlimit: int,
323
+ verb_limit: int = 6000,
324
+ dot_parameter: bool,
325
+ ) -> str:
326
+ final = message
327
+ total_work = 0
328
+ for index, node in enumerate(node_ordered_list):
329
+ start, end = node.coordinates
330
+ ctx = self._get_context(
331
+ node,
332
+ final,
333
+ response=response,
334
+ original_message=message,
335
+ verb_limit=verb_limit,
336
+ dot_parameter=dot_parameter,
337
+ )
338
+ log.debug("Processing context %r at (%r, %r)", ctx, start, end)
339
+ try:
340
+ output = self._process_blocks(ctx, node)
341
+ except StopError as exc:
342
+ log.debug("StopError raised on node %r", node, exc_info=exc)
343
+ return final[:start] + exc.message
344
+ if output is None:
345
+ continue # If there was no value output, no need to text deform.
346
+
347
+ total_work = self._check_workload(charlimit, cast(int, total_work), output)
348
+ final, differential = self._text_deform(start, end, final, output)
349
+ self._translate_nodes(node_ordered_list, index, start, differential)
350
+ return final
351
+
352
+ @staticmethod
353
+ def _return_response(response: Response, output: str) -> Response:
354
+ if response.body is None:
355
+ response.body = output.strip()
356
+ else:
357
+ # Dont override an overridden response.
358
+ response.body = response.body.strip()
359
+ return response
360
+
361
+ def process(
362
+ self,
363
+ message: str,
364
+ seed_variables: Optional[AdapterDict] = None,
365
+ *,
366
+ charlimit: Optional[int] = None,
367
+ dot_parameter: bool = False,
368
+ **kwargs: Any,
369
+ ) -> Response:
370
+ """
371
+ Processes a given TagScript string.
372
+
373
+ Parameters
374
+ ----------
375
+ message: str
376
+ A TagScript string to be processed.
377
+ seed_variables: Dict[str, Adapter]
378
+ A dictionary containing strings to adapters to provide context variables for processing.
379
+ charlimit: int
380
+ The maximum characters to process.
381
+ dot_parameter: bool
382
+ Whether the parameter should be followed after a "." or use the default of parantheses.
383
+ kwargs: Dict[str, Any]
384
+ Additional keyword arguments that may be used by blocks during processing.
385
+
386
+ Returns
387
+ -------
388
+ Response
389
+ A response object containing the processed body, actions and variables.
390
+
391
+ Raises
392
+ ------
393
+ TagScriptError
394
+ A block intentionally raised an exception, most likely due to invalid user input.
395
+ WorkloadExceededError
396
+ Signifies the interpreter reached the character limit, if one was provided.
397
+ ProcessError
398
+ An unexpected error occurred while processing blocks.
399
+ """
400
+ response = Response(variables=seed_variables, extra_kwargs=kwargs)
401
+ node_ordered_list = build_node_tree(message)
402
+ try:
403
+ output = self._solve(
404
+ message,
405
+ node_ordered_list,
406
+ response,
407
+ charlimit=cast(int, charlimit),
408
+ dot_parameter=dot_parameter,
409
+ )
410
+ except TagScriptError:
411
+ raise
412
+ except Exception as error:
413
+ raise ProcessError(error, response, self) from error
414
+ return self._return_response(response, output)
415
+
416
+
417
+ class AsyncInterpreter(Interpreter):
418
+ """
419
+ An asynchronous subclass of `Interpreter` that allows blocks to implement asynchronous methods.
420
+ Synchronous blocks are still supported.
421
+
422
+ This subclass has no additional attributes from the `Interpreter` class.
423
+ See `Interpreter` for full documentation.
424
+ """
425
+
426
+ async def _get_acceptors(self, ctx: Context) -> List[Block]: # type: ignore
427
+ return [b for b in self.blocks if await maybe_await(b.will_accept, ctx)]
428
+
429
+ async def _process_blocks(self, ctx: Context, node: Node) -> Optional[str]: # type: ignore
430
+ acceptors = await self._get_acceptors(ctx)
431
+ for b in acceptors:
432
+ value = await maybe_await(b.process, ctx)
433
+ if value is not None: # Value found? We're done here.
434
+ value = str(value)
435
+ node.output = value
436
+ return value
437
+
438
+ async def _solve( # type: ignore
439
+ self,
440
+ message: str,
441
+ node_ordered_list: List[Node],
442
+ response: Response,
443
+ *,
444
+ charlimit: int,
445
+ verb_limit: int = 6000,
446
+ dot_parameter: bool,
447
+ ) -> str:
448
+ final = message
449
+ total_work = 0
450
+
451
+ for index, node in enumerate(node_ordered_list):
452
+ start, end = node.coordinates
453
+ ctx = self._get_context(
454
+ node,
455
+ final,
456
+ response=response,
457
+ original_message=message,
458
+ verb_limit=verb_limit,
459
+ dot_parameter=dot_parameter,
460
+ )
461
+ try:
462
+ output = await self._process_blocks(ctx, node)
463
+ except StopError as exc:
464
+ return final[:start] + exc.message
465
+ if output is None:
466
+ continue # If there was no value output, no need to text deform.
467
+
468
+ total_work = self._check_workload(charlimit, cast(int, total_work), output)
469
+ final, differential = self._text_deform(start, end, final, output)
470
+ self._translate_nodes(node_ordered_list, index, start, differential)
471
+ return final
472
+
473
+ async def process( # type: ignore
474
+ self,
475
+ message: str,
476
+ seed_variables: Optional[AdapterDict] = None,
477
+ *,
478
+ charlimit: Optional[int] = None,
479
+ dot_parameter: bool = False,
480
+ **kwargs: Any,
481
+ ) -> Response:
482
+ """
483
+ Asynchronously process a given TagScript string.
484
+
485
+ This method has no additional attributes from the `Interpreter` class.
486
+ See `Interpreter.process` for full documentation.
487
+ """
488
+ response = Response(variables=seed_variables, extra_kwargs=kwargs)
489
+ node_ordered_list = build_node_tree(message)
490
+ try:
491
+ output = await self._solve(
492
+ message,
493
+ node_ordered_list,
494
+ response,
495
+ charlimit=cast(int, charlimit),
496
+ dot_parameter=dot_parameter,
497
+ )
498
+ except TagScriptError:
499
+ raise
500
+ except Exception as error:
501
+ raise ProcessError(error, response, self) from error
502
+ return self._return_response(response, output)
File without changes
@@ -0,0 +1,71 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from inspect import isawaitable
5
+ from typing import Any, Awaitable, Callable, Tuple, TypeVar, Union
6
+
7
+
8
+ __all__: Tuple[str, ...] = ("truncate", "escape_content", "maybe_await")
9
+
10
+ T = TypeVar("T")
11
+
12
+ pattern: re.Pattern[str] = re.compile(r"(?<!\\)([{():|}])")
13
+
14
+
15
+ def _sub_match(match: re.Match) -> str:
16
+ return "\\" + match[1]
17
+
18
+
19
+ def truncate(text: str, *, max: int = 2000, var: str = "...") -> str:
20
+ """
21
+ Truncate the given string to avoid hitting the character limit.
22
+
23
+ Parameters
24
+ ----------
25
+ text
26
+ The string to be truncated.
27
+ max
28
+ On what character length the string should be truncated.
29
+ var
30
+ The custom string used for trunication (defaults to '...').
31
+
32
+ Returns
33
+ -------
34
+ str
35
+ The truncated content.
36
+
37
+ .. versionadded:: 3.2.0
38
+ """
39
+ if len(text) <= max:
40
+ return text
41
+ truncated: str = text[: max - 3]
42
+ return truncated + var
43
+
44
+
45
+ def escape_content(string: str) -> str:
46
+ """
47
+ Escapes given input to avoid tampering with engine/block behavior.
48
+
49
+ Returns
50
+ -------
51
+ str
52
+ The escaped content.
53
+ """
54
+ if string is None:
55
+ return
56
+ return pattern.sub(_sub_match, string)
57
+
58
+
59
+ async def maybe_await(
60
+ func: Callable[..., Union[T, Awaitable[T]]], *args: Any, **kwargs: Any
61
+ ) -> Union[T, Any]:
62
+ """
63
+ Await the given function if it is awaitable or call it synchronously.
64
+
65
+ Returns
66
+ -------
67
+ Any
68
+ The result of the awaitable function.
69
+ """
70
+ value: Union[T, Awaitable[T]] = func(*args, **kwargs)
71
+ return await value if isawaitable(value) else value