Browse Source

Add setting and continue working on project.

Cixo Develop 7 months ago
parent
commit
6f9bf7af58
6 changed files with 284 additions and 6 deletions
  1. 1 0
      .gitignore
  2. 2 0
      assets/__init__.py
  3. 46 0
      assets/application.py
  4. 216 0
      assets/settings.py
  5. 1 0
      config.json
  6. 18 6
      core.py

+ 1 - 0
.gitignore

@@ -1,5 +1,6 @@
 # SQL
 *.db
+*.sqlite3
 
 # ---> Python
 # Byte-compiled / optimized / DLL files

+ 2 - 0
assets/__init__.py

@@ -13,3 +13,5 @@ from .builder import builder
 from .code_key import code_key
 from .code_key import code_key_generator
 from .code_key import code_key_manager
+from .settings import settings
+from .application import application

+ 46 - 0
assets/application.py

@@ -0,0 +1,46 @@
+import sqlmodel
+import sqlalchemy.engine.base
+
+from .settings import settings
+from .user import user
+from .user import user_builder
+from .user_loader import user_loader
+
+class application:
+    def __init__(self, config: settings) -> None:
+        self.__config = config
+        self.__init_database(config.database)
+
+    def get_provider(self) -> dict:
+        return {
+            "app_name": self.config.app_name,
+            "description": self.config.app_description,
+            "organization_name": self.config.organization_name
+        }
+
+    def post_register(self, nick: str, password: str) -> dict:
+        builder = user_builder()
+        builder.nick = nick
+        builder.set_password(password)
+
+        with user_loader(self.database) as loader:
+            if loader.register(builder.result):
+                return {
+                    "status": True
+                }
+
+            return {
+                "status": False
+            }
+
+    @property
+    def config(self) -> settings:
+        return self.__config
+
+    @property
+    def database(self) -> sqlalchemy.engine.base.Engine:
+        return self.__database
+
+    def __init_database(self, url: str) -> None:
+        self.__database = sqlmodel.create_engine(url)
+        sqlmodel.SQLModel.metadata.create_all(self.__database)

+ 216 - 0
assets/settings.py

@@ -0,0 +1,216 @@
+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
+

+ 1 - 0
config.json

@@ -0,0 +1 @@
+{}

+ 18 - 6
core.py

@@ -1,20 +1,32 @@
 import asyncio
+import pathlib
 import fastapi
 import fastapi.responses
 import fastapi.staticfiles
-import pathlib
 
-from assets import database
+from assets.application import application
+from assets.settings import settings
 
 core_directory = pathlib.Path(__file__).parent
+app_directory = core_directory / pathlib.Path("static/")
+app_files = fastapi.staticfiles.StaticFiles(directory = str(app_directory))
+
+config_file = core_directory / pathlib.Path("config.json")
+config = settings(config_file)
 
+app = application(config)
 api = fastapi.FastAPI()
-app_directory = fastapi.staticfiles.StaticFiles(
-    directory = str(core_directory) + "/static/"
-)
 
 @api.get("/")
 async def index():
     return fastapi.responses.RedirectResponse("/app/core.html")
 
-api.mount("/app/", app_directory, name = "app_static")
[email protected]("/provider")
+async def provider():
+    return fastapi.responses.JSONResponse(app.get_provider())
+
+api.mount("/app/", app_files, name = "app_files")
+
+if __name__ == "__main__":
+    import uvicorn
+    uvicorn.run(api, host = config.host, port = config.port)