Преглед на файлове

Add user, user helper and documentary to it.

Cixo Develop преди 6 месеца
родител
ревизия
f38f3cb945
променени са 8 файла, в които са добавени 577 реда и са изтрити 0 реда
  1. 13 0
      assets/__init__.py
  2. 29 0
      assets/apikey.py
  3. 6 0
      assets/exception.py
  4. 283 0
      assets/user.py
  5. 85 0
      assets/users_collection.py
  6. 49 0
      assets/users_loader.py
  7. 61 0
      assets/validator.py
  8. 51 0
      tests/004-user.py

+ 13 - 0
assets/__init__.py

@@ -4,6 +4,9 @@ from .validator import name_validator
 from .validator import description_validator
 from .validator import barcode_validator
 from .validator import author_validator
+from .validator import nick_validator
+from .validator import password_validator
+from .validator import apikey_validator
 
 """ Exceptions. """
 from .exception import validator_exception
@@ -16,3 +19,13 @@ from .product_loader import product_loader
 """ Apps and routes. """
 from .app_route import app_route
 from .product_app import product_app
+
+""" User and user helpers. """
+from .user import user
+from .user import user_factory
+from .user import user_builder
+from .user import user_exporter
+
+""" User loaders and collections. """
+from .users_collection import users_collection
+from .users_loader import users_loader

+ 29 - 0
assets/apikey.py

@@ -0,0 +1,29 @@
+import os
+
+class apikey_generator:
+    """
+    This is ApiKey generator.
+    """
+
+    def __new__(cls) -> str:
+        """
+        It create new ApiKey, and return it as string.
+        
+        Returns:
+            (str): New random ApiKey 
+        """
+
+        random = os.urandom(cls.size())
+        string = random.hex()
+
+        return string
+
+    def size() -> int:
+        """
+        It return size of ApiKey in bytes.
+
+        Returns:
+            (int): Size of ApiKey in bytes
+        """
+
+        return 128

+ 6 - 0
assets/exception.py

@@ -23,3 +23,9 @@ class not_ready_exception(Exception):
         info = info + "Dump:\n" + what + "\n\n"
 
         super().__init__(info)
+
+class in_collection_exception(Exception):
+    pass
+
+class config_exception(Exception):
+    pass

+ 283 - 0
assets/user.py

@@ -0,0 +1,283 @@
+from .apikey import apikey_generator
+from .exception import config_exception
+from .exception import not_ready_exception
+from .exception import validator_exception
+from .validator import nick_validator
+from .validator import apikey_validator
+from .validator import password_validator
+
+class user:
+    """
+    This represents user in the system. It would be stored in the config 
+    file. Creating new must be done by builders, which checks content with
+    validators. User class is immutable.
+    """
+
+    def __init__(self, nick: str, password: str, apikey: str) -> None:
+        """
+        This create new user, and require all parameters of it.
+
+        Parameters:
+            nick (str): Nick of the user
+            password (str): Password of the user
+            apikey (str): ApiKey of the user
+        """
+
+        self.__nick = nick
+        self.__password = password
+        self.__apikey = apikey
+        self.__result = None
+
+    @property
+    def nick(self) -> str:
+        """ It returns nick of the user. """
+
+        return self.__nick
+
+    @property
+    def password(self) -> str:
+        """ It returns password of the user. """
+
+        return self.__password
+
+    @property
+    def apikey(self) -> str:
+        """ It returns apikey of the user. """
+
+        return self.__apikey
+
+    def __str__(self) -> str:
+        """ 
+        This create dump of the user.
+
+        Returns:
+            (str): Dump of the user
+        """
+
+        target = "User:\n"
+        target = target + "Nick: \"" + str(self.nick) + "\"\n"
+        target = target + "Password: \"" + str(self.password) + "\"\n"
+        target = target + "ApiKey: \"" + str(self.apikey) + "\"\n"
+
+        return target
+
+class user_factory:
+    """ 
+    This class is responsible for generating new users. It make avairable
+    creating user step by step, and it is responsible for generating ApiKey.
+    """
+
+    def __init__(self, target: user | None = None) -> None:
+        """
+        This create new user factory. When user had been provided, then it is
+        used to initialize nick, ApiKey and password of the factory.
+
+        Parameters
+            target (user | None): User to initialize factory (default = None)
+        """
+
+        self.__nick = None
+        self.__password = None
+        self.__apikey = None
+        self.__result = None
+
+        if target is not None:
+            self.__nick = target.nick
+            self.__password = target.password
+            self.__apikey = target.apikey
+
+    @property
+    def apikey(self) -> str:
+        """ This return factory ApiKey. """
+
+        if self.__apikey is None:
+            self.__apikey = apikey_generator()
+
+        return self.__apikey
+
+    @property
+    def nick(self) -> str | None:
+        """ This return nick of the factory. """
+
+        return self.__nick
+
+    @property
+    def password(self) -> str | None:
+        """ This return password of the factory. """
+
+        return self.__password
+
+    @nick.setter
+    def nick(self, target: str) -> None:
+        """ This set new nick, and validate it. """
+
+        if nick_validator(target).invalid:
+            raise validator_exception("user.nick")
+
+        self.__nick = target
+
+    @password.setter
+    def password(self, target: str) -> None:
+        """ This set new password, and validate it. """
+
+        if password_validator(target).invalid:
+            raise validator_exception("user.password")
+
+        self.__password = target
+
+    @property
+    def ready(self) -> bool:
+        """ This check that factory is ready or not. """
+
+        if self.__nick is None:
+            return False
+
+        if self.__password is None:
+            return False
+
+        return True
+
+    @property
+    def result(self) -> user:
+        """ This create new user. Factory must be ready, to do it. """
+
+        if not self.ready:
+            raise not_ready_exception(self)
+
+        if self.__result is not None:
+            return self.__result
+
+        self.__result = user(
+            self.nick,
+            self.password,
+            self.apikey
+        )
+
+        return self.__result
+
+class user_builder:
+    """
+    This create new user builder. User builder is responsble for loading users
+    from dicts, which would be provided from config file.
+    """
+
+    def __init__(self, target: dict):
+        """
+        This create new user builder.
+
+        Parameters:
+            target (dict): Target dict to load user from
+        """
+
+        self.__target = target
+        self.__result = None
+
+    @property
+    def nick(self) -> str:
+        """ This return nick from dict. """
+
+        if not "nick" in self.__target:
+            return ""
+
+        return self.__target["nick"]
+
+    @property
+    def password(self) -> str:
+        """ This return password from dict. """
+
+        if not "password" in self.__target:
+            return ""
+        
+        return self.__target["password"]
+
+    @property
+    def apikey(self) -> str:
+        """ This return ApiKey from dict. """
+
+        if not "apikey" in self.__target:
+            return ""
+
+        return self.__target["apikey"]
+
+    def check(self) -> None | str:
+        """
+        This validate all properties, nick, password and ApiKey. All must 
+        be valid, if any is not valid or is not provided, then it return
+        error. When all is valid, return None, when anything is wrong, 
+        then return it as string.
+        
+        Returns:
+            (None): When all is valid
+            (str): Content of the error
+        """
+
+        if not "nick" in self.__target:
+            return "User not contain nick."
+
+        if nick_validator(self.nick).invalid:
+            return "User nick \"" + self.nick + "\" is invalid."
+
+        if not "password" in self.__target:
+            return self.nick + " not contain password."
+
+        if password_validator(self.password).invalid:
+            return self.nick + " has invalid password."
+
+        if not "apikey" in self.__target:
+            return self.nick + " not contain apikey."
+
+        if apikey_validator(self.apikey).invalid:
+            return self.nick + " has invalid apikey."
+
+        return None
+
+    @property
+    def result(self) -> user:
+        """ Ready user form dict. """
+
+        if self.__result is not None:
+            return self.__result
+
+        check = self.check()
+
+        if check is not None:
+            raise config_exception("User config contain error. " + check)
+
+        self.__result = user(
+            self.nick,
+            self.password,
+            self.apikey
+        )
+
+        return self.__result
+
+class user_exporter:
+    """
+    This export user to dict, which could be safe into config file.
+    """
+
+    def __init__(self, target: user) -> None:
+        """
+        This initialize new exporter.
+        
+        Parameters:
+            target (user): Target user to export
+        """
+
+        self.__target = target
+
+    @property
+    def target(self) -> user:
+        """ Target user to export. """
+
+        return self.__target
+
+    @property
+    def result(self) -> dict:
+        """ Exported user as dict. """
+        
+        return {
+            "nick": self.target.nick,
+            "password": self.target.password,
+            "apikey": self.target.apikey
+        }

+ 85 - 0
assets/users_collection.py

@@ -0,0 +1,85 @@
+from .user import user
+from .user import user_builder
+from .user import user_factory
+from .exception import in_collection_exception
+
+class users_collection:
+    """
+    This is responsible for managing users in the system. It could login new 
+    users, or load them by ApiKey.
+    """
+
+    def __init__(self):
+        """
+        This create new collection.
+        """
+
+        self.__by_apikey = dict()
+        self.__by_nick = dict()
+
+    def add(self, target: user) -> None:
+        """
+        This add new user to the collection. When ApiKey or login already
+        exists in the collection, then error had been raised.
+
+        Parameters:
+            target (user): New user to add
+        """
+
+        if target.apikey in self.__by_apikey:
+            error = "User with ApiKey \"" + target.apikey + "\" "
+            error = error + "already in collection."
+
+            raise in_collection_exception(error)
+
+        if target.nick in self.__by_nick:
+            error = "User with nick \"" + target.nick + "\" "
+            error = error + "already in collection."
+
+            raise in_collection_exception(error)
+
+        self.__by_apikey[target.apikey] = target
+        self.__by_nick[target.nick] = target
+
+    def login(self, nick: str, password: str) -> user | None:
+        """
+        This try to login user by nick and password. When nick or password 
+        had been wrong, then None had been returned. When nick and password
+        are correct, then return that user.
+
+        Parameters:
+            nick (str): Nick of the user
+            password (str): Password of the user
+
+        Returns:
+            (user): User with that nick and password, if exists
+            (None): When user with that nick and password not exists
+        """
+
+        if not nick in self.__by_nick:
+            return None
+
+        target = self.__by_nick[nick]
+
+        if target.password != password:
+            return None
+
+        return target
+
+    def get(self, apikey: str) -> user | None:
+        """
+        This try to load user by ApiKey. When user with that ApiKey exists
+        then return it. When not, return None.
+
+        Parameters:
+            apikey (str): ApiKey of the user to load
+
+        Returns:
+            (user): User with that ApiKey
+            (None): When user with that ApiKey not exists
+        """
+
+        if not apikey in self.__by_apikey:
+            return None
+
+        return self.__by_apikey[apikey]

+ 49 - 0
assets/users_loader.py

@@ -0,0 +1,49 @@
+import json
+import pathlib
+
+from .user import user
+from .user import user_factory
+from .user import user_builder
+from .users_collection import users_collection
+from .exception import config_exception
+
+class users_loader:
+    """
+    This is responsible for loading users from the config file. It create 
+    collection with all of the users from the file.
+    """
+
+    def __init__(self, target: pathlib.Path) -> None:
+        """
+        This create new loader. It get file as pathlib location, to open it
+        and load. File must be a JSON, with array of the users object.
+
+        Parameters:
+            target (Path): Path to JSON file with users
+        """
+
+        self.__collection = users_collection()
+        
+        if not target.is_file():
+            error = "Users config file \"" + str(target) + "\" not exists."
+            raise config_exception(error)
+
+        with target.open() as handler:
+            self.__parse(json.loads(handler.read()))
+
+    def __parse(self, target: list) -> None:
+        """
+        This parse array from JSON, as users.
+
+        Parameters:
+            target (list): List of users dicts from JSON
+        """
+
+        for count in target:
+            self.collection.add(user_builder(count).result)
+
+    @property
+    def collection(self) -> users_collection:
+        """ Collections with the users. """
+
+        return self.__collection

+ 61 - 0
assets/validator.py

@@ -1,3 +1,5 @@
+from .apikey import apikey_generator
+
 class validator:
     """
     This is base validator class. It implements base validator mechanism, 
@@ -74,6 +76,65 @@ class validator:
 
         return self.result
 
+class nick_validator(validator):
+    """
+    This validate nick. It is slow, and best to use only when loading
+    users from file.
+    """
+
+    def _check_all(self) -> None:
+        
+        # Minimum 4 characters
+        if len(self.target) < 4:
+            return self._failed()
+
+        # Maximum 30 characters
+        if len(self.target) > 30:
+            return self._failed()
+
+        # Could contain only 
+        for letter in self.target:
+            
+            # Letters
+            if letter.isalpha():
+                continue
+
+            # Numbers
+            if letter.isnumeric():
+                continue
+
+            return self._failed()
+
+class password_validator(validator):
+    """
+    This validate password. It is slow, and best to use only when
+    loading users from file.
+    """
+
+    def _check_all(self) -> None:
+
+        # Minimum 8 characters
+        if len(self.target) < 8:
+            return self._failed()
+
+        # Maximum 40 characters
+        if len(self.target) > 40:
+            return self._failed()
+
+        # Can not contain white chars
+        for letter in self.target:
+            if letter.isspace():
+                return self._failed()
+
+class apikey_validator(validator):
+    """ This is simple ApiKey validator """
+
+    def _check_all(self) -> None:
+        
+        # ApiKey must contain proof size 
+        if len(self.target) != apikey_generator.size() * 2:
+            return self._failed()
+
 class name_validator(validator):
     """
     This is validator for product name. It check that it is not blank,

+ 51 - 0
tests/004-user.py

@@ -0,0 +1,51 @@
+import pathlib
+
+current = pathlib.Path(__file__).parent
+root = current.parent
+
+import sys
+sys.path.append(str(root))
+
+import assets
+
+user_1 = assets.user("user", "password1", "APIKEY")
+
+print("Sample user dump:")
+print(user_1)
+print()
+
+user_factory = assets.user_factory()
+user_factory.nick = "nick1"
+user_factory.password = "password_a"
+
+print("User from factory:")
+print(user_factory.result)
+print()
+
+export = assets.user_exporter(user_factory.result).result
+
+print("User export:")
+print(export)
+print()
+
+builder = assets.user_builder(export)
+
+print("User builded from export:")
+print(builder.result)
+print()
+
+collection = assets.users_collection()
+
+collection.add(user_1)
+collection.add(user_factory.result)
+
+print("User collection test:")
+print("Login:")
+print(collection.login("user", "password1"))
+print(collection.login("nick1", "password_a"))
+print("Get:")
+print(collection.get(user_factory.result.apikey))
+print("Get not found:")
+print(collection.get("not_found"))
+print("Login not found:")
+print(collection.login("user", "pask"))