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/ioreader.py ADDED
@@ -0,0 +1,55 @@
1
+ """ioreader.py - IO operations through `IOReader` and their mixins for config extensions."""
2
+
3
+ # pylint: disable=too-few-public-methods
4
+
5
+ from typing import Dict, override
6
+
7
+ import json
8
+ import os
9
+
10
+ from untils.utils.type_aliases import UnknownConfigType, ConfigSupportedExtensions
11
+ from untils.iovalidator import IOValidator
12
+ from untils.utils.protocols import IOReaderMixin, IOReaderProtocol
13
+
14
+ from untils.settings import Settings
15
+
16
+ class JSONMixin(IOReaderMixin):
17
+ """The JSON extension for `IOReader`."""
18
+
19
+ @override
20
+ @staticmethod
21
+ def read(settings: Settings, file_path: str) -> UnknownConfigType:
22
+ with open(file_path, 'r', encoding='utf-8') as file:
23
+ content: UnknownConfigType = json.loads(file.read())
24
+ return content
25
+
26
+ class IOReader():
27
+ """Reader class for config loading by paths and settings."""
28
+
29
+ _MIXINS: Dict[ConfigSupportedExtensions, IOReaderProtocol] = {
30
+ ".json": JSONMixin,
31
+ ".json5": JSONMixin
32
+ }
33
+ """All supported mixins for `IOReader`."""
34
+
35
+ @staticmethod
36
+ def read_file(settings: Settings, file_path: str) -> UnknownConfigType:
37
+ """Reads a config file by path.
38
+
39
+ Args:
40
+ settings: The settings.
41
+ file_path: The file path.
42
+
43
+ Returns:
44
+ The raw unvalidated config.
45
+ """
46
+
47
+ IOValidator.validate_config_path(settings, file_path)
48
+
49
+ extension: str = os.path.splitext(file_path)[1]
50
+ content: UnknownConfigType = {}
51
+
52
+ if extension in IOReader._MIXINS:
53
+ content: UnknownConfigType = IOReader._MIXINS[extension].read(settings, file_path)
54
+
55
+ return content
untils/iovalidator.py ADDED
@@ -0,0 +1,55 @@
1
+ """iovalidator.py - `IOValidator` for IO validations."""
2
+
3
+ # pylint: disable=too-few-public-methods
4
+
5
+ import os
6
+
7
+ from untils.utils.enums import WarningsLevel
8
+ from untils.utils.constants import Constants, Strings
9
+ from untils.utils.lib_warnings import FileWarning, FileError
10
+
11
+ from untils.settings import Settings
12
+
13
+ class IOValidator:
14
+ """Validator class for the all IO operations for `IOReader`."""
15
+
16
+ @staticmethod
17
+ def validate_config_path(settings: Settings, file_path: str) -> bool:
18
+ """Validates a config path.
19
+
20
+ Args:
21
+ settings: The settings.
22
+ file_path: The file path.
23
+
24
+ Returns:
25
+ `True` if the config path is valid and supported, else `False`.
26
+
27
+ Raises:
28
+ FileWarning: The config file is not exists or the config extension is not in standart.
29
+
30
+ FileError: The config file is not exists.
31
+ """
32
+
33
+ if os.path.exists(file_path) and os.path.isfile(file_path):
34
+ # Processing the config file path.
35
+ extension: str = os.path.splitext(file_path)[1]
36
+ if extension in Constants.SUPPORTED_CONFIG_FORMATS:
37
+ if extension not in Constants.STANDART_CONFIG_FORMATS:
38
+ # The config format is not in standart.
39
+ settings.warning(
40
+ Strings.CONFIG_EXTENSION_NOT_SUPPORTS,
41
+ Strings.AUTO_CORRECT_WITH_ACCEPTING,
42
+ FileWarning,
43
+ warning_levels=(WarningsLevel.BASIC, WarningsLevel.STRICT),
44
+ exception_levels=()
45
+ )
46
+ return True
47
+ else:
48
+ settings.warning(
49
+ Strings.CONFIG_FILE_NOT_EXISTS,
50
+ Strings.AUTO_CORRECT_WITH_SKIPPING,
51
+ FileWarning,
52
+ FileError
53
+ )
54
+
55
+ return False
untils/parser.py ADDED
@@ -0,0 +1,138 @@
1
+ """parser.py - Parses config and input."""
2
+
3
+ from typing import Dict, List, Any, cast, get_args, Optional
4
+
5
+ from untils.utils.type_aliases import (
6
+ CommandClass, CommandType, ConfigType, InternalCommandStates, InputDict
7
+ )
8
+ from untils.utils.protocols import FinalInputProtocol
9
+ from untils.utils.enums import FinalTokenType
10
+
11
+ from untils.command import CommandNode, AliasNode, StateNode
12
+ from untils.factories import CommandNodeFactory
13
+ from untils.commands_config import CommandsConfig
14
+ from untils.input_token import FinalInputTokenWord, FinalInputTokenFlag, FinalInputTokenOption
15
+ from untils.settings import Settings
16
+
17
+ class Parser:
18
+ """This class parses raw data to intermediate reference."""
19
+
20
+ @staticmethod
21
+ def parse_commands(commands: Dict[str, CommandClass]) -> List[CommandNode]:
22
+ """Parses a command dictionary to the AST tree of command nodes.
23
+
24
+ Args:
25
+ commands: The commands dictionary.
26
+
27
+ Returns:
28
+ Parsed AST tree of the command nodes.
29
+ """
30
+
31
+ def parse_command(name: str, command_dict: CommandClass) -> CommandNode:
32
+ """Parses a single command.
33
+
34
+ Args:
35
+ name: The command name.
36
+ command_dict: The command dictionary.
37
+
38
+ Returns:
39
+ The command node.
40
+ """
41
+
42
+ command_type: CommandType = command_dict.get("type")
43
+ aliases: List[AliasNode] = []
44
+ default: Any = None
45
+ children: List[CommandNode] = []
46
+
47
+ if command_type in ("word", "flag", "option"):
48
+ aliases = [AliasNode(name, alias) for alias in command_dict.get("aliases", [])]
49
+
50
+ if command_type in ("flag", "option"):
51
+ default: Any = command_dict.get("default", None)
52
+
53
+ if command_type in ("word", "fallback"):
54
+ children: List[CommandNode] = []
55
+
56
+ for child_name, child_dict in command_dict.get("children", {}).items():
57
+ children.append(parse_command(child_name, cast(CommandClass, child_dict)))
58
+
59
+ return CommandNodeFactory.create(name, command_type, aliases, default, children)
60
+
61
+ return [parse_command(name, command_dict) for name, command_dict in commands.items()]
62
+
63
+ @staticmethod
64
+ def parse_states(states_dict: Dict[str, List[str]]) -> List[StateNode]:
65
+ """Parses states in config.
66
+
67
+ Args:
68
+ states_dict: The states dictionary.
69
+
70
+ Returns:
71
+ The list of the state nodes.
72
+ """
73
+
74
+ return [
75
+ StateNode(state, state in get_args(InternalCommandStates), aliases)
76
+ for state, aliases in states_dict.items()
77
+ ]
78
+
79
+ @staticmethod
80
+ def parse_config(config_dict: ConfigType) -> CommandsConfig:
81
+ """Parses a validated config.
82
+
83
+ Args:
84
+ config_dict: The config dictionary.
85
+
86
+ Returns:
87
+ The parsed config.
88
+ """
89
+
90
+ return CommandsConfig(
91
+ config_dict["version"],
92
+ Parser.parse_states(config_dict["states"]),
93
+ Parser.parse_commands(config_dict["commands"])
94
+ )
95
+
96
+ @staticmethod
97
+ def parse_input(settings: Settings, tokens: List[FinalInputProtocol]) -> InputDict:
98
+ """Parses a user input to input dictionary.
99
+
100
+ Args:
101
+ tokens: Final tokenized tokens.
102
+ debug: Determines debug messages display.
103
+
104
+ Returns:
105
+ The parsed input dictionary.
106
+ """
107
+
108
+ settings.logger.debug(f"Parser.parse_input(tokens={tokens}).")
109
+
110
+ path: List[str] = []
111
+ flags: Dict[str, Optional[bool]] = {}
112
+ options: Dict[str, Any] = {}
113
+
114
+ i: int = 0
115
+ while i < len(tokens):
116
+ settings.logger.debug(f"Iteration: {i}.")
117
+
118
+ token: FinalInputProtocol = tokens[i]
119
+
120
+ if token.type == FinalTokenType.WORD and isinstance(token, FinalInputTokenWord):
121
+ settings.logger.debug("Process `Word` token.")
122
+ path.append(token.value)
123
+
124
+ elif token.type == FinalTokenType.FLAG and isinstance(token, FinalInputTokenFlag):
125
+ settings.logger.debug("Process `Flag` token.")
126
+ flags[token.name] = token.value
127
+
128
+ elif token.type == FinalTokenType.OPTION and isinstance(token, FinalInputTokenOption):
129
+ settings.logger.debug("Process `Option` token.")
130
+ options[token.name] = token.value
131
+
132
+ i += 1
133
+
134
+ return {
135
+ "path": path,
136
+ "flags": flags,
137
+ "options": options
138
+ }
untils/processor.py ADDED
@@ -0,0 +1,88 @@
1
+ """processor.py - `Processor` class for universal pipe-lines."""
2
+
3
+ from typing import List, Optional
4
+
5
+ from untils.utils.type_aliases import UnknownConfigType, ConfigType, InputDict
6
+ from untils.utils.protocols import FinalInputProtocol
7
+
8
+ from untils.ioreader import IOReader
9
+ from untils.config_validator import ConfigValidator
10
+ from untils.parser import Parser
11
+ from untils.commands_config import CommandsConfig
12
+ from untils.settings import Settings
13
+ from untils.input_token import RawInputToken
14
+ from untils.tokenizer import Tokenizer
15
+ from untils.input_validator import InputValidator
16
+
17
+ class Processor:
18
+ """Processor class for config processing."""
19
+
20
+ @staticmethod
21
+ def load_config(settings: Settings, file_path: str) -> CommandsConfig:
22
+ """Loads config.
23
+
24
+ Args:
25
+ settings: The settings.
26
+ file_path: The file path.
27
+ debug: Determines debug messages display.
28
+
29
+ Returns:
30
+ Validated and parsed config.
31
+ """
32
+
33
+ settings.logger.debug(f"Load config by path: '{file_path}'.")
34
+
35
+ ### 1. IOReader ###
36
+ settings.logger.debug("Reading the file.")
37
+ content: UnknownConfigType = IOReader.read_file(settings, file_path)
38
+ settings.logger.debug(f"Content: {content}.")
39
+
40
+ ### 2. ConfigValidator ###
41
+ settings.logger.debug("Validating the config.")
42
+ raw_config: ConfigType = ConfigValidator.validate_config(settings, content)
43
+ settings.logger.debug(f"Intermediate config: {raw_config}.")
44
+
45
+ ### 3. Parser ###
46
+ settings.logger.debug("Parsing.")
47
+ config: CommandsConfig = Parser.parse_config(raw_config)
48
+ settings.logger.debug(f"Parsed config: {config}.")
49
+
50
+ return config
51
+
52
+ @staticmethod
53
+ def process_input(
54
+ settings: Settings,
55
+ config: Optional[CommandsConfig],
56
+ input_str: str
57
+ ) -> InputDict:
58
+ """Validates a user input.
59
+
60
+ Args:
61
+ settings: The settings.
62
+ input_str: The user input.
63
+ debug: Determines debug messages display.
64
+
65
+ Returns:
66
+ Validated and parsed input dict.
67
+ """
68
+
69
+ settings.logger.debug(f"Processing input string: '{input_str}'.")
70
+
71
+ ### 1. Tokenizer ###
72
+ settings.logger.debug("Tokenizing the input.")
73
+ tokenizer: Tokenizer = Tokenizer(settings, input_str)
74
+ tokens: List[RawInputToken] = tokenizer.tokenize_input()
75
+ settings.logger.debug(f"Tokens: {tokens}.")
76
+
77
+ ### 2. InputValidator ###
78
+ settings.logger.debug("Validating the input.")
79
+ input_validator: InputValidator = InputValidator(settings, config, tokens)
80
+ validated_tokens: List[FinalInputProtocol] = input_validator.validate_input(settings)
81
+ settings.logger.debug(f"Validated tokens: {validated_tokens}.")
82
+
83
+ ### 3. Parser ###
84
+ settings.logger.debug("Parsing the input.")
85
+ parsed_representation: InputDict = Parser.parse_input(settings, validated_tokens)
86
+ settings.logger.debug(f"Parsed input: {parsed_representation}.")
87
+
88
+ return parsed_representation
untils/settings.py ADDED
@@ -0,0 +1,114 @@
1
+ """settings.py - Global context settings for much part of all library."""
2
+
3
+ # pyright: reportUnnecessaryIsInstance=false
4
+
5
+ from typing import Optional, Tuple, Type, Union
6
+
7
+ import warnings
8
+ import logging
9
+
10
+ from untils.utils.enums import WarningsLevel, InternalState
11
+ from untils.utils.decorators import alternative
12
+ from untils.utils.constants import Strings
13
+ from untils.utils.lib_warnings import ConfigError, ConfigWarning
14
+
15
+ class Settings:
16
+ """The global context settings."""
17
+
18
+ __slots__ = ["__warnings_level", "__current_state", "__logger"]
19
+
20
+ __warnings_level: WarningsLevel
21
+ __current_state: str
22
+ __logger: logging.Logger
23
+
24
+ @property
25
+ def warnings_level(self) -> WarningsLevel:
26
+ """Warnings level, which defines strictness of code flow."""
27
+ return self.__warnings_level
28
+
29
+ @warnings_level.setter
30
+ def warnings_level(self, value: WarningsLevel) -> None:
31
+ self.__warnings_level = value
32
+
33
+ @property
34
+ def current_state(self) -> str:
35
+ """Current commands state."""
36
+ return self.__current_state
37
+
38
+ @current_state.setter
39
+ def current_state(self, value: Union[str, InternalState]) -> None:
40
+ if isinstance(value, str):
41
+ if value != InternalState.INIT.value:
42
+ self.warning(
43
+ Strings.INVALID_INTERNAL_STATE_CHANGE.substitute(state=value),
44
+ Strings.AUTO_CORRECT_TO_LATEST,
45
+ RuntimeWarning,
46
+ ValueError
47
+ )
48
+ else:
49
+ self.__current_state = value
50
+ elif isinstance(value, InternalState):
51
+ if value != InternalState.INIT:
52
+ self.warning(
53
+ Strings.INVALID_INTERNAL_STATE_CHANGE.substitute(state=value),
54
+ Strings.AUTO_CORRECT_TO_LATEST,
55
+ RuntimeWarning,
56
+ ValueError
57
+ )
58
+ else:
59
+ self.__current_state = value.value
60
+
61
+ @property
62
+ def logger(self) -> logging.Logger:
63
+ """Returns settings logger."""
64
+ return self.__logger
65
+
66
+ def __init__(self) -> None:
67
+ self.__warnings_level = WarningsLevel.STRICT
68
+ self.__current_state = InternalState.INIT.value
69
+ self.__logger = logging.getLogger(__name__)
70
+ self.__logger.debug(Strings.LOG_SETTINGS_INIT)
71
+
72
+ @alternative(version=Strings.ANY_VERSION)
73
+ def get_warnings_level(self) -> WarningsLevel:
74
+ """[!ALT] Get a warnings level.\n
75
+ <This method is alternative for the standart. In real product use the property `Settings.warnings_level`.>
76
+
77
+ Returns:
78
+ The current warnings level.
79
+ """
80
+
81
+ return self.__warnings_level
82
+
83
+ @alternative(version=Strings.ANY_VERSION)
84
+ def set_warnings_level(self, warnings_level: WarningsLevel=WarningsLevel.STRICT) -> None:
85
+ """[!ALT] Sets a warnings level.\n
86
+ <This method is alternative for the standart. In real product use the property `Settings.warnings_level`.>
87
+
88
+ Args:
89
+ warnings_level: The warnings level.
90
+ """
91
+
92
+ self.__warnings_level = warnings_level
93
+
94
+ def warning(
95
+ self,
96
+ message: str,
97
+ auto_correct: str,
98
+ warning_type: Type[Warning]=ConfigWarning,
99
+ exception_type: Type[Exception]=ConfigError,
100
+ warning_levels: Optional[Union[Tuple[WarningsLevel, ...], Tuple[None]]]=None,
101
+ exception_levels: Optional[Union[Tuple[WarningsLevel, ...], Tuple[None]]]=None
102
+ ) -> None:
103
+ """Warning or exception in validators."""
104
+
105
+ if self.warnings_level in (warning_levels or (WarningsLevel.BASIC,)):
106
+ self.logger.warning(message + ' ' + auto_correct, stacklevel=3)
107
+ warnings.warn(
108
+ message + ' ' + auto_correct,
109
+ warning_type,
110
+ stacklevel=3
111
+ )
112
+ elif self.warnings_level in (exception_levels or (WarningsLevel.STRICT,)):
113
+ self.logger.error(message, stacklevel=3)
114
+ raise exception_type(message)
untils/tokenizer.py ADDED
@@ -0,0 +1,108 @@
1
+ """tokenizer.py - Tokenize raw user input string to raw tokens."""
2
+
3
+ from typing import List, Literal, cast
4
+
5
+ from untils.utils.enums import RawTokenType
6
+ from untils.input_token import RawInputToken
7
+
8
+ from untils.settings import Settings
9
+
10
+ class Tokenizer:
11
+ """Tokenizer class, which tokenizes user input."""
12
+
13
+ __slots__ = ["_settings", "_input_str", "_result", "_i"]
14
+
15
+ _settings: Settings
16
+ """The settings."""
17
+ _input_str: str
18
+ """The user input string."""
19
+ _result: List[RawInputToken]
20
+ """The processed raw tokens."""
21
+ _i: int
22
+ """Tokenize index."""
23
+
24
+ def __init__(self, settings: Settings, input_str: str) -> None:
25
+ """
26
+ Args:
27
+ input_str: The user input.
28
+ debug: Determines debug messages display.
29
+ """
30
+
31
+ self._settings = settings
32
+ self._input_str = input_str
33
+ self._result = []
34
+ self._i = 0
35
+
36
+ def tokenize_string(self) -> None:
37
+ """Tokenizes the `String` type."""
38
+
39
+ string_char: Literal['\'', '\"'] = cast(Literal['\'', '\"'], self._input_str[self._i])
40
+ self._i += 1
41
+ string: str = ""
42
+
43
+ while self._i < len(self._input_str) and self._input_str[self._i] != string_char:
44
+ if self._input_str[self._i] == '\\':
45
+ if self._input_str[self._i + 1] in ('\'', '\"'):
46
+ string += self._input_str[self._i + 1]
47
+ elif self._input_str[self._i + 1] == '\\':
48
+ string += self._input_str[self._i]
49
+
50
+ self._i += 1
51
+ else:
52
+ string += self._input_str[self._i]
53
+
54
+ self._i += 1
55
+
56
+ self._result.append(RawInputToken(RawTokenType.STRING, string))
57
+ self._settings.logger.debug(f"String: '{string}'")
58
+
59
+ def tokenize_word(self) -> None:
60
+ """Tokenizes the `Word` type."""
61
+
62
+ start: int = self._i
63
+
64
+ while self._i < len(self._input_str) and self._input_str[self._i].isalnum():
65
+ self._i += 1
66
+
67
+ word: str = self._input_str[start:self._i]
68
+ self._result.append(RawInputToken(RawTokenType.WORD, word))
69
+ self._settings.logger.debug(f"Word: '{word}'")
70
+
71
+ def tokenize_input(self) -> List[RawInputToken]:
72
+ """Tokenizes the input.
73
+
74
+ Returns:
75
+ Unvalidated raw tokens.
76
+ """
77
+
78
+ self._settings.logger.debug(f"Tokenizer.tokenize_input(input_str='{self._input_str}')")
79
+
80
+ self._result = []
81
+ self._i = 0
82
+ while self._i < len(self._input_str):
83
+ self._settings.logger.debug(f"Current character: {self._input_str[self._i]}.")
84
+
85
+ if self._input_str[self._i] == ' ':
86
+ self._settings.logger.debug("Process Space character.")
87
+ self._result.append(RawInputToken(RawTokenType.SPACE, ' '))
88
+
89
+ elif self._input_str[self._i] == '-':
90
+ self._settings.logger.debug("Process Minus character.")
91
+ self._result.append(RawInputToken(RawTokenType.MINUS, '-'))
92
+
93
+ if self._input_str[self._i] == '!':
94
+ self._settings.logger.debug("Process Not character.")
95
+ self._result.append(RawInputToken(RawTokenType.NOT, '!'))
96
+
97
+ elif self._input_str[self._i] in ('\'', '\"'):
98
+ self._settings.logger.debug("Process `String` construction.")
99
+ self.tokenize_string()
100
+
101
+ elif self._input_str[self._i].isalnum():
102
+ self._settings.logger.debug("Process `Word` construction.")
103
+ self.tokenize_word()
104
+ continue
105
+
106
+ self._i += 1
107
+
108
+ return self._result
@@ -0,0 +1,22 @@
1
+ """This module represents all tools, which need for main module."""
2
+
3
+ # pyright: reportUnusedImport=false
4
+ # ^^^^^^^ (Public imports.)
5
+
6
+ from untils.utils.lib_warnings import (
7
+ ConfigWarning, FileWarning, ConfigStructureWarning, ConfigValuesWarning, InputWarning,
8
+ InputStructureWarning,InputValuesWarning, ConfigError, FileError, ConfigStructureError,
9
+ ConfigValuesError, InputError, InputStructureError, InputValuesError
10
+ )
11
+ from untils.utils.type_aliases import (
12
+ ConfigVersion, CommandClass, UnknownCommandClass, CommandType,
13
+ InternalCommandStates, CommandStates, ConfigSupportedExtensions, UnknownCommandConfig,
14
+ WordCommandConfig, FallbackCommandConfig, FlagCommandConfig, OptionCommandConfig,
15
+ ConfigType, UnknownConfigType, InputDict, CommandPath
16
+ )
17
+ from untils.utils.constants import Constants, Strings
18
+ from untils.utils.decorators import deprecated, alternative
19
+ from untils.utils.enums import (
20
+ WarningsLevel, ConfigVersions, RawTokenType, FinalTokenType, InternalState
21
+ )
22
+ from untils.utils.protocols import IOReaderMixin, Factoric, FinalInputProtocol