untils 1.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.
untils/__init__.py ADDED
@@ -0,0 +1,28 @@
1
+ """*untils* - Is light-weight library for console games or small utils to process user input as commands and describe structure with config."""
2
+
3
+ # pyright: reportUnusedImport=false
4
+ # ^^^^^^^ (Public imports.)
5
+
6
+ from untils import utils
7
+
8
+ from untils.command_system import CommandSystem
9
+ from untils.command import (
10
+ AliasNode, CommandNode, CommandWordNode, CommandFallbackNode, CommandFlagNode,
11
+ CommandOptionNode, StateNode
12
+ )
13
+ from untils.commands_config import CommandsConfig
14
+ from untils.config_validator import ConfigValidator
15
+ from untils.factories import CommandNodeFactory
16
+ from untils.input_token import (
17
+ RawInputToken, FinalInputTokenWord, FinalInputTokenFlag, FinalInputTokenOption
18
+ )
19
+ from untils.input_validator import InputValidator, ParsedInputValidator
20
+ from untils.ioreader import IOReader
21
+ from untils.iovalidator import IOValidator
22
+ from untils.parser import Parser
23
+ from untils.processor import Processor
24
+ from untils.settings import Settings
25
+ from untils.tokenizer import Tokenizer
26
+
27
+ __version__ = "1.0.0"
28
+ __author__ = "BesBobowyy"
untils/command.py ADDED
@@ -0,0 +1,130 @@
1
+ """command.py - Command nodes."""
2
+
3
+ from typing import List, Any
4
+
5
+ from dataclasses import dataclass
6
+
7
+ from untils.utils.type_aliases import CommandType
8
+
9
+ @dataclass(frozen=True)
10
+ class AliasNode():
11
+ """Command alias name container."""
12
+
13
+ original_name: str
14
+ """Original command name."""
15
+ alias_name: str
16
+ """Alias name."""
17
+
18
+ def __str__(self) -> str:
19
+ return f"AliasNode('{self.original_name}' -> '{self.alias_name}')"
20
+
21
+ def __eq__(self, value: object) -> bool:
22
+ if isinstance(value, AliasNode):
23
+ return self.original_name == value.original_name and self.alias_name == value.alias_name
24
+ if isinstance(value, CommandNode):
25
+ if value.type == "fallback":
26
+ return False
27
+ return self.original_name == value.name
28
+ if isinstance(value, str):
29
+ return self.original_name == value and self.alias_name == value
30
+ return False
31
+
32
+ def __ne__(self, value: object) -> bool:
33
+ if isinstance(value, AliasNode):
34
+ return self.original_name != value.original_name or self.alias_name != value.alias_name
35
+ if isinstance(value, CommandNode):
36
+ if value.type == "fallback":
37
+ return True
38
+ return self.original_name != value.name
39
+ if isinstance(value, str):
40
+ return self.original_name != value or self.alias_name != value
41
+ return True
42
+
43
+ @dataclass(frozen=True)
44
+ class CommandNode:
45
+ """Universal template for command nodes."""
46
+
47
+ name: str
48
+ """Command name."""
49
+ type: CommandType
50
+ """Command type."""
51
+
52
+ def __eq__(self, value: object) -> bool:
53
+ if isinstance(value, CommandNode):
54
+ return self.name == value.name and self.type == value.type
55
+ if isinstance(value, str):
56
+ if self.type != "fallback":
57
+ return self.name == value
58
+ return True
59
+ return False
60
+
61
+ def __ne__(self, value: object) -> bool:
62
+ if isinstance(value, CommandNode):
63
+ return self.name != value.name or self.type != value.type
64
+ if isinstance(value, str):
65
+ if self.type != "fallback":
66
+ return self.name != value
67
+ return False
68
+ return True
69
+
70
+ @dataclass(frozen=True)
71
+ class CommandWordNode(CommandNode):
72
+ """The word command type."""
73
+
74
+ aliases: List[AliasNode]
75
+ """Command aliases."""
76
+ children: List[CommandNode]
77
+ """Next commands below this."""
78
+
79
+ def __str__(self) -> str:
80
+ return f"CommandWordNode[{self.name} : {self.aliases}]{self.children}"
81
+
82
+ @dataclass(frozen=True)
83
+ class CommandFallbackNode(CommandNode):
84
+ """Command node for fallback type."""
85
+
86
+ default: str
87
+ """A default value."""
88
+ children: List[CommandNode]
89
+ """Next commands below this."""
90
+
91
+ def __str__(self) -> str:
92
+ return f"CommandFallbackNode[{self.name}](default='{self.default}'){self.children}"
93
+
94
+ @dataclass(frozen=True)
95
+ class CommandFlagNode(CommandNode):
96
+ """Command node for flag type."""
97
+
98
+ aliases: List[AliasNode]
99
+ """Command aliases."""
100
+ default: Any
101
+ """A default value."""
102
+
103
+ def __str__(self) -> str:
104
+ return f"CommandFlagNode[{self.name} : {self.aliases}](default={repr(self.default)})"
105
+
106
+ @dataclass(frozen=True)
107
+ class CommandOptionNode(CommandNode):
108
+ """Command node for option type."""
109
+
110
+ aliases: List[AliasNode]
111
+ """Command aliases."""
112
+ default: Any
113
+ """A default value."""
114
+
115
+ def __str__(self) -> str:
116
+ return f"CommandOptionNode[{self.name} : {self.aliases}](default={repr(self.default)})"
117
+
118
+ @dataclass(frozen=True)
119
+ class StateNode:
120
+ """A state node in config."""
121
+
122
+ name: str
123
+ """The state name."""
124
+ is_internal: bool
125
+ """Is internal state, which typed in the format `__{name}__`."""
126
+ commands: List[str]
127
+ """Allowed commands by this state."""
128
+
129
+ def __str__(self) -> str:
130
+ return f"StateNode[{'!' if self.is_internal else ''}{self.name}]{self.commands}"
@@ -0,0 +1,403 @@
1
+ """command_system.py - Command system."""
2
+
3
+ # pyright: reportUnnecessaryIsInstance=false
4
+
5
+ from typing import Optional, List, Union, cast, Dict, Tuple
6
+
7
+ from untils.utils.type_aliases import InputDict, CommandPath, CallableCommand, CommandHistory
8
+ from untils.utils.constants import Strings
9
+
10
+ from untils.commands_config import CommandsConfig
11
+ from untils.settings import Settings
12
+ from untils.processor import Processor
13
+ from untils.input_validator import ParsedInputValidator
14
+ from untils.command import CommandNode, CommandWordNode, CommandFallbackNode
15
+
16
+ class CommandSystem:
17
+ """Core class with command config, API, processing and much more."""
18
+
19
+ __slots__ = ["settings", "config", "route", "history"]
20
+
21
+ settings: Settings
22
+ config: Optional[CommandsConfig]
23
+ route: Dict[CommandPath, CallableCommand]
24
+ history: CommandHistory
25
+
26
+ def __init__(
27
+ self,
28
+ settings: Settings,
29
+ config: Optional[CommandsConfig]=None,
30
+ history: Optional[CommandHistory]=None
31
+ ) -> None:
32
+ """
33
+ Args:
34
+ settings: A `Settings` object as context.
35
+ config: A `Config` object as configuration.
36
+ history: A command history object.
37
+ """
38
+
39
+ self.settings = settings
40
+ self.config = config
41
+ self.route = {}
42
+ self.history = {
43
+ "max_size": 100,
44
+ "is_write_overflow": True,
45
+ "notes": []
46
+ } if history is None else history
47
+
48
+ def is_config_loaded(self) -> bool:
49
+ """Returns a `bool` value, what determines is config loaded."""
50
+
51
+ return self.config is not None
52
+
53
+ def load_config(self, config_path: str) -> None:
54
+ """Loads a `CommandsConfig` object.
55
+
56
+ Args:
57
+ config_path: Path of config file.
58
+ """
59
+
60
+ self.config = Processor.load_config(self.settings, config_path)
61
+
62
+ def set_config(self, config: Optional[CommandsConfig]) -> None:
63
+ """Sets an already processed config or deletes exist.
64
+
65
+ Args:
66
+ config: A `Config` object or `None`.
67
+ """
68
+
69
+ self.config = config
70
+
71
+ def process_input(self, input_str: str) -> InputDict:
72
+ """Processes a user input and returns `InputDict` as input representation.
73
+
74
+ Args:
75
+ input_str: Input raw string.
76
+
77
+ Returns:
78
+ An input representation.
79
+ """
80
+
81
+ return Processor.process_input(self.settings, self.config, input_str)
82
+
83
+ def is_input_valid(self, input_dict: InputDict) -> bool:
84
+ """Validates an input.
85
+
86
+ Args:
87
+ input_dict: A cached input for validation.
88
+
89
+ Returns:
90
+ `False` if an input is not valid or config not loaded. `True` if an input is valid.
91
+ """
92
+
93
+ if self.config is not None:
94
+ return ParsedInputValidator.validate_input_dict(self.settings, input_dict, self.config)
95
+
96
+ self.settings.logger.warning(Strings.LOG_CONFIG_NOT_LOADED)
97
+
98
+ return False
99
+
100
+ def get_normalized_path(self, input_dict: InputDict) -> List[str]:
101
+ """Returns original key-names in config by `path` in `input_dict`.
102
+
103
+ Args:
104
+ input_dict: A cached input for validation.
105
+
106
+ Returns:
107
+ Normalized path from original keys in config.
108
+ """
109
+
110
+ if self.config is None:
111
+ self.settings.logger.warning(Strings.LOG_CONFIG_NOT_LOADED)
112
+ return []
113
+
114
+ input_path: List[str] = input_dict["path"]
115
+ commands: List[CommandNode] = self.config.commands
116
+ result: List[str] = []
117
+
118
+ self.settings.logger.info(Strings.LOG_CALCULATE_NORMALIZED_PATH_START)
119
+
120
+ for part in input_path:
121
+ for command in commands:
122
+ if command.type == "word":
123
+ command = cast(CommandWordNode, command)
124
+ if (
125
+ part == command.name
126
+ or part in [alias.alias_name for alias in command.aliases]
127
+ ):
128
+ result.append(command.name)
129
+ commands = command.children
130
+ break
131
+ elif command.type == "fallback":
132
+ command = cast(CommandFallbackNode, command)
133
+ result.append(command.name)
134
+ commands = command.children
135
+ break
136
+
137
+ self.settings.logger.info(Strings.LOG_CALCULATE_NORMALIZED_PATH_END)
138
+
139
+ return result
140
+
141
+ def get_all_commands(self) -> List[CommandNode]:
142
+ """Returns all commands as command nodes."""
143
+
144
+ if self.config is None:
145
+ self.settings.logger.warning(Strings.LOG_CONFIG_NOT_LOADED)
146
+ return []
147
+
148
+ result: List[CommandNode] = []
149
+
150
+ for command in self.config.commands:
151
+ if command.type in ("word", "fallback"):
152
+ result.append(command)
153
+
154
+ return result
155
+
156
+ def get_all_commands_str(self) -> List[str]:
157
+ """Returns all commands as name strings."""
158
+
159
+ if self.config is None:
160
+ self.settings.logger.warning(Strings.LOG_CONFIG_NOT_LOADED)
161
+ return []
162
+
163
+ result: List[str] = []
164
+
165
+ for command in self.config.commands:
166
+ if command.type in ("word", "fallback"):
167
+ result.append(command.name)
168
+
169
+ return result
170
+
171
+ def get_available_commands(self) -> List[CommandNode]:
172
+ """Returns all available commands in current state as command nodes."""
173
+
174
+ if self.config is None:
175
+ self.settings.logger.warning(Strings.LOG_CONFIG_NOT_LOADED)
176
+ return []
177
+
178
+ result: List[CommandNode] = []
179
+
180
+ for command in self.config.commands:
181
+ if command.type in ("word", "fallback"):
182
+ for state in self.config.states:
183
+ if command.name in state.commands:
184
+ result.append(command)
185
+
186
+ return result
187
+
188
+ def get_available_commands_str(self) -> List[str]:
189
+ """Returns all available commands in current state as name strings."""
190
+
191
+ if self.config is None:
192
+ self.settings.logger.warning(Strings.LOG_CONFIG_NOT_LOADED)
193
+ return []
194
+
195
+ result: List[str] = []
196
+
197
+ for command in self.config.commands:
198
+ if command.type in ("word", "fallback"):
199
+ for state in self.config.states:
200
+ if command.name in state.commands:
201
+ result.append(command.name)
202
+
203
+ return result
204
+
205
+ def access_path(
206
+ self,
207
+ input_dict: Union[InputDict, List[str]],
208
+ path: CommandPath,
209
+ is_inclusive: bool=True
210
+ ) -> bool:
211
+ """Validates command path by input.
212
+
213
+ Args:
214
+ input_dict: A parsed input dict. Accepts `InputDict` for a path and `List[str]` only for a path.
215
+ path: A command path, which determines all posible correct ways in path.
216
+ is_inclusive: Always returns `False` if length of two paths are different. This argument changes validation mode: `False` (determines any command input from deferred path) and `True` (determines a single variant for command tree branching).
217
+
218
+ Returns:
219
+ `False` if input path is mispath. `True` if input path equals the deferred path.
220
+ """
221
+
222
+ if isinstance(input_dict, dict):
223
+ input_path: List[str] = input_dict["path"]
224
+ elif isinstance(input_dict, list):
225
+ input_path: List[str] = input_dict
226
+ else:
227
+ return False
228
+
229
+ if is_inclusive and len(path) != len(input_path):
230
+ return False
231
+
232
+ i: int = 0
233
+ for part in path:
234
+ if i >= len(input_path):
235
+ return True
236
+
237
+ if isinstance(part, str):
238
+ if part in (input_path[i], "-any"):
239
+ i += 1
240
+ continue
241
+ elif isinstance(part, (list, tuple)):
242
+ if input_path[i] in part:
243
+ i += 1
244
+ continue
245
+ return False
246
+ return True
247
+
248
+ def get_command(self, path: CommandPath) -> Optional[CallableCommand]:
249
+ """Get a command from the command routing.
250
+
251
+ Args:
252
+ path: A command path, which determines all posible correct ways in path.
253
+
254
+ Returns:
255
+ `CallableCommand` if a function was found by a path. `None` if a function was not found.
256
+ """
257
+
258
+ return self.route.get(path)
259
+
260
+ def register_command(self, path: CommandPath, func: CallableCommand) -> bool:
261
+ """Registers a command from the command routing.
262
+
263
+ Args:
264
+ path: A command path, which determines all posible correct ways in path.
265
+ func: A command implementation as function.
266
+
267
+ Returns:
268
+ `False` if path in the command routing. `True` if path not in the command routing and was added.
269
+ """
270
+
271
+ if path in self.route:
272
+ return False
273
+
274
+ self.route[path] = func
275
+ return True
276
+
277
+ def change_command(self, path: CommandPath, func: CallableCommand) -> bool:
278
+ """Changes already registered command in the command routing.
279
+
280
+ Args:
281
+ path: A command path, which determines all posible correct ways in path.
282
+ func: A command implementation as function.
283
+
284
+ Returns:
285
+ `False` if path not in the command routing. `True` if path in the command routing and was changed.
286
+ """
287
+
288
+ if path not in self.route:
289
+ return False
290
+
291
+ self.route[path] = func
292
+ return True
293
+
294
+ def unload_command(self, path: CommandPath) -> bool:
295
+ """Removes already registered command from the command routing.
296
+
297
+ Args:
298
+ path: A command path, which determines all posible correct ways in path.
299
+
300
+ Returns:
301
+ `False` if path not in the command routing. `True` if path in the command routing and was deleted.
302
+ """
303
+
304
+ if path not in self.route:
305
+ return False
306
+
307
+ del self.route[path]
308
+ return True
309
+
310
+ def get_history(self) -> List[Tuple[str, InputDict]]:
311
+ """Returns all notes from command history.
312
+
313
+ Returns:
314
+ List from tuples with input string and parsed input dict.
315
+ """
316
+
317
+ return self.history["notes"]
318
+
319
+ def get_history_input(self) -> List[str]:
320
+ """Returns all input strings from command history.
321
+
322
+ Returns:
323
+ List with input string.
324
+ """
325
+
326
+ return [note[0] for note in self.history["notes"]]
327
+
328
+ def get_history_dict(self) -> List[InputDict]:
329
+ """Returns all parsed input dicts from command history.
330
+
331
+ Returns:
332
+ List with parsed input dict.
333
+ """
334
+
335
+ return [note[1] for note in self.history["notes"]]
336
+
337
+ def write_history(self, input_str: str, input_dict: InputDict) -> bool:
338
+ """Writes a new note to command history.
339
+
340
+ Args:
341
+ input_str: Original input string.
342
+ input_dict: Parsed input dict.
343
+
344
+ Returns:
345
+ `False` if max note count limit reached and overwrite disabled, else `True`.
346
+ """
347
+
348
+ if (
349
+ len(self.history["notes"]) >= self.history["max_size"]
350
+ and not self.history["is_write_overflow"]
351
+ ):
352
+ return False
353
+
354
+ self.history["notes"].append((input_str, input_dict))
355
+ return True
356
+
357
+ def read_history(self, index: int) -> Tuple[str, InputDict]:
358
+ """Returns a note by index in saved indexes.
359
+
360
+ Args:
361
+ index: Positive note index by latest. Will clamped to limits.
362
+
363
+ Returns:
364
+ A note from history.
365
+ """
366
+
367
+ return self.history["notes"][max(min(0, index), self.history["max_size"] - 1)]
368
+
369
+ def execute(
370
+ self,
371
+ input_str: str,
372
+ input_dict: InputDict,
373
+ normalized_path: List[str],
374
+ tracking: bool=True
375
+ ) -> bool:
376
+ """Executes an input string with the command routing.
377
+
378
+ Args:
379
+ input_str: An input string.
380
+ input_dict: A cached input for validation.
381
+ normalized_path: A command path, which determines all posible correct ways in path.
382
+ tracking: Is save current command in history.
383
+
384
+ Returns:
385
+ `False` if a command is not written or not in the command routing. `True` if a command was called.
386
+ """
387
+
388
+ if tracking:
389
+ self.write_history(input_str, input_dict)
390
+
391
+ if len(normalized_path) == 0:
392
+ self.settings.logger.info(Strings.COMMAND_NOT_WRITTEN)
393
+ return False
394
+
395
+ for path, func in self.route.items():
396
+ if self.access_path(normalized_path, path, False):
397
+ func(input_str, input_dict)
398
+ return True
399
+
400
+ self.settings.logger.warning(
401
+ Strings.COMMAND_NOT_IMPLEMENTED.substitute(input_str=input_str)
402
+ )
403
+ return False
@@ -0,0 +1,23 @@
1
+ """commands_config.py - Commands config."""
2
+
3
+ from typing import List
4
+
5
+ from dataclasses import dataclass
6
+
7
+ from untils.utils.type_aliases import ConfigVersion
8
+
9
+ from untils.command import StateNode, CommandNode
10
+
11
+ @dataclass(frozen=True)
12
+ class CommandsConfig:
13
+ """Configuration class."""
14
+
15
+ version: ConfigVersion
16
+ """The configuration version. All supported versions are defined in `ConfigVersions`. The latest version is defined in `Constants.LATEST_CONFIG_VERSION`."""
17
+ states: List[StateNode]
18
+ """All written states. Use states for context separation."""
19
+ commands: List[CommandNode]
20
+ """All available commands for user."""
21
+
22
+ def __str__(self) -> str:
23
+ return f"CommandsConfig(version={self.version}, states={self.states}, commands={self.commands})"