import json import pathlib import ipaddress import sqlalchemy class config_error(TypeError): pass class settings: """ This class is responsible for handling settings in app. It read config file from specified path, and handle options from it. When any config option is specified wrong, for example bad formated IP address, then config_error will be raised. """ def __init__(self, path: pathlib.Path) -> None: """ This function initialize settings, it require path to parse it as config file, and reading parameters from it. Parameters: path (pathlib.Path): Path of config file """ if not path.is_file(): raise FileNotFoundError("Config JSON not found.") with path.open() as config: try: self.__source = json.loads(config.read()) except: raise config_error("Syntax error in config file.") @property def app_name(self) -> str: """ It return provider app name. """ return self.__get("provider.app_name", str) or "Key App" @property def app_description(self) -> str: """ It return description of app given by provider. """ return self.__get("provider.description", str) or "Secure keys store." @property def organization_name(self) -> str: """ This return name of organization which own app. """ return self.__get("provider.organization_name", str) or "cx-org" @property def database(self) -> str: """ This return URL of the database, to build database connector. It parse all options, that mean user could give all parameters one by one, or URL as string. Returns: (str): URL of the database connection """ username = self.__get("database.username", str) or None password = self.__get("database.password", str) or None port = self.__get("database.port", int) or None host = self.__get("database.host", str) or None engine = self.__get("database.engine", str) or None database = self.__get("database.name", str) or None url = self.__get("database.url", str) or None if url is not None: check = username or None check = check or password or None check = check or port or None check = check or host or None check = check or engine or None check = check or database or None if check is None: return url error = "When database URL config parameter is in use, any other " error = error + "database config can not being specified. Decide " error = error + "which method to setting database connection use." raise config_rror(error) engine = engine or "sqlite" database = database or "database.sqlite3" url = sqlalchemy.URL( engine, username = username, password = password, host = host, database = database, port = port, query = None ) return str(url) @property def host(self) -> str: """ This return host to listen on. When in config is not valid ip address v4 or v6, then raise error. Returns: (str): Host to app listen on """ host = self.__get("listen.ip", str) or "0.0.0.0" try: ipaddress.ip_address(host) except: raise config_error("IP address is in invald form.") return host @property def port(self) -> int: """ This return port, or raise exception when port is failed. Returns: (int): Number of port to listen on """ port = self.__get("listen.port", int) or 8000 if not self.__is_port(port): raise config_error("Port is not in valid range.") return port @property def address(self) -> str: """ This is address of app to make server listen on. Returns: (str): Address of app to listen on """ port = self.port address = self.host ip_type = ipaddress.ip_address(host) if type(ip_type) is ipaddress.IPv4Address: return "http://" + address + ":" + port return "http://[" + address + "]:" + port def __is_port(self, port: int) -> bool: """ This function check that given number is proof port. Parameters: port (int): Port number to check Returns: (bool): True when this is valid port, false when not """ return port >= 0 and port <= 65535 def __get( self, name: str, typed: type | None = None ) -> str | int | float | bool | None: """ This function load parameter, from source config file. It could check type of loaded parameter, and raise error, if type is not valid. When typed parameter is not provided, then it skip check of parameter type. When item not exists in config file, then return None. It could be used with "or". Config file has parts, to navigate in parts use ".". For example: ["x"]["y"]["z"] => "x.y.z". Example: param = self.__get("default.param", str) or "default" Parameters: name (str): Name of the parameter, with dot "." notation typed (type | None): Type of returned parameter, None to skip checking. Default = None Returns: (str | int | float | bool | None): Value of parameter, of None when not exists """ parts = name.split(".") result = self.__source for part in parts: if not part in result: return None result = result[part] if typed is not None and type(result) is not typed: error = "" error = error + "Error, type of " + name + " " error = error + "must be in " + str(type(default)) + " type, " error = error + "but it is in " + str(type(result)) + "type." error = error + "Change it in config file." raise config_error(error) return result