bTagScript 5.0.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.
Files changed (47) hide show
  1. bTagScript/__init__.py +66 -0
  2. bTagScript/adapter/__init__.py +16 -0
  3. bTagScript/adapter/discord_adapters.py +275 -0
  4. bTagScript/adapter/function_adapter.py +33 -0
  5. bTagScript/adapter/int_adapter.py +30 -0
  6. bTagScript/adapter/object_adapter.py +41 -0
  7. bTagScript/adapter/string_adapter.py +69 -0
  8. bTagScript/block/__init__.py +67 -0
  9. bTagScript/block/break_block.py +41 -0
  10. bTagScript/block/case_block.py +62 -0
  11. bTagScript/block/comment_block.py +33 -0
  12. bTagScript/block/control_block.py +161 -0
  13. bTagScript/block/counting_blocks.py +88 -0
  14. bTagScript/block/digitshorthand_block.py +38 -0
  15. bTagScript/block/discord_blocks/__init__.py +20 -0
  16. bTagScript/block/discord_blocks/command_block.py +52 -0
  17. bTagScript/block/discord_blocks/cooldown_block.py +101 -0
  18. bTagScript/block/discord_blocks/delete_block.py +47 -0
  19. bTagScript/block/discord_blocks/embed_block.py +248 -0
  20. bTagScript/block/discord_blocks/override_block.py +61 -0
  21. bTagScript/block/discord_blocks/react_block.py +49 -0
  22. bTagScript/block/discord_blocks/redirect_block.py +42 -0
  23. bTagScript/block/discord_blocks/requirement_blocks.py +83 -0
  24. bTagScript/block/helpers.py +121 -0
  25. bTagScript/block/math_blocks.py +257 -0
  26. bTagScript/block/random_block.py +65 -0
  27. bTagScript/block/range_block.py +54 -0
  28. bTagScript/block/replace_block.py +110 -0
  29. bTagScript/block/stop_block.py +38 -0
  30. bTagScript/block/strf_block.py +70 -0
  31. bTagScript/block/url_blocks.py +76 -0
  32. bTagScript/block/util_blocks/__init__.py +3 -0
  33. bTagScript/block/util_blocks/debug_block.py +107 -0
  34. bTagScript/block/var_block.py +49 -0
  35. bTagScript/block/vargetter_blocks.py +91 -0
  36. bTagScript/exceptions.py +139 -0
  37. bTagScript/interface/__init__.py +4 -0
  38. bTagScript/interface/adapter.py +43 -0
  39. bTagScript/interface/block.py +136 -0
  40. bTagScript/interpreter.py +614 -0
  41. bTagScript/utils.py +65 -0
  42. bTagScript/verb.py +129 -0
  43. btagscript-5.0.0.dist-info/METADATA +109 -0
  44. btagscript-5.0.0.dist-info/RECORD +47 -0
  45. btagscript-5.0.0.dist-info/WHEEL +5 -0
  46. btagscript-5.0.0.dist-info/licenses/LICENSE +1 -0
  47. btagscript-5.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,614 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from itertools import islice
5
+ from typing import Any, Dict, List, Optional, Tuple, Union
6
+
7
+ from .exceptions import (
8
+ BlocknameDuplicateError,
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__ = (
19
+ "Interpreter",
20
+ "AsyncInterpreter",
21
+ "Context",
22
+ "Response",
23
+ "Node",
24
+ "build_node_tree",
25
+ )
26
+
27
+ log = logging.getLogger(__name__)
28
+
29
+ AdapterDict = Dict[str, Adapter]
30
+
31
+
32
+ class Node:
33
+ """
34
+ A low-level object representing a bracketed block.
35
+
36
+ Attributes
37
+ ----------
38
+ coordinates: Tuple[int, int]
39
+ The start and end position of the bracketed text block.
40
+ verb: Optional[Verb]
41
+ The determined Verb for this node.
42
+ output:
43
+ The `Block` processed output for this node.
44
+ """
45
+
46
+ __slots__ = ("output", "verb", "coordinates")
47
+
48
+ def __init__(self, coordinates: Tuple[int, int], verb: Optional[Verb] = None) -> None:
49
+ """
50
+ Constructing the Node
51
+ """
52
+ self.coordinates = coordinates
53
+ self.verb = verb
54
+ self.output: Optional[str] = None
55
+
56
+ def __str__(self) -> str:
57
+ """
58
+ String function
59
+ """
60
+ return str(self.verb) + " at " + str(self.coordinates)
61
+
62
+ def __repr__(self) -> str:
63
+ """
64
+ String repr
65
+ """
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
+ Parameters
74
+ ----------
75
+ message: str
76
+ The string to find nodes in.
77
+
78
+ Returns
79
+ -------
80
+ List[Node]
81
+ A list of all possible text bracket blocks.
82
+ """
83
+ nodes = []
84
+ # previous = r""
85
+
86
+ starts = []
87
+ for i, ch in enumerate(message):
88
+ if ch == "{": # and previous[1:] != "\\":
89
+ starts.append(i)
90
+ if ch == "}": # and previous[1:] != "\\":
91
+ if not starts:
92
+ continue
93
+ coords = (starts.pop(), i)
94
+ n = Node(coords)
95
+ nodes.append(n)
96
+
97
+ # previous = previous[:1] + ch
98
+ return nodes
99
+
100
+
101
+ class Response:
102
+ """
103
+ An object containing information on a completed TagScript process.
104
+
105
+ Attributes
106
+ ----------
107
+ body: str
108
+ The cleaned message with all verbs interpreted.
109
+ actions: Dict[str, Any]
110
+ A dictionary that blocks can access and modify to define post-processing actions.
111
+ variables: Dict[str, Adapter]
112
+ A dictionary of variables that blocks such as the `LooseVariableGetterBlock` can access.
113
+ extras: Dict[str, Any]
114
+ A dictionary of extra keyword arguments that blocks can use to define their own behavior.
115
+ """
116
+
117
+ __slots__ = ("body", "actions", "variables", "extras")
118
+
119
+ def __init__(self, *, variables: AdapterDict = None, extras: Dict[str, Any] = None) -> None:
120
+ """
121
+ Construct the response
122
+ """
123
+ self.body: str = None
124
+ self.actions: Dict[str, Any] = {}
125
+ self.variables: AdapterDict = variables if variables is not None else {}
126
+ self.extras: Dict[str, Any] = extras if extras is not None else {}
127
+
128
+ def __repr__(self) -> str:
129
+ """
130
+ String repr
131
+ """
132
+ return f"<Response body={self.body!r} actions={self.actions!r} variables={self.variables!r} extras={self.extras!r}>"
133
+
134
+
135
+ class Context:
136
+ """
137
+ An object containing data on the TagScript block processed by the interpreter.
138
+ This class is passed to adapters and blocks during processing.
139
+
140
+ Attributes
141
+ ----------
142
+ verb: Verb
143
+ The Verb object representing a TagScript block.
144
+ original_message: str
145
+ The original message passed to the interpreter.
146
+ interpreter: Interpreter
147
+ The interpreter processing the TagScript.
148
+ """
149
+
150
+ __slots__ = ("verb", "original_message", "interpreter", "response")
151
+
152
+ def __init__(self, verb: Verb, res: Response, interpreter: Interpreter, og: str) -> None:
153
+ """
154
+ Construct the context
155
+ """
156
+ self.verb: Verb = verb
157
+ self.original_message: str = og
158
+ self.interpreter: Interpreter = interpreter
159
+ self.response: Response = res
160
+
161
+ def __repr__(self) -> str:
162
+ """
163
+ String repr
164
+ """
165
+ return f"<Context verb={self.verb!r}>"
166
+
167
+
168
+ class Interpreter:
169
+ """
170
+ The TagScript interpreter.
171
+
172
+ Attributes
173
+ ----------
174
+ blocks: UnionList[Block]
175
+ A list or tuple of blocks to be used for TagScript processing.
176
+ """
177
+
178
+ __slots__ = ("blocks", "_blocknames")
179
+
180
+ def __init__(self, blocks: Union[List[Block], Tuple[Block]]) -> None:
181
+ """
182
+ Creates a list of blocks, and also gets all acceptable names for processing
183
+
184
+ Raises
185
+ ------
186
+ BlocknameDuplicateError
187
+ If there are duplicate blocknames.
188
+ """
189
+ self.blocks: Union[List[Block], Tuple[Block]] = blocks
190
+ self._blocknames = set()
191
+ for block in blocks:
192
+ for name in block.ACCEPTED_NAMES:
193
+ if name in self._blocknames:
194
+ raise BlocknameDuplicateError(name)
195
+ self._blocknames.add(name)
196
+
197
+ def __repr__(self) -> str:
198
+ """
199
+ String repr
200
+ """
201
+ return f"<{type(self).__name__} blocks={self.blocks!r}>"
202
+
203
+ def _get_context(
204
+ self,
205
+ node: Node,
206
+ final: str,
207
+ *,
208
+ response: Response,
209
+ original_message: str,
210
+ verb_limit: int,
211
+ ) -> Context:
212
+ """
213
+ Construct a context object for a node.
214
+
215
+ Parameters
216
+ ----------
217
+ node: Node
218
+ The node to construct the context for.
219
+ final: str
220
+ The final message to be processed.
221
+ response: Response
222
+ The response object to be passed to the context.
223
+ original_message: str
224
+ The original message passed to the interpreter.
225
+ verb_limit: int
226
+ The maximum number of verbs to process.
227
+
228
+ Returns
229
+ -------
230
+ Context
231
+ The constructed context.
232
+ """
233
+ # Get the updated verb string from coordinates and make the context
234
+ start, end = node.coordinates
235
+ node.verb = Verb(final[start : end + 1], limit=verb_limit)
236
+ return Context(node.verb, response, self, original_message)
237
+
238
+ def _get_acceptors(self, ctx: Context) -> Tuple[Block]:
239
+ """
240
+ Get a list of acceptors
241
+
242
+ Parameters
243
+ ----------
244
+ ctx: Context
245
+ The context to get the acceptors for.
246
+
247
+ Returns
248
+ -------
249
+ Tuple[Block]
250
+ """
251
+ return (b for b in self.blocks if b.will_accept(ctx))
252
+
253
+ def _process_blocks(self, ctx: Context, node: Node) -> Optional[str]:
254
+ """
255
+ Process the blocks
256
+
257
+ Parameters
258
+ ----------
259
+ ctx: Context
260
+ The context to process the blocks from.
261
+ node: Node
262
+ The node to process the blocks from.
263
+
264
+ Returns
265
+ -------
266
+ Optional[str]
267
+ The final message
268
+ """
269
+ acceptors = self._get_acceptors(ctx)
270
+ for b in acceptors:
271
+ value = b.process(ctx)
272
+ if value is not None: # Value found? We're done here.
273
+ value = str(value)
274
+ node.output = value
275
+ return value
276
+ return None
277
+
278
+ @staticmethod
279
+ def _check_workload(charlimit: int, total_work: int, output: str) -> Optional[int]:
280
+ """
281
+ Check if the workload has been exceeded.
282
+
283
+ Parameters
284
+ ----------
285
+ charlimit: int
286
+ The maximum number of characters to process.
287
+ total_work: int
288
+ The total number of characters processed.
289
+ output: str
290
+ The output string.
291
+
292
+ Returns
293
+ -------
294
+ Optional[int]
295
+ The total amount of work that has been processed.
296
+
297
+ Raises
298
+ ------
299
+ WorkloadExceededError
300
+ If the workload has been exceeded.
301
+ """
302
+ if not charlimit:
303
+ return None
304
+ total_work += len(output)
305
+ if total_work > charlimit:
306
+ raise WorkloadExceededError(
307
+ "The TSE interpreter had its workload exceeded. The total characters "
308
+ f"attempted were {total_work}/{charlimit}"
309
+ )
310
+ return total_work
311
+
312
+ @staticmethod
313
+ def _text_deform(start: int, end: int, final: str, output: str) -> Tuple[str, int]:
314
+ """
315
+ Deform the text, replacing code with what was outputted.
316
+
317
+ Parameters
318
+ ----------
319
+ start: int
320
+ The start index of the code.
321
+ end: int
322
+ The end index of the code.
323
+ final: str
324
+ The final message.
325
+ output: str
326
+ The output string.
327
+
328
+ Returns
329
+ -------
330
+ Tuple[str, int]
331
+ The new final message, and the change in final length after the change has been applied.
332
+ """
333
+ message_slice_len = (end + 1) - start
334
+ replacement_len = len(output)
335
+ differential = (
336
+ replacement_len - message_slice_len
337
+ ) # The change in size of `final` after the change is applied
338
+ final = final[:start] + output + final[end + 1 :]
339
+ return final, differential
340
+
341
+ @staticmethod
342
+ def _translate_nodes(
343
+ node_ordered_list: List[Node], index: int, start: int, differential: int
344
+ ) -> None:
345
+ """
346
+ Get the new coordinates for each node.
347
+
348
+ Parameters
349
+ ----------
350
+ node_ordered_list: List[Node]
351
+ The list of nodes to translate.
352
+ index: int
353
+ The index of the node to translate.
354
+ start: int
355
+ The start index of the code.
356
+ differential: int
357
+ The change in final length after the change has been applied.
358
+
359
+ Returns
360
+ -------
361
+ None
362
+ """
363
+ for future_n in islice(node_ordered_list, index + 1, None):
364
+ fut_start, fut_end = future_n.coordinates
365
+ future_n.coordinates = (
366
+ fut_start + differential if fut_start > start else fut_start,
367
+ fut_end + differential if fut_end > start else fut_end,
368
+ )
369
+
370
+ def _solve(
371
+ self,
372
+ message: str,
373
+ node_ordered_list: List[Node],
374
+ response: Response,
375
+ *,
376
+ charlimit: int,
377
+ verb_limit: int = 2000,
378
+ ) -> Optional[str]:
379
+ """
380
+ Solve the tagscript by proccessing all possible nodes.
381
+
382
+ Parameters
383
+ ----------
384
+ message: str
385
+ The message to process.
386
+ node_ordered_list: List[Node]
387
+ The list of nodes to process.
388
+ response: Response
389
+ The response object to be passed to the context.
390
+ charlimit: int
391
+ The maximum number of characters to process.
392
+ verb_limit: int
393
+ The maximum number of verbs to process.
394
+
395
+ Returns
396
+ -------
397
+ Optional[str]
398
+ The final, completely processed message.
399
+ """
400
+ final = message
401
+ total_work = 0
402
+ for index, node in enumerate(node_ordered_list):
403
+ start, end = node.coordinates
404
+ ctx = self._get_context(
405
+ node,
406
+ final,
407
+ response=response,
408
+ original_message=message,
409
+ verb_limit=verb_limit,
410
+ )
411
+ log.debug("Processing context %r at (%r, %r)", ctx, start, end)
412
+ try:
413
+ output = self._process_blocks(ctx, node)
414
+ except StopError as exc:
415
+ log.debug("StopError raised on node %r", node, exc_info=exc)
416
+ return final[:start] + exc.message
417
+ if output is None:
418
+ continue # If there was no value output, no need to text deform.
419
+
420
+ total_work = self._check_workload(charlimit, total_work, output)
421
+ final, differential = self._text_deform(start, end, final, output)
422
+ self._translate_nodes(node_ordered_list, index, start, differential)
423
+ return final
424
+
425
+ @staticmethod
426
+ def _return_response(response: Response, output: str) -> Response:
427
+ """
428
+ Return the response object.
429
+
430
+ Parameters
431
+ ----------
432
+ response: Response
433
+ The response object to be returned.
434
+ output: str
435
+ The output string.
436
+
437
+ Returns
438
+ -------
439
+ Response
440
+ The response object.
441
+ """
442
+ if response.body is None:
443
+ response.body = output.strip()
444
+ else:
445
+ # Dont override an overridden response.
446
+ response.body = response.body.strip()
447
+ return response
448
+
449
+ def process(
450
+ self,
451
+ message: str,
452
+ seed_variables: AdapterDict = None,
453
+ *,
454
+ charlimit: Optional[int] = None,
455
+ **kwargs,
456
+ ) -> Response:
457
+ """
458
+ Processes a given TagScript string.
459
+
460
+ Parameters
461
+ ----------
462
+ message: str
463
+ A TagScript string to be processed.
464
+ seed_variables: Dict[str, Adapter]
465
+ A dictionary containing strings to adapters to provide context variables for processing.
466
+ charlimit: int
467
+ The maximum characters to process.
468
+ kwargs: Dict[str, Any]
469
+ Additional keyword arguments that may be used by blocks during processing.
470
+
471
+ Returns
472
+ -------
473
+ Response
474
+ A response object containing the processed body, actions and variables.
475
+
476
+ Raises
477
+ ------
478
+ TagScriptError
479
+ A block intentionally raised an exception, most likely due to invalid user input.
480
+ WorkloadExceededError
481
+ Signifies the interpreter reached the character limit, if one was provided.
482
+ ProcessError
483
+ An unexpected error occurred while processing blocks.
484
+ """
485
+ response = Response(variables=seed_variables, extras=kwargs)
486
+ node_ordered_list = build_node_tree(message)
487
+ try:
488
+ output = self._solve(
489
+ message,
490
+ node_ordered_list,
491
+ response,
492
+ charlimit=charlimit,
493
+ )
494
+ except TagScriptError:
495
+ raise
496
+ except Exception as error:
497
+ raise ProcessError(error, response, self) from error
498
+ return self._return_response(response, output)
499
+
500
+
501
+ class AsyncInterpreter(Interpreter):
502
+ """
503
+ An asynchronous subclass of `Interpreter` that allows blocks to implement asynchronous methods.
504
+ Synchronous blocks are still supported.
505
+ This subclass has no additional attributes from the `Interpreter` class.
506
+ See `Interpreter` for full documentation.
507
+ """
508
+
509
+ async def _process_blocks(self, ctx: Context, node: Node) -> Optional[str]:
510
+ """
511
+ Process the blocks
512
+
513
+ Parameters
514
+ ----------
515
+ ctx: Context
516
+ The context to process the blocks from.
517
+ node: Node
518
+ The node to process the blocks from.
519
+
520
+ Returns
521
+ -------
522
+ Optional[str]
523
+ The final message
524
+ """
525
+ for b in self.blocks:
526
+ if not await maybe_await(b.will_accept, ctx):
527
+ continue
528
+ value = await maybe_await(b.process, ctx)
529
+ if value is not None: # Value found? We're done here.
530
+ value = str(value)
531
+ node.output = value
532
+ return value
533
+
534
+ async def _solve(
535
+ self,
536
+ message: str,
537
+ node_ordered_list: List[Node],
538
+ response: Response,
539
+ *,
540
+ charlimit: int,
541
+ verb_limit: int = 2000,
542
+ ) -> Optional[str]:
543
+ """
544
+ Solve the tagscript by proccessing all possible nodes.
545
+
546
+ Parameters
547
+ ----------
548
+ message: str
549
+ The message to process.
550
+ node_ordered_list: List[Node]
551
+ The list of nodes to process.
552
+ response: Response
553
+ The response object to be passed to the context.
554
+ charlimit: int
555
+ The maximum number of characters to process.
556
+ verb_limit: int
557
+ The maximum number of verbs to process.
558
+
559
+ Returns
560
+ -------
561
+ Optional[str]
562
+ The final, completely processed message.
563
+ """
564
+ final = message
565
+ total_work = 0
566
+
567
+ for index, node in enumerate(node_ordered_list):
568
+ start, end = node.coordinates
569
+ ctx = self._get_context(
570
+ node,
571
+ final,
572
+ response=response,
573
+ original_message=message,
574
+ verb_limit=verb_limit,
575
+ )
576
+ try:
577
+ output = await self._process_blocks(ctx, node)
578
+ except StopError as exc:
579
+ return final[:start] + exc.message
580
+ if output is None:
581
+ continue # If there was no value output, no need to text deform.
582
+
583
+ total_work = self._check_workload(charlimit, total_work, output)
584
+ final, differential = self._text_deform(start, end, final, output)
585
+ self._translate_nodes(node_ordered_list, index, start, differential)
586
+ return final
587
+
588
+ async def process(
589
+ self,
590
+ message: str,
591
+ seed_variables: AdapterDict = None,
592
+ *,
593
+ charlimit: Optional[int] = None,
594
+ **kwargs,
595
+ ) -> Response:
596
+ """
597
+ Asynchronously process a given TagScript string.
598
+ This method has no additional attributes from the `Interpreter` class.
599
+ See `Interpreter.process` for full documentation.
600
+ """
601
+ response = Response(variables=seed_variables, extras=kwargs)
602
+ node_ordered_list = build_node_tree(message)
603
+ try:
604
+ output = await self._solve(
605
+ message,
606
+ node_ordered_list,
607
+ response,
608
+ charlimit=charlimit,
609
+ )
610
+ except TagScriptError:
611
+ raise
612
+ except Exception as error:
613
+ raise ProcessError(error, response, self) from error
614
+ return self._return_response(response, output)
bTagScript/utils.py ADDED
@@ -0,0 +1,65 @@
1
+ import re
2
+ from inspect import isawaitable
3
+ from typing import Any, Awaitable, Callable, Union
4
+
5
+ __all__ = ("escape_content", "maybe_await")
6
+
7
+ pattern = re.compile(r"(?<!\\)([{():|}])")
8
+
9
+
10
+ def _sub_match(match: re.Match) -> str:
11
+ r"""
12
+ Check if the character has a \ in front of it
13
+
14
+ Parameters
15
+ ----------
16
+ match: re.Match
17
+ The match object.
18
+
19
+ Returns
20
+ -------
21
+ str
22
+ The escaped character.
23
+ """
24
+ return "\\" + match.group(1)
25
+
26
+
27
+ def escape_content(string: str) -> str:
28
+ """
29
+ Escapes given input to avoid tampering with engine/block behavior.
30
+
31
+ Parameters
32
+ ----------
33
+ string: str
34
+ The string to escape.
35
+
36
+ Returns
37
+ -------
38
+ str
39
+ The escaped content.
40
+ """
41
+ if string is None:
42
+ return
43
+ return pattern.sub(_sub_match, string)
44
+
45
+
46
+ async def maybe_await(func: Union[Callable[..., Any], Awaitable[Any]], *args, **kwargs) -> Any:
47
+ """
48
+ Await the given function if it is awaitable or call it synchronously.
49
+
50
+ Parameters
51
+ ----------
52
+ func: Union[Callable[..., Any], Awaitable[Any]]
53
+ The function callable to call.
54
+ *args: Any
55
+ The arguments to pass to the function.
56
+ **kwargs: Any
57
+ The keyword arguments to pass to the function.
58
+
59
+ Returns
60
+ -------
61
+ Any
62
+ The result of the awaitable function.
63
+ """
64
+ value = func(*args, **kwargs)
65
+ return await value if isawaitable(value) else value