|
| 1 | +import json |
| 2 | +import pickle |
| 3 | +import os |
| 4 | +import io |
| 5 | +from abc import ABC, abstractmethod |
| 6 | + |
| 7 | +__all__ = ("FileIO", "JsonFileIO", "BinaryFileIO") |
| 8 | + |
| 9 | + |
| 10 | +def create_db(db_name: str, data_dir: str | None): |
| 11 | + """ |
| 12 | + Create a file if it doesn't exist yet. |
| 13 | +
|
| 14 | + :param db_name: Database-file to create. |
| 15 | + :param data_dir: Where the Database-file will be stored. |
| 16 | + """ |
| 17 | + |
| 18 | + # Default Database-file path. |
| 19 | + _db_path = db_name |
| 20 | + |
| 21 | + if data_dir is not None: |
| 22 | + _db_path = os.path.join(data_dir, db_name) |
| 23 | + |
| 24 | + # Check if path is already exist or not |
| 25 | + if not os.path.exists(_db_path): |
| 26 | + |
| 27 | + # Check if we need to create data directories |
| 28 | + if not os.path.exists(data_dir): |
| 29 | + os.makedirs(data_dir) |
| 30 | + |
| 31 | + # Create the file by opening it in "a" mode which creates the file if it |
| 32 | + # does not exist yet but does not modify its contents |
| 33 | + with open(_db_path, 'ab'): |
| 34 | + pass |
| 35 | + |
| 36 | + |
| 37 | +class FileIO(ABC): |
| 38 | + """ |
| 39 | + The abstract base class for all FileIO Classes. |
| 40 | +
|
| 41 | + A FileIO (de)serializes the current state of the database and stores it in |
| 42 | + some place. |
| 43 | + """ |
| 44 | + |
| 45 | + # Using ABCMeta as metaclass allows instantiating only storages that have |
| 46 | + # implemented read and write |
| 47 | + |
| 48 | + @abstractmethod |
| 49 | + def read(self) -> dict: |
| 50 | + """ |
| 51 | + Read the current state of the database from the Database-file. |
| 52 | +
|
| 53 | + Any kind of deserialization should go here. |
| 54 | +
|
| 55 | + Return ``None`` if the Database-file is empty. |
| 56 | + """ |
| 57 | + |
| 58 | + raise NotImplementedError('To be overridden!') |
| 59 | + |
| 60 | + @abstractmethod |
| 61 | + def write(self, data: dict): |
| 62 | + """ |
| 63 | + Write the current state of the database to the Database-file. |
| 64 | +
|
| 65 | + Any kind of serialization should go here. |
| 66 | +
|
| 67 | + :param data: The current state of the database. |
| 68 | + """ |
| 69 | + |
| 70 | + raise NotImplementedError('To be overridden!') |
| 71 | + |
| 72 | + |
| 73 | +class BinaryFileIO(FileIO): |
| 74 | + def __init__(self, db_name: str, data_dir: str | None): |
| 75 | + """ |
| 76 | + Create a new instance. |
| 77 | +
|
| 78 | + Also creates the Database-file, if it doesn't exist. |
| 79 | +
|
| 80 | + [Recommended] Don't add any file extension |
| 81 | +
|
| 82 | + :param db_name: Name of Database |
| 83 | + :param data_dir: Where to store the data |
| 84 | + """ |
| 85 | + |
| 86 | + super().__init__() |
| 87 | + |
| 88 | + self._data_dir = data_dir |
| 89 | + |
| 90 | + # Adding ``FileXdb`` specific file extention to the Database-file. |
| 91 | + self._db_name = f"{db_name}.fxdb" |
| 92 | + |
| 93 | + # Setting default Database-file path. |
| 94 | + self._db_file_path = self._db_name |
| 95 | + |
| 96 | + # Checking if Data Directory is on root or not. |
| 97 | + if self._data_dir is not None: |
| 98 | + |
| 99 | + # Creating Database-file full path by joining data_dir & db_name. |
| 100 | + self._db_file_path = os.path.join(self._data_dir, self._db_name) |
| 101 | + |
| 102 | + # Create the Database/File if it doesn't exist |
| 103 | + create_db(self._db_name, self._data_dir) |
| 104 | + |
| 105 | + def read(self) -> dict: |
| 106 | + """ |
| 107 | + Reads existing Database-file, either it is empty or non-empty. |
| 108 | +
|
| 109 | + If empty returns an empty dict, else returns saved Data. |
| 110 | +
|
| 111 | + :return: Database as a python Dictionary. |
| 112 | + """ |
| 113 | + database = None |
| 114 | + |
| 115 | + with open(self._db_file_path, "rb") as file: |
| 116 | + # Get the file size by moving the cursor to the file end and reading its location. |
| 117 | + file.seek(0, os.SEEK_END) |
| 118 | + size = file.tell() |
| 119 | + |
| 120 | + # check if size of file is 0 |
| 121 | + if size is not 0: |
| 122 | + try: |
| 123 | + # Load whole Database form Database-file |
| 124 | + database = pickle.load(file) |
| 125 | + |
| 126 | + except io.UnsupportedOperation: |
| 127 | + # Through an Unsupported Operation Error. |
| 128 | + raise IOError(f"Cannot read file.\n\t`{self._db_name}` is not a database") |
| 129 | + |
| 130 | + else: |
| 131 | + # Returns an empty dict as |
| 132 | + database = {} |
| 133 | + |
| 134 | + return database |
| 135 | + |
| 136 | + def write(self, data: dict) -> None: |
| 137 | + """ |
| 138 | + Write the current state of entire Database to the Database-file. |
| 139 | +
|
| 140 | + :param data: Dictionary object to write on Database. |
| 141 | + :return: None. |
| 142 | + """ |
| 143 | + with open(self._db_file_path, "wb") as file: |
| 144 | + |
| 145 | + # Move the cursor to the beginning of the file just in case. |
| 146 | + file.seek(0) |
| 147 | + |
| 148 | + # Serialize the database state using the user-provided arguments |
| 149 | + serialized = pickle.dumps(data) |
| 150 | + |
| 151 | + # Write the serialized data to the file |
| 152 | + try: |
| 153 | + file.write(serialized) |
| 154 | + except io.UnsupportedOperation: |
| 155 | + raise IOError(f"Cannot write to the file.\n\t`{self._db_name}` is not a database") |
| 156 | + |
| 157 | + # Ensure the file has been written |
| 158 | + file.flush() |
| 159 | + os.fsync(file.fileno()) |
| 160 | + |
| 161 | + # Remove data that is behind the new cursor if the file has gotten shorter. |
| 162 | + file.truncate() |
| 163 | + |
| 164 | + |
| 165 | +class JsonFileIO(FileIO): |
| 166 | + def __init__(self, db_name): |
| 167 | + super().__init__() |
| 168 | + self.db_name = db_name |
| 169 | + |
| 170 | + def read(self) -> dict: |
| 171 | + pass |
| 172 | + |
| 173 | + def write(self, data: dict): |
| 174 | + pass |
0 commit comments