diff --git a/Python/Drivers/FilesDirver.py b/Python/Drivers/FilesDirver.py new file mode 100644 index 0000000..5240473 --- /dev/null +++ b/Python/Drivers/FilesDirver.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from typing import Any, Self, Sequence +from os.path import dirname as directory_name, abspath as absolute_path, exists as path_exists, isfile as is_file, isdir as is_directory +from io import FileIO +from json import loads as json_loads +from Interfaces.Application.AnPInterface import AnPInterface +from Utils.Options import Options +from Utils.Validate import Validate +from Utils.Patterns import RE + +class FilesDriver: + + ROOT:str = directory_name(absolute_path(__file__)) + SLASH:str = "/" if "/" in ROOT else "\\" + + GET_ABSOLUTE_PATH_OPTIONS:Options = Options(Options.ALLOW_CHECKS + Options.SHOW_ERRORS) + LOAD_OPTIONS:Options = Options(Options.ALLOW_CHECKS + Options.SHOW_ERRORS) + LOAD_JSON_OPTIONS:Options = Options(Options.ALLOW_CHECKS + Options.SHOW_ERRORS) + + def __init__(self:Self, anp:AnPInterface) -> None: + self.anp:AnPInterface = anp + + self.__roots:list[str] = ["", self.ROOT] + + for _ in range(2): + self.__roots += [RE.PARENT_DIRECTORY.sub(r'\1', self.__roots[-1])] + + @classmethod + def fix_path(cls:type[Self], path:str) -> str: + + path = RE.SLASHES.sub(cls.SLASH, path) + + return path if path[0] != "\\" else "\\" + path + + def get_absolute_path(self:Self, path:str, custom_options:int = 0) -> tuple[str|None, int]: + """ 5 bits. """ + + options:Options = Options(custom_options, self.GET_ABSOLUTE_PATH_OPTIONS) + error:int = ( + (Validate.string(path, Validate.TRIM | Validate.EMPTY) << 1) | + 0) if options.allow_checks else 0 + absolute:str|None = None + + if not error: + for root in self.__roots: + try: + + absolute_path:str = self.fix_path((root + self.SLASH if root else "") + path) + + if path_exists(absolute_path): + absolute = absolute_path + break + + except Exception as exception: + error |= 1 << 0 + + if absolute is None: + error |= 1 << 4 + + return absolute, error + + def load(self:Self, path:str, mode:str = "r", custom_options:int = 0) -> tuple[str|bytes|None, int]: + """ 10 bits. """ + + options:Options = Options(custom_options, self.LOAD_OPTIONS) + error:int = ( + # (Validate.string(path, Validate.TRIM | Validate.EMPTY) << 1) | + (( + Validate.string(mode, Validate.TRIM | Validate.EMPTY) or + (0 if mode in ("r", "rb") else 8) + ) << 6) | + 0) if options.allow_checks else 0 + results:str|bytes|None = None + absolute:str|None + absolute_error:int + + absolute, absolute_error = self.get_absolute_path(path, custom_options) + if options.allow_checks: + error |= absolute_error << 1 + + if not error: + try: + + file:FileIO + + with open(absolute, mode) as file: + results = file.read() + + except Exception as exception: + error |= 1 << 0 + + return results, error + + def load_json(self:Self, data:str|Sequence[Any|None]|dict[str, Any|None], custom_options:int = 0) -> tuple[Sequence[Any|None]|dict[str, Any|None]|None, int]: + """ 10 bits. """ + + options:Options = Options(custom_options, self.LOAD_JSON_OPTIONS) + error:int = ( + (Validate.null_or_undefined(data) << 1) | + 0) if options.allow_checks else 0 + results:Sequence[Any|None]|dict[str, Any|None]|None = None + + if isinstance(data, str): + + if not options.allow_checks or not ( + error := error | Validate.string(data, Validate.FROM_TYPE | Validate.TRIM | Validate.EMPTY) << 1 + ): + + data = data.strip() + + if data[0] + data[-1] in ("{}", "[]"): + try: + results = json_loads(data) + if options.allow_checks and results == None: + error |= 1 << 6 + except Exception as exception: + error |= 1 << 0 + else: + + load_error:int + + data, load_error = self.load(data, "r", custom_options) + + if not options.allow_checks or ( + error := error | (load_error << 7) + ) or not ( + error := error | Validate.string(data, Validate.FROM_TYPE | Validate.TRIM | Validate.EMPTY) << 1 + ): + + data = data.strip() + + if data[0] + data[-1] in ("{}", "[]"): + try: + results = json_loads(data) + if options.allow_checks and results == None: + error |= 1 << 6 + except Exception as exception: + error |= 1 << 0 + else: + error |= 1 << 18 + + if not error: + try: + results = json_loads(data) + if options.allow_checks and results == None: + error |= 1 << 6 + except Exception as exception: + error |= 1 << 0 + elif isinstance(data, (list, dict)): + results = data + else: + error |= 1 << 3 + + return results, error + + def save(self:Self, path:str, content:str|bytes, mode:str = "w", custom_options:int = 0) -> int: + pass + + def delete(self:Self, path:str, custom_options:int = 0) -> int: + pass + + def is_file(self:Self, path:str, custom_options:int = 0) -> int: + pass + + def is_directory(self:Self, path:str, custom_options:int = 0) -> int: + pass \ No newline at end of file diff --git a/Python/Utils/Options.py b/Python/Utils/Options.py index 78d685d..0ec0891 100644 --- a/Python/Utils/Options.py +++ b/Python/Utils/Options.py @@ -13,6 +13,14 @@ class Options: ALLOW_NULLS:int = 1 * 3 ** NULLISH NOT_NULLS:int = 2 * 3 ** NULLISH + CHECKS:int = 2 + ALLOW_CHECKS:int = 1 * 3 ** CHECKS + NO_CHECKS:int = 2 * 3 ** CHECKS + + SHOWING_ERRORS:int = 3 + SHOW_ERRORS:int = 1 * 3 ** SHOWING_ERRORS + HIDE_ERRORS:int = 2 * 3 ** SHOWING_ERRORS + DEFAULT:Self = Self(NO_OVERWRITE + ALLOW_NULLS) @classmethod @@ -29,13 +37,17 @@ class Options: self.overwrite:bool|None self.allow_null:bool|None + self.check_errors:bool|None + self.show_errors:bool|None if default is None: default = self.DEFAULT for key, option in ( ("overwrite", self.OVERWRITE), - ("allow_null", self.NULLISH) + ("allow_null", self.NULLISH), + ("check_errors", self.CHECKS), + ("show_errors", self.SHOWING_ERRORS) ): value:bool|None = self.get_value(options, option) @@ -50,7 +62,9 @@ class Options: for option, value in ( (self.OVERWRITING, self.overwrite), - (self.NULLISH, self.allow_null) + (self.NULLISH, self.allow_null), + (self.CHECKS, self.allow_checks), + (self.SHOWING_ERRORS, self.show_errors) ): code += ( 1 if value is True else diff --git a/Python/Utils/Patterns.py b/Python/Utils/Patterns.py index 1bfd4a7..4914aae 100644 --- a/Python/Utils/Patterns.py +++ b/Python/Utils/Patterns.py @@ -5,4 +5,6 @@ from re import compile as re_compile, Pattern as REPattern, I as RE_IGNORE_CASE class RE: KEY:REPattern = re_compile(r'^[a-z_][a-z0-9_]*$', RE_IGNORE_CASE) - STRING_VARIABLES:REPattern = re_compile(r'\{([a-z_][a-z0-9_]*)\}', RE_IGNORE_CASE) \ No newline at end of file + STRING_VARIABLES:REPattern = re_compile(r'\{([a-z_][a-z0-9_]*)\}', RE_IGNORE_CASE) + SLASHES:REPattern = re_compile(r'[\\\/]+') + PARENT_DIRECTORY:REPattern = re_compile(r'^(.+)[\\\/][^\\\/]+[\\\/]?$') \ No newline at end of file diff --git a/Python/Utils/Validate.py b/Python/Utils/Validate.py new file mode 100644 index 0000000..81c0be7 --- /dev/null +++ b/Python/Utils/Validate.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from typing import Any, Self +from Utils.Patterns import RE + +class Validate: + + IGNORE_NULL_OR_UNDEFINED:int = 1 << 0 + EMPTY:int = 1 << 1 + TRIM:int = 1 << 2 + KEY:int = 1 << 3 + FROM_TYPE:int = 1 << 4 + + @classmethod + def null_or_undefined(cls:type[Self], value:Any|None, options:int = 0) -> int: + """ 2 Options. 2 bits. """ + return 0 if options & (cls.IGNORE_NULL_OR_UNDEFINED | cls.FROM_TYPE) else ( + # 1 if value is undefined else + 2 if value is None else + 0) + + @staticmethod + def null_or_undefined_message(key:str) -> list[str]: + return [ + f"{key}_undefined", + f"{key}_null" + ] + + @classmethod + def string(cls:type[Self], value:Any|None, options:int = 0) -> int: + """ 7 options. 3 bits. """ + return cls.null_or_undefined(value, options) or ( + 3 if not (options & cls.FROM_TYPE) and not isinstance(value, str) else + (4 if options & cls.EMPTY else 0) if value == "" else + (5 if options & cls.TRIM else 0) if value != (value := value.strip()) else + (6 if options & (cls.TRIM | cls.EMPTY) else 0) if value == "" else + (7 if options & cls.KEY else 0) if not RE.KEY.match(value) else + 0) + + @classmethod + def string_message(cls:type[Self], key:str) -> list[str]: + return cls.null_or_undefined_message(key) + [ + f"{key}_not_string", + f"{key}_empty", + f"{key}_not_trimmed", + f"{key}_empty_trimmed", + f"{key}_not_key" + ] \ No newline at end of file