Browse Source

Sync with work.

Cixo Develop 1 month ago
parent
commit
77094df2ef

+ 9 - 4
server_source/__init__.py

@@ -18,19 +18,24 @@ from .category import category
 from .category import category_manager
 
 from .author import author
+
 from .author import author_proxy
 
 from .product_type import product_type
-from .product_type import product_type_proxy
+from .product_type import product_type_manager
 
 from .attachment import attachment 
 from .attachment import attachment_proxy
-from .attachment_file import attachment_file
+from .aleatory_file_name import aleatory_file_name
 from .attachments_manager import attachments_manager
+from .attachments_directory import attachments_directory
 
-from .item import item
+from .container_blob import container_blob
+from .container_blob import decoded
+from .container_blob import encoded
 
-from .aleatory_file_name import aleatory_file_name
+from .item import item
+from .item import item_proxy
 
 from .config_exceptions import config_exception
 from .config_exceptions import key_not_implemented

+ 0 - 53
server_source/attachment_file.py

@@ -1,53 +0,0 @@
-import pathlib 
-import aiofiles
-
-from .attachment import attachment
-from .attachment import attachment_proxy
-from .aleatory_file_name import aleatory_file_name
-
-class attachment_file:
-    @classmethod
-    def create(cls, directory: pathlib.Path, extension: str) -> object:
-        while True:
-            new_name = aleatory_file_name.path(extension)
-            new_path = directory / new_name
-
-            if not new_path.exists() and not new_path.is_file():
-                return cls(new_name, directory)
-    
-    def __init__(
-        self, 
-        file_name: pathlib.Path, 
-        directory: pathlib.Path
-    ) -> None:
-        self.__file_name = file_name
-        self.__directory = directory
-        self.__file_path = directory / file_name
-
-    @property
-    def path(self) -> pathlib.Path:
-        return self.__file_path
-
-    @property
-    def directory(self) -> pathlib.Path:
-        return self.__directory
-
-    @property
-    def name(self) -> str:
-        return str(self.__file_name)
-
-    @property
-    def relative_path(self) -> pathlib.Path:
-        return self.__file_name
-
-    async def store(self, content: bytes) -> attachment_proxy:
-        async with aiofiles.open(self.path, "wb") as handler:
-            await handler.write(content)
-
-        return attachment_proxy.create(self.name)
-
-    async def load(self) -> bytes:
-        async with aiofiles.open(self.path, "rb") as handler:
-            return await handler.read()
-        
-        

+ 64 - 0
server_source/attachments_directory.py

@@ -0,0 +1,64 @@
+import asyncio
+import pathlib
+import aiofiles
+
+from .attachment import attachment
+from .attachment import attachment_proxy
+from .aleatory_file_name import aleatory_file_name
+
+class attachments_directory:    
+    def __init__(self, directory: pathlib.Path) -> None:
+        if not directory.exists() or not directory.is_dir():
+            raise RuntimeError("Directory for attachments not exists.")
+
+        self.__directory = directory
+
+    async def store(self, content: bytes, extension: str) -> attachment_proxy:
+        name = await self.__get_new_name(extension)
+        path = self.directory / pathlib.Path(name)
+
+        async with aiofiles.open(path, "wb") as handler:
+            await handler.write(content)
+
+        return attachment_proxy.create(name)
+
+    async def change(self, file: attachment, content: bytes) -> bytes:
+        before = await self.load(file)
+        path = self.directory / file.resources_path
+        
+        async with aiofiles(path, "wb") as handler:
+            handler.write(content)
+    
+        return before
+
+    async def load(self, file: attachment) -> bytes:   
+        path = self.directory / file.resources_path
+
+        async with aiofiles.open(path, "rb") as handler:
+            return await handler.read()
+
+    async def remove(self, file: attachment) -> bytes:
+        path = self.directory / file.resources_path
+        content = await self.load(file)
+
+        path.unlink()
+        return content
+
+    async def __get_new_name(self, extension: str) -> str:
+        return await asyncio.to_thread(
+            self.__get_new_name_loop, 
+            extension
+        )
+
+    def __get_new_name_loop(self, extension: str) -> str:
+        while True:
+            new_name = aleatory_file_name(extension)
+            new_path = self.directory / pathlib.Path(new_name)
+
+            if not new_path.exists():   
+                return new_name
+
+    @property
+    def directory(self) -> pathlib.Path:
+        return self.__directory
+

+ 70 - 29
server_source/attachments_manager.py

@@ -1,41 +1,82 @@
-import base64
-import pathlib
-import asyncio
+import tortoise.transactions
 
+from .container_blob import encoded 
 from .attachment import attachment
 from .attachment import attachment_proxy
-from .attachment_file import attachment_file
-from .exceptions import resources_directory_not_exists
+from .attachments_directory import attachments_directory
+from .exceptions import id_is_invalid
+from .exceptions import not_found
+from .validators import validators
 
 class attachments_manager:
-    def __init__(
+    def __init__(self, directory: attachments_directory) -> None:
+        self.__directory = directory
+
+    @property
+    def directory(self) -> attachments_directory:
+        return self.__directory
+
+    async def get_all(self) -> tuple:
+        items = await attachment.all()
+        return tuple(items)
+
+    async def upload(
         self, 
-        resources: pathlib.Path, 
-        init_directory: bool = False
-    ) -> None:
-        if init_directory:
-            resources.mkdir()
-            
-        if not resources.is_dir() or not resources.exists():
-            raise resources_directory_not_exists(resources)
+        content: str, 
+        extension: str, 
+        name: str, 
+        description: str
+    ) -> int:
+        decoded = await encoded(content).decode()
+        decoded = decoded.result()
 
-        self.__resources = resources
+        proxy = await self.directory.store(decoded, extension)
+        proxy.set_name(name)
+        proxy.set_description(description)
 
-    @property
-    def resources(self) -> pathlib.Path:
-        return self.__resources
+        created = proxy.result()
+
+        async with tortoise.transactions.in_transaction():
+            await created.save()
+
+        return created.get_identifier()   
+
+    async def edit(
+        self, 
+        id: int,
+        name: str,
+        description: str
+    ) -> int:
+        async with tortoise.transactions.in_transaction():
+            target = await self.require_by_id(id)
+        
+            proxy = attachment_proxy(target)
+            proxy.set_name(name)
+            proxy.set_description(description)
+
+            result = proxy.result()
+            await result.save()
+
+            return result.get_identifier()
+
+    async def remove(self, id: int) -> None:
+        target = await self.require_by_id(id)
+        
+        items = await target.get_related("item", "attachments")
+       
+        await target.delete()
+
+    async def get_by_id(self, id: int) -> attachment | None:
+        if validators.id(id) is False:
+            raise id_is_invalid(id)
 
-    async def __decode(self, content: str) -> bytes:
-        return await asyncio.to_thread(
-            base64.b64decode,
-            content.encode("ascii")
-        )   
+        async with tortoise.transactions.in_transaction():
+            return await attachment.filter(id = id).first()
 
-    async def upload(self, content: str, extension: str) -> attachment_file:
-        decoded = await self.__decode(content)
-        file_handler = attachment_file.create(self.resources, extension)
+    async def require_by_id(self, id: int) -> attachment:
+        result = await self.get_by_id(id)
 
-        return await file_handler.store(decoded)
+        if result is None:
+            raise not_found("attachment", id = id)
 
-    def restore(self, file: attachment) -> attachment_file:
-        return attachment_file(file.resources_path, self.resources) 
+        return result

+ 8 - 5
server_source/author.py

@@ -105,10 +105,10 @@ class author_proxy(proxy):
             name = name,
             surname = surname,
             is_person = True,
-            description = contants.empty_text()
+            description = constants.empty_text()
         ))
 
-    def set_name(self, name: str, surname: str) -> None:
+    def set_name(self, name: str, surname: str) -> proxy:
         """
         It set new name and surname of the author.
 
@@ -123,8 +123,9 @@ class author_proxy(proxy):
 
         self._target.name = name
         self._target.surname = surname
+        return self
 
-    def set_description(self, target: str) -> None:
+    def set_description(self, target: str) -> proxy:
         """
         It set new description of the author.
 
@@ -135,18 +136,20 @@ class author_proxy(proxy):
         """
 
         self._target.description = target
+        return self
 
-    def make_person(self) -> None:
+    def make_person(self) -> proxy:
         """
         It make author the person.
         """
 
         self._target.is_person = True
 
-    def make_company(self) -> None:
+    def make_company(self) -> proxy:
         """
         It make author the company.
         """
 
         self._target.is_person = False
+        return self
 

+ 16 - 0
server_source/constants.py

@@ -19,8 +19,24 @@ class constants:
     
     @staticmethod get_related_name : str
         It create name of the reverse relation field for other models.
+    
+    @staticmethod base_on_stock : int
+        Init count on stock.
     """
 
+    @staticmethod
+    def base_on_stock() -> int:
+        """
+        Return base count of items on stock.
+
+        Returns
+        -------
+        int
+            Init count on stock.
+        """
+
+        return 1
+
     @staticmethod
     def empty_text() -> str:
         """

+ 60 - 0
server_source/container_blob.py

@@ -0,0 +1,60 @@
+import base64
+import asyncio
+
+class container_blob:
+    def __init__(self, content: bytes | str) -> None:
+        self.__content = content
+
+    @property
+    def content(self) -> bytes | str:
+        return self.__content
+
+    def result(self) -> bytes | str:
+        return self.__content
+
+class decoded(container_blob):
+    def __init__(self, content: bytes | str) -> None:
+        if type(content) is str:
+            content = content.encode("UTF-8")
+
+        super().__init__(content)
+
+    @staticmethod
+    def __encode(content: bytes) -> container_blob:
+        coded = base64.b64encode(content)
+        coded = coded.decode("ascii")
+
+        return encoded(coded)
+
+    async def encode(self) -> container_blob:
+        return await asyncio.to_thread(
+            self.__class__.__encode, 
+            self.content
+        )
+
+    def __str__(self) -> str:
+        return self.content.decode("UTF-8")
+
+    def __bytes__(self) -> bytes:
+        return self.content
+
+class encoded(container_blob):
+    def __init__(self, content: str) -> None:
+        super().__init__(content)
+
+    @staticmethod
+    def __decode(content: str) -> container_blob:
+        content = content.encode("ascii")
+        coded = base64.b64decode(content)
+
+        return decoded(coded)
+
+    async def decode(self) -> container_blob:
+        return await asyncio.to_thread(
+            self.__class__.__decode,
+            self.content
+        ) 
+
+    def __str__(self) -> str:
+        return self.content
+

+ 38 - 0
server_source/exceptions.py

@@ -52,3 +52,41 @@ class resources_not_exists(Exception):
     def __init__(self, target: pathlib.Path) -> None:
         comment = "Attachment file \"" + str(target) + "\" does not exists."
         super().__init__(comment)
+
+class model_must_being_saved(Exception):
+    def __init__(self, target: object) -> None:
+        comment = "To do that operation, model must being saved.\n"
+        comment = comment + "Dump of the model: \n" + repr(target)
+        
+        super().__init__(comment)
+
+
+class not_exists(Exception):
+    def __init__(self, target: object) -> None:
+        comment = "Can not get identifier of object, which currently "
+        comment = comment + "not exists.\nDump of the object:\n"
+        comment = comment + repr(target)
+
+        super().__init__(comment)
+
+class id_is_invalid(Exception):
+    def __init__(self, id: int) -> None:
+        super().__init__("Id \"" + str(id) + "\" is invalid.")
+
+class not_found(Exception):
+    def __init__(self, name: str, **kwargs) -> None:
+        comment = "Can not found " + name + " in database. "
+        
+        if len(kwargs) == 0:
+            super().__init__(comment)
+            return    
+        
+        comment = comment + "Searching by: "
+
+        for argument, value in kwargs.items():
+            comment = comment + "\"" + argument + "\" = \""
+            comment = comment + value + "\", "
+
+        comment = comment[:-2]
+
+        super().__init__(comment)

+ 60 - 0
server_source/item.py

@@ -6,6 +6,8 @@ from .product_type import product_type
 from .proxy import proxy
 from .attachment import attachment
 from .validators import validators
+from .constants import constants
+from .exceptions import model_must_being_saved
 
 class item(model):
     id = field_generator.id()
@@ -36,3 +38,61 @@ class item(model):
             "barcode": validators.barcode
         }
 
+class item_proxy(proxy):
+    @classmethod
+    def create(cls, name: str, barcode: str) -> proxy:
+        return cls(item(
+            name = name,
+            barcode = barcode,
+            on_stock = constants.base_on_stock(),
+            description = constants.empty_text(),
+            category = None,
+            author = None,
+            product_type = None
+        ))
+
+    def set_cover(self, target: attachment) -> proxy:
+        self._target.cover = target
+        return self
+
+    async def remove_attachment(self, target: attachment) -> proxy:
+        if not self._target.is_in_database():
+            raise model_must_being_saved(self._target)
+
+        await self._target.attachments.remove(target)
+        return self
+
+    async def add_attachment(self, target: attachment) -> proxy:
+        if not self._target.is_in_database():
+            raise model_must_being_saved(self._target)
+
+        await self._target.attachments.add(target)
+        return self
+
+    def set_barcode(self, target: str) -> proxy:
+        self._target.barcode = target
+        return self
+
+    def set_name(self, target: str) -> proxy:   
+        self._target.name = target
+        return self
+
+    def set_on_stock(self, target: int) -> proxy:   
+        self._target.on_stock = target
+        return self
+
+    def set_description(self, target: str) -> proxy:
+        self._target.description = target
+        return self
+
+    def set_product_type(self, target: product_type) -> proxy:   
+        self._target.product_type = target
+        return self
+
+    def set_author(self, target: author) -> proxy:
+        self._target.author = target
+        return self
+
+    def set_category(self, target: category) -> proxy:
+        self._target.category = target
+        return self

+ 65 - 0
server_source/model.py

@@ -2,6 +2,7 @@ import tortoise.models
 
 from .field_generator import field_generator
 from .constants import constants
+from .exceptions import not_exists
 
 class model(tortoise.models.Model):
     """
@@ -148,6 +149,36 @@ class model(tortoise.models.Model):
 
         return self.__getattribute__(key)
 
+    async def get_related(
+        self, 
+        target_model: type | str, 
+        target_field: str
+    ) -> any:
+        """
+        That load models related to that model. It require name of 
+        the related model, and field which is related to that model.
+
+        Parameters
+        ----------
+        target_model : type | str
+            Target model which has relation with.
+
+        target_model : str
+            Field which use that relation.
+
+        Returns
+        -------
+        any
+            Models which is related to given parameters.
+        """
+
+        related_name = constants.get_related_name(
+            target_model, 
+            target_field
+        )
+        
+        return await self.fetch_related(related_name)
+
     def validate(self) -> object:
         """
         That function make full validation of the object. It validate all 
@@ -171,5 +202,39 @@ class model(tortoise.models.Model):
         
         return self
 
+    def is_in_database(self) -> bool:
+        """
+        This function return that model is in the database
+        or not.
+
+        Returns
+        -------
+        bool
+            True when objects exists in database, False when not.
+        """
+
+        return self.id is not None
+
+    def get_identifier(self) -> int:
+        """
+        That return currently identifier of the object, or raise error
+        when object not exists.
+
+        Raises
+        ------
+        not_exists
+            When object not exists yet.
+
+        Returns
+        -------
+        int
+            Identifier of the object.
+        """
+
+        if not self.is_in_database():
+            raise not_exists(self)
+
+        return self.id   
+
     class Meta:
         app = constants.app_name()

+ 15 - 17
server_source/product_type.py

@@ -1,24 +1,22 @@
-from .proxy import proxy
 from .model import model
+from .proxy import proxy
+from .single_set_proxy import single_set_proxy
+from .single_set_model import single_set_model
 from .field_generator import field_generator
 from .validators import validators
+from .single_set_manager import single_set_manager
+from .constants import constants
 
-class product_type(model):
-    id = field_generator.id()
-    name = field_generator.name()
-
-    def _validators(self) -> dict:
-        return {
-            "name": validators.name
-        }
+class product_type(single_set_model):
+    content = field_generator.name()
 
-class product_type_proxy(proxy):
-    @classmethod
-    def create(cls, name: str) -> proxy:    
-        return cls(product_type(
-            name = name
-        ))
+    def _single_validator(self) -> callable:
+        return validators.name
 
-    def set_name(self, target: str) -> None:
-        self._target.name = target
+class product_type_manager(
+    single_set_manager,
+    target_model = product_type,
+    related_models = constants.get_related_name('item')
+):
+    pass
 

+ 9 - 0
server_source/validators.py

@@ -4,6 +4,13 @@ from .apikey import apikey
 from .validators_base import validators_base
 
 class validators(validators_base):
+    @staticmethod
+    def id(content: int) -> int:
+        if content <= 0:
+            raise ValueError("ID could not be less than 1.")
+
+        return content
+
     @staticmethod
     def password(content: str) -> str:
         content = validators._validate_length(content, "password", 8, 64)
@@ -20,12 +27,14 @@ class validators(validators_base):
     def nick(content: str) -> str:
         content = validators._validate_lenght(content, "nick", 4, 20)
         content = validators._validate_generic_name(content, "nick")
+        content = validators._validate_white_chars(content, "nick")
 
         return content
 
     @staticmethod
     def path(content: str) -> str:
         content = validators._validate_lenght(content, "path", 1, 4096)
+        content = validators._validate_white_chars(content, "path")
 
         return content
 

+ 10 - 6
server_source/validators_base.py

@@ -3,18 +3,22 @@ import re
 class validators_base:
     @staticmethod
     def _validate_generic_name(content: str, name: str = "it") -> str:
-        if re.search("\s", content) is not None:
-            raise ValueError(
-                name.title() + " can not contain whitespace chars."
-            )
-
-        if re.search("\W", content) is not None:
+        if re.search("\W ", content) is not None:
             raise ValueError(
                 name.title() + " can contain only _ and alphanumeric chars."
             )
 
         return content 
 
+    @staticmethod
+    def _validate_white_chars(content: str, name: str = "it") -> str:
+        if re.search("\s", content) is not None:
+            raise ValueError(
+                name.title() + " can not contain whitespace chars."
+            )
+        
+        return content
+
     @staticmethod 
     def _validate_lenght(
         content: str, 

+ 14 - 22
server_tests/009-attachment.py

@@ -2,7 +2,8 @@ import sys
 import pathlib
 
 test_file = pathlib.Path(__file__)
-project = test_file.parent.parent
+test_dir = test_file.parent
+project = test_dir.parent
 
 sys.path.append(str(project))
 
@@ -26,6 +27,8 @@ def prepare_dir() -> pathlib.Path:
     return resources
 
 async def main():
+    global test_dir
+
     modules = {
         source.model.Meta.app: [ "server_source" ]
     }
@@ -38,31 +41,20 @@ async def main():
     await tortoise.Tortoise.generate_schemas()
 
     content = "IyBVd1UKICogRmlyc3QgcG9pbnQKICogU2Vjb25kIHBvaW50Cg=="
+    
+    decoded = await source.encoded(content).decode()
+    decoded = decoded.result()
+    proxy = await source.attachments_directory(test_dir).store(decoded, "txt")
 
-    manager = source.attachments_manager(prepare_dir())
-    proxy = await manager.upload(content, "md")
-    attachment = proxy \
-    .set_name("sample_file") \
-    .set_description("This describe it.") \
-    .result()
-
-    print("Attachment before insert to DB:")
-    print(repr(attachment))
-    print()
+    proxy.set_name("Attach")
+    proxy.set_description("That is it.")
 
-    await attachment.save()
-    
-    print("Attachment after inserting to DB:")
-    print(repr(attachment))
-    print()
+    result = proxy.result()
+    await result.save()
 
-    readed = await manager.restore(attachment).load()
+    in_file = await source.attachments_directory(test_dir).remove(result)
 
-    print("Data loaded from Base64:")
-    print("\"\"\"")
-    print(readed.decode("UTF-8"))
-    print("\"\"\"")
-    print()
+    test(in_file, decoded)
 
     await tortoise.Tortoise.close_connections()
 

+ 40 - 0
server_tests/010-container-blob.py

@@ -0,0 +1,40 @@
+import asyncio
+import sys
+import pathlib
+import os
+
+test_file = pathlib.Path(__file__)
+project = test_file.parent.parent
+
+sys.path.append(str(project))
+
+import server_source as source
+from test import test
+
+async def main():
+    text = "This is sample encrypted text."
+    
+    print("Encoding text: ")
+    print("\"" + text + "\"")
+
+    encrypted = await source.decoded(text).encode()
+
+    print("Encoded: ")
+    print("\"" + str(encrypted) + "\"")
+
+    decrypted = await encrypted.decode()
+
+    test(text, str(decrypted))
+
+    print("Testing on random bytes...")
+    
+    blob = os.urandom(1024 * 1024 * 100)
+    encrypted_blob = await source.decoded(blob).encode()
+    
+    print("Encrypted. Decrypting...")
+
+    decrypted_blob = await encrypted_blob.decode()
+
+    test(blob, bytes(decrypted_blob))
+
+asyncio.run(main())

+ 72 - 0
server_tests/011-item.py

@@ -0,0 +1,72 @@
+import sys
+import pathlib
+
+test_file = pathlib.Path(__file__)
+project = test_file.parent.parent
+
+sys.path.append(str(project))
+
+import server_source as source
+from test import test
+
+
+import asyncio
+import tortoise
+
+async def main():
+    modules = {
+        source.model.Meta.app: [ "server_source" ]
+    }
+
+    await tortoise.Tortoise.init(
+        db_url = "sqlite://:memory:", 
+        modules = modules
+    )
+
+    await tortoise.Tortoise.generate_schemas()
+    
+    sample_author = source.author_proxy \
+    .create("Sample", "Example") \
+    .set_description("That is sample author.") \
+    .result()
+
+    await sample_author.save()
+
+    sample_cover = source.attachment_proxy \
+    .create("cover.jpg") \
+    .set_name("Cover title") \
+    .set_description("Cover description.") \
+    .result()
+
+    await sample_cover.save()
+
+    sample_attachment = source.attachment_proxy \
+    .create("instruction.pdf") \
+    .set_name("User instruction") \
+    .set_description("That is simple user instruction.") \
+    .result()
+
+    await sample_attachment.save()
+
+    sample_item = source.item_proxy \
+    .create("Sample item", "123456789012") \
+    .set_description("That is sample object.") \
+    .set_on_stock(10) \
+    .set_product_type(await source.product_type_manager.add("book")) \
+    .set_category(await source.category_manager.add("adrenalin")) \
+    .set_author(sample_author) \
+    .set_cover(sample_cover) \
+    .result()
+
+    await sample_item.save()
+
+    new_proxy = await source.item_proxy(sample_item) \
+    .add_attachment(sample_attachment)
+
+    await new_proxy.result().save()
+
+    print(repr(new_proxy.result()))
+
+    await tortoise.Tortoise.close_connections()
+
+asyncio.run(main())

+ 42 - 0
server_tests/012-product_type.py

@@ -0,0 +1,42 @@
+import sys
+import pathlib
+
+test_file = pathlib.Path(__file__)
+project = test_file.parent.parent
+
+sys.path.append(str(project))
+
+import server_source as source
+from test import test
+
+
+import asyncio
+import tortoise
+
+async def main():
+    modules = {
+        source.model.Meta.app: [ "server_source" ]
+    }
+
+    await tortoise.Tortoise.init(
+        db_url = "sqlite://:memory:", 
+        modules = modules
+    )
+
+    await tortoise.Tortoise.generate_schemas()
+    
+    books = source.product_type.get_proxy().create("books").result()
+    await books.save()
+
+    test(str(books), "books")
+    test(len(await source.product_type_manager.all()), 1)
+    loaded_books = await source.product_type_manager.add("books")
+    test(len(await source.product_type_manager.all()), 1)
+    games = await source.product_type_manager.add("games")
+    test(len(await source.product_type_manager.all()), 2)
+    await source.product_type_manager.clean("books")
+    test(len(await source.product_type_manager.all()), 1)
+
+    await tortoise.Tortoise.close_connections()
+
+asyncio.run(main())

+ 38 - 0
server_tests/013-author.py

@@ -0,0 +1,38 @@
+import sys
+import pathlib
+
+test_file = pathlib.Path(__file__)
+project = test_file.parent.parent
+
+sys.path.append(str(project))
+
+import server_source as source
+from test import test
+
+
+import asyncio
+import tortoise
+
+async def main():
+    modules = {
+        source.model.Meta.app: [ "server_source" ]
+    }
+
+    await tortoise.Tortoise.init(
+        db_url = "sqlite://:memory:", 
+        modules = modules
+    )
+
+    await tortoise.Tortoise.generate_schemas()
+    
+    proxy = source.author_proxy.create("Sample", "Example")
+    proxy.set_description("That is sample author.")
+    sample = proxy.result()
+
+    await sample.save()
+
+    print(repr(sample))
+
+    await tortoise.Tortoise.close_connections()
+
+asyncio.run(main())

+ 70 - 0
server_tests/014-attachments_manager.py

@@ -0,0 +1,70 @@
+import sys
+import pathlib
+
+test_file = pathlib.Path(__file__)
+test_dir = test_file.parent
+project = test_dir.parent
+
+sys.path.append(str(project))
+
+import server_source as source
+from test import test
+
+
+import asyncio
+import tortoise
+
+async def main():
+    global test_dir
+
+    modules = {
+        source.model.Meta.app: [ "server_source" ]
+    }
+
+    await tortoise.Tortoise.init(
+        db_url = "sqlite://:memory:", 
+        modules = modules
+    )
+
+    await tortoise.Tortoise.generate_schemas()
+    
+    directory = source.attachments_directory(test_dir)
+    manager = source.attachments_manager(directory)
+    
+    first_id = await manager.upload(
+        "IyBVd1UKICogRmlyc3QgcG9pbnQKICogU2Vjb25kIHBvaW50Cg==",
+        "txt",
+        "Sample",
+        "That is example attachment."
+    )
+
+    first = await manager.get_by_id(first_id)
+
+    print("Result after creation:")
+    print(repr(first))
+    print()
+
+    await manager.edit(first_id, "new_name", "That is new description.")
+
+    first = await manager.get_by_id(first_id)
+
+    print("Result after edit:")
+    print(repr(first))
+    print()
+
+    second_id = await manager.upload(
+        "IyBVd1UKICogRmlyc3QgcG9pbnQKICogU2Vjb25kIHBvaW50Cg==",
+        "md",
+        "second_item",
+        "That is second item."
+    )
+
+    alls = await manager.get_all()
+    
+    print("Created second. All items:")
+    print(repr(alls))
+    print()
+
+    await tortoise.Tortoise.close_connections()
+
+asyncio.run(main())

+ 3 - 0
server_tests/CAgSYsMgDnpdwKMzvtWZiMHIR8brl.md

@@ -0,0 +1,3 @@
+# UwU
+ * First point
+ * Second point

+ 3 - 0
server_tests/EToOCFzJyqm4AAnzPW8SErvlX5MI.txt

@@ -0,0 +1,3 @@
+# UwU
+ * First point
+ * Second point

+ 3 - 0
server_tests/sE5T2nd7UvBXQNfNFCbfl5QQ0BKG.txt

@@ -0,0 +1,3 @@
+# UwU
+ * First point
+ * Second point

+ 3 - 0
server_tests/tK9CNRsZdDYlKd5yJdxmbECavpwd.txt

@@ -0,0 +1,3 @@
+# UwU
+ * First point
+ * Second point