zakat
xxx

_____ _ _ _ _ _
|__ /__ _| | ____ _| |_ | | (_) |__ _ __ __ _ _ __ _ _ / // _| |/ / _ | __| | | | | '_ \| '__/ _` | '__| | | | / /| (_| | < (_| | |_ | |___| | |_) | | | (_| | | | |_| | /______,_|_|___,_|__| |_____|_|_.__/|_| __,_|_| __, | |___/

"رَبَّنَا افْتَحْ بَيْنَنَا وَبَيْنَ قَوْمِنَا بِالْحَقِّ وَأَنتَ خَيْرُ الْفَاتِحِينَ (89)" -- سورة الأعراف ... Never Trust, Always Verify ...

This file provides the ZakatLibrary classes, functions for tracking and calculating Zakat.

 1"""
 2 _____     _         _     _     _ _                          
 3|__  /__ _| | ____ _| |_  | |   (_) |__  _ __ __ _ _ __ _   _ 
 4  / // _` | |/ / _` | __| | |   | | '_ \| '__/ _` | '__| | | |
 5 / /| (_| |   < (_| | |_  | |___| | |_) | | | (_| | |  | |_| |
 6/____\__,_|_|\_\__,_|\__| |_____|_|_.__/|_|  \__,_|_|   \__, |
 7                                                        |___/ 
 8
 9"رَبَّنَا افْتَحْ بَيْنَنَا وَبَيْنَ قَوْمِنَا بِالْحَقِّ وَأَنتَ خَيْرُ الْفَاتِحِينَ (89)" -- سورة الأعراف
10... Never Trust, Always Verify ...
11
12This file provides the ZakatLibrary classes, functions for tracking and calculating Zakat.
13"""
14# Importing necessary classes and functions from the main module
15from zakat.zakat_tracker import (
16    ZakatTracker,
17    test,
18    Action,
19    JSONEncoder,
20    MathOperation,
21)
22
23from zakat.file_server import (
24    start_file_server,
25    find_available_port,
26    FileType,
27)
28
29# Version information for the module
30__version__ = ZakatTracker.Version()
31__all__ = [
32    "ZakatTracker",
33    "test",
34    "Action",
35    "JSONEncoder",
36    "MathOperation",
37    "start_file_server",
38    "find_available_port",
39    "FileType",
40]
class ZakatTracker:
 104class ZakatTracker:
 105    """
 106    A class for tracking and calculating Zakat.
 107
 108    This class provides functionalities for recording transactions, calculating Zakat due,
 109    and managing account balances. It also offers features like importing transactions from
 110    CSV files, exporting data to JSON format, and saving/loading the tracker state.
 111
 112    The `ZakatTracker` class is designed to handle both positive and negative transactions,
 113    allowing for flexible tracking of financial activities related to Zakat. It also supports
 114    the concept of a "Nisab" (minimum threshold for Zakat) and a "haul" (complete one year for Transaction) can calculate Zakat due
 115    based on the current silver price.
 116
 117    The class uses a pickle file as its database to persist the tracker state,
 118    ensuring data integrity across sessions. It also provides options for enabling or
 119    disabling history tracking, allowing users to choose their preferred level of detail.
 120
 121    In addition, the `ZakatTracker` class includes various helper methods like
 122    `time`, `time_to_datetime`, `lock`, `free`, `recall`, `export_json`,
 123    and more. These methods provide additional functionalities and flexibility
 124    for interacting with and managing the Zakat tracker.
 125
 126    Attributes:
 127        ZakatTracker.ZakatCut (function): A function to calculate the Zakat percentage.
 128        ZakatTracker.TimeCycle (function): A function to determine the time cycle for Zakat.
 129        ZakatTracker.Nisab (function): A function to calculate the Nisab based on the silver price.
 130        ZakatTracker.Version (function): The version of the ZakatTracker class.
 131
 132    Data Structure:
 133        The ZakatTracker class utilizes a nested dictionary structure called "_vault" to store and manage data.
 134
 135        _vault (dict):
 136            - account (dict):
 137                - {account_number} (dict):
 138                    - balance (int): The current balance of the account.
 139                    - box (dict): A dictionary storing transaction details.
 140                        - {timestamp} (dict):
 141                            - capital (int): The initial amount of the transaction.
 142                            - count (int): The number of times Zakat has been calculated for this transaction.
 143                            - last (int): The timestamp of the last Zakat calculation.
 144                            - rest (int): The remaining amount after Zakat deductions and withdrawal.
 145                            - total (int): The total Zakat deducted from this transaction.
 146                    - count (int): The total number of transactions for the account.
 147                    - log (dict): A dictionary storing transaction logs.
 148                        - {timestamp} (dict):
 149                            - value (int): The transaction amount (positive or negative).
 150                            - desc (str): The description of the transaction.
 151                            - ref (int): The box reference (positive or None).
 152                            - file (dict): A dictionary storing file references associated with the transaction.
 153                    - hide (bool): Indicates whether the account is hidden or not.
 154                    - zakatable (bool): Indicates whether the account is subject to Zakat.
 155            - exchange (dict):
 156                - account (dict):
 157                    - {timestamps} (dict):
 158                        - rate (float): Exchange rate when compared to local currency.
 159                        - description (str): The description of the exchange rate.
 160            - history (dict):
 161                - {timestamp} (list): A list of dictionaries storing the history of actions performed.
 162                    - {action_dict} (dict):
 163                        - action (Action): The type of action (CREATE, TRACK, LOG, SUB, ADD_FILE, REMOVE_FILE, BOX_TRANSFER, EXCHANGE, REPORT, ZAKAT).
 164                        - account (str): The account number associated with the action.
 165                        - ref (int): The reference number of the transaction.
 166                        - file (int): The reference number of the file (if applicable).
 167                        - key (str): The key associated with the action (e.g., 'rest', 'total').
 168                        - value (int): The value associated with the action.
 169                        - math (MathOperation): The mathematical operation performed (if applicable).
 170            - lock (int or None): The timestamp indicating the current lock status (None if not locked).
 171            - report (dict):
 172                - {timestamp} (tuple): A tuple storing Zakat report details.
 173
 174    """
 175
 176    @staticmethod
 177    def Version():
 178        """
 179        Returns the current version of the software.
 180
 181        This function returns a string representing the current version of the software,
 182        including major, minor, and patch version numbers in the format "X.Y.Z".
 183
 184        Returns:
 185        str: The current version of the software.
 186        """
 187        return '0.2.78'
 188
 189    @staticmethod
 190    def ZakatCut(x: float) -> float:
 191        """
 192        Calculates the Zakat amount due on an asset.
 193
 194        This function calculates the zakat amount due on a given asset value over one lunar year.
 195        Zakat is an Islamic obligatory alms-giving, calculated as a fixed percentage of an individual's wealth
 196        that exceeds a certain threshold (Nisab).
 197
 198        Parameters:
 199        x: The total value of the asset on which Zakat is to be calculated.
 200
 201        Returns:
 202        The amount of Zakat due on the asset, calculated as 2.5% of the asset's value.
 203        """
 204        return 0.025 * x  # Zakat Cut in one Lunar Year
 205
 206    @staticmethod
 207    def TimeCycle(days: int = 355) -> int:
 208        """
 209        Calculates the approximate duration of a lunar year in nanoseconds.
 210
 211        This function calculates the approximate duration of a lunar year based on the given number of days.
 212        It converts the given number of days into nanoseconds for use in high-precision timing applications.
 213
 214        Parameters:
 215        days: The number of days in a lunar year. Defaults to 355,
 216              which is an approximation of the average length of a lunar year.
 217
 218        Returns:
 219        The approximate duration of a lunar year in nanoseconds.
 220        """
 221        return int(60 * 60 * 24 * days * 1e9)  # Lunar Year in nanoseconds
 222
 223    @staticmethod
 224    def Nisab(gram_price: float, gram_quantity: float = 595) -> float:
 225        """
 226        Calculate the total value of Nisab (a unit of weight in Islamic jurisprudence) based on the given price per gram.
 227
 228        This function calculates the Nisab value, which is the minimum threshold of wealth,
 229        that makes an individual liable for paying Zakat.
 230        The Nisab value is determined by the equivalent value of a specific amount
 231        of gold or silver (currently 595 grams in silver) in the local currency.
 232
 233        Parameters:
 234        - gram_price (float): The price per gram of Nisab.
 235        - gram_quantity (float): The quantity of grams in a Nisab. Default is 595 grams of silver.
 236
 237        Returns:
 238        - float: The total value of Nisab based on the given price per gram.
 239        """
 240        return gram_price * gram_quantity
 241
 242    def __init__(self, db_path: str = "zakat.pickle", history_mode: bool = True):
 243        """
 244        Initialize ZakatTracker with database path and history mode.
 245
 246        Parameters:
 247        db_path (str): The path to the database file. Default is "zakat.pickle".
 248        history_mode (bool): The mode for tracking history. Default is True.
 249
 250        Returns:
 251        None
 252        """
 253        self._vault_path = None
 254        self._vault = None
 255        self.reset()
 256        self._history(history_mode)
 257        self.path(db_path)
 258        self.load()
 259
 260    def path(self, path: str = None) -> str:
 261        """
 262        Set or get the path to the database file.
 263
 264        If no path is provided, the current path is returned.
 265        If a path is provided, it is set as the new path.
 266        The function also creates the necessary directories if the provided path is a file.
 267
 268        Parameters:
 269        path (str): The new path to the database file. If not provided, the current path is returned.
 270
 271        Returns:
 272        str: The current or new path to the database file.
 273        """
 274        if path is None:
 275            return self._vault_path
 276        self._vault_path = Path(path).resolve()
 277        base_path = Path(path).resolve()
 278        if base_path.is_file() or base_path.suffix:
 279            base_path = base_path.parent
 280        base_path.mkdir(parents=True, exist_ok=True)
 281        self._base_path = base_path
 282        return self._vault_path
 283
 284    def base_path(self, *args) -> str:
 285        """
 286        Generate a base path by joining the provided arguments with the existing base path.
 287
 288        Parameters:
 289        *args (str): Variable length argument list of strings to be joined with the base path.
 290
 291        Returns:
 292        str: The generated base path. If no arguments are provided, the existing base path is returned.
 293        """
 294        if not args:
 295            return self._base_path
 296        filtered_args = []
 297        ignored_filename = None
 298        for arg in args:
 299            if Path(arg).suffix:
 300                ignored_filename = arg
 301            else:
 302                filtered_args.append(arg)
 303        base_path = Path(self._base_path)
 304        full_path = base_path.joinpath(*filtered_args)
 305        full_path.mkdir(parents=True, exist_ok=True)
 306        if ignored_filename is not None:
 307            return full_path.resolve() / ignored_filename  # Join with the ignored filename
 308        return full_path.resolve() 
 309    
 310    @staticmethod
 311    def scale(x: float | int | Decimal, decimal_places: int = 2) -> int:
 312        """
 313        Scales a numerical value by a specified power of 10, returning an integer.
 314
 315        This function is designed to handle various numeric types (`float`, `int`, or `Decimal`) and
 316        facilitate precise scaling operations, particularly useful in financial or scientific calculations.
 317
 318        Parameters:
 319        x: The numeric value to scale. Can be a floating-point number, integer, or decimal.
 320        decimal_places: The exponent for the scaling factor (10**y). Defaults to 2, meaning the input is scaled
 321            by a factor of 100 (e.g., converts 1.23 to 123).
 322
 323        Returns:
 324        The scaled value, rounded to the nearest integer.
 325
 326        Raises:
 327        TypeError: If the input `x` is not a valid numeric type.
 328
 329        Examples:
 330        >>> scale(3.14159)
 331        314
 332        >>> scale(1234, decimal_places=3)
 333        1234000
 334        >>> scale(Decimal("0.005"), decimal_places=4)
 335        50
 336        """
 337        if not isinstance(x, (float, int, Decimal)):
 338            raise TypeError("Input 'x' must be a float, int, or Decimal.")
 339        return int(Decimal(f"{x:.{decimal_places}f}") * (10 ** decimal_places))
 340
 341    @staticmethod
 342    def unscale(x: int, return_type: type = float, decimal_places: int = 2) -> float | Decimal:
 343        """
 344        Unscales an integer by a power of 10.
 345
 346        Parameters:
 347        x: The integer to unscale.
 348        return_type: The desired type for the returned value. Can be float, int, or Decimal. Defaults to float.
 349        decimal_places: The power of 10 to use. Defaults to 2.
 350
 351        Returns:
 352        The unscaled number, converted to the specified return_type.
 353
 354        Raises:
 355        TypeError: If the return_type is not float or Decimal.
 356        """
 357        if return_type not in (float, Decimal):
 358            raise TypeError(f'Invalid return_type({return_type}). Supported types are float, int, and Decimal.')
 359        return round(return_type(x / (10 ** decimal_places)), decimal_places)
 360
 361    def _history(self, status: bool = None) -> bool:
 362        """
 363        Enable or disable history tracking.
 364
 365        Parameters:
 366        status (bool): The status of history tracking. Default is True.
 367
 368        Returns:
 369        None
 370        """
 371        if status is not None:
 372            self._history_mode = status
 373        return self._history_mode
 374
 375    def reset(self) -> None:
 376        """
 377        Reset the internal data structure to its initial state.
 378
 379        Parameters:
 380        None
 381
 382        Returns:
 383        None
 384        """
 385        self._vault = {
 386            'account': {},
 387            'exchange': {},
 388            'history': {},
 389            'lock': None,
 390            'report': {},
 391        }
 392
 393    @staticmethod
 394    def time(now: datetime = None) -> int:
 395        """
 396        Generates a timestamp based on the provided datetime object or the current datetime.
 397
 398        Parameters:
 399        now (datetime, optional): The datetime object to generate the timestamp from.
 400        If not provided, the current datetime is used.
 401
 402        Returns:
 403        int: The timestamp in positive nanoseconds since the Unix epoch (January 1, 1970),
 404            before 1970 will return in negative until 1000AD.
 405        """
 406        if now is None:
 407            now = datetime.datetime.now()
 408        ordinal_day = now.toordinal()
 409        ns_in_day = (now - now.replace(hour=0, minute=0, second=0, microsecond=0)).total_seconds() * 10 ** 9
 410        return int((ordinal_day - 719_163) * 86_400_000_000_000 + ns_in_day)
 411
 412    @staticmethod
 413    def time_to_datetime(ordinal_ns: int) -> datetime:
 414        """
 415        Converts an ordinal number (number of days since 1000-01-01) to a datetime object.
 416
 417        Parameters:
 418        ordinal_ns (int): The ordinal number of days since 1000-01-01.
 419
 420        Returns:
 421        datetime: The corresponding datetime object.
 422        """
 423        ordinal_day = ordinal_ns // 86_400_000_000_000 + 719_163
 424        ns_in_day = ordinal_ns % 86_400_000_000_000
 425        d = datetime.datetime.fromordinal(ordinal_day)
 426        t = datetime.timedelta(seconds=ns_in_day // 10 ** 9)
 427        return datetime.datetime.combine(d, datetime.time()) + t
 428
 429    def clean_history(self, lock: int | None = None) -> int:
 430        """
 431        Cleans up the history of actions performed on the ZakatTracker instance.
 432
 433        Parameters:
 434        lock (int, optional): The lock ID is used to clean up the empty history.
 435            If not provided, it cleans up the empty history records for all locks.
 436
 437        Returns:
 438        int: The number of locks cleaned up.
 439        """
 440        count = 0
 441        if lock in self._vault['history']:
 442            if len(self._vault['history'][lock]) <= 0:
 443                count += 1
 444                del self._vault['history'][lock]
 445            return count
 446        self.free(self.lock())
 447        for lock in self._vault['history']:
 448            if len(self._vault['history'][lock]) <= 0:
 449                count += 1
 450                del self._vault['history'][lock]
 451        return count
 452
 453    def _step(self, action: Action = None, account=None, ref: int = None, file: int = None, value: float = None,
 454              key: str = None, math_operation: MathOperation = None) -> int:
 455        """
 456        This method is responsible for recording the actions performed on the ZakatTracker.
 457
 458        Parameters:
 459        - action (Action): The type of action performed.
 460        - account (str): The account number on which the action was performed.
 461        - ref (int): The reference number of the action.
 462        - file (int): The file reference number of the action.
 463        - value (int): The value associated with the action.
 464        - key (str): The key associated with the action.
 465        - math_operation (MathOperation): The mathematical operation performed during the action.
 466
 467        Returns:
 468        - int: The lock time of the recorded action. If no lock was performed, it returns 0.
 469        """
 470        if not self._history():
 471            return 0
 472        lock = self._vault['lock']
 473        if self.nolock():
 474            lock = self._vault['lock'] = self.time()
 475            self._vault['history'][lock] = []
 476        if action is None:
 477            return lock
 478        self._vault['history'][lock].append({
 479            'action': action,
 480            'account': account,
 481            'ref': ref,
 482            'file': file,
 483            'key': key,
 484            'value': value,
 485            'math': math_operation,
 486        })
 487        return lock
 488
 489    def nolock(self) -> bool:
 490        """
 491        Check if the vault lock is currently not set.
 492
 493        Returns:
 494        bool: True if the vault lock is not set, False otherwise.
 495        """
 496        return self._vault['lock'] is None
 497
 498    def lock(self) -> int:
 499        """
 500        Acquires a lock on the ZakatTracker instance.
 501
 502        Returns:
 503        int: The lock ID. This ID can be used to release the lock later.
 504        """
 505        return self._step()
 506
 507    def vault(self) -> dict:
 508        """
 509        Returns a copy of the internal vault dictionary.
 510
 511        This method is used to retrieve the current state of the ZakatTracker object.
 512        It provides a snapshot of the internal data structure, allowing for further
 513        processing or analysis.
 514
 515        Returns:
 516        dict: A copy of the internal vault dictionary.
 517        """
 518        return self._vault.copy()
 519
 520    def stats(self) -> dict[str, tuple]:
 521        """
 522        Calculates and returns statistics about the object's data storage.
 523
 524        This method determines the size of the database file on disk and the
 525        size of the data currently held in RAM (likely within a dictionary).
 526        Both sizes are reported in bytes and in a human-readable format
 527        (e.g., KB, MB).
 528
 529        Returns:
 530        dict[str, tuple]: A dictionary containing the following statistics:
 531
 532            * 'database': A tuple with two elements:
 533                - The database file size in bytes (int).
 534                - The database file size in human-readable format (str).
 535            * 'ram': A tuple with two elements:
 536                - The RAM usage (dictionary size) in bytes (int).
 537                - The RAM usage in human-readable format (str).
 538
 539        Example:
 540        >>> stats = my_object.stats()
 541        >>> print(stats['database'])
 542        (256000, '250.0 KB')
 543        >>> print(stats['ram'])
 544        (12345, '12.1 KB')
 545        """
 546        ram_size = self.get_dict_size(self.vault())
 547        file_size = os.path.getsize(self.path())
 548        return {
 549            'database': (file_size, self.human_readable_size(file_size)),
 550            'ram': (ram_size, self.human_readable_size(ram_size)),
 551        }
 552
 553    def steps(self) -> dict:
 554        """
 555        Returns a copy of the history of steps taken in the ZakatTracker.
 556
 557        The history is a dictionary where each key is a unique identifier for a step,
 558        and the corresponding value is a dictionary containing information about the step.
 559
 560        Returns:
 561        dict: A copy of the history of steps taken in the ZakatTracker.
 562        """
 563        return self._vault['history'].copy()
 564
 565    def free(self, lock: int, auto_save: bool = True) -> bool:
 566        """
 567        Releases the lock on the database.
 568
 569        Parameters:
 570        lock (int): The lock ID to be released.
 571        auto_save (bool): Whether to automatically save the database after releasing the lock.
 572
 573        Returns:
 574        bool: True if the lock is successfully released and (optionally) saved, False otherwise.
 575        """
 576        if lock == self._vault['lock']:
 577            self._vault['lock'] = None
 578            self.clean_history(lock)
 579            if auto_save:
 580                return self.save(self.path())
 581            return True
 582        return False
 583
 584    def account_exists(self, account) -> bool:
 585        """
 586        Check if the given account exists in the vault.
 587
 588        Parameters:
 589        account (str): The account number to check.
 590
 591        Returns:
 592        bool: True if the account exists, False otherwise.
 593        """
 594        return account in self._vault['account']
 595
 596    def box_size(self, account) -> int:
 597        """
 598        Calculate the size of the box for a specific account.
 599
 600        Parameters:
 601        account (str): The account number for which the box size needs to be calculated.
 602
 603        Returns:
 604        int: The size of the box for the given account. If the account does not exist, -1 is returned.
 605        """
 606        if self.account_exists(account):
 607            return len(self._vault['account'][account]['box'])
 608        return -1
 609
 610    def log_size(self, account) -> int:
 611        """
 612        Get the size of the log for a specific account.
 613
 614        Parameters:
 615        account (str): The account number for which the log size needs to be calculated.
 616
 617        Returns:
 618        int: The size of the log for the given account. If the account does not exist, -1 is returned.
 619        """
 620        if self.account_exists(account):
 621            return len(self._vault['account'][account]['log'])
 622        return -1
 623
 624    @staticmethod
 625    def file_hash(file_path: str, algorithm: str = "blake2b") -> str:
 626        """
 627        Calculates the hash of a file using the specified algorithm.
 628
 629        Parameters:
 630        file_path (str): The path to the file.
 631        algorithm (str, optional): The hashing algorithm to use. Defaults to "blake2b".
 632
 633        Returns:
 634        str: The hexadecimal representation of the file's hash.
 635        """
 636        hash_obj = hashlib.new(algorithm)  # Create the hash object
 637        with open(file_path, "rb") as f:  # Open file in binary mode for reading
 638            for chunk in iter(lambda: f.read(4096), b""):  # Read file in chunks
 639                hash_obj.update(chunk)
 640        return hash_obj.hexdigest()  # Return the hash as a hexadecimal string
 641
 642    def snapshot_cache_path(self):
 643        """
 644        Generate the path for the cache file used to store snapshots.
 645
 646        The cache file is a pickle file that stores the timestamps of the snapshots.
 647        The file name is derived from the main database file name by replacing the ".pickle" extension with ".snapshots.pickle".
 648
 649        Returns:
 650        str: The path to the cache file.
 651        """
 652        path = str(self.path())
 653        if path.endswith(".pickle"):
 654            path = path[:-7]
 655        return path + '.snapshots.pickle'
 656    
 657    def snapshot(self) -> bool:
 658        """
 659        This function creates a snapshot of the current database state.
 660
 661        The function calculates the hash of the current database file and checks if a snapshot with the same hash already exists.
 662        If a snapshot with the same hash exists, the function returns True without creating a new snapshot.
 663        If a snapshot with the same hash does not exist, the function creates a new snapshot by saving the current database state
 664        in a new pickle file with a unique timestamp as the file name. The function also updates the snapshot cache file with the new snapshot's hash and timestamp.
 665
 666        Parameters:
 667        None
 668
 669        Returns:
 670        bool: True if a snapshot with the same hash already exists or if the snapshot is successfully created. False if the snapshot creation fails.
 671        """
 672        current_hash = self.file_hash(self.path())
 673        cache: dict[str, int] = {} # hash: time_ns
 674        try:
 675            with open(self.snapshot_cache_path(), "rb") as f:
 676                cache = pickle.load(f)
 677        except:
 678            pass
 679        if current_hash in cache:
 680            return True
 681        time = time_ns()
 682        cache[current_hash] = time
 683        if not self.save(self.base_path('snapshots', f'{time}.pickle')):
 684            return False
 685        with open(self.snapshot_cache_path(), "wb") as f:
 686            pickle.dump(cache, f)
 687        return True
 688
 689    def snapshots(self, hide_missing: bool = True, verified_hash_only: bool = False) -> dict[int, tuple[str, str, bool]]:
 690        """
 691        Retrieve a dictionary of snapshots, with their respective hashes, paths, and existence status.
 692
 693        Parameters:
 694        - hide_missing (bool): If True, only include snapshots that exist in the dictionary. Default is True.
 695        - verified_hash_only (bool): If True, only include snapshots with a valid hash. Default is False.
 696
 697        Returns:
 698        - dict[int, tuple[str, str, bool]]: A dictionary where the keys are the timestamps of the snapshots,
 699        and the values are tuples containing the snapshot's hash, path, and existence status.
 700        """
 701        cache: dict[str, int] = {} # hash: time_ns
 702        try:
 703            with open(self.snapshot_cache_path(), "rb") as f:
 704                cache = pickle.load(f)
 705        except:
 706            pass
 707        if not cache:
 708            return {}
 709        result: dict[int, tuple[str, str, bool]] = {} # time_ns: (hash, path, exists)
 710        for file_hash, ref in cache.items():
 711            path = self.base_path('snapshots', f'{ref}.pickle')
 712            exists = os.path.exists(path)
 713            valid_hash = self.file_hash(path) == file_hash if verified_hash_only else True
 714            if (verified_hash_only and not valid_hash) or (verified_hash_only and not exists):
 715                continue
 716            if exists or not hide_missing:
 717                result[ref] = (file_hash, path, exists)
 718        return result
 719
 720    def recall(self, dry=True, debug=False) -> bool:
 721        """
 722        Revert the last operation.
 723
 724        Parameters:
 725        dry (bool): If True, the function will not modify the data, but will simulate the operation. Default is True.
 726        debug (bool): If True, the function will print debug information. Default is False.
 727
 728        Returns:
 729        bool: True if the operation was successful, False otherwise.
 730        """
 731        if not self.nolock() or len(self._vault['history']) == 0:
 732            return False
 733        if len(self._vault['history']) <= 0:
 734            return False
 735        ref = sorted(self._vault['history'].keys())[-1]
 736        if debug:
 737            print('recall', ref)
 738        memory = self._vault['history'][ref]
 739        if debug:
 740            print(type(memory), 'memory', memory)
 741        self.snapshot()
 742        limit = len(memory) + 1
 743        sub_positive_log_negative = 0
 744        for i in range(-1, -limit, -1):
 745            x = memory[i]
 746            if debug:
 747                print(type(x), x)
 748            match x['action']:
 749                case Action.CREATE:
 750                    if x['account'] is not None:
 751                        if self.account_exists(x['account']):
 752                            if debug:
 753                                print('account', self._vault['account'][x['account']])
 754                            assert len(self._vault['account'][x['account']]['box']) == 0
 755                            assert self._vault['account'][x['account']]['balance'] == 0
 756                            assert self._vault['account'][x['account']]['count'] == 0
 757                            if dry:
 758                                continue
 759                            del self._vault['account'][x['account']]
 760
 761                case Action.TRACK:
 762                    if x['account'] is not None:
 763                        if self.account_exists(x['account']):
 764                            if dry:
 765                                continue
 766                            self._vault['account'][x['account']]['balance'] -= x['value']
 767                            self._vault['account'][x['account']]['count'] -= 1
 768                            del self._vault['account'][x['account']]['box'][x['ref']]
 769
 770                case Action.LOG:
 771                    if x['account'] is not None:
 772                        if self.account_exists(x['account']):
 773                            if x['ref'] in self._vault['account'][x['account']]['log']:
 774                                if dry:
 775                                    continue
 776                                if sub_positive_log_negative == -x['value']:
 777                                    self._vault['account'][x['account']]['count'] -= 1
 778                                    sub_positive_log_negative = 0
 779                                box_ref = self._vault['account'][x['account']]['log'][x['ref']]['ref']
 780                                if not box_ref is None:
 781                                    assert self.box_exists(x['account'], box_ref)
 782                                    box_value = self._vault['account'][x['account']]['log'][x['ref']]['value']
 783                                    assert box_value < 0
 784
 785                                    try:
 786                                        self._vault['account'][x['account']]['box'][box_ref]['rest'] += -box_value
 787                                    except TypeError:
 788                                        self._vault['account'][x['account']]['box'][box_ref]['rest'] += Decimal(-box_value)
 789
 790                                    try:
 791                                        self._vault['account'][x['account']]['balance'] += -box_value
 792                                    except TypeError:
 793                                        self._vault['account'][x['account']]['balance'] += Decimal(-box_value)
 794
 795                                    self._vault['account'][x['account']]['count'] -= 1
 796                                del self._vault['account'][x['account']]['log'][x['ref']]
 797
 798                case Action.SUB:
 799                    if x['account'] is not None:
 800                        if self.account_exists(x['account']):
 801                            if x['ref'] in self._vault['account'][x['account']]['box']:
 802                                if dry:
 803                                    continue
 804                                self._vault['account'][x['account']]['box'][x['ref']]['rest'] += x['value']
 805                                self._vault['account'][x['account']]['balance'] += x['value']
 806                                sub_positive_log_negative = x['value']
 807
 808                case Action.ADD_FILE:
 809                    if x['account'] is not None:
 810                        if self.account_exists(x['account']):
 811                            if x['ref'] in self._vault['account'][x['account']]['log']:
 812                                if x['file'] in self._vault['account'][x['account']]['log'][x['ref']]['file']:
 813                                    if dry:
 814                                        continue
 815                                    del self._vault['account'][x['account']]['log'][x['ref']]['file'][x['file']]
 816
 817                case Action.REMOVE_FILE:
 818                    if x['account'] is not None:
 819                        if self.account_exists(x['account']):
 820                            if x['ref'] in self._vault['account'][x['account']]['log']:
 821                                if dry:
 822                                    continue
 823                                self._vault['account'][x['account']]['log'][x['ref']]['file'][x['file']] = x['value']
 824
 825                case Action.BOX_TRANSFER:
 826                    if x['account'] is not None:
 827                        if self.account_exists(x['account']):
 828                            if x['ref'] in self._vault['account'][x['account']]['box']:
 829                                if dry:
 830                                    continue
 831                                self._vault['account'][x['account']]['box'][x['ref']]['rest'] -= x['value']
 832
 833                case Action.EXCHANGE:
 834                    if x['account'] is not None:
 835                        if x['account'] in self._vault['exchange']:
 836                            if x['ref'] in self._vault['exchange'][x['account']]:
 837                                if dry:
 838                                    continue
 839                                del self._vault['exchange'][x['account']][x['ref']]
 840
 841                case Action.REPORT:
 842                    if x['ref'] in self._vault['report']:
 843                        if dry:
 844                            continue
 845                        del self._vault['report'][x['ref']]
 846
 847                case Action.ZAKAT:
 848                    if x['account'] is not None:
 849                        if self.account_exists(x['account']):
 850                            if x['ref'] in self._vault['account'][x['account']]['box']:
 851                                if x['key'] in self._vault['account'][x['account']]['box'][x['ref']]:
 852                                    if dry:
 853                                        continue
 854                                    match x['math']:
 855                                        case MathOperation.ADDITION:
 856                                            self._vault['account'][x['account']]['box'][x['ref']][x['key']] -= x[
 857                                                'value']
 858                                        case MathOperation.EQUAL:
 859                                            self._vault['account'][x['account']]['box'][x['ref']][x['key']] = x['value']
 860                                        case MathOperation.SUBTRACTION:
 861                                            self._vault['account'][x['account']]['box'][x['ref']][x['key']] += x[
 862                                                'value']
 863
 864        if not dry:
 865            del self._vault['history'][ref]
 866        return True
 867
 868    def ref_exists(self, account: str, ref_type: str, ref: int) -> bool:
 869        """
 870        Check if a specific reference (transaction) exists in the vault for a given account and reference type.
 871
 872        Parameters:
 873        account (str): The account number for which to check the existence of the reference.
 874        ref_type (str): The type of reference (e.g., 'box', 'log', etc.).
 875        ref (int): The reference (transaction) number to check for existence.
 876
 877        Returns:
 878        bool: True if the reference exists for the given account and reference type, False otherwise.
 879        """
 880        if account in self._vault['account']:
 881            return ref in self._vault['account'][account][ref_type]
 882        return False
 883
 884    def box_exists(self, account: str, ref: int) -> bool:
 885        """
 886        Check if a specific box (transaction) exists in the vault for a given account and reference.
 887
 888        Parameters:
 889        - account (str): The account number for which to check the existence of the box.
 890        - ref (int): The reference (transaction) number to check for existence.
 891
 892        Returns:
 893        - bool: True if the box exists for the given account and reference, False otherwise.
 894        """
 895        return self.ref_exists(account, 'box', ref)
 896
 897    def track(self, value: float = 0, desc: str = '', account: str = 1, logging: bool = True, created: int = None,
 898              debug: bool = False) -> int:
 899        """
 900        This function tracks a transaction for a specific account.
 901
 902        Parameters:
 903        value (float): The value of the transaction. Default is 0.
 904        desc (str): The description of the transaction. Default is an empty string.
 905        account (str): The account for which the transaction is being tracked. Default is '1'.
 906        logging (bool): Whether to log the transaction. Default is True.
 907        created (int): The timestamp of the transaction. If not provided, it will be generated. Default is None.
 908        debug (bool): Whether to print debug information. Default is False.
 909
 910        Returns:
 911        int: The timestamp of the transaction.
 912
 913        This function creates a new account if it doesn't exist, logs the transaction if logging is True, and updates the account's balance and box.
 914
 915        Raises:
 916        ValueError: The log transaction happened again in the same nanosecond time.
 917        ValueError: The box transaction happened again in the same nanosecond time.
 918        """
 919        if debug:
 920            print('track', f'debug={debug}')
 921        if created is None:
 922            created = self.time()
 923        no_lock = self.nolock()
 924        self.lock()
 925        if not self.account_exists(account):
 926            if debug:
 927                print(f"account {account} created")
 928            self._vault['account'][account] = {
 929                'balance': 0,
 930                'box': {},
 931                'count': 0,
 932                'log': {},
 933                'hide': False,
 934                'zakatable': True,
 935            }
 936            self._step(Action.CREATE, account)
 937        if value == 0:
 938            if no_lock:
 939                self.free(self.lock())
 940            return 0
 941        if logging:
 942            self._log(value=value, desc=desc, account=account, created=created, ref=None, debug=debug)
 943        if debug:
 944            print('create-box', created)
 945        if self.box_exists(account, created):
 946            raise ValueError(f"The box transaction happened again in the same nanosecond time({created}).")
 947        if debug:
 948            print('created-box', created)
 949        self._vault['account'][account]['box'][created] = {
 950            'capital': value,
 951            'count': 0,
 952            'last': 0,
 953            'rest': value,
 954            'total': 0,
 955        }
 956        self._step(Action.TRACK, account, ref=created, value=value)
 957        if no_lock:
 958            self.free(self.lock())
 959        return created
 960
 961    def log_exists(self, account: str, ref: int) -> bool:
 962        """
 963        Checks if a specific transaction log entry exists for a given account.
 964
 965        Parameters:
 966        account (str): The account number associated with the transaction log.
 967        ref (int): The reference to the transaction log entry.
 968
 969        Returns:
 970        bool: True if the transaction log entry exists, False otherwise.
 971        """
 972        return self.ref_exists(account, 'log', ref)
 973
 974    def _log(self, value: float, desc: str = '', account: str = 1, created: int = None, ref: int = None,
 975             debug: bool = False) -> int:
 976        """
 977        Log a transaction into the account's log.
 978
 979        Parameters:
 980        value (float): The value of the transaction.
 981        desc (str): The description of the transaction.
 982        account (str): The account to log the transaction into. Default is '1'.
 983        created (int): The timestamp of the transaction. If not provided, it will be generated.
 984
 985        Returns:
 986        int: The timestamp of the logged transaction.
 987
 988        This method updates the account's balance, count, and log with the transaction details.
 989        It also creates a step in the history of the transaction.
 990
 991        Raises:
 992        ValueError: The log transaction happened again in the same nanosecond time.
 993        """
 994        if debug:
 995            print('_log', f'debug={debug}')
 996        if created is None:
 997            created = self.time()
 998        try:
 999            self._vault['account'][account]['balance'] += value
1000        except TypeError:
1001            self._vault['account'][account]['balance'] += Decimal(value)
1002        self._vault['account'][account]['count'] += 1
1003        if debug:
1004            print('create-log', created)
1005        if self.log_exists(account, created):
1006            raise ValueError(f"The log transaction happened again in the same nanosecond time({created}).")
1007        if debug:
1008            print('created-log', created)
1009        self._vault['account'][account]['log'][created] = {
1010            'value': value,
1011            'desc': desc,
1012            'ref': ref,
1013            'file': {},
1014        }
1015        self._step(Action.LOG, account, ref=created, value=value)
1016        return created
1017
1018    def exchange(self, account, created: int = None, rate: float = None, description: str = None,
1019                 debug: bool = False) -> dict:
1020        """
1021        This method is used to record or retrieve exchange rates for a specific account.
1022
1023        Parameters:
1024        - account (str): The account number for which the exchange rate is being recorded or retrieved.
1025        - created (int): The timestamp of the exchange rate. If not provided, the current timestamp will be used.
1026        - rate (float): The exchange rate to be recorded. If not provided, the method will retrieve the latest exchange rate.
1027        - description (str): A description of the exchange rate.
1028
1029        Returns:
1030        - dict: A dictionary containing the latest exchange rate and its description. If no exchange rate is found,
1031        it returns a dictionary with default values for the rate and description.
1032        """
1033        if debug:
1034            print('exchange', f'debug={debug}')
1035        if created is None:
1036            created = self.time()
1037        no_lock = self.nolock()
1038        self.lock()
1039        if rate is not None:
1040            if rate <= 0:
1041                return dict()
1042            if account not in self._vault['exchange']:
1043                self._vault['exchange'][account] = {}
1044            if len(self._vault['exchange'][account]) == 0 and rate <= 1:
1045                return {"time": created, "rate": 1, "description": None}
1046            self._vault['exchange'][account][created] = {"rate": rate, "description": description}
1047            self._step(Action.EXCHANGE, account, ref=created, value=rate)
1048            if no_lock:
1049                self.free(self.lock())
1050            if debug:
1051                print("exchange-created-1",
1052                      f'account: {account}, created: {created}, rate:{rate}, description:{description}')
1053
1054        if account in self._vault['exchange']:
1055            valid_rates = [(ts, r) for ts, r in self._vault['exchange'][account].items() if ts <= created]
1056            if valid_rates:
1057                latest_rate = max(valid_rates, key=lambda x: x[0])
1058                if debug:
1059                    print("exchange-read-1",
1060                          f'account: {account}, created: {created}, rate:{rate}, description:{description}',
1061                          'latest_rate', latest_rate)
1062                result = latest_rate[1]
1063                result['time'] = latest_rate[0]
1064                return result  # إرجاع قاموس يحتوي على المعدل والوصف
1065        if debug:
1066            print("exchange-read-0", f'account: {account}, created: {created}, rate:{rate}, description:{description}')
1067        return {"time": created, "rate": 1, "description": None}  # إرجاع القيمة الافتراضية مع وصف فارغ
1068
1069    @staticmethod
1070    def exchange_calc(x: float, x_rate: float, y_rate: float) -> float:
1071        """
1072        This function calculates the exchanged amount of a currency.
1073
1074        Args:
1075            x (float): The original amount of the currency.
1076            x_rate (float): The exchange rate of the original currency.
1077            y_rate (float): The exchange rate of the target currency.
1078
1079        Returns:
1080            float: The exchanged amount of the target currency.
1081        """
1082        return (x * x_rate) / y_rate
1083
1084    def exchanges(self) -> dict:
1085        """
1086        Retrieve the recorded exchange rates for all accounts.
1087
1088        Parameters:
1089        None
1090
1091        Returns:
1092        dict: A dictionary containing all recorded exchange rates.
1093        The keys are account names or numbers, and the values are dictionaries containing the exchange rates.
1094        Each exchange rate dictionary has timestamps as keys and exchange rate details as values.
1095        """
1096        return self._vault['exchange'].copy()
1097
1098    def accounts(self) -> dict:
1099        """
1100        Returns a dictionary containing account numbers as keys and their respective balances as values.
1101
1102        Parameters:
1103        None
1104
1105        Returns:
1106        dict: A dictionary where keys are account numbers and values are their respective balances.
1107        """
1108        result = {}
1109        for i in self._vault['account']:
1110            result[i] = self._vault['account'][i]['balance']
1111        return result
1112
1113    def boxes(self, account) -> dict:
1114        """
1115        Retrieve the boxes (transactions) associated with a specific account.
1116
1117        Parameters:
1118        account (str): The account number for which to retrieve the boxes.
1119
1120        Returns:
1121        dict: A dictionary containing the boxes associated with the given account.
1122        If the account does not exist, an empty dictionary is returned.
1123        """
1124        if self.account_exists(account):
1125            return self._vault['account'][account]['box']
1126        return {}
1127
1128    def logs(self, account) -> dict:
1129        """
1130        Retrieve the logs (transactions) associated with a specific account.
1131
1132        Parameters:
1133        account (str): The account number for which to retrieve the logs.
1134
1135        Returns:
1136        dict: A dictionary containing the logs associated with the given account.
1137        If the account does not exist, an empty dictionary is returned.
1138        """
1139        if self.account_exists(account):
1140            return self._vault['account'][account]['log']
1141        return {}
1142
1143    def add_file(self, account: str, ref: int, path: str) -> int:
1144        """
1145        Adds a file reference to a specific transaction log entry in the vault.
1146
1147        Parameters:
1148        account (str): The account number associated with the transaction log.
1149        ref (int): The reference to the transaction log entry.
1150        path (str): The path of the file to be added.
1151
1152        Returns:
1153        int: The reference of the added file. If the account or transaction log entry does not exist, returns 0.
1154        """
1155        if self.account_exists(account):
1156            if ref in self._vault['account'][account]['log']:
1157                file_ref = self.time()
1158                self._vault['account'][account]['log'][ref]['file'][file_ref] = path
1159                no_lock = self.nolock()
1160                self.lock()
1161                self._step(Action.ADD_FILE, account, ref=ref, file=file_ref)
1162                if no_lock:
1163                    self.free(self.lock())
1164                return file_ref
1165        return 0
1166
1167    def remove_file(self, account: str, ref: int, file_ref: int) -> bool:
1168        """
1169        Removes a file reference from a specific transaction log entry in the vault.
1170
1171        Parameters:
1172        account (str): The account number associated with the transaction log.
1173        ref (int): The reference to the transaction log entry.
1174        file_ref (int): The reference of the file to be removed.
1175
1176        Returns:
1177        bool: True if the file reference is successfully removed, False otherwise.
1178        """
1179        if self.account_exists(account):
1180            if ref in self._vault['account'][account]['log']:
1181                if file_ref in self._vault['account'][account]['log'][ref]['file']:
1182                    x = self._vault['account'][account]['log'][ref]['file'][file_ref]
1183                    del self._vault['account'][account]['log'][ref]['file'][file_ref]
1184                    no_lock = self.nolock()
1185                    self.lock()
1186                    self._step(Action.REMOVE_FILE, account, ref=ref, file=file_ref, value=x)
1187                    if no_lock:
1188                        self.free(self.lock())
1189                    return True
1190        return False
1191
1192    def balance(self, account: str = 1, cached: bool = True) -> int:
1193        """
1194        Calculate and return the balance of a specific account.
1195
1196        Parameters:
1197        account (str): The account number. Default is '1'.
1198        cached (bool): If True, use the cached balance. If False, calculate the balance from the box. Default is True.
1199
1200        Returns:
1201        int: The balance of the account.
1202
1203        Note:
1204        If cached is True, the function returns the cached balance.
1205        If cached is False, the function calculates the balance from the box by summing up the 'rest' values of all box items.
1206        """
1207        if cached:
1208            return self._vault['account'][account]['balance']
1209        x = 0
1210        return [x := x + y['rest'] for y in self._vault['account'][account]['box'].values()][-1]
1211
1212    def hide(self, account, status: bool = None) -> bool:
1213        """
1214        Check or set the hide status of a specific account.
1215
1216        Parameters:
1217        account (str): The account number.
1218        status (bool, optional): The new hide status. If not provided, the function will return the current status.
1219
1220        Returns:
1221        bool: The current or updated hide status of the account.
1222
1223        Raises:
1224        None
1225
1226        Example:
1227        >>> tracker = ZakatTracker()
1228        >>> ref = tracker.track(51, 'desc', 'account1')
1229        >>> tracker.hide('account1')  # Set the hide status of 'account1' to True
1230        False
1231        >>> tracker.hide('account1', True)  # Set the hide status of 'account1' to True
1232        True
1233        >>> tracker.hide('account1')  # Get the hide status of 'account1' by default
1234        True
1235        >>> tracker.hide('account1', False)
1236        False
1237        """
1238        if self.account_exists(account):
1239            if status is None:
1240                return self._vault['account'][account]['hide']
1241            self._vault['account'][account]['hide'] = status
1242            return status
1243        return False
1244
1245    def zakatable(self, account, status: bool = None) -> bool:
1246        """
1247        Check or set the zakatable status of a specific account.
1248
1249        Parameters:
1250        account (str): The account number.
1251        status (bool, optional): The new zakatable status. If not provided, the function will return the current status.
1252
1253        Returns:
1254        bool: The current or updated zakatable status of the account.
1255
1256        Raises:
1257        None
1258
1259        Example:
1260        >>> tracker = ZakatTracker()
1261        >>> ref = tracker.track(51, 'desc', 'account1')
1262        >>> tracker.zakatable('account1')  # Set the zakatable status of 'account1' to True
1263        True
1264        >>> tracker.zakatable('account1', True)  # Set the zakatable status of 'account1' to True
1265        True
1266        >>> tracker.zakatable('account1')  # Get the zakatable status of 'account1' by default
1267        True
1268        >>> tracker.zakatable('account1', False)
1269        False
1270        """
1271        if self.account_exists(account):
1272            if status is None:
1273                return self._vault['account'][account]['zakatable']
1274            self._vault['account'][account]['zakatable'] = status
1275            return status
1276        return False
1277
1278    def sub(self, x: float, desc: str = '', account: str = 1, created: int = None, debug: bool = False) -> tuple:
1279        """
1280        Subtracts a specified value from an account's balance.
1281
1282        Parameters:
1283        x (float): The amount to be subtracted.
1284        desc (str): A description for the transaction. Defaults to an empty string.
1285        account (str): The account from which the value will be subtracted. Defaults to '1'.
1286        created (int): The timestamp of the transaction. If not provided, the current timestamp will be used.
1287        debug (bool): A flag indicating whether to print debug information. Defaults to False.
1288
1289        Returns:
1290        tuple: A tuple containing the timestamp of the transaction and a list of tuples representing the age of each transaction.
1291
1292        If the amount to subtract is greater than the account's balance,
1293        the remaining amount will be transferred to a new transaction with a negative value.
1294
1295        Raises:
1296        ValueError: The box transaction happened again in the same nanosecond time.
1297        ValueError: The log transaction happened again in the same nanosecond time.
1298        """
1299        if debug:
1300            print('sub', f'debug={debug}')
1301        if x < 0:
1302            return tuple()
1303        if x == 0:
1304            ref = self.track(x, '', account)
1305            return ref, ref
1306        if created is None:
1307            created = self.time()
1308        no_lock = self.nolock()
1309        self.lock()
1310        self.track(0, '', account)
1311        self._log(value=-x, desc=desc, account=account, created=created, ref=None, debug=debug)
1312        ids = sorted(self._vault['account'][account]['box'].keys())
1313        limit = len(ids) + 1
1314        target = x
1315        if debug:
1316            print('ids', ids)
1317        ages = []
1318        for i in range(-1, -limit, -1):
1319            if target == 0:
1320                break
1321            j = ids[i]
1322            if debug:
1323                print('i', i, 'j', j)
1324            rest = self._vault['account'][account]['box'][j]['rest']
1325            if rest >= target:
1326                self._vault['account'][account]['box'][j]['rest'] -= target
1327                self._step(Action.SUB, account, ref=j, value=target)
1328                ages.append((j, target))
1329                target = 0
1330                break
1331            elif target > rest > 0:
1332                chunk = rest
1333                target -= chunk
1334                self._step(Action.SUB, account, ref=j, value=chunk)
1335                ages.append((j, chunk))
1336                self._vault['account'][account]['box'][j]['rest'] = 0
1337        if target > 0:
1338            self.track(-target, desc, account, False, created)
1339            ages.append((created, target))
1340        if no_lock:
1341            self.free(self.lock())
1342        return created, ages
1343
1344    def transfer(self, amount: int, from_account: str, to_account: str, desc: str = '', created: int = None,
1345                 debug: bool = False) -> list[int]:
1346        """
1347        Transfers a specified value from one account to another.
1348
1349        Parameters:
1350        amount (int): The amount to be transferred.
1351        from_account (str): The account from which the value will be transferred.
1352        to_account (str): The account to which the value will be transferred.
1353        desc (str, optional): A description for the transaction. Defaults to an empty string.
1354        created (int, optional): The timestamp of the transaction. If not provided, the current timestamp will be used.
1355        debug (bool): A flag indicating whether to print debug information. Defaults to False.
1356
1357        Returns:
1358        list[int]: A list of timestamps corresponding to the transactions made during the transfer.
1359
1360        Raises:
1361        ValueError: Transfer to the same account is forbidden.
1362        ValueError: The box transaction happened again in the same nanosecond time.
1363        ValueError: The log transaction happened again in the same nanosecond time.
1364        """
1365        if debug:
1366            print('transfer', f'debug={debug}')
1367        if from_account == to_account:
1368            raise ValueError(f'Transfer to the same account is forbidden. {to_account}')
1369        if amount <= 0:
1370            return []
1371        if created is None:
1372            created = self.time()
1373        (_, ages) = self.sub(amount, desc, from_account, created, debug=debug)
1374        times = []
1375        source_exchange = self.exchange(from_account, created)
1376        target_exchange = self.exchange(to_account, created)
1377
1378        if debug:
1379            print('ages', ages)
1380
1381        for age, value in ages:
1382            target_amount = self.exchange_calc(value, source_exchange['rate'], target_exchange['rate'])
1383            # Perform the transfer
1384            if self.box_exists(to_account, age):
1385                if debug:
1386                    print('box_exists', age)
1387                capital = self._vault['account'][to_account]['box'][age]['capital']
1388                rest = self._vault['account'][to_account]['box'][age]['rest']
1389                if debug:
1390                    print(
1391                        f"Transfer {value} from `{from_account}` to `{to_account}` (equivalent to {target_amount} `{to_account}`).")
1392                selected_age = age
1393                if rest + target_amount > capital:
1394                    self._vault['account'][to_account]['box'][age]['capital'] += target_amount
1395                    selected_age = ZakatTracker.time()
1396                self._vault['account'][to_account]['box'][age]['rest'] += target_amount
1397                self._step(Action.BOX_TRANSFER, to_account, ref=selected_age, value=target_amount)
1398                y = self._log(value=target_amount, desc=f'TRANSFER {from_account} -> {to_account}', account=to_account,
1399                              created=None, ref=None, debug=debug)
1400                times.append((age, y))
1401                continue
1402            y = self.track(target_amount, desc, to_account, logging=True, created=age, debug=debug)
1403            if debug:
1404                print(
1405                    f"Transferred {value} from `{from_account}` to `{to_account}` (equivalent to {target_amount} `{to_account}`).")
1406            times.append(y)
1407        return times
1408
1409    def check(self, silver_gram_price: float, nisab: float = None, debug: bool = False, now: int = None,
1410              cycle: float = None) -> tuple:
1411        """
1412        Check the eligibility for Zakat based on the given parameters.
1413
1414        Parameters:
1415        silver_gram_price (float): The price of a gram of silver.
1416        nisab (float): The minimum amount of wealth required for Zakat. If not provided,
1417                        it will be calculated based on the silver_gram_price.
1418        debug (bool): Flag to enable debug mode.
1419        now (int): The current timestamp. If not provided, it will be calculated using ZakatTracker.time().
1420        cycle (float): The time cycle for Zakat. If not provided, it will be calculated using ZakatTracker.TimeCycle().
1421
1422        Returns:
1423        tuple: A tuple containing a boolean indicating the eligibility for Zakat, a list of brief statistics,
1424        and a dictionary containing the Zakat plan.
1425        """
1426        if debug:
1427            print('check', f'debug={debug}')
1428        if now is None:
1429            now = self.time()
1430        if cycle is None:
1431            cycle = ZakatTracker.TimeCycle()
1432        if nisab is None:
1433            nisab = ZakatTracker.Nisab(silver_gram_price)
1434        plan = {}
1435        below_nisab = 0
1436        brief = [0, 0, 0]
1437        valid = False
1438        if debug:
1439            print('exchanges', self.exchanges())
1440        for x in self._vault['account']:
1441            if not self.zakatable(x):
1442                continue
1443            _box = self._vault['account'][x]['box']
1444            _log = self._vault['account'][x]['log']
1445            limit = len(_box) + 1
1446            ids = sorted(self._vault['account'][x]['box'].keys())
1447            for i in range(-1, -limit, -1):
1448                j = ids[i]
1449                rest = float(_box[j]['rest'])
1450                if rest <= 0:
1451                    continue
1452                exchange = self.exchange(x, created=self.time())
1453                rest = ZakatTracker.exchange_calc(rest, float(exchange['rate']), 1)
1454                brief[0] += rest
1455                index = limit + i - 1
1456                epoch = (now - j) / cycle
1457                if debug:
1458                    print(f"Epoch: {epoch}", _box[j])
1459                if _box[j]['last'] > 0:
1460                    epoch = (now - _box[j]['last']) / cycle
1461                if debug:
1462                    print(f"Epoch: {epoch}")
1463                epoch = floor(epoch)
1464                if debug:
1465                    print(f"Epoch: {epoch}", type(epoch), epoch == 0, 1 - epoch, epoch)
1466                if epoch == 0:
1467                    continue
1468                if debug:
1469                    print("Epoch - PASSED")
1470                brief[1] += rest
1471                if rest >= nisab:
1472                    total = 0
1473                    for _ in range(epoch):
1474                        total += ZakatTracker.ZakatCut(float(rest) - float(total))
1475                    if total > 0:
1476                        if x not in plan:
1477                            plan[x] = {}
1478                        valid = True
1479                        brief[2] += total
1480                        plan[x][index] = {
1481                            'total': total,
1482                            'count': epoch,
1483                            'box_time': j,
1484                            'box_capital': _box[j]['capital'],
1485                            'box_rest': _box[j]['rest'],
1486                            'box_last': _box[j]['last'],
1487                            'box_total': _box[j]['total'],
1488                            'box_count': _box[j]['count'],
1489                            'box_log': _log[j]['desc'],
1490                            'exchange_rate': exchange['rate'],
1491                            'exchange_time': exchange['time'],
1492                            'exchange_desc': exchange['description'],
1493                        }
1494                else:
1495                    chunk = ZakatTracker.ZakatCut(float(rest))
1496                    if chunk > 0:
1497                        if x not in plan:
1498                            plan[x] = {}
1499                        if j not in plan[x].keys():
1500                            plan[x][index] = {}
1501                        below_nisab += rest
1502                        brief[2] += chunk
1503                        plan[x][index]['below_nisab'] = chunk
1504                        plan[x][index]['total'] = chunk
1505                        plan[x][index]['count'] = epoch
1506                        plan[x][index]['box_time'] = j
1507                        plan[x][index]['box_capital'] = _box[j]['capital']
1508                        plan[x][index]['box_rest'] = _box[j]['rest']
1509                        plan[x][index]['box_last'] = _box[j]['last']
1510                        plan[x][index]['box_total'] = _box[j]['total']
1511                        plan[x][index]['box_count'] = _box[j]['count']
1512                        plan[x][index]['box_log'] = _log[j]['desc']
1513                        plan[x][index]['exchange_rate'] = exchange['rate']
1514                        plan[x][index]['exchange_time'] = exchange['time']
1515                        plan[x][index]['exchange_desc'] = exchange['description']
1516        valid = valid or below_nisab >= nisab
1517        if debug:
1518            print(f"below_nisab({below_nisab}) >= nisab({nisab})")
1519        return valid, brief, plan
1520
1521    def build_payment_parts(self, demand: float, positive_only: bool = True) -> dict:
1522        """
1523        Build payment parts for the Zakat distribution.
1524
1525        Parameters:
1526        demand (float): The total demand for payment in local currency.
1527        positive_only (bool): If True, only consider accounts with positive balance. Default is True.
1528
1529        Returns:
1530        dict: A dictionary containing the payment parts for each account. The dictionary has the following structure:
1531        {
1532            'account': {
1533                'account_id': {'balance': float, 'rate': float, 'part': float},
1534                ...
1535            },
1536            'exceed': bool,
1537            'demand': float,
1538            'total': float,
1539        }
1540        """
1541        total = 0
1542        parts = {
1543            'account': {},
1544            'exceed': False,
1545            'demand': demand,
1546        }
1547        for x, y in self.accounts().items():
1548            if positive_only and y <= 0:
1549                continue
1550            total += float(y)
1551            exchange = self.exchange(x)
1552            parts['account'][x] = {'balance': y, 'rate': exchange['rate'], 'part': 0}
1553        parts['total'] = total
1554        return parts
1555
1556    @staticmethod
1557    def check_payment_parts(parts: dict, debug: bool = False) -> int:
1558        """
1559        Checks the validity of payment parts.
1560
1561        Parameters:
1562        parts (dict): A dictionary containing payment parts information.
1563        debug (bool): Flag to enable debug mode.
1564
1565        Returns:
1566        int: Returns 0 if the payment parts are valid, otherwise returns the error code.
1567
1568        Error Codes:
1569        1: 'demand', 'account', 'total', or 'exceed' key is missing in parts.
1570        2: 'balance', 'rate' or 'part' key is missing in parts['account'][x].
1571        3: 'part' value in parts['account'][x] is less than 0.
1572        4: If 'exceed' is False, 'balance' value in parts['account'][x] is less than or equal to 0.
1573        5: If 'exceed' is False, 'part' value in parts['account'][x] is greater than 'balance' value.
1574        6: The sum of 'part' values in parts['account'] does not match with 'demand' value.
1575        """
1576        if debug:
1577            print('check_payment_parts', f'debug={debug}')
1578        for i in ['demand', 'account', 'total', 'exceed']:
1579            if i not in parts:
1580                return 1
1581        exceed = parts['exceed']
1582        for x in parts['account']:
1583            for j in ['balance', 'rate', 'part']:
1584                if j not in parts['account'][x]:
1585                    return 2
1586                if parts['account'][x]['part'] < 0:
1587                    return 3
1588                if not exceed and parts['account'][x]['balance'] <= 0:
1589                    return 4
1590        demand = parts['demand']
1591        z = 0
1592        for _, y in parts['account'].items():
1593            if not exceed and y['part'] > y['balance']:
1594                return 5
1595            z += ZakatTracker.exchange_calc(y['part'], y['rate'], 1)
1596        z = round(z, 2)
1597        demand = round(demand, 2)
1598        if debug:
1599            print('check_payment_parts', f'z = {z}, demand = {demand}')
1600            print('check_payment_parts', type(z), type(demand))
1601            print('check_payment_parts', z != demand)
1602            print('check_payment_parts', str(z) != str(demand))
1603        if z != demand and str(z) != str(demand):
1604            return 6
1605        return 0
1606
1607    def zakat(self, report: tuple, parts: Dict[str, Dict | bool | Any] = None, debug: bool = False) -> bool:
1608        """
1609        Perform Zakat calculation based on the given report and optional parts.
1610
1611        Parameters:
1612        report (tuple): A tuple containing the validity of the report, the report data, and the zakat plan.
1613        parts (dict): A dictionary containing the payment parts for the zakat.
1614        debug (bool): A flag indicating whether to print debug information.
1615
1616        Returns:
1617        bool: True if the zakat calculation is successful, False otherwise.
1618        """
1619        if debug:
1620            print('zakat', f'debug={debug}')
1621        valid, _, plan = report
1622        if not valid:
1623            return valid
1624        parts_exist = parts is not None
1625        if parts_exist:
1626            if self.check_payment_parts(parts, debug=debug) != 0:
1627                return False
1628        if debug:
1629            print('######### zakat #######')
1630            print('parts_exist', parts_exist)
1631        no_lock = self.nolock()
1632        self.lock()
1633        report_time = self.time()
1634        self._vault['report'][report_time] = report
1635        self._step(Action.REPORT, ref=report_time)
1636        created = self.time()
1637        for x in plan:
1638            target_exchange = self.exchange(x)
1639            if debug:
1640                print(plan[x])
1641                print('-------------')
1642                print(self._vault['account'][x]['box'])
1643            ids = sorted(self._vault['account'][x]['box'].keys())
1644            if debug:
1645                print('plan[x]', plan[x])
1646            for i in plan[x].keys():
1647                j = ids[i]
1648                if debug:
1649                    print('i', i, 'j', j)
1650                self._step(Action.ZAKAT, account=x, ref=j, value=self._vault['account'][x]['box'][j]['last'],
1651                           key='last',
1652                           math_operation=MathOperation.EQUAL)
1653                self._vault['account'][x]['box'][j]['last'] = created
1654                amount = ZakatTracker.exchange_calc(float(plan[x][i]['total']), 1, float(target_exchange['rate']))
1655                self._vault['account'][x]['box'][j]['total'] += amount
1656                self._step(Action.ZAKAT, account=x, ref=j, value=amount, key='total',
1657                           math_operation=MathOperation.ADDITION)
1658                self._vault['account'][x]['box'][j]['count'] += plan[x][i]['count']
1659                self._step(Action.ZAKAT, account=x, ref=j, value=plan[x][i]['count'], key='count',
1660                           math_operation=MathOperation.ADDITION)
1661                if not parts_exist:
1662                    try:
1663                        self._vault['account'][x]['box'][j]['rest'] -= amount
1664                    except TypeError:
1665                        self._vault['account'][x]['box'][j]['rest'] -= Decimal(amount)
1666                    # self._step(Action.ZAKAT, account=x, ref=j, value=amount, key='rest',
1667                    #            math_operation=MathOperation.SUBTRACTION)
1668                    self._log(-float(amount), desc='zakat-زكاة', account=x, created=None, ref=j, debug=debug)
1669        if parts_exist:
1670            for account, part in parts['account'].items():
1671                if part['part'] == 0:
1672                    continue
1673                if debug:
1674                    print('zakat-part', account, part['rate'])
1675                target_exchange = self.exchange(account)
1676                amount = ZakatTracker.exchange_calc(part['part'], part['rate'], target_exchange['rate'])
1677                self.sub(amount, desc='zakat-part-دفعة-زكاة', account=account, debug=debug)
1678        if no_lock:
1679            self.free(self.lock())
1680        return True
1681
1682    def export_json(self, path: str = "data.json") -> bool:
1683        """
1684        Exports the current state of the ZakatTracker object to a JSON file.
1685
1686        Parameters:
1687        path (str): The path where the JSON file will be saved. Default is "data.json".
1688
1689        Returns:
1690        bool: True if the export is successful, False otherwise.
1691
1692        Raises:
1693        No specific exceptions are raised by this method.
1694        """
1695        with open(path, "w") as file:
1696            json.dump(self._vault, file, indent=4, cls=JSONEncoder)
1697            return True
1698
1699    def save(self, path: str = None) -> bool:
1700        """
1701        Saves the ZakatTracker's current state to a pickle file.
1702
1703        This method serializes the internal data (`_vault`) along with metadata
1704        (Python version, pickle protocol) for future compatibility.
1705
1706        Parameters:
1707        path (str, optional): File path for saving. Defaults to a predefined location.
1708
1709        Returns:
1710        bool: True if the save operation is successful, False otherwise.
1711        """
1712        if path is None:
1713            path = self.path()
1714        with open(path, "wb") as f:
1715            version = f'{version_info.major}.{version_info.minor}.{version_info.micro}'
1716            pickle_protocol = pickle.HIGHEST_PROTOCOL
1717            data = {
1718                'python_version': version,
1719                'pickle_protocol': pickle_protocol,
1720                'data': self._vault,
1721            }
1722            pickle.dump(data, f, protocol=pickle_protocol)
1723            return True
1724
1725    def load(self, path: str = None) -> bool:
1726        """
1727        Load the current state of the ZakatTracker object from a pickle file.
1728
1729        Parameters:
1730        path (str): The path where the pickle file is located. If not provided, it will use the default path.
1731
1732        Returns:
1733        bool: True if the load operation is successful, False otherwise.
1734        """
1735        if path is None:
1736            path = self.path()
1737        if os.path.exists(path):
1738            with open(path, "rb") as f:
1739                data = pickle.load(f)
1740                self._vault = data['data']
1741                return True
1742        return False
1743
1744    def import_csv_cache_path(self):
1745        """
1746        Generates the cache file path for imported CSV data.
1747
1748        This function constructs the file path where cached data from CSV imports
1749        will be stored. The cache file is a pickle file (.pickle extension) appended
1750        to the base path of the object.
1751
1752        Returns:
1753        str: The full path to the import CSV cache file.
1754
1755        Example:
1756            >>> obj = ZakatTracker('/data/reports')
1757            >>> obj.import_csv_cache_path()
1758            '/data/reports.import_csv.pickle'
1759        """
1760        path = str(self.path())
1761        if path.endswith(".pickle"):
1762            path = path[:-7]
1763        return path + '.import_csv.pickle'
1764
1765    def import_csv(self, path: str = 'file.csv', debug: bool = False) -> tuple:
1766        """
1767        The function reads the CSV file, checks for duplicate transactions, and creates the transactions in the system.
1768
1769        Parameters:
1770        path (str): The path to the CSV file. Default is 'file.csv'.
1771        debug (bool): A flag indicating whether to print debug information.
1772
1773        Returns:
1774        tuple: A tuple containing the number of transactions created, the number of transactions found in the cache,
1775                and a dictionary of bad transactions.
1776
1777        Notes:
1778            * Currency Pair Assumption: This function assumes that the exchange rates stored for each account
1779                                        are appropriate for the currency pairs involved in the conversions.
1780            * The exchange rate for each account is based on the last encountered transaction rate that is not equal
1781                to 1.0 or the previous rate for that account.
1782            * Those rates will be merged into the exchange rates main data, and later it will be used for all subsequent
1783              transactions of the same account within the whole imported and existing dataset when doing `check` and
1784              `zakat` operations.
1785
1786        Example Usage:
1787            The CSV file should have the following format, rate is optional per transaction:
1788            account, desc, value, date, rate
1789            For example:
1790            safe-45, "Some text", 34872, 1988-06-30 00:00:00, 1
1791        """
1792        if debug:
1793            print('import_csv', f'debug={debug}')
1794        cache: list[int] = []
1795        try:
1796            with open(self.import_csv_cache_path(), "rb") as f:
1797                cache = pickle.load(f)
1798        except:
1799            pass
1800        date_formats = [
1801            "%Y-%m-%d %H:%M:%S",
1802            "%Y-%m-%dT%H:%M:%S",
1803            "%Y-%m-%dT%H%M%S",
1804            "%Y-%m-%d",
1805        ]
1806        created, found, bad = 0, 0, {}
1807        data: dict[int, list] = {}
1808        with open(path, newline='', encoding="utf-8") as f:
1809            i = 0
1810            for row in csv.reader(f, delimiter=','):
1811                i += 1
1812                hashed = hash(tuple(row))
1813                if hashed in cache:
1814                    found += 1
1815                    continue
1816                account = row[0]
1817                desc = row[1]
1818                value = float(row[2])
1819                rate = 1.0
1820                if row[4:5]:  # Empty list if index is out of range
1821                    rate = float(row[4])
1822                date: int = 0
1823                for time_format in date_formats:
1824                    try:
1825                        date = self.time(datetime.datetime.strptime(row[3], time_format))
1826                        break
1827                    except:
1828                        pass
1829                # TODO: not allowed for negative dates
1830                if date == 0 or value == 0:
1831                    bad[i] = row
1832                    continue
1833                if date not in data:
1834                    data[date] = []
1835                # TODO: If duplicated time with different accounts with the same amount it is an indicator of a transfer
1836                data[date].append((date, value, desc, account, rate, hashed))
1837
1838        if debug:
1839            print('import_csv', len(data))
1840
1841        def process(row, index=0):
1842            nonlocal created
1843            (date, value, desc, account, rate, hashed) = row
1844            date += index
1845            if rate > 1:
1846                self.exchange(account, created=date, rate=rate)
1847            if value > 0:
1848                self.track(value, desc, account, True, date)
1849            elif value < 0:
1850                self.sub(-value, desc, account, date)
1851            created += 1
1852            cache.append(hashed)
1853
1854        for date, rows in sorted(data.items()):
1855            len_rows = len(rows)
1856            if len_rows == 1:
1857                process(rows[0])
1858                continue
1859            if debug:
1860                print('-- Duplicated time detected', date, 'len', len_rows)
1861                print(rows)
1862                print('---------------------------------')
1863            for index, row in enumerate(rows):
1864                process(row, index)
1865        with open(self.import_csv_cache_path(), "wb") as f:
1866            pickle.dump(cache, f)
1867        return created, found, bad
1868
1869    ########
1870    # TESTS #
1871    #######
1872
1873    @staticmethod
1874    def human_readable_size(size: float, decimal_places: int = 2) -> str:
1875        """
1876        Converts a size in bytes to a human-readable format (e.g., KB, MB, GB).
1877
1878        This function iterates through progressively larger units of information
1879        (B, KB, MB, GB, etc.) and divides the input size until it fits within a
1880        range that can be expressed with a reasonable number before the unit.
1881
1882        Parameters:
1883        size (float): The size in bytes to convert.
1884        decimal_places (int, optional): The number of decimal places to display
1885            in the result. Defaults to 2.
1886
1887        Returns:
1888        str: A string representation of the size in a human-readable format,
1889            rounded to the specified number of decimal places. For example:
1890                - "1.50 KB" (1536 bytes)
1891                - "23.00 MB" (24117248 bytes)
1892                - "1.23 GB" (1325899906 bytes)
1893        """
1894        if type(size) not in (float, int):
1895            raise TypeError("size must be a float or integer")
1896        if type(decimal_places) != int:
1897            raise TypeError("decimal_places must be an integer")
1898        for unit in ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']:
1899            if size < 1024.0:
1900                break
1901            size /= 1024.0
1902        return f"{size:.{decimal_places}f} {unit}"
1903
1904    @staticmethod
1905    def get_dict_size(obj: dict, seen: set = None) -> float:
1906        """
1907        Recursively calculates the approximate memory size of a dictionary and its contents in bytes.
1908
1909        This function traverses the dictionary structure, accounting for the size of keys, values,
1910        and any nested objects. It handles various data types commonly found in dictionaries
1911        (e.g., lists, tuples, sets, numbers, strings) and prevents infinite recursion in case
1912        of circular references.
1913
1914        Parameters:
1915        obj (dict): The dictionary whose size is to be calculated.
1916        seen (set, optional): A set used internally to track visited objects
1917                             and avoid circular references. Defaults to None.
1918
1919        Returns:
1920            float: An approximate size of the dictionary and its contents in bytes.
1921
1922        Note:
1923        - This function is a method of the `ZakatTracker` class and is likely used to
1924          estimate the memory footprint of data structures relevant to Zakat calculations.
1925        - The size calculation is approximate as it relies on `sys.getsizeof()`, which might
1926          not account for all memory overhead depending on the Python implementation.
1927        - Circular references are handled to prevent infinite recursion.
1928        - Basic numeric types (int, float, complex) are assumed to have fixed sizes.
1929        - String sizes are estimated based on character length and encoding.
1930        """
1931        size = 0
1932        if seen is None:
1933            seen = set()
1934
1935        obj_id = id(obj)
1936        if obj_id in seen:
1937            return 0
1938
1939        seen.add(obj_id)
1940        size += sys.getsizeof(obj)
1941
1942        if isinstance(obj, dict):
1943            for k, v in obj.items():
1944                size += ZakatTracker.get_dict_size(k, seen)
1945                size += ZakatTracker.get_dict_size(v, seen)
1946        elif isinstance(obj, (list, tuple, set, frozenset)):
1947            for item in obj:
1948                size += ZakatTracker.get_dict_size(item, seen)
1949        elif isinstance(obj, (int, float, complex)):  # Handle numbers
1950            pass  # Basic numbers have a fixed size, so nothing to add here
1951        elif isinstance(obj, str):  # Handle strings
1952            size += len(obj) * sys.getsizeof(str().encode())  # Size per character in bytes
1953        return size
1954
1955    @staticmethod
1956    def duration_from_nanoseconds(ns: int,
1957                                  show_zeros_in_spoken_time: bool = False,
1958                                  spoken_time_separator=',',
1959                                  millennia: str = 'Millennia',
1960                                  century: str = 'Century',
1961                                  years: str = 'Years',
1962                                  days: str = 'Days',
1963                                  hours: str = 'Hours',
1964                                  minutes: str = 'Minutes',
1965                                  seconds: str = 'Seconds',
1966                                  milli_seconds: str = 'MilliSeconds',
1967                                  micro_seconds: str = 'MicroSeconds',
1968                                  nano_seconds: str = 'NanoSeconds',
1969                                  ) -> tuple:
1970        """
1971        REF https://github.com/JayRizzo/Random_Scripts/blob/master/time_measure.py#L106
1972        Convert NanoSeconds to Human Readable Time Format.
1973        A NanoSeconds is a unit of time in the International System of Units (SI) equal
1974        to one millionth (0.000001 or 10−6 or 1⁄1,000,000) of a second.
1975        Its symbol is μs, sometimes simplified to us when Unicode is not available.
1976        A microsecond is equal to 1000 nanoseconds or 1⁄1,000 of a millisecond.
1977
1978        INPUT : ms (AKA: MilliSeconds)
1979        OUTPUT: tuple(string time_lapsed, string spoken_time) like format.
1980        OUTPUT Variables: time_lapsed, spoken_time
1981
1982        Example  Input: duration_from_nanoseconds(ns)
1983        **"Millennium:Century:Years:Days:Hours:Minutes:Seconds:MilliSeconds:MicroSeconds:NanoSeconds"**
1984        Example Output: ('039:0001:047:325:05:02:03:456:789:012', ' 39 Millennia,    1 Century,  47 Years,  325 Days,  5 Hours,  2 Minutes,  3 Seconds,  456 MilliSeconds,  789 MicroSeconds,  12 NanoSeconds')
1985        duration_from_nanoseconds(1234567890123456789012)
1986        """
1987        us, ns = divmod(ns, 1000)
1988        ms, us = divmod(us, 1000)
1989        s, ms = divmod(ms, 1000)
1990        m, s = divmod(s, 60)
1991        h, m = divmod(m, 60)
1992        d, h = divmod(h, 24)
1993        y, d = divmod(d, 365)
1994        c, y = divmod(y, 100)
1995        n, c = divmod(c, 10)
1996        time_lapsed = f"{n:03.0f}:{c:04.0f}:{y:03.0f}:{d:03.0f}:{h:02.0f}:{m:02.0f}:{s:02.0f}::{ms:03.0f}::{us:03.0f}::{ns:03.0f}"
1997        spoken_time_part = []
1998        if n > 0 or show_zeros_in_spoken_time:
1999            spoken_time_part.append(f"{n: 3d} {millennia}")
2000        if c > 0 or show_zeros_in_spoken_time:
2001            spoken_time_part.append(f"{c: 4d} {century}")
2002        if y > 0 or show_zeros_in_spoken_time:
2003            spoken_time_part.append(f"{y: 3d} {years}")
2004        if d > 0 or show_zeros_in_spoken_time:
2005            spoken_time_part.append(f"{d: 4d} {days}")
2006        if h > 0 or show_zeros_in_spoken_time:
2007            spoken_time_part.append(f"{h: 2d} {hours}")
2008        if m > 0 or show_zeros_in_spoken_time:
2009            spoken_time_part.append(f"{m: 2d} {minutes}")
2010        if s > 0 or show_zeros_in_spoken_time:
2011            spoken_time_part.append(f"{s: 2d} {seconds}")
2012        if ms > 0 or show_zeros_in_spoken_time:
2013            spoken_time_part.append(f"{ms: 3d} {milli_seconds}")
2014        if us > 0 or show_zeros_in_spoken_time:
2015            spoken_time_part.append(f"{us: 3d} {micro_seconds}")
2016        if ns > 0 or show_zeros_in_spoken_time:
2017            spoken_time_part.append(f"{ns: 3d} {nano_seconds}")
2018        return time_lapsed, spoken_time_separator.join(spoken_time_part)
2019
2020    @staticmethod
2021    def day_to_time(day: int, month: int = 6, year: int = 2024) -> int:  # افتراض أن الشهر هو يونيو والسنة 2024
2022        """
2023        Convert a specific day, month, and year into a timestamp.
2024
2025        Parameters:
2026        day (int): The day of the month.
2027        month (int): The month of the year. Default is 6 (June).
2028        year (int): The year. Default is 2024.
2029
2030        Returns:
2031        int: The timestamp representing the given day, month, and year.
2032
2033        Note:
2034        This method assumes the default month and year if not provided.
2035        """
2036        return ZakatTracker.time(datetime.datetime(year, month, day))
2037
2038    @staticmethod
2039    def generate_random_date(start_date: datetime.datetime, end_date: datetime.datetime) -> datetime.datetime:
2040        """
2041        Generate a random date between two given dates.
2042
2043        Parameters:
2044        start_date (datetime.datetime): The start date from which to generate a random date.
2045        end_date (datetime.datetime): The end date until which to generate a random date.
2046
2047        Returns:
2048        datetime.datetime: A random date between the start_date and end_date.
2049        """
2050        time_between_dates = end_date - start_date
2051        days_between_dates = time_between_dates.days
2052        random_number_of_days = random.randrange(days_between_dates)
2053        return start_date + datetime.timedelta(days=random_number_of_days)
2054
2055    @staticmethod
2056    def generate_random_csv_file(path: str = "data.csv", count: int = 1000, with_rate: bool = False,
2057                                 debug: bool = False) -> int:
2058        """
2059        Generate a random CSV file with specified parameters.
2060
2061        Parameters:
2062        path (str): The path where the CSV file will be saved. Default is "data.csv".
2063        count (int): The number of rows to generate in the CSV file. Default is 1000.
2064        with_rate (bool): If True, a random rate between 1.2% and 12% is added. Default is False.
2065        debug (bool): A flag indicating whether to print debug information.
2066
2067        Returns:
2068        None. The function generates a CSV file at the specified path with the given count of rows.
2069        Each row contains a randomly generated account, description, value, and date.
2070        The value is randomly generated between 1000 and 100000,
2071        and the date is randomly generated between 1950-01-01 and 2023-12-31.
2072        If the row number is not divisible by 13, the value is multiplied by -1.
2073        """
2074        if debug:
2075            print('generate_random_csv_file', f'debug={debug}')
2076        i = 0
2077        with open(path, "w", newline="") as csvfile:
2078            writer = csv.writer(csvfile)
2079            for i in range(count):
2080                account = f"acc-{random.randint(1, 1000)}"
2081                desc = f"Some text {random.randint(1, 1000)}"
2082                value = random.randint(1000, 100000)
2083                date = ZakatTracker.generate_random_date(datetime.datetime(1950, 1, 1),
2084                                                         datetime.datetime(2023, 12, 31)).strftime("%Y-%m-%d %H:%M:%S")
2085                if not i % 13 == 0:
2086                    value *= -1
2087                row = [account, desc, value, date]
2088                if with_rate:
2089                    rate = random.randint(1, 100) * 0.12
2090                    if debug:
2091                        print('before-append', row)
2092                    row.append(rate)
2093                    if debug:
2094                        print('after-append', row)
2095                writer.writerow(row)
2096                i = i + 1
2097        return i
2098
2099    @staticmethod
2100    def create_random_list(max_sum, min_value=0, max_value=10):
2101        """
2102        Creates a list of random integers whose sum does not exceed the specified maximum.
2103
2104        Args:
2105            max_sum: The maximum allowed sum of the list elements.
2106            min_value: The minimum possible value for an element (inclusive).
2107            max_value: The maximum possible value for an element (inclusive).
2108
2109        Returns:
2110            A list of random integers.
2111        """
2112        result = []
2113        current_sum = 0
2114
2115        while current_sum < max_sum:
2116            # Calculate the remaining space for the next element
2117            remaining_sum = max_sum - current_sum
2118            # Determine the maximum possible value for the next element
2119            next_max_value = min(remaining_sum, max_value)
2120            # Generate a random element within the allowed range
2121            next_element = random.randint(min_value, next_max_value)
2122            result.append(next_element)
2123            current_sum += next_element
2124
2125        return result
2126
2127    def _test_core(self, restore=False, debug=False):
2128
2129        if debug:
2130            random.seed(1234567890)
2131
2132        # sanity check - random forward time
2133
2134        xlist = []
2135        limit = 1000
2136        for _ in range(limit):
2137            y = ZakatTracker.time()
2138            z = '-'
2139            if y not in xlist:
2140                xlist.append(y)
2141            else:
2142                z = 'x'
2143            if debug:
2144                print(z, y)
2145        xx = len(xlist)
2146        if debug:
2147            print('count', xx, ' - unique: ', (xx / limit) * 100, '%')
2148        assert limit == xx
2149
2150        # sanity check - convert date since 1000AD
2151
2152        for year in range(1000, 9000):
2153            ns = ZakatTracker.time(datetime.datetime.strptime(f"{year}-12-30 18:30:45", "%Y-%m-%d %H:%M:%S"))
2154            date = ZakatTracker.time_to_datetime(ns)
2155            if debug:
2156                print(date)
2157            assert date.year == year
2158            assert date.month == 12
2159            assert date.day == 30
2160            assert date.hour == 18
2161            assert date.minute == 30
2162            assert date.second in [44, 45]
2163
2164        # human_readable_size
2165
2166        assert ZakatTracker.human_readable_size(0) == "0.00 B"
2167        assert ZakatTracker.human_readable_size(512) == "512.00 B"
2168        assert ZakatTracker.human_readable_size(1023) == "1023.00 B"
2169
2170        assert ZakatTracker.human_readable_size(1024) == "1.00 KB"
2171        assert ZakatTracker.human_readable_size(2048) == "2.00 KB"
2172        assert ZakatTracker.human_readable_size(5120) == "5.00 KB"
2173
2174        assert ZakatTracker.human_readable_size(1024 ** 2) == "1.00 MB"
2175        assert ZakatTracker.human_readable_size(2.5 * 1024 ** 2) == "2.50 MB"
2176
2177        assert ZakatTracker.human_readable_size(1024 ** 3) == "1.00 GB"
2178        assert ZakatTracker.human_readable_size(1024 ** 4) == "1.00 TB"
2179        assert ZakatTracker.human_readable_size(1024 ** 5) == "1.00 PB"
2180
2181        assert ZakatTracker.human_readable_size(1536, decimal_places=0) == "2 KB"
2182        assert ZakatTracker.human_readable_size(2.5 * 1024 ** 2, decimal_places=1) == "2.5 MB"
2183        assert ZakatTracker.human_readable_size(1234567890, decimal_places=3) == "1.150 GB"
2184
2185        try:
2186            ZakatTracker.human_readable_size("not a number")
2187            assert False, "Expected TypeError for invalid input"
2188        except TypeError:
2189            pass
2190
2191        try:
2192            ZakatTracker.human_readable_size(1024, decimal_places="not an int")
2193            assert False, "Expected TypeError for invalid decimal_places"
2194        except TypeError:
2195            pass
2196
2197        # get_dict_size
2198        assert ZakatTracker.get_dict_size({}) == sys.getsizeof({}), "Empty dictionary size mismatch"
2199        assert ZakatTracker.get_dict_size({"a": 1, "b": 2.5, "c": True}) != sys.getsizeof({}), "Not Empty dictionary"
2200
2201        # number scale
2202        error = 0
2203        total = 0
2204        for max_i, max_j, decimal_places in [
2205            (101, 101, 2),  # fiat currency minimum unit took 2 decimal places
2206            (1, 1_000, 8),  # cryptocurrency like Satoshi in Bitcoin took 8 decimal places
2207            (1, 1_000, 18)  # cryptocurrency like Wei in Ethereum took 18 decimal places
2208        ]:
2209            for return_type in (
2210                    float,
2211                    Decimal,
2212            ):
2213                for i in range(max_i):
2214                    for j in range(max_j):
2215                        total += 1
2216                        num_str = f'{i}.{j:0{decimal_places}d}'
2217                        num = return_type(num_str)
2218                        scaled = self.scale(num, decimal_places=decimal_places)
2219                        unscaled = self.unscale(scaled, return_type=return_type, decimal_places=decimal_places)
2220                        if debug:
2221                            print(
2222                                f'return_type: {return_type}, num_str: {num_str} - num: {num} - scaled: {scaled} - unscaled: {unscaled}')
2223                        if unscaled != num:
2224                            if debug:
2225                                print('***** SCALE ERROR *****')
2226                            error += 1
2227        if debug:
2228            print(f'total: {total}, error({error}): {100 * error / total}%')
2229        assert error == 0
2230
2231        assert self.nolock()
2232        assert self._history() is True
2233
2234        table = {
2235            1: [
2236                (0, 10, 10, 10, 10, 1, 1),
2237                (0, 20, 30, 30, 30, 2, 2),
2238                (0, 30, 60, 60, 60, 3, 3),
2239                (1, 15, 45, 45, 45, 3, 4),
2240                (1, 50, -5, -5, -5, 4, 5),
2241                (1, 100, -105, -105, -105, 5, 6),
2242            ],
2243            'wallet': [
2244                (1, 90, -90, -90, -90, 1, 1),
2245                (0, 100, 10, 10, 10, 2, 2),
2246                (1, 190, -180, -180, -180, 3, 3),
2247                (0, 1000, 820, 820, 820, 4, 4),
2248            ],
2249        }
2250        for x in table:
2251            for y in table[x]:
2252                self.lock()
2253                if y[0] == 0:
2254                    ref = self.track(y[1], 'test-add', x, True, ZakatTracker.time(), debug)
2255                else:
2256                    (ref, z) = self.sub(y[1], 'test-sub', x, ZakatTracker.time())
2257                    if debug:
2258                        print('_sub', z, ZakatTracker.time())
2259                assert ref != 0
2260                assert len(self._vault['account'][x]['log'][ref]['file']) == 0
2261                for i in range(3):
2262                    file_ref = self.add_file(x, ref, 'file_' + str(i))
2263                    sleep(0.0000001)
2264                    assert file_ref != 0
2265                    if debug:
2266                        print('ref', ref, 'file', file_ref)
2267                    assert len(self._vault['account'][x]['log'][ref]['file']) == i + 1
2268                file_ref = self.add_file(x, ref, 'file_' + str(3))
2269                assert self.remove_file(x, ref, file_ref)
2270                assert self.balance(x) == y[2]
2271                z = self.balance(x, False)
2272                if debug:
2273                    print("debug-1", z, y[3])
2274                assert z == y[3]
2275                o = self._vault['account'][x]['log']
2276                z = 0
2277                for i in o:
2278                    z += o[i]['value']
2279                if debug:
2280                    print("debug-2", z, type(z))
2281                    print("debug-2", y[4], type(y[4]))
2282                assert z == y[4]
2283                if debug:
2284                    print('debug-2 - PASSED')
2285                assert self.box_size(x) == y[5]
2286                assert self.log_size(x) == y[6]
2287                assert not self.nolock()
2288                self.free(self.lock())
2289                assert self.nolock()
2290            assert self.boxes(x) != {}
2291            assert self.logs(x) != {}
2292
2293            assert not self.hide(x)
2294            assert self.hide(x, False) is False
2295            assert self.hide(x) is False
2296            assert self.hide(x, True)
2297            assert self.hide(x)
2298
2299            assert self.zakatable(x)
2300            assert self.zakatable(x, False) is False
2301            assert self.zakatable(x) is False
2302            assert self.zakatable(x, True)
2303            assert self.zakatable(x)
2304
2305        if restore is True:
2306            count = len(self._vault['history'])
2307            if debug:
2308                print('history-count', count)
2309            assert count == 10
2310            # try mode
2311            for _ in range(count):
2312                assert self.recall(True, debug)
2313            count = len(self._vault['history'])
2314            if debug:
2315                print('history-count', count)
2316            assert count == 10
2317            _accounts = list(table.keys())
2318            accounts_limit = len(_accounts) + 1
2319            for i in range(-1, -accounts_limit, -1):
2320                account = _accounts[i]
2321                if debug:
2322                    print(account, len(table[account]))
2323                transaction_limit = len(table[account]) + 1
2324                for j in range(-1, -transaction_limit, -1):
2325                    row = table[account][j]
2326                    if debug:
2327                        print(row, self.balance(account), self.balance(account, False))
2328                    assert self.balance(account) == self.balance(account, False)
2329                    assert self.balance(account) == row[2]
2330                    assert self.recall(False, debug)
2331            assert self.recall(False, debug) is False
2332            count = len(self._vault['history'])
2333            if debug:
2334                print('history-count', count)
2335            assert count == 0
2336            self.reset()
2337
2338    def test(self, debug: bool = False) -> bool:
2339        if debug:
2340            print('test', f'debug={debug}')
2341        try:
2342
2343            assert self._history()
2344
2345            # Not allowed for duplicate transactions in the same account and time
2346
2347            created = ZakatTracker.time()
2348            self.track(100, 'test-1', 'same', True, created)
2349            failed = False
2350            try:
2351                self.track(50, 'test-1', 'same', True, created)
2352            except:
2353                failed = True
2354            assert failed is True
2355
2356            self.reset()
2357
2358            # Same account transfer
2359            for x in [1, 'a', True, 1.8, None]:
2360                failed = False
2361                try:
2362                    self.transfer(1, x, x, 'same-account', debug=debug)
2363                except:
2364                    failed = True
2365                assert failed is True
2366
2367            # Always preserve box age during transfer
2368
2369            series: list[tuple] = [
2370                (30, 4),
2371                (60, 3),
2372                (90, 2),
2373            ]
2374            case = {
2375                30: {
2376                    'series': series,
2377                    'rest': 150,
2378                },
2379                60: {
2380                    'series': series,
2381                    'rest': 120,
2382                },
2383                90: {
2384                    'series': series,
2385                    'rest': 90,
2386                },
2387                180: {
2388                    'series': series,
2389                    'rest': 0,
2390                },
2391                270: {
2392                    'series': series,
2393                    'rest': -90,
2394                },
2395                360: {
2396                    'series': series,
2397                    'rest': -180,
2398                },
2399            }
2400
2401            selected_time = ZakatTracker.time() - ZakatTracker.TimeCycle()
2402
2403            for total in case:
2404                for x in case[total]['series']:
2405                    self.track(x[0], f"test-{x} ages", 'ages', True, selected_time * x[1])
2406
2407                refs = self.transfer(total, 'ages', 'future', 'Zakat Movement', debug=debug)
2408
2409                if debug:
2410                    print('refs', refs)
2411
2412                ages_cache_balance = self.balance('ages')
2413                ages_fresh_balance = self.balance('ages', False)
2414                rest = case[total]['rest']
2415                if debug:
2416                    print('source', ages_cache_balance, ages_fresh_balance, rest)
2417                assert ages_cache_balance == rest
2418                assert ages_fresh_balance == rest
2419
2420                future_cache_balance = self.balance('future')
2421                future_fresh_balance = self.balance('future', False)
2422                if debug:
2423                    print('target', future_cache_balance, future_fresh_balance, total)
2424                    print('refs', refs)
2425                assert future_cache_balance == total
2426                assert future_fresh_balance == total
2427
2428                for ref in self._vault['account']['ages']['box']:
2429                    ages_capital = self._vault['account']['ages']['box'][ref]['capital']
2430                    ages_rest = self._vault['account']['ages']['box'][ref]['rest']
2431                    future_capital = 0
2432                    if ref in self._vault['account']['future']['box']:
2433                        future_capital = self._vault['account']['future']['box'][ref]['capital']
2434                    future_rest = 0
2435                    if ref in self._vault['account']['future']['box']:
2436                        future_rest = self._vault['account']['future']['box'][ref]['rest']
2437                    if ages_capital != 0 and future_capital != 0 and future_rest != 0:
2438                        if debug:
2439                            print('================================================================')
2440                            print('ages', ages_capital, ages_rest)
2441                            print('future', future_capital, future_rest)
2442                        if ages_rest == 0:
2443                            assert ages_capital == future_capital
2444                        elif ages_rest < 0:
2445                            assert -ages_capital == future_capital
2446                        elif ages_rest > 0:
2447                            assert ages_capital == ages_rest + future_capital
2448                self.reset()
2449                assert len(self._vault['history']) == 0
2450
2451            assert self._history()
2452            assert self._history(False) is False
2453            assert self._history() is False
2454            assert self._history(True)
2455            assert self._history()
2456
2457            self._test_core(True, debug)
2458            self._test_core(False, debug)
2459
2460            transaction = [
2461                (
2462                    20, 'wallet', 1, 800, 800, 800, 4, 5,
2463                    -85, -85, -85, 6, 7,
2464                ),
2465                (
2466                    750, 'wallet', 'safe', 50, 50, 50, 4, 6,
2467                    750, 750, 750, 1, 1,
2468                ),
2469                (
2470                    600, 'safe', 'bank', 150, 150, 150, 1, 2,
2471                    600, 600, 600, 1, 1,
2472                ),
2473            ]
2474            for z in transaction:
2475                self.lock()
2476                x = z[1]
2477                y = z[2]
2478                self.transfer(z[0], x, y, 'test-transfer', debug=debug)
2479                assert self.balance(x) == z[3]
2480                xx = self.accounts()[x]
2481                assert xx == z[3]
2482                assert self.balance(x, False) == z[4]
2483                assert xx == z[4]
2484
2485                s = 0
2486                log = self._vault['account'][x]['log']
2487                for i in log:
2488                    s += log[i]['value']
2489                if debug:
2490                    print('s', s, 'z[5]', z[5])
2491                assert s == z[5]
2492
2493                assert self.box_size(x) == z[6]
2494                assert self.log_size(x) == z[7]
2495
2496                yy = self.accounts()[y]
2497                assert self.balance(y) == z[8]
2498                assert yy == z[8]
2499                assert self.balance(y, False) == z[9]
2500                assert yy == z[9]
2501
2502                s = 0
2503                log = self._vault['account'][y]['log']
2504                for i in log:
2505                    s += log[i]['value']
2506                assert s == z[10]
2507
2508                assert self.box_size(y) == z[11]
2509                assert self.log_size(y) == z[12]
2510
2511            if debug:
2512                pp().pprint(self.check(2.17))
2513
2514            assert not self.nolock()
2515            history_count = len(self._vault['history'])
2516            if debug:
2517                print('history-count', history_count)
2518            assert history_count == 11
2519            assert not self.free(ZakatTracker.time())
2520            assert self.free(self.lock())
2521            assert self.nolock()
2522            assert len(self._vault['history']) == 11
2523
2524            # storage
2525
2526            _path = self.path('test.pickle')
2527            if os.path.exists(_path):
2528                os.remove(_path)
2529            self.save()
2530            assert os.path.getsize(_path) > 0
2531            self.reset()
2532            assert self.recall(False, debug) is False
2533            self.load()
2534            assert self._vault['account'] is not None
2535
2536            # recall
2537
2538            assert self.nolock()
2539            assert len(self._vault['history']) == 11
2540            assert self.recall(False, debug) is True
2541            assert len(self._vault['history']) == 10
2542            assert self.recall(False, debug) is True
2543            assert len(self._vault['history']) == 9
2544
2545            # exchange
2546
2547            self.exchange("cash", 25, 3.75, "2024-06-25")
2548            self.exchange("cash", 22, 3.73, "2024-06-22")
2549            self.exchange("cash", 15, 3.69, "2024-06-15")
2550            self.exchange("cash", 10, 3.66)
2551
2552            for i in range(1, 30):
2553                exchange = self.exchange("cash", i)
2554                rate, description, created = exchange['rate'], exchange['description'], exchange['time']
2555                if debug:
2556                    print(i, rate, description, created)
2557                assert created
2558                if i < 10:
2559                    assert rate == 1
2560                    assert description is None
2561                elif i == 10:
2562                    assert rate == 3.66
2563                    assert description is None
2564                elif i < 15:
2565                    assert rate == 3.66
2566                    assert description is None
2567                elif i == 15:
2568                    assert rate == 3.69
2569                    assert description is not None
2570                elif i < 22:
2571                    assert rate == 3.69
2572                    assert description is not None
2573                elif i == 22:
2574                    assert rate == 3.73
2575                    assert description is not None
2576                elif i >= 25:
2577                    assert rate == 3.75
2578                    assert description is not None
2579                exchange = self.exchange("bank", i)
2580                rate, description, created = exchange['rate'], exchange['description'], exchange['time']
2581                if debug:
2582                    print(i, rate, description, created)
2583                assert created
2584                assert rate == 1
2585                assert description is None
2586
2587            assert len(self._vault['exchange']) > 0
2588            assert len(self.exchanges()) > 0
2589            self._vault['exchange'].clear()
2590            assert len(self._vault['exchange']) == 0
2591            assert len(self.exchanges()) == 0
2592
2593            # حفظ أسعار الصرف باستخدام التواريخ بالنانو ثانية
2594            self.exchange("cash", ZakatTracker.day_to_time(25), 3.75, "2024-06-25")
2595            self.exchange("cash", ZakatTracker.day_to_time(22), 3.73, "2024-06-22")
2596            self.exchange("cash", ZakatTracker.day_to_time(15), 3.69, "2024-06-15")
2597            self.exchange("cash", ZakatTracker.day_to_time(10), 3.66)
2598
2599            for i in [x * 0.12 for x in range(-15, 21)]:
2600                if i <= 0:
2601                    assert len(self.exchange("test", ZakatTracker.time(), i, f"range({i})")) == 0
2602                else:
2603                    assert len(self.exchange("test", ZakatTracker.time(), i, f"range({i})")) > 0
2604
2605            # اختبار النتائج باستخدام التواريخ بالنانو ثانية
2606            for i in range(1, 31):
2607                timestamp_ns = ZakatTracker.day_to_time(i)
2608                exchange = self.exchange("cash", timestamp_ns)
2609                rate, description, created = exchange['rate'], exchange['description'], exchange['time']
2610                if debug:
2611                    print(i, rate, description, created)
2612                assert created
2613                if i < 10:
2614                    assert rate == 1
2615                    assert description is None
2616                elif i == 10:
2617                    assert rate == 3.66
2618                    assert description is None
2619                elif i < 15:
2620                    assert rate == 3.66
2621                    assert description is None
2622                elif i == 15:
2623                    assert rate == 3.69
2624                    assert description is not None
2625                elif i < 22:
2626                    assert rate == 3.69
2627                    assert description is not None
2628                elif i == 22:
2629                    assert rate == 3.73
2630                    assert description is not None
2631                elif i >= 25:
2632                    assert rate == 3.75
2633                    assert description is not None
2634                exchange = self.exchange("bank", i)
2635                rate, description, created = exchange['rate'], exchange['description'], exchange['time']
2636                if debug:
2637                    print(i, rate, description, created)
2638                assert created
2639                assert rate == 1
2640                assert description is None
2641
2642            # csv
2643
2644            csv_count = 1000
2645
2646            for with_rate, path in {
2647                False: 'test-import_csv-no-exchange',
2648                True: 'test-import_csv-with-exchange',
2649            }.items():
2650
2651                if debug:
2652                    print('test_import_csv', with_rate, path)
2653
2654                # csv
2655
2656                csv_path = path + '.csv'
2657                if os.path.exists(csv_path):
2658                    os.remove(csv_path)
2659                c = self.generate_random_csv_file(csv_path, csv_count, with_rate, debug)
2660                if debug:
2661                    print('generate_random_csv_file', c)
2662                assert c == csv_count
2663                assert os.path.getsize(csv_path) > 0
2664                cache_path = self.import_csv_cache_path()
2665                if os.path.exists(cache_path):
2666                    os.remove(cache_path)
2667                self.reset()
2668                (created, found, bad) = self.import_csv(csv_path, debug)
2669                bad_count = len(bad)
2670                if debug:
2671                    print(f"csv-imported: ({created}, {found}, {bad_count}) = count({csv_count})")
2672                tmp_size = os.path.getsize(cache_path)
2673                assert tmp_size > 0
2674                assert created + found + bad_count == csv_count
2675                assert created == csv_count
2676                assert bad_count == 0
2677                (created_2, found_2, bad_2) = self.import_csv(csv_path)
2678                bad_2_count = len(bad_2)
2679                if debug:
2680                    print(f"csv-imported: ({created_2}, {found_2}, {bad_2_count})")
2681                    print(bad)
2682                assert tmp_size == os.path.getsize(cache_path)
2683                assert created_2 + found_2 + bad_2_count == csv_count
2684                assert created == found_2
2685                assert bad_count == bad_2_count
2686                assert found_2 == csv_count
2687                assert bad_2_count == 0
2688                assert created_2 == 0
2689
2690                # payment parts
2691
2692                positive_parts = self.build_payment_parts(100, positive_only=True)
2693                assert self.check_payment_parts(positive_parts) != 0
2694                assert self.check_payment_parts(positive_parts) != 0
2695                all_parts = self.build_payment_parts(300, positive_only=False)
2696                assert self.check_payment_parts(all_parts) != 0
2697                assert self.check_payment_parts(all_parts) != 0
2698                if debug:
2699                    pp().pprint(positive_parts)
2700                    pp().pprint(all_parts)
2701                # dynamic discount
2702                suite = []
2703                count = 3
2704                for exceed in [False, True]:
2705                    case = []
2706                    for parts in [positive_parts, all_parts]:
2707                        part = parts.copy()
2708                        demand = part['demand']
2709                        if debug:
2710                            print(demand, part['total'])
2711                        i = 0
2712                        z = demand / count
2713                        cp = {
2714                            'account': {},
2715                            'demand': demand,
2716                            'exceed': exceed,
2717                            'total': part['total'],
2718                        }
2719                        j = ''
2720                        for x, y in part['account'].items():
2721                            x_exchange = self.exchange(x)
2722                            zz = self.exchange_calc(z, 1, x_exchange['rate'])
2723                            if exceed and zz <= demand:
2724                                i += 1
2725                                y['part'] = zz
2726                                if debug:
2727                                    print(exceed, y)
2728                                cp['account'][x] = y
2729                                case.append(y)
2730                            elif not exceed and y['balance'] >= zz:
2731                                i += 1
2732                                y['part'] = zz
2733                                if debug:
2734                                    print(exceed, y)
2735                                cp['account'][x] = y
2736                                case.append(y)
2737                            j = x
2738                            if i >= count:
2739                                break
2740                        if len(cp['account'][j]) > 0:
2741                            suite.append(cp)
2742                if debug:
2743                    print('suite', len(suite))
2744                # vault = self._vault.copy()
2745                for case in suite:
2746                    # self._vault = vault.copy()
2747                    if debug:
2748                        print('case', case)
2749                    result = self.check_payment_parts(case)
2750                    if debug:
2751                        print('check_payment_parts', result, f'exceed: {exceed}')
2752                    assert result == 0
2753
2754                    report = self.check(2.17, None, debug)
2755                    (valid, brief, plan) = report
2756                    if debug:
2757                        print('valid', valid)
2758                    zakat_result = self.zakat(report, parts=case, debug=debug)
2759                    if debug:
2760                        print('zakat-result', zakat_result)
2761                    assert valid == zakat_result
2762
2763            assert self.save(path + '.pickle')
2764            assert self.export_json(path + '.json')
2765
2766            assert self.export_json("1000-transactions-test.json")
2767            assert self.save("1000-transactions-test.pickle")
2768
2769            self.reset()
2770
2771            # test transfer between accounts with different exchange rate
2772
2773            a_SAR = "Bank (SAR)"
2774            b_USD = "Bank (USD)"
2775            c_SAR = "Safe (SAR)"
2776            # 0: track, 1: check-exchange, 2: do-exchange, 3: transfer
2777            for case in [
2778                (0, a_SAR, "SAR Gift", 1000, 1000),
2779                (1, a_SAR, 1),
2780                (0, b_USD, "USD Gift", 500, 500),
2781                (1, b_USD, 1),
2782                (2, b_USD, 3.75),
2783                (1, b_USD, 3.75),
2784                (3, 100, b_USD, a_SAR, "100 USD -> SAR", 400, 1375),
2785                (0, c_SAR, "Salary", 750, 750),
2786                (3, 375, c_SAR, b_USD, "375 SAR -> USD", 375, 500),
2787                (3, 3.75, a_SAR, b_USD, "3.75 SAR -> USD", 1371.25, 501),
2788            ]:
2789                match (case[0]):
2790                    case 0:  # track
2791                        _, account, desc, x, balance = case
2792                        self.track(value=x, desc=desc, account=account, debug=debug)
2793
2794                        cached_value = self.balance(account, cached=True)
2795                        fresh_value = self.balance(account, cached=False)
2796                        if debug:
2797                            print('account', account, 'cached_value', cached_value, 'fresh_value', fresh_value)
2798                        assert cached_value == balance
2799                        assert fresh_value == balance
2800                    case 1:  # check-exchange
2801                        _, account, expected_rate = case
2802                        t_exchange = self.exchange(account, created=ZakatTracker.time(), debug=debug)
2803                        if debug:
2804                            print('t-exchange', t_exchange)
2805                        assert t_exchange['rate'] == expected_rate
2806                    case 2:  # do-exchange
2807                        _, account, rate = case
2808                        self.exchange(account, rate=rate, debug=debug)
2809                        b_exchange = self.exchange(account, created=ZakatTracker.time(), debug=debug)
2810                        if debug:
2811                            print('b-exchange', b_exchange)
2812                        assert b_exchange['rate'] == rate
2813                    case 3:  # transfer
2814                        _, x, a, b, desc, a_balance, b_balance = case
2815                        self.transfer(x, a, b, desc, debug=debug)
2816
2817                        cached_value = self.balance(a, cached=True)
2818                        fresh_value = self.balance(a, cached=False)
2819                        if debug:
2820                            print('account', a, 'cached_value', cached_value, 'fresh_value', fresh_value)
2821                        assert cached_value == a_balance
2822                        assert fresh_value == a_balance
2823
2824                        cached_value = self.balance(b, cached=True)
2825                        fresh_value = self.balance(b, cached=False)
2826                        if debug:
2827                            print('account', b, 'cached_value', cached_value, 'fresh_value', fresh_value)
2828                        assert cached_value == b_balance
2829                        assert fresh_value == b_balance
2830
2831            # Transfer all in many chunks randomly from B to A
2832            a_SAR_balance = 1371.25
2833            b_USD_balance = 501
2834            b_USD_exchange = self.exchange(b_USD)
2835            amounts = ZakatTracker.create_random_list(b_USD_balance)
2836            if debug:
2837                print('amounts', amounts)
2838            i = 0
2839            for x in amounts:
2840                if debug:
2841                    print(f'{i} - transfer-with-exchange({x})')
2842                self.transfer(x, b_USD, a_SAR, f"{x} USD -> SAR", debug=debug)
2843
2844                b_USD_balance -= x
2845                cached_value = self.balance(b_USD, cached=True)
2846                fresh_value = self.balance(b_USD, cached=False)
2847                if debug:
2848                    print('account', b_USD, 'cached_value', cached_value, 'fresh_value', fresh_value, 'excepted',
2849                          b_USD_balance)
2850                assert cached_value == b_USD_balance
2851                assert fresh_value == b_USD_balance
2852
2853                a_SAR_balance += x * b_USD_exchange['rate']
2854                cached_value = self.balance(a_SAR, cached=True)
2855                fresh_value = self.balance(a_SAR, cached=False)
2856                if debug:
2857                    print('account', a_SAR, 'cached_value', cached_value, 'fresh_value', fresh_value, 'expected',
2858                          a_SAR_balance, 'rate', b_USD_exchange['rate'])
2859                assert cached_value == a_SAR_balance
2860                assert fresh_value == a_SAR_balance
2861                i += 1
2862
2863            # Transfer all in many chunks randomly from C to A
2864            c_SAR_balance = 375
2865            amounts = ZakatTracker.create_random_list(c_SAR_balance)
2866            if debug:
2867                print('amounts', amounts)
2868            i = 0
2869            for x in amounts:
2870                if debug:
2871                    print(f'{i} - transfer-with-exchange({x})')
2872                self.transfer(x, c_SAR, a_SAR, f"{x} SAR -> a_SAR", debug=debug)
2873
2874                c_SAR_balance -= x
2875                cached_value = self.balance(c_SAR, cached=True)
2876                fresh_value = self.balance(c_SAR, cached=False)
2877                if debug:
2878                    print('account', c_SAR, 'cached_value', cached_value, 'fresh_value', fresh_value, 'excepted',
2879                          c_SAR_balance)
2880                assert cached_value == c_SAR_balance
2881                assert fresh_value == c_SAR_balance
2882
2883                a_SAR_balance += x
2884                cached_value = self.balance(a_SAR, cached=True)
2885                fresh_value = self.balance(a_SAR, cached=False)
2886                if debug:
2887                    print('account', a_SAR, 'cached_value', cached_value, 'fresh_value', fresh_value, 'expected',
2888                          a_SAR_balance)
2889                assert cached_value == a_SAR_balance
2890                assert fresh_value == a_SAR_balance
2891                i += 1
2892
2893            assert self.export_json("accounts-transfer-with-exchange-rates.json")
2894            assert self.save("accounts-transfer-with-exchange-rates.pickle")
2895
2896            # check & zakat with exchange rates for many cycles
2897
2898            for rate, values in {
2899                1: {
2900                    'in': [1000, 2000, 10000],
2901                    'exchanged': [1000, 2000, 10000],
2902                    'out': [25, 50, 731.40625],
2903                },
2904                3.75: {
2905                    'in': [200, 1000, 5000],
2906                    'exchanged': [750, 3750, 18750],
2907                    'out': [18.75, 93.75, 1371.38671875],
2908                },
2909            }.items():
2910                a, b, c = values['in']
2911                m, n, o = values['exchanged']
2912                x, y, z = values['out']
2913                if debug:
2914                    print('rate', rate, 'values', values)
2915                for case in [
2916                    (a, 'safe', ZakatTracker.time() - ZakatTracker.TimeCycle(), [
2917                        {'safe': {0: {'below_nisab': x}}},
2918                    ], False, m),
2919                    (b, 'safe', ZakatTracker.time() - ZakatTracker.TimeCycle(), [
2920                        {'safe': {0: {'count': 1, 'total': y}}},
2921                    ], True, n),
2922                    (c, 'cave', ZakatTracker.time() - (ZakatTracker.TimeCycle() * 3), [
2923                        {'cave': {0: {'count': 3, 'total': z}}},
2924                    ], True, o),
2925                ]:
2926                    if debug:
2927                        print(f"############# check(rate: {rate}) #############")
2928                    self.reset()
2929                    self.exchange(account=case[1], created=case[2], rate=rate)
2930                    self.track(value=case[0], desc='test-check', account=case[1], logging=True, created=case[2])
2931
2932                    # assert self.nolock()
2933                    # history_size = len(self._vault['history'])
2934                    # print('history_size', history_size)
2935                    # assert history_size == 2
2936                    assert self.lock()
2937                    assert not self.nolock()
2938                    report = self.check(2.17, None, debug)
2939                    (valid, brief, plan) = report
2940                    assert valid == case[4]
2941                    if debug:
2942                        print('brief', brief)
2943                    assert case[5] == brief[0]
2944                    assert case[5] == brief[1]
2945
2946                    if debug:
2947                        pp().pprint(plan)
2948
2949                    for x in plan:
2950                        assert case[1] == x
2951                        if 'total' in case[3][0][x][0].keys():
2952                            assert case[3][0][x][0]['total'] == brief[2]
2953                            assert plan[x][0]['total'] == case[3][0][x][0]['total']
2954                            assert plan[x][0]['count'] == case[3][0][x][0]['count']
2955                        else:
2956                            assert plan[x][0]['below_nisab'] == case[3][0][x][0]['below_nisab']
2957                    if debug:
2958                        pp().pprint(report)
2959                    result = self.zakat(report, debug=debug)
2960                    if debug:
2961                        print('zakat-result', result, case[4])
2962                    assert result == case[4]
2963                    report = self.check(2.17, None, debug)
2964                    (valid, brief, plan) = report
2965                    assert valid is False
2966
2967            history_size = len(self._vault['history'])
2968            if debug:
2969                print('history_size', history_size)
2970            assert history_size == 3
2971            assert not self.nolock()
2972            assert self.recall(False, debug) is False
2973            self.free(self.lock())
2974            assert self.nolock()
2975
2976            for i in range(3, 0, -1):
2977                history_size = len(self._vault['history'])
2978                if debug:
2979                    print('history_size', history_size)
2980                assert history_size == i
2981                assert self.recall(False, debug) is True
2982
2983            assert self.nolock()
2984            assert self.recall(False, debug) is False
2985
2986            history_size = len(self._vault['history'])
2987            if debug:
2988                print('history_size', history_size)
2989            assert history_size == 0
2990
2991            account_size = len(self._vault['account'])
2992            if debug:
2993                print('account_size', account_size)
2994            assert account_size == 0
2995
2996            report_size = len(self._vault['report'])
2997            if debug:
2998                print('report_size', report_size)
2999            assert report_size == 0
3000
3001            assert self.nolock()
3002            return True
3003        except:
3004            # pp().pprint(self._vault)
3005            assert self.export_json("test-snapshot.json")
3006            assert self.save("test-snapshot.pickle")
3007            raise

A class for tracking and calculating Zakat.

This class provides functionalities for recording transactions, calculating Zakat due, and managing account balances. It also offers features like importing transactions from CSV files, exporting data to JSON format, and saving/loading the tracker state.

The ZakatTracker class is designed to handle both positive and negative transactions, allowing for flexible tracking of financial activities related to Zakat. It also supports the concept of a "Nisab" (minimum threshold for Zakat) and a "haul" (complete one year for Transaction) can calculate Zakat due based on the current silver price.

The class uses a pickle file as its database to persist the tracker state, ensuring data integrity across sessions. It also provides options for enabling or disabling history tracking, allowing users to choose their preferred level of detail.

In addition, the ZakatTracker class includes various helper methods like time, time_to_datetime, lock, free, recall, export_json, and more. These methods provide additional functionalities and flexibility for interacting with and managing the Zakat tracker.

Attributes: ZakatTracker.ZakatCut (function): A function to calculate the Zakat percentage. ZakatTracker.TimeCycle (function): A function to determine the time cycle for Zakat. ZakatTracker.Nisab (function): A function to calculate the Nisab based on the silver price. ZakatTracker.Version (function): The version of the ZakatTracker class.

Data Structure: The ZakatTracker class utilizes a nested dictionary structure called "_vault" to store and manage data.

_vault (dict):
    - account (dict):
        - {account_number} (dict):
            - balance (int): The current balance of the account.
            - box (dict): A dictionary storing transaction details.
                - {timestamp} (dict):
                    - capital (int): The initial amount of the transaction.
                    - count (int): The number of times Zakat has been calculated for this transaction.
                    - last (int): The timestamp of the last Zakat calculation.
                    - rest (int): The remaining amount after Zakat deductions and withdrawal.
                    - total (int): The total Zakat deducted from this transaction.
            - count (int): The total number of transactions for the account.
            - log (dict): A dictionary storing transaction logs.
                - {timestamp} (dict):
                    - value (int): The transaction amount (positive or negative).
                    - desc (str): The description of the transaction.
                    - ref (int): The box reference (positive or None).
                    - file (dict): A dictionary storing file references associated with the transaction.
            - hide (bool): Indicates whether the account is hidden or not.
            - zakatable (bool): Indicates whether the account is subject to Zakat.
    - exchange (dict):
        - account (dict):
            - {timestamps} (dict):
                - rate (float): Exchange rate when compared to local currency.
                - description (str): The description of the exchange rate.
    - history (dict):
        - {timestamp} (list): A list of dictionaries storing the history of actions performed.
            - {action_dict} (dict):
                - action (Action): The type of action (CREATE, TRACK, LOG, SUB, ADD_FILE, REMOVE_FILE, BOX_TRANSFER, EXCHANGE, REPORT, ZAKAT).
                - account (str): The account number associated with the action.
                - ref (int): The reference number of the transaction.
                - file (int): The reference number of the file (if applicable).
                - key (str): The key associated with the action (e.g., 'rest', 'total').
                - value (int): The value associated with the action.
                - math (MathOperation): The mathematical operation performed (if applicable).
    - lock (int or None): The timestamp indicating the current lock status (None if not locked).
    - report (dict):
        - {timestamp} (tuple): A tuple storing Zakat report details.
ZakatTracker(db_path: str = 'zakat.pickle', history_mode: bool = True)
242    def __init__(self, db_path: str = "zakat.pickle", history_mode: bool = True):
243        """
244        Initialize ZakatTracker with database path and history mode.
245
246        Parameters:
247        db_path (str): The path to the database file. Default is "zakat.pickle".
248        history_mode (bool): The mode for tracking history. Default is True.
249
250        Returns:
251        None
252        """
253        self._vault_path = None
254        self._vault = None
255        self.reset()
256        self._history(history_mode)
257        self.path(db_path)
258        self.load()

Initialize ZakatTracker with database path and history mode.

Parameters: db_path (str): The path to the database file. Default is "zakat.pickle". history_mode (bool): The mode for tracking history. Default is True.

Returns: None

@staticmethod
def Version():
176    @staticmethod
177    def Version():
178        """
179        Returns the current version of the software.
180
181        This function returns a string representing the current version of the software,
182        including major, minor, and patch version numbers in the format "X.Y.Z".
183
184        Returns:
185        str: The current version of the software.
186        """
187        return '0.2.78'

Returns the current version of the software.

This function returns a string representing the current version of the software, including major, minor, and patch version numbers in the format "X.Y.Z".

Returns: str: The current version of the software.

@staticmethod
def ZakatCut(x: float) -> float:
189    @staticmethod
190    def ZakatCut(x: float) -> float:
191        """
192        Calculates the Zakat amount due on an asset.
193
194        This function calculates the zakat amount due on a given asset value over one lunar year.
195        Zakat is an Islamic obligatory alms-giving, calculated as a fixed percentage of an individual's wealth
196        that exceeds a certain threshold (Nisab).
197
198        Parameters:
199        x: The total value of the asset on which Zakat is to be calculated.
200
201        Returns:
202        The amount of Zakat due on the asset, calculated as 2.5% of the asset's value.
203        """
204        return 0.025 * x  # Zakat Cut in one Lunar Year

Calculates the Zakat amount due on an asset.

This function calculates the zakat amount due on a given asset value over one lunar year. Zakat is an Islamic obligatory alms-giving, calculated as a fixed percentage of an individual's wealth that exceeds a certain threshold (Nisab).

Parameters: x: The total value of the asset on which Zakat is to be calculated.

Returns: The amount of Zakat due on the asset, calculated as 2.5% of the asset's value.

@staticmethod
def TimeCycle(days: int = 355) -> int:
206    @staticmethod
207    def TimeCycle(days: int = 355) -> int:
208        """
209        Calculates the approximate duration of a lunar year in nanoseconds.
210
211        This function calculates the approximate duration of a lunar year based on the given number of days.
212        It converts the given number of days into nanoseconds for use in high-precision timing applications.
213
214        Parameters:
215        days: The number of days in a lunar year. Defaults to 355,
216              which is an approximation of the average length of a lunar year.
217
218        Returns:
219        The approximate duration of a lunar year in nanoseconds.
220        """
221        return int(60 * 60 * 24 * days * 1e9)  # Lunar Year in nanoseconds

Calculates the approximate duration of a lunar year in nanoseconds.

This function calculates the approximate duration of a lunar year based on the given number of days. It converts the given number of days into nanoseconds for use in high-precision timing applications.

Parameters: days: The number of days in a lunar year. Defaults to 355, which is an approximation of the average length of a lunar year.

Returns: The approximate duration of a lunar year in nanoseconds.

@staticmethod
def Nisab(gram_price: float, gram_quantity: float = 595) -> float:
223    @staticmethod
224    def Nisab(gram_price: float, gram_quantity: float = 595) -> float:
225        """
226        Calculate the total value of Nisab (a unit of weight in Islamic jurisprudence) based on the given price per gram.
227
228        This function calculates the Nisab value, which is the minimum threshold of wealth,
229        that makes an individual liable for paying Zakat.
230        The Nisab value is determined by the equivalent value of a specific amount
231        of gold or silver (currently 595 grams in silver) in the local currency.
232
233        Parameters:
234        - gram_price (float): The price per gram of Nisab.
235        - gram_quantity (float): The quantity of grams in a Nisab. Default is 595 grams of silver.
236
237        Returns:
238        - float: The total value of Nisab based on the given price per gram.
239        """
240        return gram_price * gram_quantity

Calculate the total value of Nisab (a unit of weight in Islamic jurisprudence) based on the given price per gram.

This function calculates the Nisab value, which is the minimum threshold of wealth, that makes an individual liable for paying Zakat. The Nisab value is determined by the equivalent value of a specific amount of gold or silver (currently 595 grams in silver) in the local currency.

Parameters:

  • gram_price (float): The price per gram of Nisab.
  • gram_quantity (float): The quantity of grams in a Nisab. Default is 595 grams of silver.

Returns:

  • float: The total value of Nisab based on the given price per gram.
def path(self, path: str = None) -> str:
260    def path(self, path: str = None) -> str:
261        """
262        Set or get the path to the database file.
263
264        If no path is provided, the current path is returned.
265        If a path is provided, it is set as the new path.
266        The function also creates the necessary directories if the provided path is a file.
267
268        Parameters:
269        path (str): The new path to the database file. If not provided, the current path is returned.
270
271        Returns:
272        str: The current or new path to the database file.
273        """
274        if path is None:
275            return self._vault_path
276        self._vault_path = Path(path).resolve()
277        base_path = Path(path).resolve()
278        if base_path.is_file() or base_path.suffix:
279            base_path = base_path.parent
280        base_path.mkdir(parents=True, exist_ok=True)
281        self._base_path = base_path
282        return self._vault_path

Set or get the path to the database file.

If no path is provided, the current path is returned. If a path is provided, it is set as the new path. The function also creates the necessary directories if the provided path is a file.

Parameters: path (str): The new path to the database file. If not provided, the current path is returned.

Returns: str: The current or new path to the database file.

def base_path(self, *args) -> str:
284    def base_path(self, *args) -> str:
285        """
286        Generate a base path by joining the provided arguments with the existing base path.
287
288        Parameters:
289        *args (str): Variable length argument list of strings to be joined with the base path.
290
291        Returns:
292        str: The generated base path. If no arguments are provided, the existing base path is returned.
293        """
294        if not args:
295            return self._base_path
296        filtered_args = []
297        ignored_filename = None
298        for arg in args:
299            if Path(arg).suffix:
300                ignored_filename = arg
301            else:
302                filtered_args.append(arg)
303        base_path = Path(self._base_path)
304        full_path = base_path.joinpath(*filtered_args)
305        full_path.mkdir(parents=True, exist_ok=True)
306        if ignored_filename is not None:
307            return full_path.resolve() / ignored_filename  # Join with the ignored filename
308        return full_path.resolve() 

Generate a base path by joining the provided arguments with the existing base path.

Parameters: *args (str): Variable length argument list of strings to be joined with the base path.

Returns: str: The generated base path. If no arguments are provided, the existing base path is returned.

@staticmethod
def scale(x: float | int | decimal.Decimal, decimal_places: int = 2) -> int:
310    @staticmethod
311    def scale(x: float | int | Decimal, decimal_places: int = 2) -> int:
312        """
313        Scales a numerical value by a specified power of 10, returning an integer.
314
315        This function is designed to handle various numeric types (`float`, `int`, or `Decimal`) and
316        facilitate precise scaling operations, particularly useful in financial or scientific calculations.
317
318        Parameters:
319        x: The numeric value to scale. Can be a floating-point number, integer, or decimal.
320        decimal_places: The exponent for the scaling factor (10**y). Defaults to 2, meaning the input is scaled
321            by a factor of 100 (e.g., converts 1.23 to 123).
322
323        Returns:
324        The scaled value, rounded to the nearest integer.
325
326        Raises:
327        TypeError: If the input `x` is not a valid numeric type.
328
329        Examples:
330        >>> scale(3.14159)
331        314
332        >>> scale(1234, decimal_places=3)
333        1234000
334        >>> scale(Decimal("0.005"), decimal_places=4)
335        50
336        """
337        if not isinstance(x, (float, int, Decimal)):
338            raise TypeError("Input 'x' must be a float, int, or Decimal.")
339        return int(Decimal(f"{x:.{decimal_places}f}") * (10 ** decimal_places))

Scales a numerical value by a specified power of 10, returning an integer.

This function is designed to handle various numeric types (float, int, or Decimal) and facilitate precise scaling operations, particularly useful in financial or scientific calculations.

Parameters: x: The numeric value to scale. Can be a floating-point number, integer, or decimal. decimal_places: The exponent for the scaling factor (10**y). Defaults to 2, meaning the input is scaled by a factor of 100 (e.g., converts 1.23 to 123).

Returns: The scaled value, rounded to the nearest integer.

Raises: TypeError: If the input x is not a valid numeric type.

Examples:

>>> scale(3.14159)
314
>>> scale(1234, decimal_places=3)
1234000
>>> scale(Decimal("0.005"), decimal_places=4)
50
@staticmethod
def unscale( x: int, return_type: type = <class 'float'>, decimal_places: int = 2) -> float | decimal.Decimal:
341    @staticmethod
342    def unscale(x: int, return_type: type = float, decimal_places: int = 2) -> float | Decimal:
343        """
344        Unscales an integer by a power of 10.
345
346        Parameters:
347        x: The integer to unscale.
348        return_type: The desired type for the returned value. Can be float, int, or Decimal. Defaults to float.
349        decimal_places: The power of 10 to use. Defaults to 2.
350
351        Returns:
352        The unscaled number, converted to the specified return_type.
353
354        Raises:
355        TypeError: If the return_type is not float or Decimal.
356        """
357        if return_type not in (float, Decimal):
358            raise TypeError(f'Invalid return_type({return_type}). Supported types are float, int, and Decimal.')
359        return round(return_type(x / (10 ** decimal_places)), decimal_places)

Unscales an integer by a power of 10.

Parameters: x: The integer to unscale. return_type: The desired type for the returned value. Can be float, int, or Decimal. Defaults to float. decimal_places: The power of 10 to use. Defaults to 2.

Returns: The unscaled number, converted to the specified return_type.

Raises: TypeError: If the return_type is not float or Decimal.

def reset(self) -> None:
375    def reset(self) -> None:
376        """
377        Reset the internal data structure to its initial state.
378
379        Parameters:
380        None
381
382        Returns:
383        None
384        """
385        self._vault = {
386            'account': {},
387            'exchange': {},
388            'history': {},
389            'lock': None,
390            'report': {},
391        }

Reset the internal data structure to its initial state.

Parameters: None

Returns: None

@staticmethod
def time( now: <module 'datetime' from '/opt/hostedtoolcache/Python/3.12.10/x64/lib/python3.12/datetime.py'> = None) -> int:
393    @staticmethod
394    def time(now: datetime = None) -> int:
395        """
396        Generates a timestamp based on the provided datetime object or the current datetime.
397
398        Parameters:
399        now (datetime, optional): The datetime object to generate the timestamp from.
400        If not provided, the current datetime is used.
401
402        Returns:
403        int: The timestamp in positive nanoseconds since the Unix epoch (January 1, 1970),
404            before 1970 will return in negative until 1000AD.
405        """
406        if now is None:
407            now = datetime.datetime.now()
408        ordinal_day = now.toordinal()
409        ns_in_day = (now - now.replace(hour=0, minute=0, second=0, microsecond=0)).total_seconds() * 10 ** 9
410        return int((ordinal_day - 719_163) * 86_400_000_000_000 + ns_in_day)

Generates a timestamp based on the provided datetime object or the current datetime.

Parameters: now (datetime, optional): The datetime object to generate the timestamp from. If not provided, the current datetime is used.

Returns: int: The timestamp in positive nanoseconds since the Unix epoch (January 1, 1970), before 1970 will return in negative until 1000AD.

@staticmethod
def time_to_datetime( ordinal_ns: int) -> <module 'datetime' from '/opt/hostedtoolcache/Python/3.12.10/x64/lib/python3.12/datetime.py'>:
412    @staticmethod
413    def time_to_datetime(ordinal_ns: int) -> datetime:
414        """
415        Converts an ordinal number (number of days since 1000-01-01) to a datetime object.
416
417        Parameters:
418        ordinal_ns (int): The ordinal number of days since 1000-01-01.
419
420        Returns:
421        datetime: The corresponding datetime object.
422        """
423        ordinal_day = ordinal_ns // 86_400_000_000_000 + 719_163
424        ns_in_day = ordinal_ns % 86_400_000_000_000
425        d = datetime.datetime.fromordinal(ordinal_day)
426        t = datetime.timedelta(seconds=ns_in_day // 10 ** 9)
427        return datetime.datetime.combine(d, datetime.time()) + t

Converts an ordinal number (number of days since 1000-01-01) to a datetime object.

Parameters: ordinal_ns (int): The ordinal number of days since 1000-01-01.

Returns: datetime: The corresponding datetime object.

def clean_history(self, lock: int | None = None) -> int:
429    def clean_history(self, lock: int | None = None) -> int:
430        """
431        Cleans up the history of actions performed on the ZakatTracker instance.
432
433        Parameters:
434        lock (int, optional): The lock ID is used to clean up the empty history.
435            If not provided, it cleans up the empty history records for all locks.
436
437        Returns:
438        int: The number of locks cleaned up.
439        """
440        count = 0
441        if lock in self._vault['history']:
442            if len(self._vault['history'][lock]) <= 0:
443                count += 1
444                del self._vault['history'][lock]
445            return count
446        self.free(self.lock())
447        for lock in self._vault['history']:
448            if len(self._vault['history'][lock]) <= 0:
449                count += 1
450                del self._vault['history'][lock]
451        return count

Cleans up the history of actions performed on the ZakatTracker instance.

Parameters: lock (int, optional): The lock ID is used to clean up the empty history. If not provided, it cleans up the empty history records for all locks.

Returns: int: The number of locks cleaned up.

def nolock(self) -> bool:
489    def nolock(self) -> bool:
490        """
491        Check if the vault lock is currently not set.
492
493        Returns:
494        bool: True if the vault lock is not set, False otherwise.
495        """
496        return self._vault['lock'] is None

Check if the vault lock is currently not set.

Returns: bool: True if the vault lock is not set, False otherwise.

def lock(self) -> int:
498    def lock(self) -> int:
499        """
500        Acquires a lock on the ZakatTracker instance.
501
502        Returns:
503        int: The lock ID. This ID can be used to release the lock later.
504        """
505        return self._step()

Acquires a lock on the ZakatTracker instance.

Returns: int: The lock ID. This ID can be used to release the lock later.

def vault(self) -> dict:
507    def vault(self) -> dict:
508        """
509        Returns a copy of the internal vault dictionary.
510
511        This method is used to retrieve the current state of the ZakatTracker object.
512        It provides a snapshot of the internal data structure, allowing for further
513        processing or analysis.
514
515        Returns:
516        dict: A copy of the internal vault dictionary.
517        """
518        return self._vault.copy()

Returns a copy of the internal vault dictionary.

This method is used to retrieve the current state of the ZakatTracker object. It provides a snapshot of the internal data structure, allowing for further processing or analysis.

Returns: dict: A copy of the internal vault dictionary.

def stats(self) -> dict[str, tuple]:
520    def stats(self) -> dict[str, tuple]:
521        """
522        Calculates and returns statistics about the object's data storage.
523
524        This method determines the size of the database file on disk and the
525        size of the data currently held in RAM (likely within a dictionary).
526        Both sizes are reported in bytes and in a human-readable format
527        (e.g., KB, MB).
528
529        Returns:
530        dict[str, tuple]: A dictionary containing the following statistics:
531
532            * 'database': A tuple with two elements:
533                - The database file size in bytes (int).
534                - The database file size in human-readable format (str).
535            * 'ram': A tuple with two elements:
536                - The RAM usage (dictionary size) in bytes (int).
537                - The RAM usage in human-readable format (str).
538
539        Example:
540        >>> stats = my_object.stats()
541        >>> print(stats['database'])
542        (256000, '250.0 KB')
543        >>> print(stats['ram'])
544        (12345, '12.1 KB')
545        """
546        ram_size = self.get_dict_size(self.vault())
547        file_size = os.path.getsize(self.path())
548        return {
549            'database': (file_size, self.human_readable_size(file_size)),
550            'ram': (ram_size, self.human_readable_size(ram_size)),
551        }

Calculates and returns statistics about the object's data storage.

This method determines the size of the database file on disk and the size of the data currently held in RAM (likely within a dictionary). Both sizes are reported in bytes and in a human-readable format (e.g., KB, MB).

Returns: dict[str, tuple]: A dictionary containing the following statistics:

* 'database': A tuple with two elements:
    - The database file size in bytes (int).
    - The database file size in human-readable format (str).
* 'ram': A tuple with two elements:
    - The RAM usage (dictionary size) in bytes (int).
    - The RAM usage in human-readable format (str).

Example:

>>> stats = my_object.stats()
>>> print(stats['database'])
(256000, '250.0 KB')
>>> print(stats['ram'])
(12345, '12.1 KB')
def steps(self) -> dict:
553    def steps(self) -> dict:
554        """
555        Returns a copy of the history of steps taken in the ZakatTracker.
556
557        The history is a dictionary where each key is a unique identifier for a step,
558        and the corresponding value is a dictionary containing information about the step.
559
560        Returns:
561        dict: A copy of the history of steps taken in the ZakatTracker.
562        """
563        return self._vault['history'].copy()

Returns a copy of the history of steps taken in the ZakatTracker.

The history is a dictionary where each key is a unique identifier for a step, and the corresponding value is a dictionary containing information about the step.

Returns: dict: A copy of the history of steps taken in the ZakatTracker.

def free(self, lock: int, auto_save: bool = True) -> bool:
565    def free(self, lock: int, auto_save: bool = True) -> bool:
566        """
567        Releases the lock on the database.
568
569        Parameters:
570        lock (int): The lock ID to be released.
571        auto_save (bool): Whether to automatically save the database after releasing the lock.
572
573        Returns:
574        bool: True if the lock is successfully released and (optionally) saved, False otherwise.
575        """
576        if lock == self._vault['lock']:
577            self._vault['lock'] = None
578            self.clean_history(lock)
579            if auto_save:
580                return self.save(self.path())
581            return True
582        return False

Releases the lock on the database.

Parameters: lock (int): The lock ID to be released. auto_save (bool): Whether to automatically save the database after releasing the lock.

Returns: bool: True if the lock is successfully released and (optionally) saved, False otherwise.

def account_exists(self, account) -> bool:
584    def account_exists(self, account) -> bool:
585        """
586        Check if the given account exists in the vault.
587
588        Parameters:
589        account (str): The account number to check.
590
591        Returns:
592        bool: True if the account exists, False otherwise.
593        """
594        return account in self._vault['account']

Check if the given account exists in the vault.

Parameters: account (str): The account number to check.

Returns: bool: True if the account exists, False otherwise.

def box_size(self, account) -> int:
596    def box_size(self, account) -> int:
597        """
598        Calculate the size of the box for a specific account.
599
600        Parameters:
601        account (str): The account number for which the box size needs to be calculated.
602
603        Returns:
604        int: The size of the box for the given account. If the account does not exist, -1 is returned.
605        """
606        if self.account_exists(account):
607            return len(self._vault['account'][account]['box'])
608        return -1

Calculate the size of the box for a specific account.

Parameters: account (str): The account number for which the box size needs to be calculated.

Returns: int: The size of the box for the given account. If the account does not exist, -1 is returned.

def log_size(self, account) -> int:
610    def log_size(self, account) -> int:
611        """
612        Get the size of the log for a specific account.
613
614        Parameters:
615        account (str): The account number for which the log size needs to be calculated.
616
617        Returns:
618        int: The size of the log for the given account. If the account does not exist, -1 is returned.
619        """
620        if self.account_exists(account):
621            return len(self._vault['account'][account]['log'])
622        return -1

Get the size of the log for a specific account.

Parameters: account (str): The account number for which the log size needs to be calculated.

Returns: int: The size of the log for the given account. If the account does not exist, -1 is returned.

@staticmethod
def file_hash(file_path: str, algorithm: str = 'blake2b') -> str:
624    @staticmethod
625    def file_hash(file_path: str, algorithm: str = "blake2b") -> str:
626        """
627        Calculates the hash of a file using the specified algorithm.
628
629        Parameters:
630        file_path (str): The path to the file.
631        algorithm (str, optional): The hashing algorithm to use. Defaults to "blake2b".
632
633        Returns:
634        str: The hexadecimal representation of the file's hash.
635        """
636        hash_obj = hashlib.new(algorithm)  # Create the hash object
637        with open(file_path, "rb") as f:  # Open file in binary mode for reading
638            for chunk in iter(lambda: f.read(4096), b""):  # Read file in chunks
639                hash_obj.update(chunk)
640        return hash_obj.hexdigest()  # Return the hash as a hexadecimal string

Calculates the hash of a file using the specified algorithm.

Parameters: file_path (str): The path to the file. algorithm (str, optional): The hashing algorithm to use. Defaults to "blake2b".

Returns: str: The hexadecimal representation of the file's hash.

def snapshot_cache_path(self):
642    def snapshot_cache_path(self):
643        """
644        Generate the path for the cache file used to store snapshots.
645
646        The cache file is a pickle file that stores the timestamps of the snapshots.
647        The file name is derived from the main database file name by replacing the ".pickle" extension with ".snapshots.pickle".
648
649        Returns:
650        str: The path to the cache file.
651        """
652        path = str(self.path())
653        if path.endswith(".pickle"):
654            path = path[:-7]
655        return path + '.snapshots.pickle'

Generate the path for the cache file used to store snapshots.

The cache file is a pickle file that stores the timestamps of the snapshots. The file name is derived from the main database file name by replacing the ".pickle" extension with ".snapshots.pickle".

Returns: str: The path to the cache file.

def snapshot(self) -> bool:
657    def snapshot(self) -> bool:
658        """
659        This function creates a snapshot of the current database state.
660
661        The function calculates the hash of the current database file and checks if a snapshot with the same hash already exists.
662        If a snapshot with the same hash exists, the function returns True without creating a new snapshot.
663        If a snapshot with the same hash does not exist, the function creates a new snapshot by saving the current database state
664        in a new pickle file with a unique timestamp as the file name. The function also updates the snapshot cache file with the new snapshot's hash and timestamp.
665
666        Parameters:
667        None
668
669        Returns:
670        bool: True if a snapshot with the same hash already exists or if the snapshot is successfully created. False if the snapshot creation fails.
671        """
672        current_hash = self.file_hash(self.path())
673        cache: dict[str, int] = {} # hash: time_ns
674        try:
675            with open(self.snapshot_cache_path(), "rb") as f:
676                cache = pickle.load(f)
677        except:
678            pass
679        if current_hash in cache:
680            return True
681        time = time_ns()
682        cache[current_hash] = time
683        if not self.save(self.base_path('snapshots', f'{time}.pickle')):
684            return False
685        with open(self.snapshot_cache_path(), "wb") as f:
686            pickle.dump(cache, f)
687        return True

This function creates a snapshot of the current database state.

The function calculates the hash of the current database file and checks if a snapshot with the same hash already exists. If a snapshot with the same hash exists, the function returns True without creating a new snapshot. If a snapshot with the same hash does not exist, the function creates a new snapshot by saving the current database state in a new pickle file with a unique timestamp as the file name. The function also updates the snapshot cache file with the new snapshot's hash and timestamp.

Parameters: None

Returns: bool: True if a snapshot with the same hash already exists or if the snapshot is successfully created. False if the snapshot creation fails.

def snapshots( self, hide_missing: bool = True, verified_hash_only: bool = False) -> dict[int, tuple[str, str, bool]]:
689    def snapshots(self, hide_missing: bool = True, verified_hash_only: bool = False) -> dict[int, tuple[str, str, bool]]:
690        """
691        Retrieve a dictionary of snapshots, with their respective hashes, paths, and existence status.
692
693        Parameters:
694        - hide_missing (bool): If True, only include snapshots that exist in the dictionary. Default is True.
695        - verified_hash_only (bool): If True, only include snapshots with a valid hash. Default is False.
696
697        Returns:
698        - dict[int, tuple[str, str, bool]]: A dictionary where the keys are the timestamps of the snapshots,
699        and the values are tuples containing the snapshot's hash, path, and existence status.
700        """
701        cache: dict[str, int] = {} # hash: time_ns
702        try:
703            with open(self.snapshot_cache_path(), "rb") as f:
704                cache = pickle.load(f)
705        except:
706            pass
707        if not cache:
708            return {}
709        result: dict[int, tuple[str, str, bool]] = {} # time_ns: (hash, path, exists)
710        for file_hash, ref in cache.items():
711            path = self.base_path('snapshots', f'{ref}.pickle')
712            exists = os.path.exists(path)
713            valid_hash = self.file_hash(path) == file_hash if verified_hash_only else True
714            if (verified_hash_only and not valid_hash) or (verified_hash_only and not exists):
715                continue
716            if exists or not hide_missing:
717                result[ref] = (file_hash, path, exists)
718        return result

Retrieve a dictionary of snapshots, with their respective hashes, paths, and existence status.

Parameters:

  • hide_missing (bool): If True, only include snapshots that exist in the dictionary. Default is True.
  • verified_hash_only (bool): If True, only include snapshots with a valid hash. Default is False.

Returns:

  • dict[int, tuple[str, str, bool]]: A dictionary where the keys are the timestamps of the snapshots, and the values are tuples containing the snapshot's hash, path, and existence status.
def recall(self, dry=True, debug=False) -> bool:
720    def recall(self, dry=True, debug=False) -> bool:
721        """
722        Revert the last operation.
723
724        Parameters:
725        dry (bool): If True, the function will not modify the data, but will simulate the operation. Default is True.
726        debug (bool): If True, the function will print debug information. Default is False.
727
728        Returns:
729        bool: True if the operation was successful, False otherwise.
730        """
731        if not self.nolock() or len(self._vault['history']) == 0:
732            return False
733        if len(self._vault['history']) <= 0:
734            return False
735        ref = sorted(self._vault['history'].keys())[-1]
736        if debug:
737            print('recall', ref)
738        memory = self._vault['history'][ref]
739        if debug:
740            print(type(memory), 'memory', memory)
741        self.snapshot()
742        limit = len(memory) + 1
743        sub_positive_log_negative = 0
744        for i in range(-1, -limit, -1):
745            x = memory[i]
746            if debug:
747                print(type(x), x)
748            match x['action']:
749                case Action.CREATE:
750                    if x['account'] is not None:
751                        if self.account_exists(x['account']):
752                            if debug:
753                                print('account', self._vault['account'][x['account']])
754                            assert len(self._vault['account'][x['account']]['box']) == 0
755                            assert self._vault['account'][x['account']]['balance'] == 0
756                            assert self._vault['account'][x['account']]['count'] == 0
757                            if dry:
758                                continue
759                            del self._vault['account'][x['account']]
760
761                case Action.TRACK:
762                    if x['account'] is not None:
763                        if self.account_exists(x['account']):
764                            if dry:
765                                continue
766                            self._vault['account'][x['account']]['balance'] -= x['value']
767                            self._vault['account'][x['account']]['count'] -= 1
768                            del self._vault['account'][x['account']]['box'][x['ref']]
769
770                case Action.LOG:
771                    if x['account'] is not None:
772                        if self.account_exists(x['account']):
773                            if x['ref'] in self._vault['account'][x['account']]['log']:
774                                if dry:
775                                    continue
776                                if sub_positive_log_negative == -x['value']:
777                                    self._vault['account'][x['account']]['count'] -= 1
778                                    sub_positive_log_negative = 0
779                                box_ref = self._vault['account'][x['account']]['log'][x['ref']]['ref']
780                                if not box_ref is None:
781                                    assert self.box_exists(x['account'], box_ref)
782                                    box_value = self._vault['account'][x['account']]['log'][x['ref']]['value']
783                                    assert box_value < 0
784
785                                    try:
786                                        self._vault['account'][x['account']]['box'][box_ref]['rest'] += -box_value
787                                    except TypeError:
788                                        self._vault['account'][x['account']]['box'][box_ref]['rest'] += Decimal(-box_value)
789
790                                    try:
791                                        self._vault['account'][x['account']]['balance'] += -box_value
792                                    except TypeError:
793                                        self._vault['account'][x['account']]['balance'] += Decimal(-box_value)
794
795                                    self._vault['account'][x['account']]['count'] -= 1
796                                del self._vault['account'][x['account']]['log'][x['ref']]
797
798                case Action.SUB:
799                    if x['account'] is not None:
800                        if self.account_exists(x['account']):
801                            if x['ref'] in self._vault['account'][x['account']]['box']:
802                                if dry:
803                                    continue
804                                self._vault['account'][x['account']]['box'][x['ref']]['rest'] += x['value']
805                                self._vault['account'][x['account']]['balance'] += x['value']
806                                sub_positive_log_negative = x['value']
807
808                case Action.ADD_FILE:
809                    if x['account'] is not None:
810                        if self.account_exists(x['account']):
811                            if x['ref'] in self._vault['account'][x['account']]['log']:
812                                if x['file'] in self._vault['account'][x['account']]['log'][x['ref']]['file']:
813                                    if dry:
814                                        continue
815                                    del self._vault['account'][x['account']]['log'][x['ref']]['file'][x['file']]
816
817                case Action.REMOVE_FILE:
818                    if x['account'] is not None:
819                        if self.account_exists(x['account']):
820                            if x['ref'] in self._vault['account'][x['account']]['log']:
821                                if dry:
822                                    continue
823                                self._vault['account'][x['account']]['log'][x['ref']]['file'][x['file']] = x['value']
824
825                case Action.BOX_TRANSFER:
826                    if x['account'] is not None:
827                        if self.account_exists(x['account']):
828                            if x['ref'] in self._vault['account'][x['account']]['box']:
829                                if dry:
830                                    continue
831                                self._vault['account'][x['account']]['box'][x['ref']]['rest'] -= x['value']
832
833                case Action.EXCHANGE:
834                    if x['account'] is not None:
835                        if x['account'] in self._vault['exchange']:
836                            if x['ref'] in self._vault['exchange'][x['account']]:
837                                if dry:
838                                    continue
839                                del self._vault['exchange'][x['account']][x['ref']]
840
841                case Action.REPORT:
842                    if x['ref'] in self._vault['report']:
843                        if dry:
844                            continue
845                        del self._vault['report'][x['ref']]
846
847                case Action.ZAKAT:
848                    if x['account'] is not None:
849                        if self.account_exists(x['account']):
850                            if x['ref'] in self._vault['account'][x['account']]['box']:
851                                if x['key'] in self._vault['account'][x['account']]['box'][x['ref']]:
852                                    if dry:
853                                        continue
854                                    match x['math']:
855                                        case MathOperation.ADDITION:
856                                            self._vault['account'][x['account']]['box'][x['ref']][x['key']] -= x[
857                                                'value']
858                                        case MathOperation.EQUAL:
859                                            self._vault['account'][x['account']]['box'][x['ref']][x['key']] = x['value']
860                                        case MathOperation.SUBTRACTION:
861                                            self._vault['account'][x['account']]['box'][x['ref']][x['key']] += x[
862                                                'value']
863
864        if not dry:
865            del self._vault['history'][ref]
866        return True

Revert the last operation.

Parameters: dry (bool): If True, the function will not modify the data, but will simulate the operation. Default is True. debug (bool): If True, the function will print debug information. Default is False.

Returns: bool: True if the operation was successful, False otherwise.

def ref_exists(self, account: str, ref_type: str, ref: int) -> bool:
868    def ref_exists(self, account: str, ref_type: str, ref: int) -> bool:
869        """
870        Check if a specific reference (transaction) exists in the vault for a given account and reference type.
871
872        Parameters:
873        account (str): The account number for which to check the existence of the reference.
874        ref_type (str): The type of reference (e.g., 'box', 'log', etc.).
875        ref (int): The reference (transaction) number to check for existence.
876
877        Returns:
878        bool: True if the reference exists for the given account and reference type, False otherwise.
879        """
880        if account in self._vault['account']:
881            return ref in self._vault['account'][account][ref_type]
882        return False

Check if a specific reference (transaction) exists in the vault for a given account and reference type.

Parameters: account (str): The account number for which to check the existence of the reference. ref_type (str): The type of reference (e.g., 'box', 'log', etc.). ref (int): The reference (transaction) number to check for existence.

Returns: bool: True if the reference exists for the given account and reference type, False otherwise.

def box_exists(self, account: str, ref: int) -> bool:
884    def box_exists(self, account: str, ref: int) -> bool:
885        """
886        Check if a specific box (transaction) exists in the vault for a given account and reference.
887
888        Parameters:
889        - account (str): The account number for which to check the existence of the box.
890        - ref (int): The reference (transaction) number to check for existence.
891
892        Returns:
893        - bool: True if the box exists for the given account and reference, False otherwise.
894        """
895        return self.ref_exists(account, 'box', ref)

Check if a specific box (transaction) exists in the vault for a given account and reference.

Parameters:

  • account (str): The account number for which to check the existence of the box.
  • ref (int): The reference (transaction) number to check for existence.

Returns:

  • bool: True if the box exists for the given account and reference, False otherwise.
def track( self, value: float = 0, desc: str = '', account: str = 1, logging: bool = True, created: int = None, debug: bool = False) -> int:
897    def track(self, value: float = 0, desc: str = '', account: str = 1, logging: bool = True, created: int = None,
898              debug: bool = False) -> int:
899        """
900        This function tracks a transaction for a specific account.
901
902        Parameters:
903        value (float): The value of the transaction. Default is 0.
904        desc (str): The description of the transaction. Default is an empty string.
905        account (str): The account for which the transaction is being tracked. Default is '1'.
906        logging (bool): Whether to log the transaction. Default is True.
907        created (int): The timestamp of the transaction. If not provided, it will be generated. Default is None.
908        debug (bool): Whether to print debug information. Default is False.
909
910        Returns:
911        int: The timestamp of the transaction.
912
913        This function creates a new account if it doesn't exist, logs the transaction if logging is True, and updates the account's balance and box.
914
915        Raises:
916        ValueError: The log transaction happened again in the same nanosecond time.
917        ValueError: The box transaction happened again in the same nanosecond time.
918        """
919        if debug:
920            print('track', f'debug={debug}')
921        if created is None:
922            created = self.time()
923        no_lock = self.nolock()
924        self.lock()
925        if not self.account_exists(account):
926            if debug:
927                print(f"account {account} created")
928            self._vault['account'][account] = {
929                'balance': 0,
930                'box': {},
931                'count': 0,
932                'log': {},
933                'hide': False,
934                'zakatable': True,
935            }
936            self._step(Action.CREATE, account)
937        if value == 0:
938            if no_lock:
939                self.free(self.lock())
940            return 0
941        if logging:
942            self._log(value=value, desc=desc, account=account, created=created, ref=None, debug=debug)
943        if debug:
944            print('create-box', created)
945        if self.box_exists(account, created):
946            raise ValueError(f"The box transaction happened again in the same nanosecond time({created}).")
947        if debug:
948            print('created-box', created)
949        self._vault['account'][account]['box'][created] = {
950            'capital': value,
951            'count': 0,
952            'last': 0,
953            'rest': value,
954            'total': 0,
955        }
956        self._step(Action.TRACK, account, ref=created, value=value)
957        if no_lock:
958            self.free(self.lock())
959        return created

This function tracks a transaction for a specific account.

Parameters: value (float): The value of the transaction. Default is 0. desc (str): The description of the transaction. Default is an empty string. account (str): The account for which the transaction is being tracked. Default is '1'. logging (bool): Whether to log the transaction. Default is True. created (int): The timestamp of the transaction. If not provided, it will be generated. Default is None. debug (bool): Whether to print debug information. Default is False.

Returns: int: The timestamp of the transaction.

This function creates a new account if it doesn't exist, logs the transaction if logging is True, and updates the account's balance and box.

Raises: ValueError: The log transaction happened again in the same nanosecond time. ValueError: The box transaction happened again in the same nanosecond time.

def log_exists(self, account: str, ref: int) -> bool:
961    def log_exists(self, account: str, ref: int) -> bool:
962        """
963        Checks if a specific transaction log entry exists for a given account.
964
965        Parameters:
966        account (str): The account number associated with the transaction log.
967        ref (int): The reference to the transaction log entry.
968
969        Returns:
970        bool: True if the transaction log entry exists, False otherwise.
971        """
972        return self.ref_exists(account, 'log', ref)

Checks if a specific transaction log entry exists for a given account.

Parameters: account (str): The account number associated with the transaction log. ref (int): The reference to the transaction log entry.

Returns: bool: True if the transaction log entry exists, False otherwise.

def exchange( self, account, created: int = None, rate: float = None, description: str = None, debug: bool = False) -> dict:
1018    def exchange(self, account, created: int = None, rate: float = None, description: str = None,
1019                 debug: bool = False) -> dict:
1020        """
1021        This method is used to record or retrieve exchange rates for a specific account.
1022
1023        Parameters:
1024        - account (str): The account number for which the exchange rate is being recorded or retrieved.
1025        - created (int): The timestamp of the exchange rate. If not provided, the current timestamp will be used.
1026        - rate (float): The exchange rate to be recorded. If not provided, the method will retrieve the latest exchange rate.
1027        - description (str): A description of the exchange rate.
1028
1029        Returns:
1030        - dict: A dictionary containing the latest exchange rate and its description. If no exchange rate is found,
1031        it returns a dictionary with default values for the rate and description.
1032        """
1033        if debug:
1034            print('exchange', f'debug={debug}')
1035        if created is None:
1036            created = self.time()
1037        no_lock = self.nolock()
1038        self.lock()
1039        if rate is not None:
1040            if rate <= 0:
1041                return dict()
1042            if account not in self._vault['exchange']:
1043                self._vault['exchange'][account] = {}
1044            if len(self._vault['exchange'][account]) == 0 and rate <= 1:
1045                return {"time": created, "rate": 1, "description": None}
1046            self._vault['exchange'][account][created] = {"rate": rate, "description": description}
1047            self._step(Action.EXCHANGE, account, ref=created, value=rate)
1048            if no_lock:
1049                self.free(self.lock())
1050            if debug:
1051                print("exchange-created-1",
1052                      f'account: {account}, created: {created}, rate:{rate}, description:{description}')
1053
1054        if account in self._vault['exchange']:
1055            valid_rates = [(ts, r) for ts, r in self._vault['exchange'][account].items() if ts <= created]
1056            if valid_rates:
1057                latest_rate = max(valid_rates, key=lambda x: x[0])
1058                if debug:
1059                    print("exchange-read-1",
1060                          f'account: {account}, created: {created}, rate:{rate}, description:{description}',
1061                          'latest_rate', latest_rate)
1062                result = latest_rate[1]
1063                result['time'] = latest_rate[0]
1064                return result  # إرجاع قاموس يحتوي على المعدل والوصف
1065        if debug:
1066            print("exchange-read-0", f'account: {account}, created: {created}, rate:{rate}, description:{description}')
1067        return {"time": created, "rate": 1, "description": None}  # إرجاع القيمة الافتراضية مع وصف فارغ

This method is used to record or retrieve exchange rates for a specific account.

Parameters:

  • account (str): The account number for which the exchange rate is being recorded or retrieved.
  • created (int): The timestamp of the exchange rate. If not provided, the current timestamp will be used.
  • rate (float): The exchange rate to be recorded. If not provided, the method will retrieve the latest exchange rate.
  • description (str): A description of the exchange rate.

Returns:

  • dict: A dictionary containing the latest exchange rate and its description. If no exchange rate is found, it returns a dictionary with default values for the rate and description.
@staticmethod
def exchange_calc(x: float, x_rate: float, y_rate: float) -> float:
1069    @staticmethod
1070    def exchange_calc(x: float, x_rate: float, y_rate: float) -> float:
1071        """
1072        This function calculates the exchanged amount of a currency.
1073
1074        Args:
1075            x (float): The original amount of the currency.
1076            x_rate (float): The exchange rate of the original currency.
1077            y_rate (float): The exchange rate of the target currency.
1078
1079        Returns:
1080            float: The exchanged amount of the target currency.
1081        """
1082        return (x * x_rate) / y_rate

This function calculates the exchanged amount of a currency.

Args: x (float): The original amount of the currency. x_rate (float): The exchange rate of the original currency. y_rate (float): The exchange rate of the target currency.

Returns: float: The exchanged amount of the target currency.

def exchanges(self) -> dict:
1084    def exchanges(self) -> dict:
1085        """
1086        Retrieve the recorded exchange rates for all accounts.
1087
1088        Parameters:
1089        None
1090
1091        Returns:
1092        dict: A dictionary containing all recorded exchange rates.
1093        The keys are account names or numbers, and the values are dictionaries containing the exchange rates.
1094        Each exchange rate dictionary has timestamps as keys and exchange rate details as values.
1095        """
1096        return self._vault['exchange'].copy()

Retrieve the recorded exchange rates for all accounts.

Parameters: None

Returns: dict: A dictionary containing all recorded exchange rates. The keys are account names or numbers, and the values are dictionaries containing the exchange rates. Each exchange rate dictionary has timestamps as keys and exchange rate details as values.

def accounts(self) -> dict:
1098    def accounts(self) -> dict:
1099        """
1100        Returns a dictionary containing account numbers as keys and their respective balances as values.
1101
1102        Parameters:
1103        None
1104
1105        Returns:
1106        dict: A dictionary where keys are account numbers and values are their respective balances.
1107        """
1108        result = {}
1109        for i in self._vault['account']:
1110            result[i] = self._vault['account'][i]['balance']
1111        return result

Returns a dictionary containing account numbers as keys and their respective balances as values.

Parameters: None

Returns: dict: A dictionary where keys are account numbers and values are their respective balances.

def boxes(self, account) -> dict:
1113    def boxes(self, account) -> dict:
1114        """
1115        Retrieve the boxes (transactions) associated with a specific account.
1116
1117        Parameters:
1118        account (str): The account number for which to retrieve the boxes.
1119
1120        Returns:
1121        dict: A dictionary containing the boxes associated with the given account.
1122        If the account does not exist, an empty dictionary is returned.
1123        """
1124        if self.account_exists(account):
1125            return self._vault['account'][account]['box']
1126        return {}

Retrieve the boxes (transactions) associated with a specific account.

Parameters: account (str): The account number for which to retrieve the boxes.

Returns: dict: A dictionary containing the boxes associated with the given account. If the account does not exist, an empty dictionary is returned.

def logs(self, account) -> dict:
1128    def logs(self, account) -> dict:
1129        """
1130        Retrieve the logs (transactions) associated with a specific account.
1131
1132        Parameters:
1133        account (str): The account number for which to retrieve the logs.
1134
1135        Returns:
1136        dict: A dictionary containing the logs associated with the given account.
1137        If the account does not exist, an empty dictionary is returned.
1138        """
1139        if self.account_exists(account):
1140            return self._vault['account'][account]['log']
1141        return {}

Retrieve the logs (transactions) associated with a specific account.

Parameters: account (str): The account number for which to retrieve the logs.

Returns: dict: A dictionary containing the logs associated with the given account. If the account does not exist, an empty dictionary is returned.

def add_file(self, account: str, ref: int, path: str) -> int:
1143    def add_file(self, account: str, ref: int, path: str) -> int:
1144        """
1145        Adds a file reference to a specific transaction log entry in the vault.
1146
1147        Parameters:
1148        account (str): The account number associated with the transaction log.
1149        ref (int): The reference to the transaction log entry.
1150        path (str): The path of the file to be added.
1151
1152        Returns:
1153        int: The reference of the added file. If the account or transaction log entry does not exist, returns 0.
1154        """
1155        if self.account_exists(account):
1156            if ref in self._vault['account'][account]['log']:
1157                file_ref = self.time()
1158                self._vault['account'][account]['log'][ref]['file'][file_ref] = path
1159                no_lock = self.nolock()
1160                self.lock()
1161                self._step(Action.ADD_FILE, account, ref=ref, file=file_ref)
1162                if no_lock:
1163                    self.free(self.lock())
1164                return file_ref
1165        return 0

Adds a file reference to a specific transaction log entry in the vault.

Parameters: account (str): The account number associated with the transaction log. ref (int): The reference to the transaction log entry. path (str): The path of the file to be added.

Returns: int: The reference of the added file. If the account or transaction log entry does not exist, returns 0.

def remove_file(self, account: str, ref: int, file_ref: int) -> bool:
1167    def remove_file(self, account: str, ref: int, file_ref: int) -> bool:
1168        """
1169        Removes a file reference from a specific transaction log entry in the vault.
1170
1171        Parameters:
1172        account (str): The account number associated with the transaction log.
1173        ref (int): The reference to the transaction log entry.
1174        file_ref (int): The reference of the file to be removed.
1175
1176        Returns:
1177        bool: True if the file reference is successfully removed, False otherwise.
1178        """
1179        if self.account_exists(account):
1180            if ref in self._vault['account'][account]['log']:
1181                if file_ref in self._vault['account'][account]['log'][ref]['file']:
1182                    x = self._vault['account'][account]['log'][ref]['file'][file_ref]
1183                    del self._vault['account'][account]['log'][ref]['file'][file_ref]
1184                    no_lock = self.nolock()
1185                    self.lock()
1186                    self._step(Action.REMOVE_FILE, account, ref=ref, file=file_ref, value=x)
1187                    if no_lock:
1188                        self.free(self.lock())
1189                    return True
1190        return False

Removes a file reference from a specific transaction log entry in the vault.

Parameters: account (str): The account number associated with the transaction log. ref (int): The reference to the transaction log entry. file_ref (int): The reference of the file to be removed.

Returns: bool: True if the file reference is successfully removed, False otherwise.

def balance(self, account: str = 1, cached: bool = True) -> int:
1192    def balance(self, account: str = 1, cached: bool = True) -> int:
1193        """
1194        Calculate and return the balance of a specific account.
1195
1196        Parameters:
1197        account (str): The account number. Default is '1'.
1198        cached (bool): If True, use the cached balance. If False, calculate the balance from the box. Default is True.
1199
1200        Returns:
1201        int: The balance of the account.
1202
1203        Note:
1204        If cached is True, the function returns the cached balance.
1205        If cached is False, the function calculates the balance from the box by summing up the 'rest' values of all box items.
1206        """
1207        if cached:
1208            return self._vault['account'][account]['balance']
1209        x = 0
1210        return [x := x + y['rest'] for y in self._vault['account'][account]['box'].values()][-1]

Calculate and return the balance of a specific account.

Parameters: account (str): The account number. Default is '1'. cached (bool): If True, use the cached balance. If False, calculate the balance from the box. Default is True.

Returns: int: The balance of the account.

Note: If cached is True, the function returns the cached balance. If cached is False, the function calculates the balance from the box by summing up the 'rest' values of all box items.

def hide(self, account, status: bool = None) -> bool:
1212    def hide(self, account, status: bool = None) -> bool:
1213        """
1214        Check or set the hide status of a specific account.
1215
1216        Parameters:
1217        account (str): The account number.
1218        status (bool, optional): The new hide status. If not provided, the function will return the current status.
1219
1220        Returns:
1221        bool: The current or updated hide status of the account.
1222
1223        Raises:
1224        None
1225
1226        Example:
1227        >>> tracker = ZakatTracker()
1228        >>> ref = tracker.track(51, 'desc', 'account1')
1229        >>> tracker.hide('account1')  # Set the hide status of 'account1' to True
1230        False
1231        >>> tracker.hide('account1', True)  # Set the hide status of 'account1' to True
1232        True
1233        >>> tracker.hide('account1')  # Get the hide status of 'account1' by default
1234        True
1235        >>> tracker.hide('account1', False)
1236        False
1237        """
1238        if self.account_exists(account):
1239            if status is None:
1240                return self._vault['account'][account]['hide']
1241            self._vault['account'][account]['hide'] = status
1242            return status
1243        return False

Check or set the hide status of a specific account.

Parameters: account (str): The account number. status (bool, optional): The new hide status. If not provided, the function will return the current status.

Returns: bool: The current or updated hide status of the account.

Raises: None

Example:

>>> tracker = ZakatTracker()
>>> ref = tracker.track(51, 'desc', 'account1')
>>> tracker.hide('account1')  # Set the hide status of 'account1' to True
False
>>> tracker.hide('account1', True)  # Set the hide status of 'account1' to True
True
>>> tracker.hide('account1')  # Get the hide status of 'account1' by default
True
>>> tracker.hide('account1', False)
False
def zakatable(self, account, status: bool = None) -> bool:
1245    def zakatable(self, account, status: bool = None) -> bool:
1246        """
1247        Check or set the zakatable status of a specific account.
1248
1249        Parameters:
1250        account (str): The account number.
1251        status (bool, optional): The new zakatable status. If not provided, the function will return the current status.
1252
1253        Returns:
1254        bool: The current or updated zakatable status of the account.
1255
1256        Raises:
1257        None
1258
1259        Example:
1260        >>> tracker = ZakatTracker()
1261        >>> ref = tracker.track(51, 'desc', 'account1')
1262        >>> tracker.zakatable('account1')  # Set the zakatable status of 'account1' to True
1263        True
1264        >>> tracker.zakatable('account1', True)  # Set the zakatable status of 'account1' to True
1265        True
1266        >>> tracker.zakatable('account1')  # Get the zakatable status of 'account1' by default
1267        True
1268        >>> tracker.zakatable('account1', False)
1269        False
1270        """
1271        if self.account_exists(account):
1272            if status is None:
1273                return self._vault['account'][account]['zakatable']
1274            self._vault['account'][account]['zakatable'] = status
1275            return status
1276        return False

Check or set the zakatable status of a specific account.

Parameters: account (str): The account number. status (bool, optional): The new zakatable status. If not provided, the function will return the current status.

Returns: bool: The current or updated zakatable status of the account.

Raises: None

Example:

>>> tracker = ZakatTracker()
>>> ref = tracker.track(51, 'desc', 'account1')
>>> tracker.zakatable('account1')  # Set the zakatable status of 'account1' to True
True
>>> tracker.zakatable('account1', True)  # Set the zakatable status of 'account1' to True
True
>>> tracker.zakatable('account1')  # Get the zakatable status of 'account1' by default
True
>>> tracker.zakatable('account1', False)
False
def sub( self, x: float, desc: str = '', account: str = 1, created: int = None, debug: bool = False) -> tuple:
1278    def sub(self, x: float, desc: str = '', account: str = 1, created: int = None, debug: bool = False) -> tuple:
1279        """
1280        Subtracts a specified value from an account's balance.
1281
1282        Parameters:
1283        x (float): The amount to be subtracted.
1284        desc (str): A description for the transaction. Defaults to an empty string.
1285        account (str): The account from which the value will be subtracted. Defaults to '1'.
1286        created (int): The timestamp of the transaction. If not provided, the current timestamp will be used.
1287        debug (bool): A flag indicating whether to print debug information. Defaults to False.
1288
1289        Returns:
1290        tuple: A tuple containing the timestamp of the transaction and a list of tuples representing the age of each transaction.
1291
1292        If the amount to subtract is greater than the account's balance,
1293        the remaining amount will be transferred to a new transaction with a negative value.
1294
1295        Raises:
1296        ValueError: The box transaction happened again in the same nanosecond time.
1297        ValueError: The log transaction happened again in the same nanosecond time.
1298        """
1299        if debug:
1300            print('sub', f'debug={debug}')
1301        if x < 0:
1302            return tuple()
1303        if x == 0:
1304            ref = self.track(x, '', account)
1305            return ref, ref
1306        if created is None:
1307            created = self.time()
1308        no_lock = self.nolock()
1309        self.lock()
1310        self.track(0, '', account)
1311        self._log(value=-x, desc=desc, account=account, created=created, ref=None, debug=debug)
1312        ids = sorted(self._vault['account'][account]['box'].keys())
1313        limit = len(ids) + 1
1314        target = x
1315        if debug:
1316            print('ids', ids)
1317        ages = []
1318        for i in range(-1, -limit, -1):
1319            if target == 0:
1320                break
1321            j = ids[i]
1322            if debug:
1323                print('i', i, 'j', j)
1324            rest = self._vault['account'][account]['box'][j]['rest']
1325            if rest >= target:
1326                self._vault['account'][account]['box'][j]['rest'] -= target
1327                self._step(Action.SUB, account, ref=j, value=target)
1328                ages.append((j, target))
1329                target = 0
1330                break
1331            elif target > rest > 0:
1332                chunk = rest
1333                target -= chunk
1334                self._step(Action.SUB, account, ref=j, value=chunk)
1335                ages.append((j, chunk))
1336                self._vault['account'][account]['box'][j]['rest'] = 0
1337        if target > 0:
1338            self.track(-target, desc, account, False, created)
1339            ages.append((created, target))
1340        if no_lock:
1341            self.free(self.lock())
1342        return created, ages

Subtracts a specified value from an account's balance.

Parameters: x (float): The amount to be subtracted. desc (str): A description for the transaction. Defaults to an empty string. account (str): The account from which the value will be subtracted. Defaults to '1'. created (int): The timestamp of the transaction. If not provided, the current timestamp will be used. debug (bool): A flag indicating whether to print debug information. Defaults to False.

Returns: tuple: A tuple containing the timestamp of the transaction and a list of tuples representing the age of each transaction.

If the amount to subtract is greater than the account's balance, the remaining amount will be transferred to a new transaction with a negative value.

Raises: ValueError: The box transaction happened again in the same nanosecond time. ValueError: The log transaction happened again in the same nanosecond time.

def transfer( self, amount: int, from_account: str, to_account: str, desc: str = '', created: int = None, debug: bool = False) -> list[int]:
1344    def transfer(self, amount: int, from_account: str, to_account: str, desc: str = '', created: int = None,
1345                 debug: bool = False) -> list[int]:
1346        """
1347        Transfers a specified value from one account to another.
1348
1349        Parameters:
1350        amount (int): The amount to be transferred.
1351        from_account (str): The account from which the value will be transferred.
1352        to_account (str): The account to which the value will be transferred.
1353        desc (str, optional): A description for the transaction. Defaults to an empty string.
1354        created (int, optional): The timestamp of the transaction. If not provided, the current timestamp will be used.
1355        debug (bool): A flag indicating whether to print debug information. Defaults to False.
1356
1357        Returns:
1358        list[int]: A list of timestamps corresponding to the transactions made during the transfer.
1359
1360        Raises:
1361        ValueError: Transfer to the same account is forbidden.
1362        ValueError: The box transaction happened again in the same nanosecond time.
1363        ValueError: The log transaction happened again in the same nanosecond time.
1364        """
1365        if debug:
1366            print('transfer', f'debug={debug}')
1367        if from_account == to_account:
1368            raise ValueError(f'Transfer to the same account is forbidden. {to_account}')
1369        if amount <= 0:
1370            return []
1371        if created is None:
1372            created = self.time()
1373        (_, ages) = self.sub(amount, desc, from_account, created, debug=debug)
1374        times = []
1375        source_exchange = self.exchange(from_account, created)
1376        target_exchange = self.exchange(to_account, created)
1377
1378        if debug:
1379            print('ages', ages)
1380
1381        for age, value in ages:
1382            target_amount = self.exchange_calc(value, source_exchange['rate'], target_exchange['rate'])
1383            # Perform the transfer
1384            if self.box_exists(to_account, age):
1385                if debug:
1386                    print('box_exists', age)
1387                capital = self._vault['account'][to_account]['box'][age]['capital']
1388                rest = self._vault['account'][to_account]['box'][age]['rest']
1389                if debug:
1390                    print(
1391                        f"Transfer {value} from `{from_account}` to `{to_account}` (equivalent to {target_amount} `{to_account}`).")
1392                selected_age = age
1393                if rest + target_amount > capital:
1394                    self._vault['account'][to_account]['box'][age]['capital'] += target_amount
1395                    selected_age = ZakatTracker.time()
1396                self._vault['account'][to_account]['box'][age]['rest'] += target_amount
1397                self._step(Action.BOX_TRANSFER, to_account, ref=selected_age, value=target_amount)
1398                y = self._log(value=target_amount, desc=f'TRANSFER {from_account} -> {to_account}', account=to_account,
1399                              created=None, ref=None, debug=debug)
1400                times.append((age, y))
1401                continue
1402            y = self.track(target_amount, desc, to_account, logging=True, created=age, debug=debug)
1403            if debug:
1404                print(
1405                    f"Transferred {value} from `{from_account}` to `{to_account}` (equivalent to {target_amount} `{to_account}`).")
1406            times.append(y)
1407        return times

Transfers a specified value from one account to another.

Parameters: amount (int): The amount to be transferred. from_account (str): The account from which the value will be transferred. to_account (str): The account to which the value will be transferred. desc (str, optional): A description for the transaction. Defaults to an empty string. created (int, optional): The timestamp of the transaction. If not provided, the current timestamp will be used. debug (bool): A flag indicating whether to print debug information. Defaults to False.

Returns: list[int]: A list of timestamps corresponding to the transactions made during the transfer.

Raises: ValueError: Transfer to the same account is forbidden. ValueError: The box transaction happened again in the same nanosecond time. ValueError: The log transaction happened again in the same nanosecond time.

def check( self, silver_gram_price: float, nisab: float = None, debug: bool = False, now: int = None, cycle: float = None) -> tuple:
1409    def check(self, silver_gram_price: float, nisab: float = None, debug: bool = False, now: int = None,
1410              cycle: float = None) -> tuple:
1411        """
1412        Check the eligibility for Zakat based on the given parameters.
1413
1414        Parameters:
1415        silver_gram_price (float): The price of a gram of silver.
1416        nisab (float): The minimum amount of wealth required for Zakat. If not provided,
1417                        it will be calculated based on the silver_gram_price.
1418        debug (bool): Flag to enable debug mode.
1419        now (int): The current timestamp. If not provided, it will be calculated using ZakatTracker.time().
1420        cycle (float): The time cycle for Zakat. If not provided, it will be calculated using ZakatTracker.TimeCycle().
1421
1422        Returns:
1423        tuple: A tuple containing a boolean indicating the eligibility for Zakat, a list of brief statistics,
1424        and a dictionary containing the Zakat plan.
1425        """
1426        if debug:
1427            print('check', f'debug={debug}')
1428        if now is None:
1429            now = self.time()
1430        if cycle is None:
1431            cycle = ZakatTracker.TimeCycle()
1432        if nisab is None:
1433            nisab = ZakatTracker.Nisab(silver_gram_price)
1434        plan = {}
1435        below_nisab = 0
1436        brief = [0, 0, 0]
1437        valid = False
1438        if debug:
1439            print('exchanges', self.exchanges())
1440        for x in self._vault['account']:
1441            if not self.zakatable(x):
1442                continue
1443            _box = self._vault['account'][x]['box']
1444            _log = self._vault['account'][x]['log']
1445            limit = len(_box) + 1
1446            ids = sorted(self._vault['account'][x]['box'].keys())
1447            for i in range(-1, -limit, -1):
1448                j = ids[i]
1449                rest = float(_box[j]['rest'])
1450                if rest <= 0:
1451                    continue
1452                exchange = self.exchange(x, created=self.time())
1453                rest = ZakatTracker.exchange_calc(rest, float(exchange['rate']), 1)
1454                brief[0] += rest
1455                index = limit + i - 1
1456                epoch = (now - j) / cycle
1457                if debug:
1458                    print(f"Epoch: {epoch}", _box[j])
1459                if _box[j]['last'] > 0:
1460                    epoch = (now - _box[j]['last']) / cycle
1461                if debug:
1462                    print(f"Epoch: {epoch}")
1463                epoch = floor(epoch)
1464                if debug:
1465                    print(f"Epoch: {epoch}", type(epoch), epoch == 0, 1 - epoch, epoch)
1466                if epoch == 0:
1467                    continue
1468                if debug:
1469                    print("Epoch - PASSED")
1470                brief[1] += rest
1471                if rest >= nisab:
1472                    total = 0
1473                    for _ in range(epoch):
1474                        total += ZakatTracker.ZakatCut(float(rest) - float(total))
1475                    if total > 0:
1476                        if x not in plan:
1477                            plan[x] = {}
1478                        valid = True
1479                        brief[2] += total
1480                        plan[x][index] = {
1481                            'total': total,
1482                            'count': epoch,
1483                            'box_time': j,
1484                            'box_capital': _box[j]['capital'],
1485                            'box_rest': _box[j]['rest'],
1486                            'box_last': _box[j]['last'],
1487                            'box_total': _box[j]['total'],
1488                            'box_count': _box[j]['count'],
1489                            'box_log': _log[j]['desc'],
1490                            'exchange_rate': exchange['rate'],
1491                            'exchange_time': exchange['time'],
1492                            'exchange_desc': exchange['description'],
1493                        }
1494                else:
1495                    chunk = ZakatTracker.ZakatCut(float(rest))
1496                    if chunk > 0:
1497                        if x not in plan:
1498                            plan[x] = {}
1499                        if j not in plan[x].keys():
1500                            plan[x][index] = {}
1501                        below_nisab += rest
1502                        brief[2] += chunk
1503                        plan[x][index]['below_nisab'] = chunk
1504                        plan[x][index]['total'] = chunk
1505                        plan[x][index]['count'] = epoch
1506                        plan[x][index]['box_time'] = j
1507                        plan[x][index]['box_capital'] = _box[j]['capital']
1508                        plan[x][index]['box_rest'] = _box[j]['rest']
1509                        plan[x][index]['box_last'] = _box[j]['last']
1510                        plan[x][index]['box_total'] = _box[j]['total']
1511                        plan[x][index]['box_count'] = _box[j]['count']
1512                        plan[x][index]['box_log'] = _log[j]['desc']
1513                        plan[x][index]['exchange_rate'] = exchange['rate']
1514                        plan[x][index]['exchange_time'] = exchange['time']
1515                        plan[x][index]['exchange_desc'] = exchange['description']
1516        valid = valid or below_nisab >= nisab
1517        if debug:
1518            print(f"below_nisab({below_nisab}) >= nisab({nisab})")
1519        return valid, brief, plan

Check the eligibility for Zakat based on the given parameters.

Parameters: silver_gram_price (float): The price of a gram of silver. nisab (float): The minimum amount of wealth required for Zakat. If not provided, it will be calculated based on the silver_gram_price. debug (bool): Flag to enable debug mode. now (int): The current timestamp. If not provided, it will be calculated using ZakatTracker.time(). cycle (float): The time cycle for Zakat. If not provided, it will be calculated using ZakatTracker.TimeCycle().

Returns: tuple: A tuple containing a boolean indicating the eligibility for Zakat, a list of brief statistics, and a dictionary containing the Zakat plan.

def build_payment_parts(self, demand: float, positive_only: bool = True) -> dict:
1521    def build_payment_parts(self, demand: float, positive_only: bool = True) -> dict:
1522        """
1523        Build payment parts for the Zakat distribution.
1524
1525        Parameters:
1526        demand (float): The total demand for payment in local currency.
1527        positive_only (bool): If True, only consider accounts with positive balance. Default is True.
1528
1529        Returns:
1530        dict: A dictionary containing the payment parts for each account. The dictionary has the following structure:
1531        {
1532            'account': {
1533                'account_id': {'balance': float, 'rate': float, 'part': float},
1534                ...
1535            },
1536            'exceed': bool,
1537            'demand': float,
1538            'total': float,
1539        }
1540        """
1541        total = 0
1542        parts = {
1543            'account': {},
1544            'exceed': False,
1545            'demand': demand,
1546        }
1547        for x, y in self.accounts().items():
1548            if positive_only and y <= 0:
1549                continue
1550            total += float(y)
1551            exchange = self.exchange(x)
1552            parts['account'][x] = {'balance': y, 'rate': exchange['rate'], 'part': 0}
1553        parts['total'] = total
1554        return parts

Build payment parts for the Zakat distribution.

Parameters: demand (float): The total demand for payment in local currency. positive_only (bool): If True, only consider accounts with positive balance. Default is True.

Returns: dict: A dictionary containing the payment parts for each account. The dictionary has the following structure: { 'account': { 'account_id': {'balance': float, 'rate': float, 'part': float}, ... }, 'exceed': bool, 'demand': float, 'total': float, }

@staticmethod
def check_payment_parts(parts: dict, debug: bool = False) -> int:
1556    @staticmethod
1557    def check_payment_parts(parts: dict, debug: bool = False) -> int:
1558        """
1559        Checks the validity of payment parts.
1560
1561        Parameters:
1562        parts (dict): A dictionary containing payment parts information.
1563        debug (bool): Flag to enable debug mode.
1564
1565        Returns:
1566        int: Returns 0 if the payment parts are valid, otherwise returns the error code.
1567
1568        Error Codes:
1569        1: 'demand', 'account', 'total', or 'exceed' key is missing in parts.
1570        2: 'balance', 'rate' or 'part' key is missing in parts['account'][x].
1571        3: 'part' value in parts['account'][x] is less than 0.
1572        4: If 'exceed' is False, 'balance' value in parts['account'][x] is less than or equal to 0.
1573        5: If 'exceed' is False, 'part' value in parts['account'][x] is greater than 'balance' value.
1574        6: The sum of 'part' values in parts['account'] does not match with 'demand' value.
1575        """
1576        if debug:
1577            print('check_payment_parts', f'debug={debug}')
1578        for i in ['demand', 'account', 'total', 'exceed']:
1579            if i not in parts:
1580                return 1
1581        exceed = parts['exceed']
1582        for x in parts['account']:
1583            for j in ['balance', 'rate', 'part']:
1584                if j not in parts['account'][x]:
1585                    return 2
1586                if parts['account'][x]['part'] < 0:
1587                    return 3
1588                if not exceed and parts['account'][x]['balance'] <= 0:
1589                    return 4
1590        demand = parts['demand']
1591        z = 0
1592        for _, y in parts['account'].items():
1593            if not exceed and y['part'] > y['balance']:
1594                return 5
1595            z += ZakatTracker.exchange_calc(y['part'], y['rate'], 1)
1596        z = round(z, 2)
1597        demand = round(demand, 2)
1598        if debug:
1599            print('check_payment_parts', f'z = {z}, demand = {demand}')
1600            print('check_payment_parts', type(z), type(demand))
1601            print('check_payment_parts', z != demand)
1602            print('check_payment_parts', str(z) != str(demand))
1603        if z != demand and str(z) != str(demand):
1604            return 6
1605        return 0

Checks the validity of payment parts.

Parameters: parts (dict): A dictionary containing payment parts information. debug (bool): Flag to enable debug mode.

Returns: int: Returns 0 if the payment parts are valid, otherwise returns the error code.

Error Codes: 1: 'demand', 'account', 'total', or 'exceed' key is missing in parts. 2: 'balance', 'rate' or 'part' key is missing in parts['account'][x]. 3: 'part' value in parts['account'][x] is less than 0. 4: If 'exceed' is False, 'balance' value in parts['account'][x] is less than or equal to 0. 5: If 'exceed' is False, 'part' value in parts['account'][x] is greater than 'balance' value. 6: The sum of 'part' values in parts['account'] does not match with 'demand' value.

def zakat( self, report: tuple, parts: Dict[str, Union[Dict, bool, Any]] = None, debug: bool = False) -> bool:
1607    def zakat(self, report: tuple, parts: Dict[str, Dict | bool | Any] = None, debug: bool = False) -> bool:
1608        """
1609        Perform Zakat calculation based on the given report and optional parts.
1610
1611        Parameters:
1612        report (tuple): A tuple containing the validity of the report, the report data, and the zakat plan.
1613        parts (dict): A dictionary containing the payment parts for the zakat.
1614        debug (bool): A flag indicating whether to print debug information.
1615
1616        Returns:
1617        bool: True if the zakat calculation is successful, False otherwise.
1618        """
1619        if debug:
1620            print('zakat', f'debug={debug}')
1621        valid, _, plan = report
1622        if not valid:
1623            return valid
1624        parts_exist = parts is not None
1625        if parts_exist:
1626            if self.check_payment_parts(parts, debug=debug) != 0:
1627                return False
1628        if debug:
1629            print('######### zakat #######')
1630            print('parts_exist', parts_exist)
1631        no_lock = self.nolock()
1632        self.lock()
1633        report_time = self.time()
1634        self._vault['report'][report_time] = report
1635        self._step(Action.REPORT, ref=report_time)
1636        created = self.time()
1637        for x in plan:
1638            target_exchange = self.exchange(x)
1639            if debug:
1640                print(plan[x])
1641                print('-------------')
1642                print(self._vault['account'][x]['box'])
1643            ids = sorted(self._vault['account'][x]['box'].keys())
1644            if debug:
1645                print('plan[x]', plan[x])
1646            for i in plan[x].keys():
1647                j = ids[i]
1648                if debug:
1649                    print('i', i, 'j', j)
1650                self._step(Action.ZAKAT, account=x, ref=j, value=self._vault['account'][x]['box'][j]['last'],
1651                           key='last',
1652                           math_operation=MathOperation.EQUAL)
1653                self._vault['account'][x]['box'][j]['last'] = created
1654                amount = ZakatTracker.exchange_calc(float(plan[x][i]['total']), 1, float(target_exchange['rate']))
1655                self._vault['account'][x]['box'][j]['total'] += amount
1656                self._step(Action.ZAKAT, account=x, ref=j, value=amount, key='total',
1657                           math_operation=MathOperation.ADDITION)
1658                self._vault['account'][x]['box'][j]['count'] += plan[x][i]['count']
1659                self._step(Action.ZAKAT, account=x, ref=j, value=plan[x][i]['count'], key='count',
1660                           math_operation=MathOperation.ADDITION)
1661                if not parts_exist:
1662                    try:
1663                        self._vault['account'][x]['box'][j]['rest'] -= amount
1664                    except TypeError:
1665                        self._vault['account'][x]['box'][j]['rest'] -= Decimal(amount)
1666                    # self._step(Action.ZAKAT, account=x, ref=j, value=amount, key='rest',
1667                    #            math_operation=MathOperation.SUBTRACTION)
1668                    self._log(-float(amount), desc='zakat-زكاة', account=x, created=None, ref=j, debug=debug)
1669        if parts_exist:
1670            for account, part in parts['account'].items():
1671                if part['part'] == 0:
1672                    continue
1673                if debug:
1674                    print('zakat-part', account, part['rate'])
1675                target_exchange = self.exchange(account)
1676                amount = ZakatTracker.exchange_calc(part['part'], part['rate'], target_exchange['rate'])
1677                self.sub(amount, desc='zakat-part-دفعة-زكاة', account=account, debug=debug)
1678        if no_lock:
1679            self.free(self.lock())
1680        return True

Perform Zakat calculation based on the given report and optional parts.

Parameters: report (tuple): A tuple containing the validity of the report, the report data, and the zakat plan. parts (dict): A dictionary containing the payment parts for the zakat. debug (bool): A flag indicating whether to print debug information.

Returns: bool: True if the zakat calculation is successful, False otherwise.

def export_json(self, path: str = 'data.json') -> bool:
1682    def export_json(self, path: str = "data.json") -> bool:
1683        """
1684        Exports the current state of the ZakatTracker object to a JSON file.
1685
1686        Parameters:
1687        path (str): The path where the JSON file will be saved. Default is "data.json".
1688
1689        Returns:
1690        bool: True if the export is successful, False otherwise.
1691
1692        Raises:
1693        No specific exceptions are raised by this method.
1694        """
1695        with open(path, "w") as file:
1696            json.dump(self._vault, file, indent=4, cls=JSONEncoder)
1697            return True

Exports the current state of the ZakatTracker object to a JSON file.

Parameters: path (str): The path where the JSON file will be saved. Default is "data.json".

Returns: bool: True if the export is successful, False otherwise.

Raises: No specific exceptions are raised by this method.

def save(self, path: str = None) -> bool:
1699    def save(self, path: str = None) -> bool:
1700        """
1701        Saves the ZakatTracker's current state to a pickle file.
1702
1703        This method serializes the internal data (`_vault`) along with metadata
1704        (Python version, pickle protocol) for future compatibility.
1705
1706        Parameters:
1707        path (str, optional): File path for saving. Defaults to a predefined location.
1708
1709        Returns:
1710        bool: True if the save operation is successful, False otherwise.
1711        """
1712        if path is None:
1713            path = self.path()
1714        with open(path, "wb") as f:
1715            version = f'{version_info.major}.{version_info.minor}.{version_info.micro}'
1716            pickle_protocol = pickle.HIGHEST_PROTOCOL
1717            data = {
1718                'python_version': version,
1719                'pickle_protocol': pickle_protocol,
1720                'data': self._vault,
1721            }
1722            pickle.dump(data, f, protocol=pickle_protocol)
1723            return True

Saves the ZakatTracker's current state to a pickle file.

This method serializes the internal data (_vault) along with metadata (Python version, pickle protocol) for future compatibility.

Parameters: path (str, optional): File path for saving. Defaults to a predefined location.

Returns: bool: True if the save operation is successful, False otherwise.

def load(self, path: str = None) -> bool:
1725    def load(self, path: str = None) -> bool:
1726        """
1727        Load the current state of the ZakatTracker object from a pickle file.
1728
1729        Parameters:
1730        path (str): The path where the pickle file is located. If not provided, it will use the default path.
1731
1732        Returns:
1733        bool: True if the load operation is successful, False otherwise.
1734        """
1735        if path is None:
1736            path = self.path()
1737        if os.path.exists(path):
1738            with open(path, "rb") as f:
1739                data = pickle.load(f)
1740                self._vault = data['data']
1741                return True
1742        return False

Load the current state of the ZakatTracker object from a pickle file.

Parameters: path (str): The path where the pickle file is located. If not provided, it will use the default path.

Returns: bool: True if the load operation is successful, False otherwise.

def import_csv_cache_path(self):
1744    def import_csv_cache_path(self):
1745        """
1746        Generates the cache file path for imported CSV data.
1747
1748        This function constructs the file path where cached data from CSV imports
1749        will be stored. The cache file is a pickle file (.pickle extension) appended
1750        to the base path of the object.
1751
1752        Returns:
1753        str: The full path to the import CSV cache file.
1754
1755        Example:
1756            >>> obj = ZakatTracker('/data/reports')
1757            >>> obj.import_csv_cache_path()
1758            '/data/reports.import_csv.pickle'
1759        """
1760        path = str(self.path())
1761        if path.endswith(".pickle"):
1762            path = path[:-7]
1763        return path + '.import_csv.pickle'

Generates the cache file path for imported CSV data.

This function constructs the file path where cached data from CSV imports will be stored. The cache file is a pickle file (.pickle extension) appended to the base path of the object.

Returns: str: The full path to the import CSV cache file.

Example:

obj = ZakatTracker('/data/reports') obj.import_csv_cache_path() '/data/reports.import_csv.pickle'

def import_csv(self, path: str = 'file.csv', debug: bool = False) -> tuple:
1765    def import_csv(self, path: str = 'file.csv', debug: bool = False) -> tuple:
1766        """
1767        The function reads the CSV file, checks for duplicate transactions, and creates the transactions in the system.
1768
1769        Parameters:
1770        path (str): The path to the CSV file. Default is 'file.csv'.
1771        debug (bool): A flag indicating whether to print debug information.
1772
1773        Returns:
1774        tuple: A tuple containing the number of transactions created, the number of transactions found in the cache,
1775                and a dictionary of bad transactions.
1776
1777        Notes:
1778            * Currency Pair Assumption: This function assumes that the exchange rates stored for each account
1779                                        are appropriate for the currency pairs involved in the conversions.
1780            * The exchange rate for each account is based on the last encountered transaction rate that is not equal
1781                to 1.0 or the previous rate for that account.
1782            * Those rates will be merged into the exchange rates main data, and later it will be used for all subsequent
1783              transactions of the same account within the whole imported and existing dataset when doing `check` and
1784              `zakat` operations.
1785
1786        Example Usage:
1787            The CSV file should have the following format, rate is optional per transaction:
1788            account, desc, value, date, rate
1789            For example:
1790            safe-45, "Some text", 34872, 1988-06-30 00:00:00, 1
1791        """
1792        if debug:
1793            print('import_csv', f'debug={debug}')
1794        cache: list[int] = []
1795        try:
1796            with open(self.import_csv_cache_path(), "rb") as f:
1797                cache = pickle.load(f)
1798        except:
1799            pass
1800        date_formats = [
1801            "%Y-%m-%d %H:%M:%S",
1802            "%Y-%m-%dT%H:%M:%S",
1803            "%Y-%m-%dT%H%M%S",
1804            "%Y-%m-%d",
1805        ]
1806        created, found, bad = 0, 0, {}
1807        data: dict[int, list] = {}
1808        with open(path, newline='', encoding="utf-8") as f:
1809            i = 0
1810            for row in csv.reader(f, delimiter=','):
1811                i += 1
1812                hashed = hash(tuple(row))
1813                if hashed in cache:
1814                    found += 1
1815                    continue
1816                account = row[0]
1817                desc = row[1]
1818                value = float(row[2])
1819                rate = 1.0
1820                if row[4:5]:  # Empty list if index is out of range
1821                    rate = float(row[4])
1822                date: int = 0
1823                for time_format in date_formats:
1824                    try:
1825                        date = self.time(datetime.datetime.strptime(row[3], time_format))
1826                        break
1827                    except:
1828                        pass
1829                # TODO: not allowed for negative dates
1830                if date == 0 or value == 0:
1831                    bad[i] = row
1832                    continue
1833                if date not in data:
1834                    data[date] = []
1835                # TODO: If duplicated time with different accounts with the same amount it is an indicator of a transfer
1836                data[date].append((date, value, desc, account, rate, hashed))
1837
1838        if debug:
1839            print('import_csv', len(data))
1840
1841        def process(row, index=0):
1842            nonlocal created
1843            (date, value, desc, account, rate, hashed) = row
1844            date += index
1845            if rate > 1:
1846                self.exchange(account, created=date, rate=rate)
1847            if value > 0:
1848                self.track(value, desc, account, True, date)
1849            elif value < 0:
1850                self.sub(-value, desc, account, date)
1851            created += 1
1852            cache.append(hashed)
1853
1854        for date, rows in sorted(data.items()):
1855            len_rows = len(rows)
1856            if len_rows == 1:
1857                process(rows[0])
1858                continue
1859            if debug:
1860                print('-- Duplicated time detected', date, 'len', len_rows)
1861                print(rows)
1862                print('---------------------------------')
1863            for index, row in enumerate(rows):
1864                process(row, index)
1865        with open(self.import_csv_cache_path(), "wb") as f:
1866            pickle.dump(cache, f)
1867        return created, found, bad

The function reads the CSV file, checks for duplicate transactions, and creates the transactions in the system.

Parameters: path (str): The path to the CSV file. Default is 'file.csv'. debug (bool): A flag indicating whether to print debug information.

Returns: tuple: A tuple containing the number of transactions created, the number of transactions found in the cache, and a dictionary of bad transactions.

Notes: * Currency Pair Assumption: This function assumes that the exchange rates stored for each account are appropriate for the currency pairs involved in the conversions. * The exchange rate for each account is based on the last encountered transaction rate that is not equal to 1.0 or the previous rate for that account. * Those rates will be merged into the exchange rates main data, and later it will be used for all subsequent transactions of the same account within the whole imported and existing dataset when doing check and zakat operations.

Example Usage: The CSV file should have the following format, rate is optional per transaction: account, desc, value, date, rate For example: safe-45, "Some text", 34872, 1988-06-30 00:00:00, 1

@staticmethod
def human_readable_size(size: float, decimal_places: int = 2) -> str:
1873    @staticmethod
1874    def human_readable_size(size: float, decimal_places: int = 2) -> str:
1875        """
1876        Converts a size in bytes to a human-readable format (e.g., KB, MB, GB).
1877
1878        This function iterates through progressively larger units of information
1879        (B, KB, MB, GB, etc.) and divides the input size until it fits within a
1880        range that can be expressed with a reasonable number before the unit.
1881
1882        Parameters:
1883        size (float): The size in bytes to convert.
1884        decimal_places (int, optional): The number of decimal places to display
1885            in the result. Defaults to 2.
1886
1887        Returns:
1888        str: A string representation of the size in a human-readable format,
1889            rounded to the specified number of decimal places. For example:
1890                - "1.50 KB" (1536 bytes)
1891                - "23.00 MB" (24117248 bytes)
1892                - "1.23 GB" (1325899906 bytes)
1893        """
1894        if type(size) not in (float, int):
1895            raise TypeError("size must be a float or integer")
1896        if type(decimal_places) != int:
1897            raise TypeError("decimal_places must be an integer")
1898        for unit in ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']:
1899            if size < 1024.0:
1900                break
1901            size /= 1024.0
1902        return f"{size:.{decimal_places}f} {unit}"

Converts a size in bytes to a human-readable format (e.g., KB, MB, GB).

This function iterates through progressively larger units of information (B, KB, MB, GB, etc.) and divides the input size until it fits within a range that can be expressed with a reasonable number before the unit.

Parameters: size (float): The size in bytes to convert. decimal_places (int, optional): The number of decimal places to display in the result. Defaults to 2.

Returns: str: A string representation of the size in a human-readable format, rounded to the specified number of decimal places. For example: - "1.50 KB" (1536 bytes) - "23.00 MB" (24117248 bytes) - "1.23 GB" (1325899906 bytes)

@staticmethod
def get_dict_size(obj: dict, seen: set = None) -> float:
1904    @staticmethod
1905    def get_dict_size(obj: dict, seen: set = None) -> float:
1906        """
1907        Recursively calculates the approximate memory size of a dictionary and its contents in bytes.
1908
1909        This function traverses the dictionary structure, accounting for the size of keys, values,
1910        and any nested objects. It handles various data types commonly found in dictionaries
1911        (e.g., lists, tuples, sets, numbers, strings) and prevents infinite recursion in case
1912        of circular references.
1913
1914        Parameters:
1915        obj (dict): The dictionary whose size is to be calculated.
1916        seen (set, optional): A set used internally to track visited objects
1917                             and avoid circular references. Defaults to None.
1918
1919        Returns:
1920            float: An approximate size of the dictionary and its contents in bytes.
1921
1922        Note:
1923        - This function is a method of the `ZakatTracker` class and is likely used to
1924          estimate the memory footprint of data structures relevant to Zakat calculations.
1925        - The size calculation is approximate as it relies on `sys.getsizeof()`, which might
1926          not account for all memory overhead depending on the Python implementation.
1927        - Circular references are handled to prevent infinite recursion.
1928        - Basic numeric types (int, float, complex) are assumed to have fixed sizes.
1929        - String sizes are estimated based on character length and encoding.
1930        """
1931        size = 0
1932        if seen is None:
1933            seen = set()
1934
1935        obj_id = id(obj)
1936        if obj_id in seen:
1937            return 0
1938
1939        seen.add(obj_id)
1940        size += sys.getsizeof(obj)
1941
1942        if isinstance(obj, dict):
1943            for k, v in obj.items():
1944                size += ZakatTracker.get_dict_size(k, seen)
1945                size += ZakatTracker.get_dict_size(v, seen)
1946        elif isinstance(obj, (list, tuple, set, frozenset)):
1947            for item in obj:
1948                size += ZakatTracker.get_dict_size(item, seen)
1949        elif isinstance(obj, (int, float, complex)):  # Handle numbers
1950            pass  # Basic numbers have a fixed size, so nothing to add here
1951        elif isinstance(obj, str):  # Handle strings
1952            size += len(obj) * sys.getsizeof(str().encode())  # Size per character in bytes
1953        return size

Recursively calculates the approximate memory size of a dictionary and its contents in bytes.

This function traverses the dictionary structure, accounting for the size of keys, values, and any nested objects. It handles various data types commonly found in dictionaries (e.g., lists, tuples, sets, numbers, strings) and prevents infinite recursion in case of circular references.

Parameters: obj (dict): The dictionary whose size is to be calculated. seen (set, optional): A set used internally to track visited objects and avoid circular references. Defaults to None.

Returns: float: An approximate size of the dictionary and its contents in bytes.

Note:

  • This function is a method of the ZakatTracker class and is likely used to estimate the memory footprint of data structures relevant to Zakat calculations.
  • The size calculation is approximate as it relies on sys.getsizeof(), which might not account for all memory overhead depending on the Python implementation.
  • Circular references are handled to prevent infinite recursion.
  • Basic numeric types (int, float, complex) are assumed to have fixed sizes.
  • String sizes are estimated based on character length and encoding.
@staticmethod
def duration_from_nanoseconds( ns: int, show_zeros_in_spoken_time: bool = False, spoken_time_separator=',', millennia: str = 'Millennia', century: str = 'Century', years: str = 'Years', days: str = 'Days', hours: str = 'Hours', minutes: str = 'Minutes', seconds: str = 'Seconds', milli_seconds: str = 'MilliSeconds', micro_seconds: str = 'MicroSeconds', nano_seconds: str = 'NanoSeconds') -> tuple:
1955    @staticmethod
1956    def duration_from_nanoseconds(ns: int,
1957                                  show_zeros_in_spoken_time: bool = False,
1958                                  spoken_time_separator=',',
1959                                  millennia: str = 'Millennia',
1960                                  century: str = 'Century',
1961                                  years: str = 'Years',
1962                                  days: str = 'Days',
1963                                  hours: str = 'Hours',
1964                                  minutes: str = 'Minutes',
1965                                  seconds: str = 'Seconds',
1966                                  milli_seconds: str = 'MilliSeconds',
1967                                  micro_seconds: str = 'MicroSeconds',
1968                                  nano_seconds: str = 'NanoSeconds',
1969                                  ) -> tuple:
1970        """
1971        REF https://github.com/JayRizzo/Random_Scripts/blob/master/time_measure.py#L106
1972        Convert NanoSeconds to Human Readable Time Format.
1973        A NanoSeconds is a unit of time in the International System of Units (SI) equal
1974        to one millionth (0.000001 or 10−6 or 1⁄1,000,000) of a second.
1975        Its symbol is μs, sometimes simplified to us when Unicode is not available.
1976        A microsecond is equal to 1000 nanoseconds or 1⁄1,000 of a millisecond.
1977
1978        INPUT : ms (AKA: MilliSeconds)
1979        OUTPUT: tuple(string time_lapsed, string spoken_time) like format.
1980        OUTPUT Variables: time_lapsed, spoken_time
1981
1982        Example  Input: duration_from_nanoseconds(ns)
1983        **"Millennium:Century:Years:Days:Hours:Minutes:Seconds:MilliSeconds:MicroSeconds:NanoSeconds"**
1984        Example Output: ('039:0001:047:325:05:02:03:456:789:012', ' 39 Millennia,    1 Century,  47 Years,  325 Days,  5 Hours,  2 Minutes,  3 Seconds,  456 MilliSeconds,  789 MicroSeconds,  12 NanoSeconds')
1985        duration_from_nanoseconds(1234567890123456789012)
1986        """
1987        us, ns = divmod(ns, 1000)
1988        ms, us = divmod(us, 1000)
1989        s, ms = divmod(ms, 1000)
1990        m, s = divmod(s, 60)
1991        h, m = divmod(m, 60)
1992        d, h = divmod(h, 24)
1993        y, d = divmod(d, 365)
1994        c, y = divmod(y, 100)
1995        n, c = divmod(c, 10)
1996        time_lapsed = f"{n:03.0f}:{c:04.0f}:{y:03.0f}:{d:03.0f}:{h:02.0f}:{m:02.0f}:{s:02.0f}::{ms:03.0f}::{us:03.0f}::{ns:03.0f}"
1997        spoken_time_part = []
1998        if n > 0 or show_zeros_in_spoken_time:
1999            spoken_time_part.append(f"{n: 3d} {millennia}")
2000        if c > 0 or show_zeros_in_spoken_time:
2001            spoken_time_part.append(f"{c: 4d} {century}")
2002        if y > 0 or show_zeros_in_spoken_time:
2003            spoken_time_part.append(f"{y: 3d} {years}")
2004        if d > 0 or show_zeros_in_spoken_time:
2005            spoken_time_part.append(f"{d: 4d} {days}")
2006        if h > 0 or show_zeros_in_spoken_time:
2007            spoken_time_part.append(f"{h: 2d} {hours}")
2008        if m > 0 or show_zeros_in_spoken_time:
2009            spoken_time_part.append(f"{m: 2d} {minutes}")
2010        if s > 0 or show_zeros_in_spoken_time:
2011            spoken_time_part.append(f"{s: 2d} {seconds}")
2012        if ms > 0 or show_zeros_in_spoken_time:
2013            spoken_time_part.append(f"{ms: 3d} {milli_seconds}")
2014        if us > 0 or show_zeros_in_spoken_time:
2015            spoken_time_part.append(f"{us: 3d} {micro_seconds}")
2016        if ns > 0 or show_zeros_in_spoken_time:
2017            spoken_time_part.append(f"{ns: 3d} {nano_seconds}")
2018        return time_lapsed, spoken_time_separator.join(spoken_time_part)

REF https://github.com/JayRizzo/Random_Scripts/blob/master/time_measure.py#L106 Convert NanoSeconds to Human Readable Time Format. A NanoSeconds is a unit of time in the International System of Units (SI) equal to one millionth (0.000001 or 10−6 or 1⁄1,000,000) of a second. Its symbol is μs, sometimes simplified to us when Unicode is not available. A microsecond is equal to 1000 nanoseconds or 1⁄1,000 of a millisecond.

INPUT : ms (AKA: MilliSeconds) OUTPUT: tuple(string time_lapsed, string spoken_time) like format. OUTPUT Variables: time_lapsed, spoken_time

Example Input: duration_from_nanoseconds(ns) "Millennium:Century:Years:Days:Hours:Minutes:Seconds:MilliSeconds:MicroSeconds:NanoSeconds" Example Output: ('039:0001:047:325:05:02:03:456:789:012', ' 39 Millennia, 1 Century, 47 Years, 325 Days, 5 Hours, 2 Minutes, 3 Seconds, 456 MilliSeconds, 789 MicroSeconds, 12 NanoSeconds') duration_from_nanoseconds(1234567890123456789012)

@staticmethod
def day_to_time(day: int, month: int = 6, year: int = 2024) -> int:
2020    @staticmethod
2021    def day_to_time(day: int, month: int = 6, year: int = 2024) -> int:  # افتراض أن الشهر هو يونيو والسنة 2024
2022        """
2023        Convert a specific day, month, and year into a timestamp.
2024
2025        Parameters:
2026        day (int): The day of the month.
2027        month (int): The month of the year. Default is 6 (June).
2028        year (int): The year. Default is 2024.
2029
2030        Returns:
2031        int: The timestamp representing the given day, month, and year.
2032
2033        Note:
2034        This method assumes the default month and year if not provided.
2035        """
2036        return ZakatTracker.time(datetime.datetime(year, month, day))

Convert a specific day, month, and year into a timestamp.

Parameters: day (int): The day of the month. month (int): The month of the year. Default is 6 (June). year (int): The year. Default is 2024.

Returns: int: The timestamp representing the given day, month, and year.

Note: This method assumes the default month and year if not provided.

@staticmethod
def generate_random_date( start_date: datetime.datetime, end_date: datetime.datetime) -> datetime.datetime:
2038    @staticmethod
2039    def generate_random_date(start_date: datetime.datetime, end_date: datetime.datetime) -> datetime.datetime:
2040        """
2041        Generate a random date between two given dates.
2042
2043        Parameters:
2044        start_date (datetime.datetime): The start date from which to generate a random date.
2045        end_date (datetime.datetime): The end date until which to generate a random date.
2046
2047        Returns:
2048        datetime.datetime: A random date between the start_date and end_date.
2049        """
2050        time_between_dates = end_date - start_date
2051        days_between_dates = time_between_dates.days
2052        random_number_of_days = random.randrange(days_between_dates)
2053        return start_date + datetime.timedelta(days=random_number_of_days)

Generate a random date between two given dates.

Parameters: start_date (datetime.datetime): The start date from which to generate a random date. end_date (datetime.datetime): The end date until which to generate a random date.

Returns: datetime.datetime: A random date between the start_date and end_date.

@staticmethod
def generate_random_csv_file( path: str = 'data.csv', count: int = 1000, with_rate: bool = False, debug: bool = False) -> int:
2055    @staticmethod
2056    def generate_random_csv_file(path: str = "data.csv", count: int = 1000, with_rate: bool = False,
2057                                 debug: bool = False) -> int:
2058        """
2059        Generate a random CSV file with specified parameters.
2060
2061        Parameters:
2062        path (str): The path where the CSV file will be saved. Default is "data.csv".
2063        count (int): The number of rows to generate in the CSV file. Default is 1000.
2064        with_rate (bool): If True, a random rate between 1.2% and 12% is added. Default is False.
2065        debug (bool): A flag indicating whether to print debug information.
2066
2067        Returns:
2068        None. The function generates a CSV file at the specified path with the given count of rows.
2069        Each row contains a randomly generated account, description, value, and date.
2070        The value is randomly generated between 1000 and 100000,
2071        and the date is randomly generated between 1950-01-01 and 2023-12-31.
2072        If the row number is not divisible by 13, the value is multiplied by -1.
2073        """
2074        if debug:
2075            print('generate_random_csv_file', f'debug={debug}')
2076        i = 0
2077        with open(path, "w", newline="") as csvfile:
2078            writer = csv.writer(csvfile)
2079            for i in range(count):
2080                account = f"acc-{random.randint(1, 1000)}"
2081                desc = f"Some text {random.randint(1, 1000)}"
2082                value = random.randint(1000, 100000)
2083                date = ZakatTracker.generate_random_date(datetime.datetime(1950, 1, 1),
2084                                                         datetime.datetime(2023, 12, 31)).strftime("%Y-%m-%d %H:%M:%S")
2085                if not i % 13 == 0:
2086                    value *= -1
2087                row = [account, desc, value, date]
2088                if with_rate:
2089                    rate = random.randint(1, 100) * 0.12
2090                    if debug:
2091                        print('before-append', row)
2092                    row.append(rate)
2093                    if debug:
2094                        print('after-append', row)
2095                writer.writerow(row)
2096                i = i + 1
2097        return i

Generate a random CSV file with specified parameters.

Parameters: path (str): The path where the CSV file will be saved. Default is "data.csv". count (int): The number of rows to generate in the CSV file. Default is 1000. with_rate (bool): If True, a random rate between 1.2% and 12% is added. Default is False. debug (bool): A flag indicating whether to print debug information.

Returns: None. The function generates a CSV file at the specified path with the given count of rows. Each row contains a randomly generated account, description, value, and date. The value is randomly generated between 1000 and 100000, and the date is randomly generated between 1950-01-01 and 2023-12-31. If the row number is not divisible by 13, the value is multiplied by -1.

@staticmethod
def create_random_list(max_sum, min_value=0, max_value=10):
2099    @staticmethod
2100    def create_random_list(max_sum, min_value=0, max_value=10):
2101        """
2102        Creates a list of random integers whose sum does not exceed the specified maximum.
2103
2104        Args:
2105            max_sum: The maximum allowed sum of the list elements.
2106            min_value: The minimum possible value for an element (inclusive).
2107            max_value: The maximum possible value for an element (inclusive).
2108
2109        Returns:
2110            A list of random integers.
2111        """
2112        result = []
2113        current_sum = 0
2114
2115        while current_sum < max_sum:
2116            # Calculate the remaining space for the next element
2117            remaining_sum = max_sum - current_sum
2118            # Determine the maximum possible value for the next element
2119            next_max_value = min(remaining_sum, max_value)
2120            # Generate a random element within the allowed range
2121            next_element = random.randint(min_value, next_max_value)
2122            result.append(next_element)
2123            current_sum += next_element
2124
2125        return result

Creates a list of random integers whose sum does not exceed the specified maximum.

Args: max_sum: The maximum allowed sum of the list elements. min_value: The minimum possible value for an element (inclusive). max_value: The maximum possible value for an element (inclusive).

Returns: A list of random integers.

def test(self, debug: bool = False) -> bool:
2338    def test(self, debug: bool = False) -> bool:
2339        if debug:
2340            print('test', f'debug={debug}')
2341        try:
2342
2343            assert self._history()
2344
2345            # Not allowed for duplicate transactions in the same account and time
2346
2347            created = ZakatTracker.time()
2348            self.track(100, 'test-1', 'same', True, created)
2349            failed = False
2350            try:
2351                self.track(50, 'test-1', 'same', True, created)
2352            except:
2353                failed = True
2354            assert failed is True
2355
2356            self.reset()
2357
2358            # Same account transfer
2359            for x in [1, 'a', True, 1.8, None]:
2360                failed = False
2361                try:
2362                    self.transfer(1, x, x, 'same-account', debug=debug)
2363                except:
2364                    failed = True
2365                assert failed is True
2366
2367            # Always preserve box age during transfer
2368
2369            series: list[tuple] = [
2370                (30, 4),
2371                (60, 3),
2372                (90, 2),
2373            ]
2374            case = {
2375                30: {
2376                    'series': series,
2377                    'rest': 150,
2378                },
2379                60: {
2380                    'series': series,
2381                    'rest': 120,
2382                },
2383                90: {
2384                    'series': series,
2385                    'rest': 90,
2386                },
2387                180: {
2388                    'series': series,
2389                    'rest': 0,
2390                },
2391                270: {
2392                    'series': series,
2393                    'rest': -90,
2394                },
2395                360: {
2396                    'series': series,
2397                    'rest': -180,
2398                },
2399            }
2400
2401            selected_time = ZakatTracker.time() - ZakatTracker.TimeCycle()
2402
2403            for total in case:
2404                for x in case[total]['series']:
2405                    self.track(x[0], f"test-{x} ages", 'ages', True, selected_time * x[1])
2406
2407                refs = self.transfer(total, 'ages', 'future', 'Zakat Movement', debug=debug)
2408
2409                if debug:
2410                    print('refs', refs)
2411
2412                ages_cache_balance = self.balance('ages')
2413                ages_fresh_balance = self.balance('ages', False)
2414                rest = case[total]['rest']
2415                if debug:
2416                    print('source', ages_cache_balance, ages_fresh_balance, rest)
2417                assert ages_cache_balance == rest
2418                assert ages_fresh_balance == rest
2419
2420                future_cache_balance = self.balance('future')
2421                future_fresh_balance = self.balance('future', False)
2422                if debug:
2423                    print('target', future_cache_balance, future_fresh_balance, total)
2424                    print('refs', refs)
2425                assert future_cache_balance == total
2426                assert future_fresh_balance == total
2427
2428                for ref in self._vault['account']['ages']['box']:
2429                    ages_capital = self._vault['account']['ages']['box'][ref]['capital']
2430                    ages_rest = self._vault['account']['ages']['box'][ref]['rest']
2431                    future_capital = 0
2432                    if ref in self._vault['account']['future']['box']:
2433                        future_capital = self._vault['account']['future']['box'][ref]['capital']
2434                    future_rest = 0
2435                    if ref in self._vault['account']['future']['box']:
2436                        future_rest = self._vault['account']['future']['box'][ref]['rest']
2437                    if ages_capital != 0 and future_capital != 0 and future_rest != 0:
2438                        if debug:
2439                            print('================================================================')
2440                            print('ages', ages_capital, ages_rest)
2441                            print('future', future_capital, future_rest)
2442                        if ages_rest == 0:
2443                            assert ages_capital == future_capital
2444                        elif ages_rest < 0:
2445                            assert -ages_capital == future_capital
2446                        elif ages_rest > 0:
2447                            assert ages_capital == ages_rest + future_capital
2448                self.reset()
2449                assert len(self._vault['history']) == 0
2450
2451            assert self._history()
2452            assert self._history(False) is False
2453            assert self._history() is False
2454            assert self._history(True)
2455            assert self._history()
2456
2457            self._test_core(True, debug)
2458            self._test_core(False, debug)
2459
2460            transaction = [
2461                (
2462                    20, 'wallet', 1, 800, 800, 800, 4, 5,
2463                    -85, -85, -85, 6, 7,
2464                ),
2465                (
2466                    750, 'wallet', 'safe', 50, 50, 50, 4, 6,
2467                    750, 750, 750, 1, 1,
2468                ),
2469                (
2470                    600, 'safe', 'bank', 150, 150, 150, 1, 2,
2471                    600, 600, 600, 1, 1,
2472                ),
2473            ]
2474            for z in transaction:
2475                self.lock()
2476                x = z[1]
2477                y = z[2]
2478                self.transfer(z[0], x, y, 'test-transfer', debug=debug)
2479                assert self.balance(x) == z[3]
2480                xx = self.accounts()[x]
2481                assert xx == z[3]
2482                assert self.balance(x, False) == z[4]
2483                assert xx == z[4]
2484
2485                s = 0
2486                log = self._vault['account'][x]['log']
2487                for i in log:
2488                    s += log[i]['value']
2489                if debug:
2490                    print('s', s, 'z[5]', z[5])
2491                assert s == z[5]
2492
2493                assert self.box_size(x) == z[6]
2494                assert self.log_size(x) == z[7]
2495
2496                yy = self.accounts()[y]
2497                assert self.balance(y) == z[8]
2498                assert yy == z[8]
2499                assert self.balance(y, False) == z[9]
2500                assert yy == z[9]
2501
2502                s = 0
2503                log = self._vault['account'][y]['log']
2504                for i in log:
2505                    s += log[i]['value']
2506                assert s == z[10]
2507
2508                assert self.box_size(y) == z[11]
2509                assert self.log_size(y) == z[12]
2510
2511            if debug:
2512                pp().pprint(self.check(2.17))
2513
2514            assert not self.nolock()
2515            history_count = len(self._vault['history'])
2516            if debug:
2517                print('history-count', history_count)
2518            assert history_count == 11
2519            assert not self.free(ZakatTracker.time())
2520            assert self.free(self.lock())
2521            assert self.nolock()
2522            assert len(self._vault['history']) == 11
2523
2524            # storage
2525
2526            _path = self.path('test.pickle')
2527            if os.path.exists(_path):
2528                os.remove(_path)
2529            self.save()
2530            assert os.path.getsize(_path) > 0
2531            self.reset()
2532            assert self.recall(False, debug) is False
2533            self.load()
2534            assert self._vault['account'] is not None
2535
2536            # recall
2537
2538            assert self.nolock()
2539            assert len(self._vault['history']) == 11
2540            assert self.recall(False, debug) is True
2541            assert len(self._vault['history']) == 10
2542            assert self.recall(False, debug) is True
2543            assert len(self._vault['history']) == 9
2544
2545            # exchange
2546
2547            self.exchange("cash", 25, 3.75, "2024-06-25")
2548            self.exchange("cash", 22, 3.73, "2024-06-22")
2549            self.exchange("cash", 15, 3.69, "2024-06-15")
2550            self.exchange("cash", 10, 3.66)
2551
2552            for i in range(1, 30):
2553                exchange = self.exchange("cash", i)
2554                rate, description, created = exchange['rate'], exchange['description'], exchange['time']
2555                if debug:
2556                    print(i, rate, description, created)
2557                assert created
2558                if i < 10:
2559                    assert rate == 1
2560                    assert description is None
2561                elif i == 10:
2562                    assert rate == 3.66
2563                    assert description is None
2564                elif i < 15:
2565                    assert rate == 3.66
2566                    assert description is None
2567                elif i == 15:
2568                    assert rate == 3.69
2569                    assert description is not None
2570                elif i < 22:
2571                    assert rate == 3.69
2572                    assert description is not None
2573                elif i == 22:
2574                    assert rate == 3.73
2575                    assert description is not None
2576                elif i >= 25:
2577                    assert rate == 3.75
2578                    assert description is not None
2579                exchange = self.exchange("bank", i)
2580                rate, description, created = exchange['rate'], exchange['description'], exchange['time']
2581                if debug:
2582                    print(i, rate, description, created)
2583                assert created
2584                assert rate == 1
2585                assert description is None
2586
2587            assert len(self._vault['exchange']) > 0
2588            assert len(self.exchanges()) > 0
2589            self._vault['exchange'].clear()
2590            assert len(self._vault['exchange']) == 0
2591            assert len(self.exchanges()) == 0
2592
2593            # حفظ أسعار الصرف باستخدام التواريخ بالنانو ثانية
2594            self.exchange("cash", ZakatTracker.day_to_time(25), 3.75, "2024-06-25")
2595            self.exchange("cash", ZakatTracker.day_to_time(22), 3.73, "2024-06-22")
2596            self.exchange("cash", ZakatTracker.day_to_time(15), 3.69, "2024-06-15")
2597            self.exchange("cash", ZakatTracker.day_to_time(10), 3.66)
2598
2599            for i in [x * 0.12 for x in range(-15, 21)]:
2600                if i <= 0:
2601                    assert len(self.exchange("test", ZakatTracker.time(), i, f"range({i})")) == 0
2602                else:
2603                    assert len(self.exchange("test", ZakatTracker.time(), i, f"range({i})")) > 0
2604
2605            # اختبار النتائج باستخدام التواريخ بالنانو ثانية
2606            for i in range(1, 31):
2607                timestamp_ns = ZakatTracker.day_to_time(i)
2608                exchange = self.exchange("cash", timestamp_ns)
2609                rate, description, created = exchange['rate'], exchange['description'], exchange['time']
2610                if debug:
2611                    print(i, rate, description, created)
2612                assert created
2613                if i < 10:
2614                    assert rate == 1
2615                    assert description is None
2616                elif i == 10:
2617                    assert rate == 3.66
2618                    assert description is None
2619                elif i < 15:
2620                    assert rate == 3.66
2621                    assert description is None
2622                elif i == 15:
2623                    assert rate == 3.69
2624                    assert description is not None
2625                elif i < 22:
2626                    assert rate == 3.69
2627                    assert description is not None
2628                elif i == 22:
2629                    assert rate == 3.73
2630                    assert description is not None
2631                elif i >= 25:
2632                    assert rate == 3.75
2633                    assert description is not None
2634                exchange = self.exchange("bank", i)
2635                rate, description, created = exchange['rate'], exchange['description'], exchange['time']
2636                if debug:
2637                    print(i, rate, description, created)
2638                assert created
2639                assert rate == 1
2640                assert description is None
2641
2642            # csv
2643
2644            csv_count = 1000
2645
2646            for with_rate, path in {
2647                False: 'test-import_csv-no-exchange',
2648                True: 'test-import_csv-with-exchange',
2649            }.items():
2650
2651                if debug:
2652                    print('test_import_csv', with_rate, path)
2653
2654                # csv
2655
2656                csv_path = path + '.csv'
2657                if os.path.exists(csv_path):
2658                    os.remove(csv_path)
2659                c = self.generate_random_csv_file(csv_path, csv_count, with_rate, debug)
2660                if debug:
2661                    print('generate_random_csv_file', c)
2662                assert c == csv_count
2663                assert os.path.getsize(csv_path) > 0
2664                cache_path = self.import_csv_cache_path()
2665                if os.path.exists(cache_path):
2666                    os.remove(cache_path)
2667                self.reset()
2668                (created, found, bad) = self.import_csv(csv_path, debug)
2669                bad_count = len(bad)
2670                if debug:
2671                    print(f"csv-imported: ({created}, {found}, {bad_count}) = count({csv_count})")
2672                tmp_size = os.path.getsize(cache_path)
2673                assert tmp_size > 0
2674                assert created + found + bad_count == csv_count
2675                assert created == csv_count
2676                assert bad_count == 0
2677                (created_2, found_2, bad_2) = self.import_csv(csv_path)
2678                bad_2_count = len(bad_2)
2679                if debug:
2680                    print(f"csv-imported: ({created_2}, {found_2}, {bad_2_count})")
2681                    print(bad)
2682                assert tmp_size == os.path.getsize(cache_path)
2683                assert created_2 + found_2 + bad_2_count == csv_count
2684                assert created == found_2
2685                assert bad_count == bad_2_count
2686                assert found_2 == csv_count
2687                assert bad_2_count == 0
2688                assert created_2 == 0
2689
2690                # payment parts
2691
2692                positive_parts = self.build_payment_parts(100, positive_only=True)
2693                assert self.check_payment_parts(positive_parts) != 0
2694                assert self.check_payment_parts(positive_parts) != 0
2695                all_parts = self.build_payment_parts(300, positive_only=False)
2696                assert self.check_payment_parts(all_parts) != 0
2697                assert self.check_payment_parts(all_parts) != 0
2698                if debug:
2699                    pp().pprint(positive_parts)
2700                    pp().pprint(all_parts)
2701                # dynamic discount
2702                suite = []
2703                count = 3
2704                for exceed in [False, True]:
2705                    case = []
2706                    for parts in [positive_parts, all_parts]:
2707                        part = parts.copy()
2708                        demand = part['demand']
2709                        if debug:
2710                            print(demand, part['total'])
2711                        i = 0
2712                        z = demand / count
2713                        cp = {
2714                            'account': {},
2715                            'demand': demand,
2716                            'exceed': exceed,
2717                            'total': part['total'],
2718                        }
2719                        j = ''
2720                        for x, y in part['account'].items():
2721                            x_exchange = self.exchange(x)
2722                            zz = self.exchange_calc(z, 1, x_exchange['rate'])
2723                            if exceed and zz <= demand:
2724                                i += 1
2725                                y['part'] = zz
2726                                if debug:
2727                                    print(exceed, y)
2728                                cp['account'][x] = y
2729                                case.append(y)
2730                            elif not exceed and y['balance'] >= zz:
2731                                i += 1
2732                                y['part'] = zz
2733                                if debug:
2734                                    print(exceed, y)
2735                                cp['account'][x] = y
2736                                case.append(y)
2737                            j = x
2738                            if i >= count:
2739                                break
2740                        if len(cp['account'][j]) > 0:
2741                            suite.append(cp)
2742                if debug:
2743                    print('suite', len(suite))
2744                # vault = self._vault.copy()
2745                for case in suite:
2746                    # self._vault = vault.copy()
2747                    if debug:
2748                        print('case', case)
2749                    result = self.check_payment_parts(case)
2750                    if debug:
2751                        print('check_payment_parts', result, f'exceed: {exceed}')
2752                    assert result == 0
2753
2754                    report = self.check(2.17, None, debug)
2755                    (valid, brief, plan) = report
2756                    if debug:
2757                        print('valid', valid)
2758                    zakat_result = self.zakat(report, parts=case, debug=debug)
2759                    if debug:
2760                        print('zakat-result', zakat_result)
2761                    assert valid == zakat_result
2762
2763            assert self.save(path + '.pickle')
2764            assert self.export_json(path + '.json')
2765
2766            assert self.export_json("1000-transactions-test.json")
2767            assert self.save("1000-transactions-test.pickle")
2768
2769            self.reset()
2770
2771            # test transfer between accounts with different exchange rate
2772
2773            a_SAR = "Bank (SAR)"
2774            b_USD = "Bank (USD)"
2775            c_SAR = "Safe (SAR)"
2776            # 0: track, 1: check-exchange, 2: do-exchange, 3: transfer
2777            for case in [
2778                (0, a_SAR, "SAR Gift", 1000, 1000),
2779                (1, a_SAR, 1),
2780                (0, b_USD, "USD Gift", 500, 500),
2781                (1, b_USD, 1),
2782                (2, b_USD, 3.75),
2783                (1, b_USD, 3.75),
2784                (3, 100, b_USD, a_SAR, "100 USD -> SAR", 400, 1375),
2785                (0, c_SAR, "Salary", 750, 750),
2786                (3, 375, c_SAR, b_USD, "375 SAR -> USD", 375, 500),
2787                (3, 3.75, a_SAR, b_USD, "3.75 SAR -> USD", 1371.25, 501),
2788            ]:
2789                match (case[0]):
2790                    case 0:  # track
2791                        _, account, desc, x, balance = case
2792                        self.track(value=x, desc=desc, account=account, debug=debug)
2793
2794                        cached_value = self.balance(account, cached=True)
2795                        fresh_value = self.balance(account, cached=False)
2796                        if debug:
2797                            print('account', account, 'cached_value', cached_value, 'fresh_value', fresh_value)
2798                        assert cached_value == balance
2799                        assert fresh_value == balance
2800                    case 1:  # check-exchange
2801                        _, account, expected_rate = case
2802                        t_exchange = self.exchange(account, created=ZakatTracker.time(), debug=debug)
2803                        if debug:
2804                            print('t-exchange', t_exchange)
2805                        assert t_exchange['rate'] == expected_rate
2806                    case 2:  # do-exchange
2807                        _, account, rate = case
2808                        self.exchange(account, rate=rate, debug=debug)
2809                        b_exchange = self.exchange(account, created=ZakatTracker.time(), debug=debug)
2810                        if debug:
2811                            print('b-exchange', b_exchange)
2812                        assert b_exchange['rate'] == rate
2813                    case 3:  # transfer
2814                        _, x, a, b, desc, a_balance, b_balance = case
2815                        self.transfer(x, a, b, desc, debug=debug)
2816
2817                        cached_value = self.balance(a, cached=True)
2818                        fresh_value = self.balance(a, cached=False)
2819                        if debug:
2820                            print('account', a, 'cached_value', cached_value, 'fresh_value', fresh_value)
2821                        assert cached_value == a_balance
2822                        assert fresh_value == a_balance
2823
2824                        cached_value = self.balance(b, cached=True)
2825                        fresh_value = self.balance(b, cached=False)
2826                        if debug:
2827                            print('account', b, 'cached_value', cached_value, 'fresh_value', fresh_value)
2828                        assert cached_value == b_balance
2829                        assert fresh_value == b_balance
2830
2831            # Transfer all in many chunks randomly from B to A
2832            a_SAR_balance = 1371.25
2833            b_USD_balance = 501
2834            b_USD_exchange = self.exchange(b_USD)
2835            amounts = ZakatTracker.create_random_list(b_USD_balance)
2836            if debug:
2837                print('amounts', amounts)
2838            i = 0
2839            for x in amounts:
2840                if debug:
2841                    print(f'{i} - transfer-with-exchange({x})')
2842                self.transfer(x, b_USD, a_SAR, f"{x} USD -> SAR", debug=debug)
2843
2844                b_USD_balance -= x
2845                cached_value = self.balance(b_USD, cached=True)
2846                fresh_value = self.balance(b_USD, cached=False)
2847                if debug:
2848                    print('account', b_USD, 'cached_value', cached_value, 'fresh_value', fresh_value, 'excepted',
2849                          b_USD_balance)
2850                assert cached_value == b_USD_balance
2851                assert fresh_value == b_USD_balance
2852
2853                a_SAR_balance += x * b_USD_exchange['rate']
2854                cached_value = self.balance(a_SAR, cached=True)
2855                fresh_value = self.balance(a_SAR, cached=False)
2856                if debug:
2857                    print('account', a_SAR, 'cached_value', cached_value, 'fresh_value', fresh_value, 'expected',
2858                          a_SAR_balance, 'rate', b_USD_exchange['rate'])
2859                assert cached_value == a_SAR_balance
2860                assert fresh_value == a_SAR_balance
2861                i += 1
2862
2863            # Transfer all in many chunks randomly from C to A
2864            c_SAR_balance = 375
2865            amounts = ZakatTracker.create_random_list(c_SAR_balance)
2866            if debug:
2867                print('amounts', amounts)
2868            i = 0
2869            for x in amounts:
2870                if debug:
2871                    print(f'{i} - transfer-with-exchange({x})')
2872                self.transfer(x, c_SAR, a_SAR, f"{x} SAR -> a_SAR", debug=debug)
2873
2874                c_SAR_balance -= x
2875                cached_value = self.balance(c_SAR, cached=True)
2876                fresh_value = self.balance(c_SAR, cached=False)
2877                if debug:
2878                    print('account', c_SAR, 'cached_value', cached_value, 'fresh_value', fresh_value, 'excepted',
2879                          c_SAR_balance)
2880                assert cached_value == c_SAR_balance
2881                assert fresh_value == c_SAR_balance
2882
2883                a_SAR_balance += x
2884                cached_value = self.balance(a_SAR, cached=True)
2885                fresh_value = self.balance(a_SAR, cached=False)
2886                if debug:
2887                    print('account', a_SAR, 'cached_value', cached_value, 'fresh_value', fresh_value, 'expected',
2888                          a_SAR_balance)
2889                assert cached_value == a_SAR_balance
2890                assert fresh_value == a_SAR_balance
2891                i += 1
2892
2893            assert self.export_json("accounts-transfer-with-exchange-rates.json")
2894            assert self.save("accounts-transfer-with-exchange-rates.pickle")
2895
2896            # check & zakat with exchange rates for many cycles
2897
2898            for rate, values in {
2899                1: {
2900                    'in': [1000, 2000, 10000],
2901                    'exchanged': [1000, 2000, 10000],
2902                    'out': [25, 50, 731.40625],
2903                },
2904                3.75: {
2905                    'in': [200, 1000, 5000],
2906                    'exchanged': [750, 3750, 18750],
2907                    'out': [18.75, 93.75, 1371.38671875],
2908                },
2909            }.items():
2910                a, b, c = values['in']
2911                m, n, o = values['exchanged']
2912                x, y, z = values['out']
2913                if debug:
2914                    print('rate', rate, 'values', values)
2915                for case in [
2916                    (a, 'safe', ZakatTracker.time() - ZakatTracker.TimeCycle(), [
2917                        {'safe': {0: {'below_nisab': x}}},
2918                    ], False, m),
2919                    (b, 'safe', ZakatTracker.time() - ZakatTracker.TimeCycle(), [
2920                        {'safe': {0: {'count': 1, 'total': y}}},
2921                    ], True, n),
2922                    (c, 'cave', ZakatTracker.time() - (ZakatTracker.TimeCycle() * 3), [
2923                        {'cave': {0: {'count': 3, 'total': z}}},
2924                    ], True, o),
2925                ]:
2926                    if debug:
2927                        print(f"############# check(rate: {rate}) #############")
2928                    self.reset()
2929                    self.exchange(account=case[1], created=case[2], rate=rate)
2930                    self.track(value=case[0], desc='test-check', account=case[1], logging=True, created=case[2])
2931
2932                    # assert self.nolock()
2933                    # history_size = len(self._vault['history'])
2934                    # print('history_size', history_size)
2935                    # assert history_size == 2
2936                    assert self.lock()
2937                    assert not self.nolock()
2938                    report = self.check(2.17, None, debug)
2939                    (valid, brief, plan) = report
2940                    assert valid == case[4]
2941                    if debug:
2942                        print('brief', brief)
2943                    assert case[5] == brief[0]
2944                    assert case[5] == brief[1]
2945
2946                    if debug:
2947                        pp().pprint(plan)
2948
2949                    for x in plan:
2950                        assert case[1] == x
2951                        if 'total' in case[3][0][x][0].keys():
2952                            assert case[3][0][x][0]['total'] == brief[2]
2953                            assert plan[x][0]['total'] == case[3][0][x][0]['total']
2954                            assert plan[x][0]['count'] == case[3][0][x][0]['count']
2955                        else:
2956                            assert plan[x][0]['below_nisab'] == case[3][0][x][0]['below_nisab']
2957                    if debug:
2958                        pp().pprint(report)
2959                    result = self.zakat(report, debug=debug)
2960                    if debug:
2961                        print('zakat-result', result, case[4])
2962                    assert result == case[4]
2963                    report = self.check(2.17, None, debug)
2964                    (valid, brief, plan) = report
2965                    assert valid is False
2966
2967            history_size = len(self._vault['history'])
2968            if debug:
2969                print('history_size', history_size)
2970            assert history_size == 3
2971            assert not self.nolock()
2972            assert self.recall(False, debug) is False
2973            self.free(self.lock())
2974            assert self.nolock()
2975
2976            for i in range(3, 0, -1):
2977                history_size = len(self._vault['history'])
2978                if debug:
2979                    print('history_size', history_size)
2980                assert history_size == i
2981                assert self.recall(False, debug) is True
2982
2983            assert self.nolock()
2984            assert self.recall(False, debug) is False
2985
2986            history_size = len(self._vault['history'])
2987            if debug:
2988                print('history_size', history_size)
2989            assert history_size == 0
2990
2991            account_size = len(self._vault['account'])
2992            if debug:
2993                print('account_size', account_size)
2994            assert account_size == 0
2995
2996            report_size = len(self._vault['report'])
2997            if debug:
2998                print('report_size', report_size)
2999            assert report_size == 0
3000
3001            assert self.nolock()
3002            return True
3003        except:
3004            # pp().pprint(self._vault)
3005            assert self.export_json("test-snapshot.json")
3006            assert self.save("test-snapshot.pickle")
3007            raise
def test(debug: bool = False):
3010def test(debug: bool = False):
3011    ledger = ZakatTracker()
3012    start = ZakatTracker.time()
3013    assert ledger.test(debug=debug)
3014    if debug:
3015        print("#########################")
3016        print("######## TEST DONE ########")
3017        print("#########################")
3018        print(ZakatTracker.duration_from_nanoseconds(ZakatTracker.time() - start))
3019        print("#########################")
class Action(enum.Enum):
76class Action(Enum):
77    CREATE = auto()
78    TRACK = auto()
79    LOG = auto()
80    SUB = auto()
81    ADD_FILE = auto()
82    REMOVE_FILE = auto()
83    BOX_TRANSFER = auto()
84    EXCHANGE = auto()
85    REPORT = auto()
86    ZAKAT = auto()
CREATE = <Action.CREATE: 1>
TRACK = <Action.TRACK: 2>
LOG = <Action.LOG: 3>
SUB = <Action.SUB: 4>
ADD_FILE = <Action.ADD_FILE: 5>
REMOVE_FILE = <Action.REMOVE_FILE: 6>
BOX_TRANSFER = <Action.BOX_TRANSFER: 7>
EXCHANGE = <Action.EXCHANGE: 8>
REPORT = <Action.REPORT: 9>
ZAKAT = <Action.ZAKAT: 10>
class JSONEncoder(json.encoder.JSONEncoder):
89class JSONEncoder(json.JSONEncoder):
90    def default(self, obj):
91        if isinstance(obj, Action) or isinstance(obj, MathOperation):
92            return obj.name  # Serialize as the enum member's name
93        elif isinstance(obj, Decimal):
94            return float(obj)
95        return super().default(obj)

Extensible JSON https://json.org encoder for Python data structures.

Supports the following objects and types by default:

+-------------------+---------------+ | Python | JSON | +===================+===============+ | dict | object | +-------------------+---------------+ | list, tuple | array | +-------------------+---------------+ | str | string | +-------------------+---------------+ | int, float | number | +-------------------+---------------+ | True | true | +-------------------+---------------+ | False | false | +-------------------+---------------+ | None | null | +-------------------+---------------+

To extend this to recognize other objects, subclass and implement a .default() method with another method that returns a serializable object for o if possible, otherwise it should call the superclass implementation (to raise TypeError).

def default(self, obj):
90    def default(self, obj):
91        if isinstance(obj, Action) or isinstance(obj, MathOperation):
92            return obj.name  # Serialize as the enum member's name
93        elif isinstance(obj, Decimal):
94            return float(obj)
95        return super().default(obj)

Implement this method in a subclass such that it returns a serializable object for o, or calls the base implementation (to raise a TypeError).

For example, to support arbitrary iterators, you could implement default like this::

def default(self, o):
    try:
        iterable = iter(o)
    except TypeError:
        pass
    else:
        return list(iterable)
    # Let the base class default method raise the TypeError
    return super().default(o)
class MathOperation(enum.Enum):
 98class MathOperation(Enum):
 99    ADDITION = auto()
100    EQUAL = auto()
101    SUBTRACTION = auto()
ADDITION = <MathOperation.ADDITION: 1>
EQUAL = <MathOperation.EQUAL: 2>
SUBTRACTION = <MathOperation.SUBTRACTION: 3>
def start_file_server( database_path: str, database_callback: <built-in function callable> = None, csv_callback: <built-in function callable> = None, debug: bool = False) -> tuple:
 55def start_file_server(database_path: str, database_callback: callable = None, csv_callback: callable = None,
 56                      debug: bool = False) -> tuple:
 57    """
 58    Starts a multi-purpose HTTP server to manage file interactions for a Zakat application.
 59
 60    This server facilitates the following functionalities:
 61
 62    1. GET /{file_uuid}/get: Download the database file specified by `database_path`.
 63    2. GET /{file_uuid}/upload: Display an HTML form for uploading files.
 64    3. POST /{file_uuid}/upload: Handle file uploads, distinguishing between:
 65        - Database File (.db): Replaces the existing database with the uploaded one.
 66        - CSV File (.csv): Imports data from the CSV into the existing database.
 67
 68    Args:
 69        database_path (str): The path to the pickle database file.
 70        database_callback (callable, optional): A function to call after a successful database upload.
 71                                                It receives the uploaded database path as its argument.
 72        csv_callback (callable, optional): A function to call after a successful CSV upload. It receives the uploaded CSV path,
 73                                           the database path, and the debug flag as its arguments.
 74        debug (bool, optional): If True, print debugging information. Defaults to False.
 75
 76    Returns:
 77        Tuple[str, str, str, threading.Thread, Callable[[], None]]: A tuple containing:
 78            - file_name (str): The name of the database file.
 79            - download_url (str): The URL to download the database file.
 80            - upload_url (str): The URL to access the file upload form.
 81            - server_thread (threading.Thread): The thread running the server.
 82            - shutdown_server (Callable[[], None]): A function to gracefully shut down the server.
 83
 84    Example:
 85        _, download_url, upload_url, server_thread, shutdown_server = start_file_server("zakat.db")
 86        print(f"Download database: {download_url}")
 87        print(f"Upload files: {upload_url}")
 88        server_thread.start()
 89        # ... later ...
 90        shutdown_server()
 91    """
 92    file_uuid = uuid.uuid4()
 93    file_name = os.path.basename(database_path)
 94
 95    port = find_available_port()
 96    download_url = f"http://localhost:{port}/{file_uuid}/get"
 97    upload_url = f"http://localhost:{port}/{file_uuid}/upload"
 98
 99    class Handler(http.server.SimpleHTTPRequestHandler):
100        def do_GET(self):
101            if self.path == f"/{file_uuid}/get":
102                # GET: Serve the existing file
103                try:
104                    with open(database_path, "rb") as f:
105                        self.send_response(200)
106                        self.send_header("Content-type", "application/octet-stream")
107                        self.send_header("Content-Disposition", f'attachment; filename="{file_name}"')
108                        self.end_headers()
109                        self.wfile.write(f.read())
110                except FileNotFoundError:
111                    self.send_error(404, "File not found")
112            elif self.path == f"/{file_uuid}/upload":
113                # GET: Serve the upload form
114                self.send_response(200)
115                self.send_header("Content-type", "text/html")
116                self.end_headers()
117                self.wfile.write(f"""
118                    <html lang="en">
119                        <head>
120                            <title>Zakat File Server</title>
121                        </head>
122                    <body>
123                    <h1>Zakat File Server</h1>
124                    <h3>You can download the <a target="__blank" href="{download_url}">database file</a>...</h3>
125                    <h3>Or upload a new file to restore a database or import `CSV` file:</h3>
126                    <form action="/{file_uuid}/upload" method="post" enctype="multipart/form-data">
127                        <input type="file" name="file" required><br/>
128                        <input type="radio" id="{FileType.Database.value}" name="upload_type" value="{FileType.Database.value}" required>
129                        <label for="database">Database File</label><br/>
130                        <input type="radio"id="{FileType.CSV.value}" name="upload_type" value="{FileType.CSV.value}">
131                        <label for="csv">CSV File</label><br/>
132                        <input type="submit" value="Upload"><br/>
133                    </form>
134                    </body></html>
135                """.encode())
136            else:
137                self.send_error(404)
138
139        def do_POST(self):
140            if self.path == f"/{file_uuid}/upload":
141                # POST: Handle request
142                # 1. Get the Form Data
143                form_data = cgi.FieldStorage(
144                    fp=self.rfile,
145                    headers=self.headers,
146                    environ={'REQUEST_METHOD': 'POST'}
147                )
148                upload_type = form_data.getvalue("upload_type")
149
150                if debug:
151                    print('upload_type', upload_type)
152
153                if upload_type not in [FileType.Database.value, FileType.CSV.value]:
154                    self.send_error(400, "Invalid upload type")
155                    return
156
157                # 2. Extract File Data
158                file_item = form_data['file']  # Assuming 'file' is your file input name
159
160                # 3. Get File Details
161                filename = file_item.filename
162                file_data = file_item.file.read()  # Read the file's content
163
164                if debug:
165                    print(f'Uploaded filename: {filename}')
166
167                # 4. Define Storage Path for CSV
168                upload_directory = "./uploads"  # Create this directory if it doesn't exist
169                os.makedirs(upload_directory, exist_ok=True)
170                file_path = os.path.join(upload_directory, upload_type)
171
172                # 5. Write to Disk
173                with open(file_path, 'wb') as f:
174                    f.write(file_data)
175
176                match upload_type:
177                    case FileType.Database.value:
178
179                        try:
180                            # 6. Verify database file
181                            # ZakatTracker(db_path=file_path) # FATAL, Circular Imports Error
182                            if database_callback is not None:
183                                database_callback(file_path)
184
185                            # 7. Copy database into the original path
186                            shutil.copy2(file_path, database_path)
187                        except Exception as e:
188                            self.send_error(400, str(e))
189                            return
190
191                    case FileType.CSV.value:
192                        # 6. Verify CSV file
193                        try:
194                            # x = ZakatTracker(db_path=database_path) # FATAL, Circular Imports Error
195                            # result = x.import_csv(file_path, debug=debug)
196                            if csv_callback is not None:
197                                result = csv_callback(file_path, database_path, debug)
198                                if debug:
199                                    print(f'CSV imported: {result}')
200                                if len(result[2]) != 0:
201                                    self.send_response(200)
202                                    self.end_headers()
203                                    self.wfile.write(json.dumps(result).encode())
204                                    return
205                        except Exception as e:
206                            self.send_error(400, str(e))
207                            return
208
209                self.send_response(200)
210                self.end_headers()
211                self.wfile.write(b"File uploaded successfully.")
212
213    httpd = socketserver.TCPServer(("localhost", port), Handler)
214    server_thread = threading.Thread(target=httpd.serve_forever)
215
216    def shutdown_server():
217        nonlocal httpd, server_thread
218        httpd.shutdown()
219        httpd.server_close()  # Close the socket
220        server_thread.join()  # Wait for the thread to finish
221
222    return file_name, download_url, upload_url, server_thread, shutdown_server

Starts a multi-purpose HTTP server to manage file interactions for a Zakat application.

This server facilitates the following functionalities:

  1. GET /{file_uuid}/get: Download the database file specified by database_path.
  2. GET /{file_uuid}/upload: Display an HTML form for uploading files.
  3. POST /{file_uuid}/upload: Handle file uploads, distinguishing between:
    • Database File (.db): Replaces the existing database with the uploaded one.
    • CSV File (.csv): Imports data from the CSV into the existing database.

Args: database_path (str): The path to the pickle database file. database_callback (callable, optional): A function to call after a successful database upload. It receives the uploaded database path as its argument. csv_callback (callable, optional): A function to call after a successful CSV upload. It receives the uploaded CSV path, the database path, and the debug flag as its arguments. debug (bool, optional): If True, print debugging information. Defaults to False.

Returns: Tuple[str, str, str, threading.Thread, Callable[[], None]]: A tuple containing: - file_name (str): The name of the database file. - download_url (str): The URL to download the database file. - upload_url (str): The URL to access the file upload form. - server_thread (threading.Thread): The thread running the server. - shutdown_server (Callable[[], None]): A function to gracefully shut down the server.

Example: _, download_url, upload_url, server_thread, shutdown_server = start_file_server("zakat.db") print(f"Download database: {download_url}") print(f"Upload files: {upload_url}") server_thread.start() # ... later ... shutdown_server()

def find_available_port() -> int:
32def find_available_port() -> int:
33    """
34    Finds and returns an available TCP port on the local machine.
35
36    This function utilizes a TCP server socket to bind to port 0, which
37    instructs the operating system to automatically assign an available
38    port. The assigned port is then extracted and returned.
39
40    Returns:
41        int: The available TCP port number.
42
43    Raises:
44        OSError: If an error occurs during the port binding process, such
45            as all ports being in use.
46
47    Example:
48        port = find_available_port()
49        print(f"Available port: {port}")
50    """
51    with socketserver.TCPServer(("localhost", 0), None) as s:
52        return s.server_address[1]

Finds and returns an available TCP port on the local machine.

This function utilizes a TCP server socket to bind to port 0, which instructs the operating system to automatically assign an available port. The assigned port is then extracted and returned.

Returns: int: The available TCP port number.

Raises: OSError: If an error occurs during the port binding process, such as all ports being in use.

Example: port = find_available_port() print(f"Available port: {port}")

class FileType(enum.Enum):
13class FileType(Enum):
14    Database = 'db'
15    CSV = 'csv'
Database = <FileType.Database: 'db'>
CSV = <FileType.CSV: 'csv'>