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.
@@ -0,0 +1,616 @@
1
+ """config_validator.py - Deep config validation."""
2
+
3
+ # pyright: reportUnnecessaryIsInstance=false
4
+ # ^^^^^^^ (Raw dynamic data checking.)
5
+
6
+ from typing import Dict, get_args, Optional, List, Any
7
+
8
+ from string import punctuation
9
+
10
+ from untils.utils.type_aliases import (
11
+ ConfigType, UnknownConfigType, ConfigVersion, UnknownCommandClass, CommandClass, CommandType,
12
+ UnknownCommandConfig, CommandStates, InternalCommandStates
13
+ )
14
+ from untils.utils.enums import ConfigVersions, WarningsLevel
15
+ from untils.utils.lib_warnings import (
16
+ ConfigStructureWarning, ConfigValuesWarning, ConfigStructureError, ConfigValuesError
17
+ )
18
+ from untils.utils.constants import Constants, Strings
19
+
20
+ from untils.settings import Settings
21
+
22
+ class ConfigValidator:
23
+ """Validator class for config structure and semantic."""
24
+
25
+ _empty_name_replace_index: int = -1
26
+ """Private variable, which uses for invalid names renaming. It prevents name duplication."""
27
+
28
+ @staticmethod
29
+ def validate_version(settings: Settings, config_dict: UnknownConfigType) -> ConfigVersion:
30
+ """Validates the config version.
31
+
32
+ Args:
33
+ settings: The settings.
34
+ config_dict: The config dictionary, which was not validated yet.
35
+
36
+ Returns:
37
+ `ConfigVersion` if config version is valid, else returns the latest version in `Constants.LATEST_CONFIG_VERSION`.
38
+
39
+ Raises:
40
+ ConfigStructureWarning: `version` field is not found in config.
41
+ ConfigValuesWarning: Config version is not supported or invalid.
42
+
43
+ ConfigStructureError: `version` field is not found in config.
44
+ ConfigValuesError: Config version is not supported or invalid.
45
+ """
46
+
47
+ version: int = -1
48
+
49
+ if "version" in config_dict:
50
+ version = config_dict["version"]
51
+
52
+ if version not in ConfigVersions:
53
+ settings.warning(
54
+ Strings.INVALID_CONFIG_VERSION.substitute(version=repr(version)),
55
+ Strings.AUTO_CORRECT_TO_LATEST,
56
+ ConfigValuesWarning,
57
+ ConfigValuesError
58
+ )
59
+ else:
60
+ settings.warning(
61
+ Strings.INVALID_CONFIG_VERSION.substitute(version=Strings.UNKNOWN_VERSION),
62
+ Strings.AUTO_CORRECT_TO_LATEST,
63
+ ConfigStructureWarning,
64
+ ConfigStructureError
65
+ )
66
+ version = Constants.LATEST_CONFIG_VERSION.value
67
+
68
+ return version
69
+
70
+ @staticmethod
71
+ def validate_command_type(
72
+ settings: Settings,
73
+ command_dict: UnknownCommandClass
74
+ ) -> Optional[CommandType]:
75
+ """Validates a command type.
76
+
77
+ Args:
78
+ settings: The settings.
79
+ command_dict: The command dictionary, which was not validated yet.
80
+
81
+ Returns:
82
+ `CommandType` if the command type was validated successfully, else `None`.
83
+
84
+ Raises:
85
+ ConfigValuesWarning: The command type field is not written.
86
+
87
+ ConfigValuesErorr: The command type is not valid or unknown.
88
+ """
89
+
90
+ command_type: Optional[CommandType] = None
91
+
92
+ if "type" in command_dict:
93
+ if command_dict["type"] in get_args(CommandType):
94
+ command_type = command_dict["type"]
95
+ else:
96
+ settings.warning(
97
+ Strings.COMMAND_INVALID_TYPE,
98
+ Strings.AUTO_CORRECT_WITH_SKIPPING,
99
+ ConfigValuesWarning,
100
+ ConfigValuesError
101
+ )
102
+ else:
103
+ settings.warning(
104
+ Strings.COMMAND_UNKNOWN_TYPE,
105
+ Strings.AUTO_CORRECT_WITH_SKIPPING,
106
+ ConfigStructureWarning,
107
+ ConfigStructureError
108
+ )
109
+
110
+ return command_type
111
+
112
+ @staticmethod
113
+ def validate_command_aliases(
114
+ settings: Settings,
115
+ command_dict: UnknownCommandClass
116
+ ) -> List[str]:
117
+ """Validates command aliases from a command dictionary.
118
+
119
+ Args:
120
+ settings: The settings.
121
+ command_dict: The command dictionary, which was not validated yet.
122
+
123
+ Returns:
124
+ Returns validated aliases.
125
+
126
+ Raises:
127
+ ConfigStructureWarning: The command aliases are not written or is not list.
128
+ ConfigValuesWarning: An alias in the command aliases is not string or duplicates previous.
129
+
130
+ ConfigStructureError: The command aliases are not written or is not list.
131
+ ConfigValuesError: An alias in the command aliases is not string or duplicates previous.
132
+ """
133
+
134
+ aliases: List[str] = []
135
+
136
+ if "aliases" in command_dict:
137
+ if not isinstance(command_dict["aliases"], list):
138
+ settings.warning(
139
+ Strings.COMMAND_INVALID_ALIASES,
140
+ Strings.AUTO_CORRECT_TO_DEFAULTS,
141
+ ConfigStructureWarning,
142
+ ConfigStructureError
143
+ )
144
+ else:
145
+ for alias in command_dict["aliases"]:
146
+ if not isinstance(alias, str):
147
+ settings.warning(
148
+ Strings.COMMAND_ALIAS_INVALID.substitute(alias=alias),
149
+ Strings.AUTO_CORRECT_WITH_CASTING,
150
+ ConfigValuesWarning,
151
+ ConfigValuesError
152
+ )
153
+ alias = str(alias)
154
+
155
+ if alias in aliases:
156
+ settings.warning(
157
+ Strings.COMMAND_ALIAS_COPIED.substitute(alias=alias),
158
+ Strings.AUTO_CORRECT_WITH_SKIPPING,
159
+ ConfigValuesWarning,
160
+ ConfigValuesError
161
+ )
162
+
163
+ aliases.append(alias)
164
+
165
+ return aliases
166
+
167
+ @staticmethod
168
+ def validate_command_default(command_dict: UnknownCommandClass) -> Any:
169
+ """Validates a command default value.
170
+
171
+ Args:
172
+ command_dict: The command dictionary, which was not validated yet.
173
+
174
+ Returns:
175
+ `Any` if the default value is exists, else `None`.
176
+ """
177
+
178
+ if "default" in command_dict:
179
+ return command_dict["default"]
180
+ return None
181
+
182
+ @staticmethod
183
+ def validate_command(
184
+ settings: Settings,
185
+ command_dict: UnknownCommandClass
186
+ ) -> Optional[CommandClass]:
187
+ """Validates a command dictionary.
188
+
189
+ Args:
190
+ settings: The settings.
191
+ command_dict: The command dictionary, which was not validated yet.
192
+
193
+ Returns:
194
+ `CommandClass` if command was validated successfully, else `None`. Also `None` returns if a type field is not written or required fields for this type are not written.
195
+
196
+ Raises:
197
+ ConfigStructureWarning: Structure of the command is not valid.
198
+ ConfigValuesWarning: The command values is not valid.
199
+
200
+ ConfigStructureError: Structure of the command is not valid.
201
+ ConfigValuesError: The command values is not valid.
202
+ """
203
+
204
+ command_type: Optional[CommandType] = ConfigValidator.validate_command_type(
205
+ settings,
206
+ command_dict
207
+ )
208
+ if command_type is None:
209
+ return None
210
+
211
+ arguments: UnknownCommandConfig = {
212
+ "type": command_type
213
+ }
214
+
215
+ if command_type in ("word", "flag", "option"):
216
+ arguments["aliases"] = ConfigValidator.validate_command_aliases(settings, command_dict)
217
+
218
+ if command_type not in ("word", "flag", "option") and "aliases" in command_dict:
219
+ settings.warning(
220
+ Strings.COMMAND_INVALID_TYPE,
221
+ Strings.AUTO_CORRECT_WITH_SKIPPING,
222
+ ConfigStructureWarning,
223
+ ConfigStructureError
224
+ )
225
+ return None
226
+
227
+ if command_type in ("fallback", "flag", "option"):
228
+ arguments["default"] = ConfigValidator.validate_command_default(command_dict)
229
+ if command_type not in ("fallback", "flag", "option") and "default" in command_dict:
230
+ settings.warning(
231
+ Strings.COMMAND_INVALID_TYPE,
232
+ Strings.AUTO_CORRECT_WITH_SKIPPING,
233
+ ConfigStructureWarning,
234
+ ConfigStructureError
235
+ )
236
+ return None
237
+
238
+ if command_type in ("word", "fallback"):
239
+ arguments["children"] = {}
240
+
241
+ if "children" in command_dict:
242
+ for k, cd in command_dict["children"].items():
243
+ k = ConfigValidator.validate_name(
244
+ settings,
245
+ k,
246
+ is_fallback=(cd.get("type") == "fallback")
247
+ )
248
+ child: Optional[CommandClass] = ConfigValidator.validate_command(settings, cd)
249
+
250
+ if child is not None:
251
+ arguments["children"][k] = child
252
+
253
+ if arguments["children"] == {}:
254
+ settings.warning(
255
+ Strings.COMMAND_INVALID_CHILDREN,
256
+ Strings.AUTO_CORRECT_WITH_REMOVING,
257
+ ConfigValuesWarning,
258
+ ConfigValuesError
259
+ )
260
+ if command_type not in ("word", "fallback") and "children" in command_dict:
261
+ settings.warning(
262
+ Strings.COMMAND_INVALID_TYPE,
263
+ Strings.AUTO_CORRECT_WITH_SKIPPING,
264
+ ConfigStructureWarning,
265
+ ConfigStructureError
266
+ )
267
+ return None
268
+
269
+ return arguments # pyright: ignore[reportReturnType] (The arguments are always correct.)
270
+
271
+ @staticmethod
272
+ def validate_name(
273
+ settings: Settings,
274
+ name: str,
275
+ is_state: bool=False,
276
+ is_fallback: bool=False
277
+ ) -> str:
278
+ """Validates an identifier name.
279
+
280
+ Args:
281
+ settings: The settings.
282
+ name: The string name.
283
+ is_state: Is this validation for state.
284
+ is_fallback: Is this validation for the `Fallback` command type.
285
+
286
+ Returns:
287
+ Validated and corrected string name.
288
+
289
+ Raises:
290
+ ConfigValuesWarning: The name is empty, has '-' character in name start, has special characters, has invalid `InternalState` structure (if `is_state == True`), has invalid `Fallback` structure (if `is_fallback == True`) or character is not valid.
291
+
292
+ ConfigValuesError: The name is empty, has '-' character in name start, has special characters, has invalid `InternalState` structure (if `is_state == True`), has invalid `Fallback` structure (if `is_fallback == True`) or character is not valid.
293
+ """
294
+
295
+ if not isinstance(name, str):
296
+ settings.warning(
297
+ Strings.COMMAND_INVALID_ALIASES,
298
+ Strings.AUTO_CORRECT_WITH_CASTING,
299
+ ConfigValuesWarning,
300
+ ConfigValuesError
301
+ )
302
+ name = str(name)
303
+
304
+ if name == ''.join([" "] * len(name)):
305
+ settings.warning(
306
+ Strings.COMMAND_NAME_EMPTY,
307
+ Strings.AUTO_CORRECT_WITH_REMOVING,
308
+ ConfigValuesWarning,
309
+ ConfigValuesError
310
+ )
311
+ ConfigValidator._empty_name_replace_index += 1
312
+ return f"command+{ConfigValidator._empty_name_replace_index}"
313
+
314
+ i: int = 0
315
+ found_letter: bool = False
316
+ found_dollar: bool = False
317
+ removing_indexes: List[int] = []
318
+ specials = set(punctuation)
319
+ specials.add(' ')
320
+ specials.remove('-')
321
+ while i < len(name):
322
+ if name[i] == '-' and not found_letter:
323
+ # '-' as invalid separator.
324
+ settings.warning(
325
+ Strings.COMMAND_NAME_STARTS_INVALID,
326
+ Strings.AUTO_CORRECT_WITH_REMOVING,
327
+ ConfigValuesWarning,
328
+ ConfigValuesError
329
+ )
330
+ removing_indexes.append(i)
331
+ elif name[i] in specials:
332
+ # Character is special.
333
+ if is_state:
334
+ if name[i] == "_":
335
+ # Internal state name validation.
336
+ start: int = i
337
+ while i < len(name) and name[i] == "_":
338
+ i += 1
339
+ if i - start == 2:
340
+ continue
341
+ settings.warning(
342
+ Strings.STATE_INTERNAL_NAME_INVALID.substitute(length=i - start),
343
+ Strings.AUTO_CORRECT_TO_DEFAULTS,
344
+ ConfigValuesWarning,
345
+ ConfigValuesError
346
+ )
347
+ name = name[:start] + "__" + name[i:]
348
+ i = start + 2
349
+ elif name[i] in ("-", ":", "/"):
350
+ # Allowed special characters in state name.
351
+ i += 1
352
+ continue
353
+
354
+ if is_fallback and name[i] == "$":
355
+ # `Fallback` type standart.
356
+ if i == 0:
357
+ i += 1
358
+ found_dollar = True
359
+ continue
360
+
361
+ if i > 0 and not found_dollar:
362
+ settings.warning(
363
+ Strings.COMMAND_FALLBACK_DOLLAR_MISPOSITION,
364
+ Strings.AUTO_CORRECT_WITH_REMOVING,
365
+ ConfigValuesWarning,
366
+ ConfigValuesError
367
+ )
368
+ found_dollar = True
369
+ name = name[:i] + name[i + 1:]
370
+ i += 1
371
+ continue
372
+
373
+ if found_dollar:
374
+ settings.warning(
375
+ Strings.COMMAND_FALLBACK_DOLLAR_OVERLOAD,
376
+ Strings.AUTO_CORRECT_WITH_REMOVING,
377
+ ConfigValuesWarning,
378
+ ConfigValuesError
379
+ )
380
+ name = name[:i] + name[i + 1:]
381
+ i += 1
382
+ continue
383
+
384
+ settings.warning(
385
+ Strings.COMMAND_NAME_SPECIAL.substitute(character=name[i]),
386
+ Strings.AUTO_CORRECT_WITH_REMOVING,
387
+ ConfigValuesWarning,
388
+ ConfigValuesError
389
+ )
390
+ removing_indexes.append(i)
391
+ elif name[i].isalnum():
392
+ # Character is valid.
393
+ found_letter = True
394
+ else:
395
+ # Character is unknown.
396
+ settings.warning(
397
+ Strings.UNKNOWN_CHARACTER.substitute(character=name[i]),
398
+ Strings.AUTO_CORRECT_WITH_SKIPPING,
399
+ ConfigValuesWarning,
400
+ ConfigValuesError
401
+ )
402
+
403
+ i += 1
404
+
405
+ # Deleting the removing character.
406
+ for index in reversed(removing_indexes):
407
+ name = name[:index] + name[index + 1:]
408
+
409
+ if is_fallback and not found_dollar:
410
+ # The `Fallback` standart warning.
411
+ settings.warning(
412
+ Strings.COMMAND_FALLBACK_DOLLAR_NOT_FOUND,
413
+ '',
414
+ ConfigValuesWarning,
415
+ warning_levels=(WarningsLevel.BASIC, WarningsLevel.STRICT),
416
+ exception_levels=(None,)
417
+ )
418
+
419
+ return name
420
+
421
+ @staticmethod
422
+ def validate_commands(
423
+ settings: Settings,
424
+ config_dict: UnknownConfigType
425
+ ) -> Dict[str, CommandClass]:
426
+ """Validates commands in the config field `commands`.
427
+
428
+ Args:
429
+ settings: The settings.
430
+ config_dict: The config dictionary, which was not validated yet.
431
+
432
+ Returns:
433
+ Validated commands in stable and known format.
434
+
435
+ Raises:
436
+ ConfigStructureWarning: The `commands` field is not written or not dict.
437
+ ConfigValuesWarning: Command aliases are duplicating or equals original command name.
438
+
439
+ ConfigStructureError: The `commands` field is not written or not dict.
440
+ ConfigValuesError: Command aliases are duplicating or equals original command name.
441
+ """
442
+
443
+ commands: Dict[str, CommandClass] = {}
444
+ used_aliases: List[str] = []
445
+
446
+ if "commands" not in config_dict:
447
+ settings.warning(
448
+ Strings.INVALID_CONFIG_COMMANDS,
449
+ Strings.AUTO_CORRECT_TO_DEFAULTS,
450
+ ConfigStructureWarning,
451
+ ConfigStructureError
452
+ )
453
+ return {}
454
+
455
+ if not isinstance(config_dict["commands"], dict):
456
+ settings.warning(
457
+ Strings.INVALID_CONFIG_COMMANDS,
458
+ Strings.AUTO_CORRECT_TO_DEFAULTS,
459
+ ConfigStructureWarning,
460
+ ConfigStructureError
461
+ )
462
+ return {}
463
+
464
+ for key, command_dict in config_dict["commands"].items():
465
+ # Processing a branch of command.
466
+ key = ConfigValidator.validate_name(
467
+ settings,
468
+ key,
469
+ is_fallback=(command_dict.get("type") == "fallback")
470
+ )
471
+
472
+ command: Optional[CommandClass] = ConfigValidator.validate_command(
473
+ settings,
474
+ command_dict
475
+ )
476
+ if command:
477
+ new_aliases: List[str] = []
478
+
479
+ if "aliases" in command_dict:
480
+ for alias in command_dict["aliases"]:
481
+ # Processing an alias in the command aliases.
482
+ alias = ConfigValidator.validate_name(settings, alias)
483
+
484
+ if alias in used_aliases:
485
+ settings.warning(
486
+ Strings.COMMAND_ALIAS_COPIED.substitute(alias=alias),
487
+ Strings.AUTO_CORRECT_WITH_REMOVING,
488
+ ConfigValuesWarning,
489
+ ConfigValuesError
490
+ )
491
+ elif alias == key:
492
+ settings.warning(
493
+ Strings.COMMAND_ALIAS_REDUNDANCY.substitute(alias=alias),
494
+ Strings.AUTO_CORRECT_WITH_REMOVING,
495
+ ConfigValuesWarning,
496
+ ConfigValuesError
497
+ )
498
+ else:
499
+ used_aliases.append(alias)
500
+ new_aliases.append(alias)
501
+
502
+ command_dict["aliases"] = new_aliases
503
+
504
+ key = ConfigValidator.validate_name(settings, key)
505
+ commands[key] = command
506
+
507
+ return commands
508
+
509
+ @staticmethod
510
+ def validate_states(
511
+ settings: Settings,
512
+ config_dict: UnknownConfigType,
513
+ commands: Dict[str, CommandClass]
514
+ ) -> CommandStates:
515
+ """Validates config states.
516
+
517
+ Args:
518
+ settings: The settings.
519
+ config_dict: The config dictionary, which was not validated yet.
520
+ commands: The proccessed commands dict.
521
+
522
+ Returns:
523
+ Validated command states.
524
+
525
+ Raises:
526
+ ConfigStructureWarning: The config has not the field `states` or the states type is not dict.
527
+ ConfigValuesWarning: An internal state by format is not written in `InternalCommandStates` or a command name is unknown.
528
+
529
+ ConfigStructureError: The config has not the field `states` or the states type is not dict.
530
+ ConfigValuesError: An internal state by format is not written in `InternalCommandStates` or a command name is unknown.
531
+ """
532
+
533
+ states: CommandStates = {}
534
+
535
+ command_names: List[str] = [key for key in list(commands.keys())]
536
+
537
+ if not "states" in config_dict:
538
+ settings.warning(
539
+ Strings.INVALID_CONFIG_STATES,
540
+ Strings.AUTO_CORRECT_TO_DEFAULTS,
541
+ ConfigStructureWarning,
542
+ ConfigStructureError
543
+ )
544
+ return {
545
+ "__base__": command_names
546
+ }
547
+
548
+ if not isinstance(config_dict["states"], dict):
549
+ settings.warning(
550
+ Strings.INVALID_CONFIG_STATES,
551
+ Strings.AUTO_CORRECT_TO_DEFAULTS,
552
+ ConfigStructureWarning,
553
+ ConfigStructureError
554
+ )
555
+ return {
556
+ "__base__": command_names
557
+ }
558
+
559
+ for state, names in config_dict["states"].items():
560
+ # Proccessing a state with their command names.
561
+ state = ConfigValidator.validate_name(settings, state, is_state=True)
562
+
563
+ if (
564
+ state.startswith("__")
565
+ and state.endswith("__")
566
+ and state not in get_args(InternalCommandStates)
567
+ ):
568
+ # An internal state is not written in `InternalCommandStates`.
569
+ settings.warning(
570
+ Strings.STATE_INVALID_NAME,
571
+ Strings.AUTO_CORRECT_WITH_RENAMING,
572
+ ConfigValuesWarning,
573
+ ConfigValuesError
574
+ )
575
+ state = state[:2] + state[2:-2]
576
+
577
+ states[state] = []
578
+
579
+ for name in names:
580
+ # Processing a command in the state.
581
+ name = ConfigValidator.validate_name(settings, name)
582
+
583
+ if name not in command_names:
584
+ # Unknown command name.
585
+ settings.warning(
586
+ Strings.COMMAND_UNKNOWN_NAME,
587
+ Strings.AUTO_CORRECT_WITH_SKIPPING,
588
+ ConfigValuesWarning,
589
+ ConfigValuesError
590
+ )
591
+ else:
592
+ states[state].append(name)
593
+
594
+ return states
595
+
596
+ @staticmethod
597
+ def validate_config(settings: Settings, config_dict: UnknownConfigType) -> ConfigType:
598
+ """Validates a raw config.
599
+
600
+ Args:
601
+ settings: The settings.
602
+ config_dict: The config dictionary, which was not validated yet.
603
+
604
+ Returns:
605
+ Validated config.
606
+ """
607
+
608
+ version: ConfigVersion = ConfigValidator.validate_version(settings, config_dict)
609
+ commands: Dict[str, CommandClass] = ConfigValidator.validate_commands(settings, config_dict)
610
+ states: CommandStates = ConfigValidator.validate_states(settings, config_dict, commands)
611
+
612
+ return {
613
+ "version": version,
614
+ "states": states,
615
+ "commands": commands
616
+ }
untils/factories.py ADDED
@@ -0,0 +1,40 @@
1
+ """factories.py - All factories."""
2
+
3
+ # pylint: disable=too-few-public-methods
4
+
5
+ from typing import List, Any
6
+
7
+ from untils.utils.type_aliases import CommandType
8
+
9
+ from untils.command import (
10
+ CommandNode, CommandWordNode, CommandFallbackNode, CommandFlagNode, CommandOptionNode, AliasNode
11
+ )
12
+
13
+ class CommandNodeFactory:
14
+ """Factory class for command nodes."""
15
+
16
+ @classmethod
17
+ def create(
18
+ cls,
19
+ name: str,
20
+ node_type: CommandType,
21
+ aliases: List[AliasNode],
22
+ default: Any,
23
+ children: List[CommandNode]
24
+ ) -> CommandNode:
25
+ """Creates a command node by a template.
26
+
27
+ Args:
28
+ name: The command name.
29
+ node_type: The command type.
30
+ aliases: The command aliases.
31
+ default: The command default value.
32
+ children: The nest commands.
33
+ """
34
+
35
+ return {
36
+ "word": CommandWordNode(name, node_type, aliases, children),
37
+ "fallback": CommandFallbackNode(name, node_type, default, children),
38
+ "flag": CommandFlagNode(name, node_type, aliases, default),
39
+ "option": CommandOptionNode(name, node_type, aliases, default)
40
+ }[node_type]