Browse Source

Add config and users features.

Cixo Develop 6 months ago
parent
commit
813b24f202

+ 1 - 0
006-resources.py

@@ -0,0 +1 @@
+{"database_uri": "sqlite:///database.db", "users_file": "users.json"}

+ 12 - 1
assets/__init__.py

@@ -18,6 +18,8 @@ from .product_loader import product_loader
 
 """ Apps and routes. """
 from .app_route import app_route
+from .app_route import app_route_database
+from .users_app import users_app
 from .product_app import product_app
 
 """ User and user helpers. """
@@ -28,4 +30,13 @@ from .user import user_exporter
 
 """ User loaders and collections. """
 from .users_collection import users_collection
-from .users_loader import users_loader
+from .users_loader import users_loader
+
+""" Configs and loaders. """
+from .config import config
+from .config import config_loader
+from .config import config_generator
+
+""" App config and resources. """
+from .app_config import app_config
+from .app_resources import app_resources

+ 26 - 0
assets/app_config.py

@@ -0,0 +1,26 @@
+import pathlib
+
+from .config import config
+
+class app_config(config):
+    def __defaults() -> dict:
+        return {
+            "database_uri": "sqlite:///database.db",
+            "users_file": "users.json"
+        }
+
+    def __init__(self):
+        super().__init__(app_config.__defaults())
+
+    @property
+    def database_uri(self) -> str:
+        return self._get("database_uri")
+
+    @property
+    def users_file(self) -> str:
+        return self._get("users_file")
+
+    @property
+    def users_path(self) -> pathlib.Path:
+        return pathlib.Path(self.users_file)
+

+ 43 - 0
assets/app_resources.py

@@ -0,0 +1,43 @@
+import pathlib
+import sqlmodel
+import sqlalchemy
+import sqlalchemy.engine.base
+
+from .users_loader import users_loader
+from .users_collection import users_collection
+from .product_app import product_app
+from .users_app import users_app
+
+class app_resources:
+    def __init__(self, config: object) -> None:
+        self.__config = config
+        
+        self.__database = sqlmodel.create_engine(self.config.database_uri)
+        self.__users = users_loader(self.config.users_path).collection
+
+        sqlmodel.SQLModel.metadata.create_all(self.database)
+
+        self.__product_app = product_app(self.database, self.users)
+        self.__users_app = users_app(self.users)
+
+    @property
+    def users_app(self) -> users_app:
+        return self.__users_app
+
+    @property
+    def product_app(self) -> product_app:
+        return self.__product_app
+
+    @property 
+    def database(self) -> sqlalchemy.engine.base.Engine:
+        return self.__database
+
+    @property
+    def users(self) -> users_collection:
+        return self.__users
+    
+    @property
+    def config(self) -> object:
+        return self.__config
+
+    

+ 18 - 8
assets/app_route.py

@@ -1,13 +1,8 @@
 import sqlalchemy.engine.base
 
-class app_route:
-    def __init__(self, connection: sqlalchemy.engine.base.Engine) -> None:
-        self.__connection = connection
-
-    @property
-    def _connection(self) -> sqlalchemy.engine.base.Engine:
-        return self.__connection
+from .exception import incomplete_request_exception
 
+class app_route:
     def _success(self, **kwargs) -> dict:
         return self.__response("success", **kwargs)
 
@@ -16,7 +11,22 @@ class app_route:
 
         return self.__response("fail", cause)
 
+    def _require(self, sended: dict, *args) -> None:
+        for count in args:
+            if not count in args:
+                raise incomplete_request_exception(count)
+
     def __response(self, result: str, **kwargs) -> dict:
         kwargs["result"] = result
         
-        return kwargs
+        return kwargs
+
+class app_route_database(app_route):
+    def __init__(self, connection: sqlalchemy.engine.base.Engine) -> None:
+        self.__connection = connection
+
+    @property
+    def _connection(self) -> sqlalchemy.engine.base.Engine:
+        return self.__connection
+
+   

+ 121 - 0
assets/config.py

@@ -0,0 +1,121 @@
+import pathlib
+import json
+import json.decoder
+
+from .exception import config_exception
+from .app_resources import app_resources
+
+class config:
+    def __init__(self, defaults: dict) -> None:
+        self.__defaults = defaults.copy()
+        self.__config = defaults.copy()
+    
+    def _get(self, name: str) -> str | int | float | dict | list:
+        if not name in self.__config:
+            raise TypeError("Can not found " + name + " in config.")
+
+        return self.__config[name]
+
+    def __get_default(self, name: str) -> str | int | float | dict | list:
+        return self.__defaults[name]
+
+    @property
+    def defaults(self) -> dict:
+        return self.__defaults.copy()
+
+    def exists(self, name: str) -> bool:
+        return name in self.__defaults
+
+    def load(self, config: dict) -> object:
+        for key in config:
+            if not self.exists(key):
+                content = "When parsing config file found key " + key + " "
+                content = content + "which is not recognized. Correct it."
+
+                raise config_exception(content)
+
+            if type(config[key]) is not type(self.__get_default(key)):
+                required_type = type(self.__get_default).__name__
+                detected_type = type(config[key]).__name__
+
+                content = "When processing \"" + key + "\" key in config "
+                content = content + "detected types mismatch. It would be an "
+                content = content + required_type + ", but it is an " 
+                content = content + detected_type + "."
+
+                raise config_exception(content)
+
+            self.__config[key] = config[key]
+
+        return self
+
+    def __str__(self) -> str:
+        dump = "Config dump: \n"
+        
+        for count in self.defaults.keys():
+            key = "Key \"" + count + "\": "
+            key = key + "\"" + self._get(count) + "\", "
+            key = key + "default = \"" + self.__get_default(count) + "\""
+            dump = dump + key + "\n"
+
+        return dump
+
+class config_generator:
+    def __init__(self, target: type) -> None:
+        if target is config:
+            raise TypeError("Config is skeleton class for configs.")
+
+        self.__config = target().defaults
+        self.__json_config = json.dumps(self.__config)
+
+    def save(self, where: pathlib.Path) -> None:
+        if where.exists():
+            content = "Can not create config file \"" + str(where) + "\" "
+            content = content + "because it already exists in filesystem."
+
+            raise config_exception(content)
+
+        with where.open("w") as handler:
+            handler.write(self.__json_config)
+
+class config_loader:
+    def __init__(self, target: type) -> None:
+        if target is config:
+            raise TypeError("Config is skeleton class for configs.")
+
+        self.__type = target
+        self.__target = None
+
+    @property
+    def result(self) -> config:
+        if self.__target is None:
+            raise RuntimeError("Config is not loaded yet.")
+
+        return self.__target
+
+    @property
+    def resources(self) -> app_resources:
+        return app_resources(self.result)
+
+    def load(self, where: pathlib.Path) -> object:
+        if not where.is_file():
+            content = "Can not found required config file \""
+            content = content + where.absolute() + "\"."
+
+            raise config_exception(content)
+        
+        preparing = self.__type()
+
+        with where.open() as config:
+            try:
+                config_object = json.loads(config.read())
+                preparing.load(config_object)
+                self.__target = preparing
+
+            except json.decoder.JSONDecodeError as code_error:
+                content = "Can not load config file, syntax error: "
+                content = content + str(code_error) + "."
+
+                raise config_error(content)
+
+        return self

+ 12 - 1
assets/exception.py

@@ -28,4 +28,15 @@ class in_collection_exception(Exception):
     pass
 
 class config_exception(Exception):
-    pass
+    pass
+
+class access_denied_exception(Exception):
+    def __init__(self) -> None:
+        super().__init__("Being logged in is required to access this.")
+
+class incomplete_request_exception(Exception):
+    def __init__(self, name: str) -> None:
+        content = "Request is not complete. This endpoint require " 
+        content = content + "\"" + name + "\"."
+
+        super().__init__(content)

+ 33 - 2
assets/product_app.py

@@ -1,6 +1,8 @@
 import typing
+import sqlalchemy
+import sqlalchemy.engine.base
 
-from .app_route import app_route
+from .app_route import app_route_database
 from .product import product 
 from .product import product_factory
 from .product_loader import product_loader
@@ -8,8 +10,18 @@ from .product_response import product_response
 from .product_builder import product_builder
 from .exception import bad_request_exception
 from .exception import not_found_exception
+from .exception import access_denied_exception
+from .users_collection import users_collection
+
+class product_app(app_route_database):
+    def __init__(
+        self, 
+        connection: sqlalchemy.engine.base.Engine,
+        users: users_collection
+    ) -> None:
+        super().__init__(connection)
+        self.__users_collection = users
 
-class product_app(app_route):
     def all(self) -> dict:
         with self.__products_database as loader:
             return self.__collection(loader.load_all())
@@ -39,12 +51,21 @@ class product_app(app_route):
             return self.__exists(loader.name_in_use(target))
 
     def create(self, send: dict) -> dict:
+        if not self.__logged_in(send):
+            raise access_denied_exception()
+
         with self.__products_database as loader:
             target = product_builder().modify(send).result
             result = loader.store(target)
 
             return self.__modify(result, "Can nod create product.")
 
+    def __logged_in(self, send: dict) -> bool:
+        if not "apikey" in send:
+            return False
+
+        return self.__users.get(send["apikey"]) is not None
+
     def __select_by_sended(self, send: dict) -> product | None:
         barcode = None
         name = None
@@ -78,6 +99,9 @@ class product_app(app_route):
             return result
 
     def update(self, send: dict) -> dict:
+        if not self.__logged_in(send):
+            raise access_denied_exception()
+
         target = self.__select_by_sended(send)
         updated = product_builder(target).modify(send).result
         
@@ -87,6 +111,9 @@ class product_app(app_route):
             return self.__modify(result, "Can not update product.")
 
     def delete(self, send: dict) -> dict:
+        if not self.__logged_in(send):
+            raise access_denied_exception()
+
         target = self.__select_by_sended(send)
 
         with self.__product_database as loader:
@@ -112,6 +139,10 @@ class product_app(app_route):
     def __collection(self, target: typing.Iterable[product]) -> dict:
         return self._success(collection = product_response(target))
 
+    @property
+    def __users(self) -> users_collection:
+        return self.__users_collection
+
     @property
     def __products_database(self) -> product_loader:
         return product_loader(self._connection)

+ 40 - 0
assets/users_app.py

@@ -0,0 +1,40 @@
+from .user import user
+from .user import user_factory
+from .user import user_builder
+from .users_collection import users_collection
+from .app_route import app_route
+
+class users_app(app_route):
+    def __init__(self, collection: users_collection) -> None:
+        self.__collection = collection
+
+    def login(self, sended: dict) -> dict:
+        self._require(sended, "nick", "password")
+        
+        nick = sended["nick"]
+        password = sended["password"]
+        target = self.collection.login(nick, password)
+
+        if target is None:
+            return self._fail("Bad nick or password.")
+
+        return self._success(apikey = target.apikey)
+
+    def get(self, sended: dict) -> dict:
+        self._require(sended, "apikey")
+
+        apikey = sended["apikey"]
+        target = self.collection.get(apikey)
+
+        if target is None:
+            return self._fail("User with that ApiKey not exists.")
+
+        return self._success(
+            apikey = target.apikey,
+            nick = target.nick
+        )
+
+    @property
+    def collection(self) -> users_collection:
+        return self.__collection
+

+ 33 - 1
assets/users_collection.py

@@ -17,6 +17,15 @@ class users_collection:
         self.__by_apikey = dict()
         self.__by_nick = dict()
 
+    @property
+    def all(self) -> list:
+        """
+        This return all users in the collection, as list, like in the 
+        config file.
+        """
+
+        return list(self.__by_apikey.values())
+    
     def add(self, target: user) -> None:
         """
         This add new user to the collection. When ApiKey or login already
@@ -66,6 +75,29 @@ class users_collection:
 
         return target
 
+    def remove(self, target: user) -> None:
+        """
+        This function remove target user from collection.
+
+        Parameters:
+            target (user): Target user to remove from collection
+        """
+
+        exists = target.nick in self.__by_nick
+        exists = target.apikey in self.__by_apikey
+
+        if not exists:
+            raise in_collection_exception("Can not found required user.")
+
+        by_nick = self.__by_nick.copy()
+        by_apikey = self.__by_apikey.copy()
+
+        by_nick.pop(target.nick)
+        by_apikey.pop(target.apikey)
+
+        self.__by_nick = by_nick
+        self.__by_apikey = by_apikey
+
     def get(self, apikey: str) -> user | None:
         """
         This try to load user by ApiKey. When user with that ApiKey exists
@@ -82,4 +114,4 @@ class users_collection:
         if not apikey in self.__by_apikey:
             return None
 
-        return self.__by_apikey[apikey]
+        return self.__by_apikey[apikey]

+ 1 - 1
assets/users_loader.py

@@ -46,4 +46,4 @@ class users_loader:
     def collection(self) -> users_collection:
         """ Collections with the users. """
 
-        return self.__collection
+        return self.__collection

+ 87 - 0
assets/users_saver.py

@@ -0,0 +1,87 @@
+import json
+import pathlib
+
+from .user import user
+from .user import user_factory
+from .user import user_builder
+from .exception import config_exception
+from .users_collection import users_collection
+
+class users_saver:
+    """
+    This class is responsible for saving and storing user config file.
+    """
+
+    def __init__(self, collection: users_collection) -> None:
+        """
+        This initialize class, by set users collection.
+        
+        Parameters:
+            collection (users_collection): This is collection of users to save
+        """
+
+        self.__collection = collection
+        self.__where = None
+    
+    @property
+    def users(self) -> list:
+        """ This return list of users in setup collection. """
+
+        return self.__collection.all
+
+    def drop(self, where: pathlib.Path) -> object:
+        """
+        This drop config file, when it already exists. If given path is 
+        directory, raise config exception. Path is been saving to use it 
+        in save. When first call to drop, save after it could be call without
+        parameters.
+
+        Parameters:
+            where (pathlib.Path): Place where config file had been
+        
+        Returns:
+            (users_saver): Object instance itself
+        """
+
+        self.__where = where
+
+        if not where.exists():
+            return self
+
+        if where.is_dir():
+            content = "Can not remove users config, because target \""
+            content = content + str(where) + "\" is file."
+
+            raise config_exception(content)
+        
+        where.unlink()
+        return self
+    
+    def save(self, where: pathlib.Path | None = None) -> object:
+        """
+        This function save users collection as users config file. When place
+        is not provided, trying to load it from previous place, in drop. When
+        config file already exists, raise config.
+
+        Parameters:
+            where (pathlib.Path | None): Place to save config (default: None)
+
+        Returns:
+            (users_saver): Instance of this class itself
+        """
+
+        if where is None:
+            where = self.__where
+
+        if where is None:
+            raise RuntimeError("Where is not set.")
+
+        if where.exists():
+            content = "Config file \"" + str(where) + "\" already exists.")
+            
+            raise config_exception(content)
+
+        with where.open("w") as handle:
+            handle.write(json.dumps(self.users))
+
+        return self

+ 38 - 0
tests/005-config.py

@@ -0,0 +1,38 @@
+import pathlib
+
+current = pathlib.Path(__file__).parent
+root = current.parent
+
+import sys
+sys.path.append(str(root))
+
+import assets
+
+config_file = pathlib.Path("./005-test-config.json")
+
+if config_file.is_file():
+    config_file.unlink()
+
+print("Creating config file.")
+assets.config_generator(assets.app_config).save(config_file)
+print()
+
+print("Loading config file.")
+config = assets.config_loader(assets.app_config).load(config_file).result
+print()
+
+print("Result:")
+print(config)
+print()
+
+wait = input("Change config file, save it, and please enter: ")
+
+print("Loading from config file.")
+config_2 = assets.config_loader(assets.app_config).load(config_file).result
+print()
+
+print("Result:")
+print(config_2)
+print()
+
+config_file.unlink()

+ 19 - 0
tests/006-resources.py

@@ -0,0 +1,19 @@
+import pathlib
+
+current = pathlib.Path(__file__).parent
+root = current.parent
+
+import sys
+sys.path.append(str(root))
+
+import assets
+
+config = pathlib.Path("./006-resources.py")
+
+if config.is_file():
+    config.unlink()
+
+assets.config_generator(assets.app_config).save(config)
+app = assets.config_loader(assets.app_config).load(config).resources
+
+config.unlink()