ソースを参照

Add validators, exceptions and database.

Cixo Develop 6 ヶ月 前
コミット
ebe5c10ced

+ 2 - 0
assets/__init__.py

@@ -3,6 +3,7 @@ from .validator import validator
 from .validator import name_validator
 from .validator import description_validator
 from .validator import barcode_validator
+from .validator import author_validator
 
 """ Exceptions. """
 from .exception import validator_exception
@@ -10,3 +11,4 @@ from .exception import validator_exception
 """ Product and helpers. """
 from .product import product
 from .product import product_builder
+from .product_loader import product_loader

+ 0 - 0
assets/app_route.py


+ 5 - 1
assets/exception.py

@@ -1,3 +1,7 @@
 class validator_exception(Exception):
     def __init__(self, name: str):
-        super("Invalid property names: " + name + ".")
+        super().__init__("Invalid property names: " + name + ".")
+
+class exists_exception(Exception):
+    def __init__(self, name: str):
+        super().__init__("Item with this " + name + " already in database.")

+ 82 - 6
assets/product.py

@@ -3,6 +3,8 @@ import sqlmodel
 from .exception import validator_exception
 from .validator import name_validator
 from .validator import description_validator
+from .validator import barcode_validator
+from .validator import author_validator
 
 class product(sqlmodel.SQLModel, table = True):
     """ 
@@ -29,7 +31,7 @@ class product(sqlmodel.SQLModel, table = True):
     stock_count: int = sqlmodel.Field(default = 0)
 
     """ This is barcode (EAN) of the product. """
-    badcode: str | None = sqlmodel.Field(
+    barcode: str | None = sqlmodel.Field(
         default = None, 
         index = True, 
         unique = True
@@ -41,6 +43,12 @@ class product(sqlmodel.SQLModel, table = True):
 
         return self.id is not None
 
+    @property
+    def avairable(self) -> bool:
+        """ This check that item is avairable on stock. """
+
+        return self.stock_count > 0
+
     def __str__(self) -> str:
         """
         This dump product to string, which is helpfull when debug.
@@ -49,7 +57,12 @@ class product(sqlmodel.SQLModel, table = True):
             (str): Product as string dump
         """
 
-        content = str(self.name) + " " + str(self.id or "") + "\n"
+        content = str(self.name) + " "
+
+        if self.in_database:
+            content = content + "#" + str(self.id)
+
+        content = content + "\n"
         content = content + "Description: " + str(self.description) + "\n"
         content = content + "Author: " + str(self.author) + "\n"
         content = content + "Barcode (EAN): " + str(self.barcode) + "\n"
@@ -62,10 +75,10 @@ class product(sqlmodel.SQLModel, table = True):
     def ready(self) -> bool:
         """ Check that product is ready to insert into database. """
 
-        if name is None or description is None: 
+        if self.name is None or self.description is None: 
             return False
 
-        if barcode is None or author is None:
+        if self.barcode is None or self.author is None:
             return False
 
         return True
@@ -128,6 +141,27 @@ class product_builder:
 
         return self.__target.description
 
+    def add_to_stock(self) -> None:
+        """
+        This add one item to stock.
+        """
+
+        self.__target.stock_count += 1
+
+    def get_from_stock(self) -> bool:
+        """
+        This get single item from stock.
+
+        Returns:
+            (bool): True when get success, false when not avairable        
+        """
+
+        if not self.__target.avairable:
+            return False
+
+        self.__target.stock_count -= 1
+        return True
+
     @description.setter
     def description(self, target: str) -> None:
         """ This set description of the product, and validate it."""
@@ -141,7 +175,7 @@ class product_builder:
     def barcode(self) -> str | None:
         """ It return barcode of building product. """
 
-        return self.__target.badcode
+        return self.__target.barcode
 
     @barcode.setter
     def barcode(self, target: str) -> None:
@@ -150,4 +184,46 @@ class product_builder:
         if barcode_validator(target).invalid:
             raise validator_exception("product.barcode")
 
-        self.__target.badcode = barcode
+        self.__target.barcode = target
+
+    @property
+    def author(self) -> str | None:
+        """ This return author of the product. """
+
+        return self.__target.author
+
+    @author.setter
+    def author(self, target: str) -> None:
+        """ This validate author of the product, and set it. """
+
+        if author_validator(target).invalid:
+            raise validator_exception("product.author")
+
+        self.__target.author = target
+
+    @property
+    def image(self) -> str | None:
+        """ This return image link of the product. """
+
+        return self.__target.image
+
+    @image.setter
+    def image(self, target: str | None) -> None:
+        """ This set image link of the product. """
+
+        self.__target.image
+
+    @property
+    def stock_count(self) -> int:
+        """ This return how much product is in stock. """
+
+        return self.__target.stock_count
+
+    @stock_count.setter
+    def stock_count(self, target: int) -> None:
+        """ This set stock count of the product, it mut be positive number. """
+
+        if target < 0:
+            raise validator_exception("product.stock_count")
+
+        self.__target.stock_count = target

+ 0 - 0
assets/product_app.py


+ 240 - 0
assets/product_loader.py

@@ -0,0 +1,240 @@
+import typing
+import sqlmodel
+import sqlmodel.sql._expression_select_cls
+
+from .product import product
+from .product import product_builder
+from .validator import barcode_validator
+from .validator import name_validator
+from .validator import description_validator
+from .validator import author_validator
+from .exception import validator_exception
+
+class product_loader(sqlmodel.Session):
+    """
+    This is loader for product. It help in work with product database, and
+    manage of the product of course.
+    """
+
+    def get_by_barcode(self, target: str) -> product | None:
+        """
+        It load product by barcode. When product with given barcode not 
+        exists, return None. It also validate barcode, and when it is not 
+        property, raise validation exception.
+
+        Parameters:
+            target (str): Barcode of product to load
+
+        Returns:
+            (product): Product with given barcode from database
+            (None): When product not exists
+        """
+
+        if barcode_validator(target).invalid:
+            raise validator_exception("get.barcode")
+
+        query = self.__select.where(product.barcode == target)
+        result = self.exec(query)
+
+        return result.first()
+
+    def get_by_name(self, target: str) -> product | None:
+        """
+        It load product by name. When product with given name not exists,
+        returns None. It also validate name, and if it is not correct,
+        raise validation_exception.
+
+        Parameters:
+            target (str): Name of the product to load
+
+        Returns:
+            (product): Product with given name 
+            (None): When product with that name not exists
+        """
+
+        if name_validator(target).invalid:
+            raise validator_exception("get.name")
+
+        query = self.__select.where(product.name == target)
+        result = self.exec(query)
+
+        return result.first()
+
+    def search_by_name(self, target: str) -> typing.Iterable[product]:
+        """
+        This search by name. To start searching phrase with could be valid
+        name must be given, that mean it could not be too short or too long.
+        If phrase is not valid, then validator_exception is raised.
+
+        Parameters:
+            target (str): Phrase to search in names
+
+        Returns:
+            (Iterable[product]): Generator with products found by name
+        """
+        
+        if name_validator(target).invalid:
+            raise validator_exception("search.name")
+
+        target = "%" + target + "%"
+        query = self.__select.where(product.name.like(target))
+        result = self.exec(query)
+
+        for item in result:
+            yield item
+
+    def search_by_author(self, target: str) -> typing.Iterable[product]:
+        """
+        This search products by author. Author must be valid, when it is not 
+        then validation_exception would be raised.
+
+        Parameters:
+            target (str): Author to search product for
+
+        Returns:
+            (Iterable[product]): Generator with products with author
+        """
+        
+        if author_validator(target).invalid:
+            raise validator_exception("search.author")
+
+        target = "%" + target + "%"
+        query = self.__select.where(product.author.like(target))
+        result = self.exec(query)
+
+        for item in result:
+            yield item
+
+    def load_all(self) -> typing.Iterable[product]:
+        """
+        This load all products from database.
+
+        Returns:
+            (Iterable[produdct]): Generator with products from database
+        """
+
+        query = self.__select
+        result = self.exec(query)
+
+        for item in result:
+            yield item
+
+    def barcode_in_use(self, target: str) -> bool:
+        """ 
+        It check that barcode is already in use by any product.
+
+        Parameters:
+            target (str): Barcode to check
+
+        Returns:
+            (bool): True when item exists, false when not
+        """
+
+        return self.get_by_barcode(target) is not None
+
+    def name_in_use(self, target: str) -> bool:
+        """
+        It check that name is already in use by any element.
+
+        Parameters:
+            target (str): Name to check that is in use
+
+        Returns:
+            (bool): True when name in use, False when not    
+        """
+
+        return self.get_by_name(target) is not None
+
+    def __append(self, target: product) -> bool:
+        """
+        This insert new product into database, when it is not stored yet. It
+        check that item is ready, also barcode and name can not be in use.
+
+        Parameters:
+            target (product): Product to insert into database
+
+        Returns:
+            (bool): True when success, False when not        
+        """
+
+        if self.barcode_in_use(target.barcode):
+            exists_exception("barcode")
+        
+        if self.name_in_use(target.name):
+            exists_exception("name")
+
+        self.add(target)
+        self.commit()
+        self.refresh(target)
+
+        return True
+
+    def store(self, target: product) -> bool:
+        """
+        This save product into database. When product is not in database yet,
+        then it inserting. If product is in database, then updating it. It 
+        check that new barcode and name is not already in use by another 
+        product.
+
+        Parameters: 
+            target (product): Product to save or insert
+
+        Returns:    
+            (bool): True when save success, False when not
+        """
+
+        if not target.ready:
+            return False
+
+        if not target.in_database:
+            return self.__append(target)
+
+        name_use = self.get_by_name(target.name)
+
+        if name_use is not None and name_use.id != target.id:
+            return False
+
+        barcode_use = self.get_by_barcode(target.barcode)
+
+        if barcode_use is not None and barcode_use.id != target.id:
+            return False
+
+        try:
+            self.add(target)
+            self.commit()
+            self.refresh(target)
+
+            return True
+        
+        except:
+            return False
+
+    def drop(self, target: product) -> bool:
+        """
+        This remove product from database. Product must exists, to delete 
+        it from storage.
+
+        Parameters: 
+            target (product): Item to delete
+
+        Returns:
+            (bool): True when deleted successfull, False if not
+        """
+
+        if not target.in_database:
+            return False
+
+        try:
+            self.delete(target)
+            self.commit()
+
+            return True
+
+        except:
+            return False
+
+    @property
+    def __select(self) -> sqlmodel.sql._expression_select_cls.SelectOfScalar:
+        """ New product selector. """
+        
+        return sqlmodel.select(product)

+ 27 - 3
assets/validator.py

@@ -77,13 +77,18 @@ class validator:
 class name_validator(validator):
     """
     This is validator for product name. It check that it is not blank,
-    and it not contain white chars before and after content.
+    and it not contain white chars before and after content. It must have 
+    more or qeual of 4 and less than 40 characters.
     """
 
     def _check_all(self) -> None:
 
-        # Could not being blank
-        if len(self.target.strip()) == 0:
+        # Minimum 4 characters
+        if len(self.target) < 4:
+            return self._failed()
+
+        # Maximum 40 characters
+        if len(self.target) > 40:
             return self._failed()
 
         # Must be trimed
@@ -129,3 +134,22 @@ class barcode_validator(validator):
         if lenght != 8 and lenght != 12 and lenght != 13:
             return self._failed()
 
+class author_validator(validator):
+    """
+    This validate author name. Author can not have less than 3 characters,
+    and can not have more than 40 characters. It also must be trimmed.
+    """
+
+    def _check_all(self) -> None:
+
+        # Must be trimmed
+        if len(self.target) != len(self.target.strip()):
+            return self._failed()
+
+        # Must have more than 3 characters
+        if len(self.target) < 3:
+            return self._failed()
+
+        # Must has max 40 characters
+        if len(self.target) > 40:
+            return self._failed()

+ 84 - 0
tests/002-product.py

@@ -0,0 +1,84 @@
+import pathlib
+
+current = pathlib.Path(__file__).parent
+root = current.parent
+
+import sys
+sys.path.append(str(root))
+
+import assets
+import sqlmodel
+
+def drop_database() -> None:
+    db = pathlib.Path("./002-product.db")
+
+    if db.is_file():
+        db.unlink()
+
+drop_database()
+
+connection = sqlmodel.create_engine("sqlite:///002-product.db")
+sqlmodel.SQLModel.metadata.create_all(connection)
+
+builder = assets.product_builder()
+builder.name = "Sample name"
+builder.author = "Simple UwU Artist"
+builder.description = "This is simple description"
+builder.barcode = "123456789012"
+
+product = builder.result
+
+print("Create first product:")
+print(product)
+print()
+
+with assets.product_loader(connection) as loader:
+    print("Inserting product...")
+    loader.store(product)
+
+    print("Inserted:")
+    print(product)
+    print()
+
+with assets.product_loader(connection) as loader:
+    print("Get by name:")
+    print(loader.get_by_name("Sample name"))
+    print()
+
+    print("Get by barcode:")
+    print(loader.get_by_barcode("123456789012"))
+    print()
+
+with assets.product_loader(connection) as loader:
+    print("Updating:")
+    
+    builder = assets.product_builder(loader.get_by_barcode("123456789012"))
+    builder.stock_count = 200
+    product = builder.result
+    loader.store(product)
+
+    print("After update:")
+    print(loader.get_by_barcode("123456789012"))
+    print()
+
+with assets.product_loader(connection) as loader:
+    print("Get from stock:")
+    
+    builder = assets.product_builder(loader.get_by_barcode("123456789012"))
+    builder.get_from_stock()
+
+    product = builder.result
+    loader.store(product)
+
+    print(loader.get_by_barcode("123456789012"))
+    print()
+
+with assets.product_loader(connection) as loader:
+    print("Delete from database:")
+
+    loader.drop(product)
+    
+    print(loader.get_by_barcode("123456789012"))
+    print()
+
+drop_database()