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

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:
186    @staticmethod
187    def ZakatCut(x: float) -> float:
188        """
189        Calculates the Zakat amount due on an asset.
190
191        This function calculates the zakat amount due on a given asset value over one lunar year.
192        Zakat is an Islamic obligatory alms-giving, calculated as a fixed percentage of an individual's wealth
193        that exceeds a certain threshold (Nisab).
194
195        Parameters:
196        x: The total value of the asset on which Zakat is to be calculated.
197
198        Returns:
199        The amount of Zakat due on the asset, calculated as 2.5% of the asset's value.
200        """
201        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:
203    @staticmethod
204    def TimeCycle(days: int = 355) -> int:
205        """
206        Calculates the approximate duration of a lunar year in nanoseconds.
207
208        This function calculates the approximate duration of a lunar year based on the given number of days.
209        It converts the given number of days into nanoseconds for use in high-precision timing applications.
210
211        Parameters:
212        days: The number of days in a lunar year. Defaults to 355,
213              which is an approximation of the average length of a lunar year.
214
215        Returns:
216        The approximate duration of a lunar year in nanoseconds.
217        """
218        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:
220    @staticmethod
221    def Nisab(gram_price: float, gram_quantity: float = 595) -> float:
222        """
223        Calculate the total value of Nisab (a unit of weight in Islamic jurisprudence) based on the given price per gram.
224
225        This function calculates the Nisab value, which is the minimum threshold of wealth,
226        that makes an individual liable for paying Zakat.
227        The Nisab value is determined by the equivalent value of a specific amount
228        of gold or silver (currently 595 grams in silver) in the local currency.
229
230        Parameters:
231        - gram_price (float): The price per gram of Nisab.
232        - gram_quantity (float): The quantity of grams in a Nisab. Default is 595 grams of silver.
233
234        Returns:
235        - float: The total value of Nisab based on the given price per gram.
236        """
237        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:
257    def path(self, path: str = None) -> str:
258        """
259        Set or get the database path.
260
261        Parameters:
262        path (str): The path to the database file. If not provided, it returns the current path.
263
264        Returns:
265        str: The current database path.
266        """
267        if path is not None:
268            self._vault_path = path
269        return self._vault_path

Set or get the database path.

Parameters: path (str): The path to the database file. If not provided, it returns the current path.

Returns: str: The current database path.

def reset(self) -> None:
285    def reset(self) -> None:
286        """
287        Reset the internal data structure to its initial state.
288
289        Parameters:
290        None
291
292        Returns:
293        None
294        """
295        self._vault = {
296            'account': {},
297            'exchange': {},
298            'history': {},
299            'lock': None,
300            'report': {},
301        }

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:
303    @staticmethod
304    def time(now: datetime = None) -> int:
305        """
306        Generates a timestamp based on the provided datetime object or the current datetime.
307
308        Parameters:
309        now (datetime, optional): The datetime object to generate the timestamp from.
310        If not provided, the current datetime is used.
311
312        Returns:
313        int: The timestamp in positive nanoseconds since the Unix epoch (January 1, 1970),
314            before 1970 will return in negative until 1000AD.
315        """
316        if now is None:
317            now = datetime.datetime.now()
318        ordinal_day = now.toordinal()
319        ns_in_day = (now - now.replace(hour=0, minute=0, second=0, microsecond=0)).total_seconds() * 10 ** 9
320        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'>:
322    @staticmethod
323    def time_to_datetime(ordinal_ns: int) -> datetime:
324        """
325        Converts an ordinal number (number of days since 1000-01-01) to a datetime object.
326
327        Parameters:
328        ordinal_ns (int): The ordinal number of days since 1000-01-01.
329
330        Returns:
331        datetime: The corresponding datetime object.
332        """
333        ordinal_day = ordinal_ns // 86_400_000_000_000 + 719_163
334        ns_in_day = ordinal_ns % 86_400_000_000_000
335        d = datetime.datetime.fromordinal(ordinal_day)
336        t = datetime.timedelta(seconds=ns_in_day // 10 ** 9)
337        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 nolock(self) -> bool:
375    def nolock(self) -> bool:
376        """
377        Check if the vault lock is currently not set.
378
379        Returns:
380        bool: True if the vault lock is not set, False otherwise.
381        """
382        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:
384    def lock(self) -> int:
385        """
386        Acquires a lock on the ZakatTracker instance.
387
388        Returns:
389        int: The lock ID. This ID can be used to release the lock later.
390        """
391        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:
393    def vault(self) -> dict:
394        """
395        Returns a copy of the internal vault dictionary.
396
397        This method is used to retrieve the current state of the ZakatTracker object.
398        It provides a snapshot of the internal data structure, allowing for further
399        processing or analysis.
400
401        Returns:
402        dict: A copy of the internal vault dictionary.
403        """
404        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]:
406    def stats(self) -> dict[str, tuple]:
407        """
408        Calculates and returns statistics about the object's data storage.
409
410        This method determines the size of the database file on disk and the
411        size of the data currently held in RAM (likely within a dictionary).
412        Both sizes are reported in bytes and in a human-readable format
413        (e.g., KB, MB).
414
415        Returns:
416        dict[str, tuple]: A dictionary containing the following statistics:
417
418            * 'database': A tuple with two elements:
419                - The database file size in bytes (int).
420                - The database file size in human-readable format (str).
421            * 'ram': A tuple with two elements:
422                - The RAM usage (dictionary size) in bytes (int).
423                - The RAM usage in human-readable format (str).
424
425        Example:
426        >>> stats = my_object.stats()
427        >>> print(stats['database'])
428        (256000, '250.0 KB')
429        >>> print(stats['ram'])
430        (12345, '12.1 KB')
431        """
432        ram_size = self.get_dict_size(self.vault())
433        file_size = os.path.getsize(self.path())
434        return {
435            'database': (file_size, self.human_readable_size(file_size)),
436            'ram': (ram_size, self.human_readable_size(ram_size)),
437        }

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:
439    def steps(self) -> dict:
440        """
441        Returns a copy of the history of steps taken in the ZakatTracker.
442
443        The history is a dictionary where each key is a unique identifier for a step,
444        and the corresponding value is a dictionary containing information about the step.
445
446        Returns:
447        dict: A copy of the history of steps taken in the ZakatTracker.
448        """
449        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:
451    def free(self, lock: int, auto_save: bool = True) -> bool:
452        """
453        Releases the lock on the database.
454
455        Parameters:
456        lock (int): The lock ID to be released.
457        auto_save (bool): Whether to automatically save the database after releasing the lock.
458
459        Returns:
460        bool: True if the lock is successfully released and (optionally) saved, False otherwise.
461        """
462        if lock == self._vault['lock']:
463            self._vault['lock'] = None
464            if auto_save:
465                return self.save(self.path())
466            return True
467        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:
469    def account_exists(self, account) -> bool:
470        """
471        Check if the given account exists in the vault.
472
473        Parameters:
474        account (str): The account number to check.
475
476        Returns:
477        bool: True if the account exists, False otherwise.
478        """
479        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:
481    def box_size(self, account) -> int:
482        """
483        Calculate the size of the box for a specific account.
484
485        Parameters:
486        account (str): The account number for which the box size needs to be calculated.
487
488        Returns:
489        int: The size of the box for the given account. If the account does not exist, -1 is returned.
490        """
491        if self.account_exists(account):
492            return len(self._vault['account'][account]['box'])
493        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:
495    def log_size(self, account) -> int:
496        """
497        Get the size of the log for a specific account.
498
499        Parameters:
500        account (str): The account number for which the log size needs to be calculated.
501
502        Returns:
503        int: The size of the log for the given account. If the account does not exist, -1 is returned.
504        """
505        if self.account_exists(account):
506            return len(self._vault['account'][account]['log'])
507        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.

def recall(self, dry=True, debug=False) -> bool:
509    def recall(self, dry=True, debug=False) -> bool:
510        """
511        Revert the last operation.
512
513        Parameters:
514        dry (bool): If True, the function will not modify the data, but will simulate the operation. Default is True.
515        debug (bool): If True, the function will print debug information. Default is False.
516
517        Returns:
518        bool: True if the operation was successful, False otherwise.
519        """
520        if not self.nolock() or len(self._vault['history']) == 0:
521            return False
522        if len(self._vault['history']) <= 0:
523            return False
524        ref = sorted(self._vault['history'].keys())[-1]
525        if debug:
526            print('recall', ref)
527        memory = self._vault['history'][ref]
528        if debug:
529            print(type(memory), 'memory', memory)
530
531        limit = len(memory) + 1
532        sub_positive_log_negative = 0
533        for i in range(-1, -limit, -1):
534            x = memory[i]
535            if debug:
536                print(type(x), x)
537            match x['action']:
538                case Action.CREATE:
539                    if x['account'] is not None:
540                        if self.account_exists(x['account']):
541                            if debug:
542                                print('account', self._vault['account'][x['account']])
543                            assert len(self._vault['account'][x['account']]['box']) == 0
544                            assert self._vault['account'][x['account']]['balance'] == 0
545                            assert self._vault['account'][x['account']]['count'] == 0
546                            if dry:
547                                continue
548                            del self._vault['account'][x['account']]
549
550                case Action.TRACK:
551                    if x['account'] is not None:
552                        if self.account_exists(x['account']):
553                            if dry:
554                                continue
555                            self._vault['account'][x['account']]['balance'] -= x['value']
556                            self._vault['account'][x['account']]['count'] -= 1
557                            del self._vault['account'][x['account']]['box'][x['ref']]
558
559                case Action.LOG:
560                    if x['account'] is not None:
561                        if self.account_exists(x['account']):
562                            if x['ref'] in self._vault['account'][x['account']]['log']:
563                                if dry:
564                                    continue
565                                if sub_positive_log_negative == -x['value']:
566                                    self._vault['account'][x['account']]['count'] -= 1
567                                    sub_positive_log_negative = 0
568                                box_ref = self._vault['account'][x['account']]['log'][x['ref']]['ref']
569                                if not box_ref is None:
570                                    assert self.box_exists(x['account'], box_ref)
571                                    box_value = self._vault['account'][x['account']]['log'][x['ref']]['value']
572                                    assert box_value < 0
573                                    self._vault['account'][x['account']]['box'][box_ref]['rest'] += -box_value
574                                    self._vault['account'][x['account']]['balance'] += -box_value
575                                    self._vault['account'][x['account']]['count'] -= 1
576                                del self._vault['account'][x['account']]['log'][x['ref']]
577
578                case Action.SUB:
579                    if x['account'] is not None:
580                        if self.account_exists(x['account']):
581                            if x['ref'] in self._vault['account'][x['account']]['box']:
582                                if dry:
583                                    continue
584                                self._vault['account'][x['account']]['box'][x['ref']]['rest'] += x['value']
585                                self._vault['account'][x['account']]['balance'] += x['value']
586                                sub_positive_log_negative = x['value']
587
588                case Action.ADD_FILE:
589                    if x['account'] is not None:
590                        if self.account_exists(x['account']):
591                            if x['ref'] in self._vault['account'][x['account']]['log']:
592                                if x['file'] in self._vault['account'][x['account']]['log'][x['ref']]['file']:
593                                    if dry:
594                                        continue
595                                    del self._vault['account'][x['account']]['log'][x['ref']]['file'][x['file']]
596
597                case Action.REMOVE_FILE:
598                    if x['account'] is not None:
599                        if self.account_exists(x['account']):
600                            if x['ref'] in self._vault['account'][x['account']]['log']:
601                                if dry:
602                                    continue
603                                self._vault['account'][x['account']]['log'][x['ref']]['file'][x['file']] = x['value']
604
605                case Action.BOX_TRANSFER:
606                    if x['account'] is not None:
607                        if self.account_exists(x['account']):
608                            if x['ref'] in self._vault['account'][x['account']]['box']:
609                                if dry:
610                                    continue
611                                self._vault['account'][x['account']]['box'][x['ref']]['rest'] -= x['value']
612
613                case Action.EXCHANGE:
614                    if x['account'] is not None:
615                        if x['account'] in self._vault['exchange']:
616                            if x['ref'] in self._vault['exchange'][x['account']]:
617                                if dry:
618                                    continue
619                                del self._vault['exchange'][x['account']][x['ref']]
620
621                case Action.REPORT:
622                    if x['ref'] in self._vault['report']:
623                        if dry:
624                            continue
625                        del self._vault['report'][x['ref']]
626
627                case Action.ZAKAT:
628                    if x['account'] is not None:
629                        if self.account_exists(x['account']):
630                            if x['ref'] in self._vault['account'][x['account']]['box']:
631                                if x['key'] in self._vault['account'][x['account']]['box'][x['ref']]:
632                                    if dry:
633                                        continue
634                                    match x['math']:
635                                        case MathOperation.ADDITION:
636                                            self._vault['account'][x['account']]['box'][x['ref']][x['key']] -= x[
637                                                'value']
638                                        case MathOperation.EQUAL:
639                                            self._vault['account'][x['account']]['box'][x['ref']][x['key']] = x['value']
640                                        case MathOperation.SUBTRACTION:
641                                            self._vault['account'][x['account']]['box'][x['ref']][x['key']] += x[
642                                                'value']
643
644        if not dry:
645            del self._vault['history'][ref]
646        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:
648    def ref_exists(self, account: str, ref_type: str, ref: int) -> bool:
649        """
650        Check if a specific reference (transaction) exists in the vault for a given account and reference type.
651
652        Parameters:
653        account (str): The account number for which to check the existence of the reference.
654        ref_type (str): The type of reference (e.g., 'box', 'log', etc.).
655        ref (int): The reference (transaction) number to check for existence.
656
657        Returns:
658        bool: True if the reference exists for the given account and reference type, False otherwise.
659        """
660        if account in self._vault['account']:
661            return ref in self._vault['account'][account][ref_type]
662        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:
664    def box_exists(self, account: str, ref: int) -> bool:
665        """
666        Check if a specific box (transaction) exists in the vault for a given account and reference.
667
668        Parameters:
669        - account (str): The account number for which to check the existence of the box.
670        - ref (int): The reference (transaction) number to check for existence.
671
672        Returns:
673        - bool: True if the box exists for the given account and reference, False otherwise.
674        """
675        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:
677    def track(self, value: float = 0, desc: str = '', account: str = 1, logging: bool = True, created: int = None,
678              debug: bool = False) -> int:
679        """
680        This function tracks a transaction for a specific account.
681
682        Parameters:
683        value (float): The value of the transaction. Default is 0.
684        desc (str): The description of the transaction. Default is an empty string.
685        account (str): The account for which the transaction is being tracked. Default is '1'.
686        logging (bool): Whether to log the transaction. Default is True.
687        created (int): The timestamp of the transaction. If not provided, it will be generated. Default is None.
688        debug (bool): Whether to print debug information. Default is False.
689
690        Returns:
691        int: The timestamp of the transaction.
692
693        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.
694
695        Raises:
696        ValueError: The log transaction happened again in the same nanosecond time.
697        ValueError: The box transaction happened again in the same nanosecond time.
698        """
699        if debug:
700            print('track', f'debug={debug}')
701        if created is None:
702            created = self.time()
703        no_lock = self.nolock()
704        self.lock()
705        if not self.account_exists(account):
706            if debug:
707                print(f"account {account} created")
708            self._vault['account'][account] = {
709                'balance': 0,
710                'box': {},
711                'count': 0,
712                'log': {},
713                'hide': False,
714                'zakatable': True,
715            }
716            self._step(Action.CREATE, account)
717        if value == 0:
718            if no_lock:
719                self.free(self.lock())
720            return 0
721        if logging:
722            self._log(value=value, desc=desc, account=account, created=created, ref=None, debug=debug)
723        if debug:
724            print('create-box', created)
725        if self.box_exists(account, created):
726            raise ValueError(f"The box transaction happened again in the same nanosecond time({created}).")
727        if debug:
728            print('created-box', created)
729        self._vault['account'][account]['box'][created] = {
730            'capital': value,
731            'count': 0,
732            'last': 0,
733            'rest': value,
734            'total': 0,
735        }
736        self._step(Action.TRACK, account, ref=created, value=value)
737        if no_lock:
738            self.free(self.lock())
739        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:
741    def log_exists(self, account: str, ref: int) -> bool:
742        """
743        Checks if a specific transaction log entry exists for a given account.
744
745        Parameters:
746        account (str): The account number associated with the transaction log.
747        ref (int): The reference to the transaction log entry.
748
749        Returns:
750        bool: True if the transaction log entry exists, False otherwise.
751        """
752        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:
798    def exchange(self, account, created: int = None, rate: float = None, description: str = None,
799                 debug: bool = False) -> dict:
800        """
801        This method is used to record or retrieve exchange rates for a specific account.
802
803        Parameters:
804        - account (str): The account number for which the exchange rate is being recorded or retrieved.
805        - created (int): The timestamp of the exchange rate. If not provided, the current timestamp will be used.
806        - rate (float): The exchange rate to be recorded. If not provided, the method will retrieve the latest exchange rate.
807        - description (str): A description of the exchange rate.
808
809        Returns:
810        - dict: A dictionary containing the latest exchange rate and its description. If no exchange rate is found,
811        it returns a dictionary with default values for the rate and description.
812        """
813        if debug:
814            print('exchange', f'debug={debug}')
815        if created is None:
816            created = self.time()
817        no_lock = self.nolock()
818        self.lock()
819        if rate is not None:
820            if rate <= 0:
821                return dict()
822            if account not in self._vault['exchange']:
823                self._vault['exchange'][account] = {}
824            if len(self._vault['exchange'][account]) == 0 and rate <= 1:
825                return {"time": created, "rate": 1, "description": None}
826            self._vault['exchange'][account][created] = {"rate": rate, "description": description}
827            self._step(Action.EXCHANGE, account, ref=created, value=rate)
828            if no_lock:
829                self.free(self.lock())
830            if debug:
831                print("exchange-created-1",
832                      f'account: {account}, created: {created}, rate:{rate}, description:{description}')
833
834        if account in self._vault['exchange']:
835            valid_rates = [(ts, r) for ts, r in self._vault['exchange'][account].items() if ts <= created]
836            if valid_rates:
837                latest_rate = max(valid_rates, key=lambda x: x[0])
838                if debug:
839                    print("exchange-read-1",
840                          f'account: {account}, created: {created}, rate:{rate}, description:{description}',
841                          'latest_rate', latest_rate)
842                result = latest_rate[1]
843                result['time'] = latest_rate[0]
844                return result  # إرجاع قاموس يحتوي على المعدل والوصف
845        if debug:
846            print("exchange-read-0", f'account: {account}, created: {created}, rate:{rate}, description:{description}')
847        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:
849    @staticmethod
850    def exchange_calc(x: float, x_rate: float, y_rate: float) -> float:
851        """
852        This function calculates the exchanged amount of a currency.
853
854        Args:
855            x (float): The original amount of the currency.
856            x_rate (float): The exchange rate of the original currency.
857            y_rate (float): The exchange rate of the target currency.
858
859        Returns:
860            float: The exchanged amount of the target currency.
861        """
862        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:
864    def exchanges(self) -> dict:
865        """
866        Retrieve the recorded exchange rates for all accounts.
867
868        Parameters:
869        None
870
871        Returns:
872        dict: A dictionary containing all recorded exchange rates.
873        The keys are account names or numbers, and the values are dictionaries containing the exchange rates.
874        Each exchange rate dictionary has timestamps as keys and exchange rate details as values.
875        """
876        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:
878    def accounts(self) -> dict:
879        """
880        Returns a dictionary containing account numbers as keys and their respective balances as values.
881
882        Parameters:
883        None
884
885        Returns:
886        dict: A dictionary where keys are account numbers and values are their respective balances.
887        """
888        result = {}
889        for i in self._vault['account']:
890            result[i] = self._vault['account'][i]['balance']
891        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:
893    def boxes(self, account) -> dict:
894        """
895        Retrieve the boxes (transactions) associated with a specific account.
896
897        Parameters:
898        account (str): The account number for which to retrieve the boxes.
899
900        Returns:
901        dict: A dictionary containing the boxes associated with the given account.
902        If the account does not exist, an empty dictionary is returned.
903        """
904        if self.account_exists(account):
905            return self._vault['account'][account]['box']
906        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:
908    def logs(self, account) -> dict:
909        """
910        Retrieve the logs (transactions) associated with a specific account.
911
912        Parameters:
913        account (str): The account number for which to retrieve the logs.
914
915        Returns:
916        dict: A dictionary containing the logs associated with the given account.
917        If the account does not exist, an empty dictionary is returned.
918        """
919        if self.account_exists(account):
920            return self._vault['account'][account]['log']
921        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:
923    def add_file(self, account: str, ref: int, path: str) -> int:
924        """
925        Adds a file reference to a specific transaction log entry in the vault.
926
927        Parameters:
928        account (str): The account number associated with the transaction log.
929        ref (int): The reference to the transaction log entry.
930        path (str): The path of the file to be added.
931
932        Returns:
933        int: The reference of the added file. If the account or transaction log entry does not exist, returns 0.
934        """
935        if self.account_exists(account):
936            if ref in self._vault['account'][account]['log']:
937                file_ref = self.time()
938                self._vault['account'][account]['log'][ref]['file'][file_ref] = path
939                no_lock = self.nolock()
940                self.lock()
941                self._step(Action.ADD_FILE, account, ref=ref, file=file_ref)
942                if no_lock:
943                    self.free(self.lock())
944                return file_ref
945        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:
947    def remove_file(self, account: str, ref: int, file_ref: int) -> bool:
948        """
949        Removes a file reference from a specific transaction log entry in the vault.
950
951        Parameters:
952        account (str): The account number associated with the transaction log.
953        ref (int): The reference to the transaction log entry.
954        file_ref (int): The reference of the file to be removed.
955
956        Returns:
957        bool: True if the file reference is successfully removed, False otherwise.
958        """
959        if self.account_exists(account):
960            if ref in self._vault['account'][account]['log']:
961                if file_ref in self._vault['account'][account]['log'][ref]['file']:
962                    x = self._vault['account'][account]['log'][ref]['file'][file_ref]
963                    del self._vault['account'][account]['log'][ref]['file'][file_ref]
964                    no_lock = self.nolock()
965                    self.lock()
966                    self._step(Action.REMOVE_FILE, account, ref=ref, file=file_ref, value=x)
967                    if no_lock:
968                        self.free(self.lock())
969                    return True
970        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:
972    def balance(self, account: str = 1, cached: bool = True) -> int:
973        """
974        Calculate and return the balance of a specific account.
975
976        Parameters:
977        account (str): The account number. Default is '1'.
978        cached (bool): If True, use the cached balance. If False, calculate the balance from the box. Default is True.
979
980        Returns:
981        int: The balance of the account.
982
983        Note:
984        If cached is True, the function returns the cached balance.
985        If cached is False, the function calculates the balance from the box by summing up the 'rest' values of all box items.
986        """
987        if cached:
988            return self._vault['account'][account]['balance']
989        x = 0
990        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:
 992    def hide(self, account, status: bool = None) -> bool:
 993        """
 994        Check or set the hide status of a specific account.
 995
 996        Parameters:
 997        account (str): The account number.
 998        status (bool, optional): The new hide status. If not provided, the function will return the current status.
 999
1000        Returns:
1001        bool: The current or updated hide status of the account.
1002
1003        Raises:
1004        None
1005
1006        Example:
1007        >>> tracker = ZakatTracker()
1008        >>> ref = tracker.track(51, 'desc', 'account1')
1009        >>> tracker.hide('account1')  # Set the hide status of 'account1' to True
1010        False
1011        >>> tracker.hide('account1', True)  # Set the hide status of 'account1' to True
1012        True
1013        >>> tracker.hide('account1')  # Get the hide status of 'account1' by default
1014        True
1015        >>> tracker.hide('account1', False)
1016        False
1017        """
1018        if self.account_exists(account):
1019            if status is None:
1020                return self._vault['account'][account]['hide']
1021            self._vault['account'][account]['hide'] = status
1022            return status
1023        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:
1025    def zakatable(self, account, status: bool = None) -> bool:
1026        """
1027        Check or set the zakatable status of a specific account.
1028
1029        Parameters:
1030        account (str): The account number.
1031        status (bool, optional): The new zakatable status. If not provided, the function will return the current status.
1032
1033        Returns:
1034        bool: The current or updated zakatable status of the account.
1035
1036        Raises:
1037        None
1038
1039        Example:
1040        >>> tracker = ZakatTracker()
1041        >>> ref = tracker.track(51, 'desc', 'account1')
1042        >>> tracker.zakatable('account1')  # Set the zakatable status of 'account1' to True
1043        True
1044        >>> tracker.zakatable('account1', True)  # Set the zakatable status of 'account1' to True
1045        True
1046        >>> tracker.zakatable('account1')  # Get the zakatable status of 'account1' by default
1047        True
1048        >>> tracker.zakatable('account1', False)
1049        False
1050        """
1051        if self.account_exists(account):
1052            if status is None:
1053                return self._vault['account'][account]['zakatable']
1054            self._vault['account'][account]['zakatable'] = status
1055            return status
1056        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:
1058    def sub(self, x: float, desc: str = '', account: str = 1, created: int = None, debug: bool = False) -> tuple:
1059        """
1060        Subtracts a specified value from an account's balance.
1061
1062        Parameters:
1063        x (float): The amount to be subtracted.
1064        desc (str): A description for the transaction. Defaults to an empty string.
1065        account (str): The account from which the value will be subtracted. Defaults to '1'.
1066        created (int): The timestamp of the transaction. If not provided, the current timestamp will be used.
1067        debug (bool): A flag indicating whether to print debug information. Defaults to False.
1068
1069        Returns:
1070        tuple: A tuple containing the timestamp of the transaction and a list of tuples representing the age of each transaction.
1071
1072        If the amount to subtract is greater than the account's balance,
1073        the remaining amount will be transferred to a new transaction with a negative value.
1074
1075        Raises:
1076        ValueError: The box transaction happened again in the same nanosecond time.
1077        ValueError: The log transaction happened again in the same nanosecond time.
1078        """
1079        if debug:
1080            print('sub', f'debug={debug}')
1081        if x < 0:
1082            return tuple()
1083        if x == 0:
1084            ref = self.track(x, '', account)
1085            return ref, ref
1086        if created is None:
1087            created = self.time()
1088        no_lock = self.nolock()
1089        self.lock()
1090        self.track(0, '', account)
1091        self._log(value=-x, desc=desc, account=account, created=created, ref=None, debug=debug)
1092        ids = sorted(self._vault['account'][account]['box'].keys())
1093        limit = len(ids) + 1
1094        target = x
1095        if debug:
1096            print('ids', ids)
1097        ages = []
1098        for i in range(-1, -limit, -1):
1099            if target == 0:
1100                break
1101            j = ids[i]
1102            if debug:
1103                print('i', i, 'j', j)
1104            rest = self._vault['account'][account]['box'][j]['rest']
1105            if rest >= target:
1106                self._vault['account'][account]['box'][j]['rest'] -= target
1107                self._step(Action.SUB, account, ref=j, value=target)
1108                ages.append((j, target))
1109                target = 0
1110                break
1111            elif target > rest > 0:
1112                chunk = rest
1113                target -= chunk
1114                self._step(Action.SUB, account, ref=j, value=chunk)
1115                ages.append((j, chunk))
1116                self._vault['account'][account]['box'][j]['rest'] = 0
1117        if target > 0:
1118            self.track(-target, desc, account, False, created)
1119            ages.append((created, target))
1120        if no_lock:
1121            self.free(self.lock())
1122        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]:
1124    def transfer(self, amount: int, from_account: str, to_account: str, desc: str = '', created: int = None,
1125                 debug: bool = False) -> list[int]:
1126        """
1127        Transfers a specified value from one account to another.
1128
1129        Parameters:
1130        amount (int): The amount to be transferred.
1131        from_account (str): The account from which the value will be transferred.
1132        to_account (str): The account to which the value will be transferred.
1133        desc (str, optional): A description for the transaction. Defaults to an empty string.
1134        created (int, optional): The timestamp of the transaction. If not provided, the current timestamp will be used.
1135        debug (bool): A flag indicating whether to print debug information. Defaults to False.
1136
1137        Returns:
1138        list[int]: A list of timestamps corresponding to the transactions made during the transfer.
1139
1140        Raises:
1141        ValueError: Transfer to the same account is forbidden.
1142        ValueError: The box transaction happened again in the same nanosecond time.
1143        ValueError: The log transaction happened again in the same nanosecond time.
1144        """
1145        if debug:
1146            print('transfer', f'debug={debug}')
1147        if from_account == to_account:
1148            raise ValueError(f'Transfer to the same account is forbidden. {to_account}')
1149        if amount <= 0:
1150            return []
1151        if created is None:
1152            created = self.time()
1153        (_, ages) = self.sub(amount, desc, from_account, created, debug=debug)
1154        times = []
1155        source_exchange = self.exchange(from_account, created)
1156        target_exchange = self.exchange(to_account, created)
1157
1158        if debug:
1159            print('ages', ages)
1160
1161        for age, value in ages:
1162            target_amount = self.exchange_calc(value, source_exchange['rate'], target_exchange['rate'])
1163            # Perform the transfer
1164            if self.box_exists(to_account, age):
1165                if debug:
1166                    print('box_exists', age)
1167                capital = self._vault['account'][to_account]['box'][age]['capital']
1168                rest = self._vault['account'][to_account]['box'][age]['rest']
1169                if debug:
1170                    print(
1171                        f"Transfer {value} from `{from_account}` to `{to_account}` (equivalent to {target_amount} `{to_account}`).")
1172                selected_age = age
1173                if rest + target_amount > capital:
1174                    self._vault['account'][to_account]['box'][age]['capital'] += target_amount
1175                    selected_age = ZakatTracker.time()
1176                self._vault['account'][to_account]['box'][age]['rest'] += target_amount
1177                self._step(Action.BOX_TRANSFER, to_account, ref=selected_age, value=target_amount)
1178                y = self._log(value=target_amount, desc=f'TRANSFER {from_account} -> {to_account}', account=to_account,
1179                              created=None, ref=None, debug=debug)
1180                times.append((age, y))
1181                continue
1182            y = self.track(target_amount, desc, to_account, logging=True, created=age, debug=debug)
1183            if debug:
1184                print(
1185                    f"Transferred {value} from `{from_account}` to `{to_account}` (equivalent to {target_amount} `{to_account}`).")
1186            times.append(y)
1187        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:
1189    def check(self, silver_gram_price: float, nisab: float = None, debug: bool = False, now: int = None,
1190              cycle: float = None) -> tuple:
1191        """
1192        Check the eligibility for Zakat based on the given parameters.
1193
1194        Parameters:
1195        silver_gram_price (float): The price of a gram of silver.
1196        nisab (float): The minimum amount of wealth required for Zakat. If not provided,
1197                        it will be calculated based on the silver_gram_price.
1198        debug (bool): Flag to enable debug mode.
1199        now (int): The current timestamp. If not provided, it will be calculated using ZakatTracker.time().
1200        cycle (float): The time cycle for Zakat. If not provided, it will be calculated using ZakatTracker.TimeCycle().
1201
1202        Returns:
1203        tuple: A tuple containing a boolean indicating the eligibility for Zakat, a list of brief statistics,
1204        and a dictionary containing the Zakat plan.
1205        """
1206        if debug:
1207            print('check', f'debug={debug}')
1208        if now is None:
1209            now = self.time()
1210        if cycle is None:
1211            cycle = ZakatTracker.TimeCycle()
1212        if nisab is None:
1213            nisab = ZakatTracker.Nisab(silver_gram_price)
1214        plan = {}
1215        below_nisab = 0
1216        brief = [0, 0, 0]
1217        valid = False
1218        for x in self._vault['account']:
1219            if not self.zakatable(x):
1220                continue
1221            _box = self._vault['account'][x]['box']
1222            _log = self._vault['account'][x]['log']
1223            limit = len(_box) + 1
1224            ids = sorted(self._vault['account'][x]['box'].keys())
1225            for i in range(-1, -limit, -1):
1226                j = ids[i]
1227                rest = float(_box[j]['rest'])
1228                if rest <= 0:
1229                    continue
1230                exchange = self.exchange(x, created=self.time())
1231                if debug:
1232                    print('exchanges', self.exchanges())
1233                rest = ZakatTracker.exchange_calc(rest, exchange['rate'], 1)
1234                brief[0] += rest
1235                index = limit + i - 1
1236                epoch = (now - j) / cycle
1237                if debug:
1238                    print(f"Epoch: {epoch}", _box[j])
1239                if _box[j]['last'] > 0:
1240                    epoch = (now - _box[j]['last']) / cycle
1241                if debug:
1242                    print(f"Epoch: {epoch}")
1243                epoch = floor(epoch)
1244                if debug:
1245                    print(f"Epoch: {epoch}", type(epoch), epoch == 0, 1 - epoch, epoch)
1246                if epoch == 0:
1247                    continue
1248                if debug:
1249                    print("Epoch - PASSED")
1250                brief[1] += rest
1251                if rest >= nisab:
1252                    total = 0
1253                    for _ in range(epoch):
1254                        total += ZakatTracker.ZakatCut(float(rest) - float(total))
1255                    if total > 0:
1256                        if x not in plan:
1257                            plan[x] = {}
1258                        valid = True
1259                        brief[2] += total
1260                        plan[x][index] = {
1261                            'total': total,
1262                            'count': epoch,
1263                            'box_time': j,
1264                            'box_capital': _box[j]['capital'],
1265                            'box_rest': _box[j]['rest'],
1266                            'box_last': _box[j]['last'],
1267                            'box_total': _box[j]['total'],
1268                            'box_count': _box[j]['count'],
1269                            'box_log': _log[j]['desc'],
1270                            'exchange_rate': exchange['rate'],
1271                            'exchange_time': exchange['time'],
1272                            'exchange_desc': exchange['description'],
1273                        }
1274                else:
1275                    chunk = ZakatTracker.ZakatCut(float(rest))
1276                    if chunk > 0:
1277                        if x not in plan:
1278                            plan[x] = {}
1279                        if j not in plan[x].keys():
1280                            plan[x][index] = {}
1281                        below_nisab += rest
1282                        brief[2] += chunk
1283                        plan[x][index]['below_nisab'] = chunk
1284                        plan[x][index]['total'] = chunk
1285                        plan[x][index]['count'] = epoch
1286                        plan[x][index]['box_time'] = j
1287                        plan[x][index]['box_capital'] = _box[j]['capital']
1288                        plan[x][index]['box_rest'] = _box[j]['rest']
1289                        plan[x][index]['box_last'] = _box[j]['last']
1290                        plan[x][index]['box_total'] = _box[j]['total']
1291                        plan[x][index]['box_count'] = _box[j]['count']
1292                        plan[x][index]['box_log'] = _log[j]['desc']
1293                        plan[x][index]['exchange_rate'] = exchange['rate']
1294                        plan[x][index]['exchange_time'] = exchange['time']
1295                        plan[x][index]['exchange_desc'] = exchange['description']
1296        valid = valid or below_nisab >= nisab
1297        if debug:
1298            print(f"below_nisab({below_nisab}) >= nisab({nisab})")
1299        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:
1301    def build_payment_parts(self, demand: float, positive_only: bool = True) -> dict:
1302        """
1303        Build payment parts for the Zakat distribution.
1304
1305        Parameters:
1306        demand (float): The total demand for payment in local currency.
1307        positive_only (bool): If True, only consider accounts with positive balance. Default is True.
1308
1309        Returns:
1310        dict: A dictionary containing the payment parts for each account. The dictionary has the following structure:
1311        {
1312            'account': {
1313                'account_id': {'balance': float, 'rate': float, 'part': float},
1314                ...
1315            },
1316            'exceed': bool,
1317            'demand': float,
1318            'total': float,
1319        }
1320        """
1321        total = 0
1322        parts = {
1323            'account': {},
1324            'exceed': False,
1325            'demand': demand,
1326        }
1327        for x, y in self.accounts().items():
1328            if positive_only and y <= 0:
1329                continue
1330            total += float(y)
1331            exchange = self.exchange(x)
1332            parts['account'][x] = {'balance': y, 'rate': exchange['rate'], 'part': 0}
1333        parts['total'] = total
1334        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:
1336    @staticmethod
1337    def check_payment_parts(parts: dict, debug: bool = False) -> int:
1338        """
1339        Checks the validity of payment parts.
1340
1341        Parameters:
1342        parts (dict): A dictionary containing payment parts information.
1343        debug (bool): Flag to enable debug mode.
1344
1345        Returns:
1346        int: Returns 0 if the payment parts are valid, otherwise returns the error code.
1347
1348        Error Codes:
1349        1: 'demand', 'account', 'total', or 'exceed' key is missing in parts.
1350        2: 'balance', 'rate' or 'part' key is missing in parts['account'][x].
1351        3: 'part' value in parts['account'][x] is less than 0.
1352        4: If 'exceed' is False, 'balance' value in parts['account'][x] is less than or equal to 0.
1353        5: If 'exceed' is False, 'part' value in parts['account'][x] is greater than 'balance' value.
1354        6: The sum of 'part' values in parts['account'] does not match with 'demand' value.
1355        """
1356        if debug:
1357            print('check_payment_parts', f'debug={debug}')
1358        for i in ['demand', 'account', 'total', 'exceed']:
1359            if i not in parts:
1360                return 1
1361        exceed = parts['exceed']
1362        for x in parts['account']:
1363            for j in ['balance', 'rate', 'part']:
1364                if j not in parts['account'][x]:
1365                    return 2
1366                if parts['account'][x]['part'] < 0:
1367                    return 3
1368                if not exceed and parts['account'][x]['balance'] <= 0:
1369                    return 4
1370        demand = parts['demand']
1371        z = 0
1372        for _, y in parts['account'].items():
1373            if not exceed and y['part'] > y['balance']:
1374                return 5
1375            z += ZakatTracker.exchange_calc(y['part'], y['rate'], 1)
1376        z = round(z, 2)
1377        demand = round(demand, 2)
1378        if debug:
1379            print('check_payment_parts', f'z = {z}, demand = {demand}')
1380            print('check_payment_parts', type(z), type(demand))
1381            print('check_payment_parts', z != demand)
1382            print('check_payment_parts', str(z) != str(demand))
1383        if z != demand and str(z) != str(demand):
1384            return 6
1385        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:
1387    def zakat(self, report: tuple, parts: Dict[str, Dict | bool | Any] = None, debug: bool = False) -> bool:
1388        """
1389        Perform Zakat calculation based on the given report and optional parts.
1390
1391        Parameters:
1392        report (tuple): A tuple containing the validity of the report, the report data, and the zakat plan.
1393        parts (dict): A dictionary containing the payment parts for the zakat.
1394        debug (bool): A flag indicating whether to print debug information.
1395
1396        Returns:
1397        bool: True if the zakat calculation is successful, False otherwise.
1398        """
1399        if debug:
1400            print('zakat', f'debug={debug}')
1401        valid, _, plan = report
1402        if not valid:
1403            return valid
1404        parts_exist = parts is not None
1405        if parts_exist:
1406            if self.check_payment_parts(parts, debug=debug) != 0:
1407                return False
1408        if debug:
1409            print('######### zakat #######')
1410            print('parts_exist', parts_exist)
1411        no_lock = self.nolock()
1412        self.lock()
1413        report_time = self.time()
1414        self._vault['report'][report_time] = report
1415        self._step(Action.REPORT, ref=report_time)
1416        created = self.time()
1417        for x in plan:
1418            target_exchange = self.exchange(x)
1419            if debug:
1420                print(plan[x])
1421                print('-------------')
1422                print(self._vault['account'][x]['box'])
1423            ids = sorted(self._vault['account'][x]['box'].keys())
1424            if debug:
1425                print('plan[x]', plan[x])
1426            for i in plan[x].keys():
1427                j = ids[i]
1428                if debug:
1429                    print('i', i, 'j', j)
1430                self._step(Action.ZAKAT, account=x, ref=j, value=self._vault['account'][x]['box'][j]['last'],
1431                           key='last',
1432                           math_operation=MathOperation.EQUAL)
1433                self._vault['account'][x]['box'][j]['last'] = created
1434                amount = ZakatTracker.exchange_calc(float(plan[x][i]['total']), 1, float(target_exchange['rate']))
1435                self._vault['account'][x]['box'][j]['total'] += amount
1436                self._step(Action.ZAKAT, account=x, ref=j, value=amount, key='total',
1437                           math_operation=MathOperation.ADDITION)
1438                self._vault['account'][x]['box'][j]['count'] += plan[x][i]['count']
1439                self._step(Action.ZAKAT, account=x, ref=j, value=plan[x][i]['count'], key='count',
1440                           math_operation=MathOperation.ADDITION)
1441                if not parts_exist:
1442                    try:
1443                        self._vault['account'][x]['box'][j]['rest'] -= amount
1444                    except TypeError:
1445                        self._vault['account'][x]['box'][j]['rest'] -= Decimal(amount)
1446                    # self._step(Action.ZAKAT, account=x, ref=j, value=amount, key='rest',
1447                    #            math_operation=MathOperation.SUBTRACTION)
1448                    self._log(-float(amount), desc='zakat-زكاة', account=x, created=None, ref=j, debug=debug)
1449        if parts_exist:
1450            for account, part in parts['account'].items():
1451                if part['part'] == 0:
1452                    continue
1453                if debug:
1454                    print('zakat-part', account, part['rate'])
1455                target_exchange = self.exchange(account)
1456                amount = ZakatTracker.exchange_calc(part['part'], part['rate'], target_exchange['rate'])
1457                self.sub(amount, desc='zakat-part-دفعة-زكاة', account=account, debug=debug)
1458        if no_lock:
1459            self.free(self.lock())
1460        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:
1462    def export_json(self, path: str = "data.json") -> bool:
1463        """
1464        Exports the current state of the ZakatTracker object to a JSON file.
1465
1466        Parameters:
1467        path (str): The path where the JSON file will be saved. Default is "data.json".
1468
1469        Returns:
1470        bool: True if the export is successful, False otherwise.
1471
1472        Raises:
1473        No specific exceptions are raised by this method.
1474        """
1475        with open(path, "w") as file:
1476            json.dump(self._vault, file, indent=4, cls=JSONEncoder)
1477            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:
1479    def save(self, path: str = None) -> bool:
1480        """
1481        Saves the ZakatTracker's current state to a pickle file.
1482
1483        This method serializes the internal data (`_vault`) along with metadata
1484        (Python version, pickle protocol) for future compatibility.
1485
1486        Parameters:
1487        path (str, optional): File path for saving. Defaults to a predefined location.
1488
1489        Returns:
1490        bool: True if the save operation is successful, False otherwise.
1491        """
1492        if path is None:
1493            path = self.path()
1494        with open(path, "wb") as f:
1495            version = f'{version_info.major}.{version_info.minor}.{version_info.micro}'
1496            pickle_protocol = pickle.HIGHEST_PROTOCOL
1497            data = {
1498                'python_version': version,
1499                'pickle_protocol': pickle_protocol,
1500                'data': self._vault,
1501            }
1502            pickle.dump(data, f, protocol=pickle_protocol)
1503            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:
1505    def load(self, path: str = None) -> bool:
1506        """
1507        Load the current state of the ZakatTracker object from a pickle file.
1508
1509        Parameters:
1510        path (str): The path where the pickle file is located. If not provided, it will use the default path.
1511
1512        Returns:
1513        bool: True if the load operation is successful, False otherwise.
1514        """
1515        if path is None:
1516            path = self.path()
1517        if os.path.exists(path):
1518            with open(path, "rb") as f:
1519                data = pickle.load(f)
1520                self._vault = data['data']
1521                return True
1522        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):
1524    def import_csv_cache_path(self):
1525        """
1526        Generates the cache file path for imported CSV data.
1527
1528        This function constructs the file path where cached data from CSV imports
1529        will be stored. The cache file is a pickle file (.pickle extension) appended
1530        to the base path of the object.
1531
1532        Returns:
1533        str: The full path to the import CSV cache file.
1534
1535        Example:
1536            >>> obj = ZakatTracker('/data/reports')
1537            >>> obj.import_csv_cache_path()
1538            '/data/reports.import_csv.pickle'
1539        """
1540        path = self.path()
1541        if path.endswith(".pickle"):
1542            path = path[:-7]
1543        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:
1545    def import_csv(self, path: str = 'file.csv', debug: bool = False) -> tuple:
1546        """
1547        The function reads the CSV file, checks for duplicate transactions, and creates the transactions in the system.
1548
1549        Parameters:
1550        path (str): The path to the CSV file. Default is 'file.csv'.
1551        debug (bool): A flag indicating whether to print debug information.
1552
1553        Returns:
1554        tuple: A tuple containing the number of transactions created, the number of transactions found in the cache,
1555                and a dictionary of bad transactions.
1556
1557        Notes:
1558            * Currency Pair Assumption: This function assumes that the exchange rates stored for each account
1559                                        are appropriate for the currency pairs involved in the conversions.
1560            * The exchange rate for each account is based on the last encountered transaction rate that is not equal
1561                to 1.0 or the previous rate for that account.
1562            * Those rates will be merged into the exchange rates main data, and later it will be used for all subsequent
1563              transactions of the same account within the whole imported and existing dataset when doing `check` and
1564              `zakat` operations.
1565
1566        Example Usage:
1567            The CSV file should have the following format, rate is optional per transaction:
1568            account, desc, value, date, rate
1569            For example:
1570            safe-45, "Some text", 34872, 1988-06-30 00:00:00, 1
1571        """
1572        if debug:
1573            print('import_csv', f'debug={debug}')
1574        cache: list[int] = []
1575        try:
1576            with open(self.import_csv_cache_path(), "rb") as f:
1577                cache = pickle.load(f)
1578        except:
1579            pass
1580        date_formats = [
1581            "%Y-%m-%d %H:%M:%S",
1582            "%Y-%m-%dT%H:%M:%S",
1583            "%Y-%m-%dT%H%M%S",
1584            "%Y-%m-%d",
1585        ]
1586        created, found, bad = 0, 0, {}
1587        data: dict[int, list] = {}
1588        with open(path, newline='', encoding="utf-8") as f:
1589            i = 0
1590            for row in csv.reader(f, delimiter=','):
1591                i += 1
1592                hashed = hash(tuple(row))
1593                if hashed in cache:
1594                    found += 1
1595                    continue
1596                account = row[0]
1597                desc = row[1]
1598                value = float(row[2])
1599                rate = 1.0
1600                if row[4:5]:  # Empty list if index is out of range
1601                    rate = float(row[4])
1602                date: int = 0
1603                for time_format in date_formats:
1604                    try:
1605                        date = self.time(datetime.datetime.strptime(row[3], time_format))
1606                        break
1607                    except:
1608                        pass
1609                # TODO: not allowed for negative dates
1610                if date == 0 or value == 0:
1611                    bad[i] = row
1612                    continue
1613                if date not in data:
1614                    data[date] = []
1615                # TODO: If duplicated time with different accounts with the same amount it is an indicator of a transfer
1616                data[date].append((date, value, desc, account, rate, hashed))
1617
1618        if debug:
1619            print('import_csv', len(data))
1620
1621        def process(row, index=0):
1622            nonlocal created
1623            (date, value, desc, account, rate, hashed) = row
1624            date += index
1625            if rate > 1:
1626                self.exchange(account, created=date, rate=rate)
1627            if value > 0:
1628                self.track(value, desc, account, True, date)
1629            elif value < 0:
1630                self.sub(-value, desc, account, date)
1631            created += 1
1632            cache.append(hashed)
1633
1634        for date, rows in sorted(data.items()):
1635            len_rows = len(rows)
1636            if len_rows == 1:
1637                process(rows[0])
1638                continue
1639            if debug:
1640                print('-- Duplicated time detected', date, 'len', len_rows)
1641                print(rows)
1642                print('---------------------------------')
1643            for index, row in enumerate(rows):
1644                process(row, index)
1645        with open(self.import_csv_cache_path(), "wb") as f:
1646            pickle.dump(cache, f)
1647        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:
1653    @staticmethod
1654    def human_readable_size(size: float, decimal_places: int = 2) -> str:
1655        """
1656        Converts a size in bytes to a human-readable format (e.g., KB, MB, GB).
1657
1658        This function iterates through progressively larger units of information
1659        (B, KB, MB, GB, etc.) and divides the input size until it fits within a
1660        range that can be expressed with a reasonable number before the unit.
1661
1662        Parameters:
1663        size (float): The size in bytes to convert.
1664        decimal_places (int, optional): The number of decimal places to display
1665            in the result. Defaults to 2.
1666
1667        Returns:
1668        str: A string representation of the size in a human-readable format,
1669            rounded to the specified number of decimal places. For example:
1670                - "1.50 KB" (1536 bytes)
1671                - "23.00 MB" (24117248 bytes)
1672                - "1.23 GB" (1325899906 bytes)
1673        """
1674        if type(size) not in (float, int):
1675            raise TypeError("size must be a float or integer")
1676        if type(decimal_places) != int:
1677            raise TypeError("decimal_places must be an integer")
1678        for unit in ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']:
1679            if size < 1024.0:
1680                break
1681            size /= 1024.0
1682        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:
1684    @staticmethod
1685    def get_dict_size(obj: dict, seen: set = None) -> float:
1686        """
1687        Recursively calculates the approximate memory size of a dictionary and its contents in bytes.
1688
1689        This function traverses the dictionary structure, accounting for the size of keys, values,
1690        and any nested objects. It handles various data types commonly found in dictionaries
1691        (e.g., lists, tuples, sets, numbers, strings) and prevents infinite recursion in case
1692        of circular references.
1693
1694        Parameters:
1695        obj (dict): The dictionary whose size is to be calculated.
1696        seen (set, optional): A set used internally to track visited objects
1697                             and avoid circular references. Defaults to None.
1698
1699        Returns:
1700            float: An approximate size of the dictionary and its contents in bytes.
1701
1702        Note:
1703        - This function is a method of the `ZakatTracker` class and is likely used to
1704          estimate the memory footprint of data structures relevant to Zakat calculations.
1705        - The size calculation is approximate as it relies on `sys.getsizeof()`, which might
1706          not account for all memory overhead depending on the Python implementation.
1707        - Circular references are handled to prevent infinite recursion.
1708        - Basic numeric types (int, float, complex) are assumed to have fixed sizes.
1709        - String sizes are estimated based on character length and encoding.
1710        """
1711        size = 0
1712        if seen is None:
1713            seen = set()
1714
1715        obj_id = id(obj)
1716        if obj_id in seen:
1717            return 0
1718
1719        seen.add(obj_id)
1720        size += sys.getsizeof(obj)
1721
1722        if isinstance(obj, dict):
1723            for k, v in obj.items():
1724                size += ZakatTracker.get_dict_size(k, seen)
1725                size += ZakatTracker.get_dict_size(v, seen)
1726        elif isinstance(obj, (list, tuple, set, frozenset)):
1727            for item in obj:
1728                size += ZakatTracker.get_dict_size(item, seen)
1729        elif isinstance(obj, (int, float, complex)):  # Handle numbers
1730            pass  # Basic numbers have a fixed size, so nothing to add here
1731        elif isinstance(obj, str):  # Handle strings
1732            size += len(obj) * sys.getsizeof(str().encode())  # Size per character in bytes
1733        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) -> tuple:
1735    @staticmethod
1736    def duration_from_nanoseconds(ns: int) -> tuple:
1737        """
1738        REF https://github.com/JayRizzo/Random_Scripts/blob/master/time_measure.py#L106
1739        Convert NanoSeconds to Human Readable Time Format.
1740        A NanoSeconds is a unit of time in the International System of Units (SI) equal
1741        to one millionth (0.000001 or 10−6 or 1⁄1,000,000) of a second.
1742        Its symbol is μs, sometimes simplified to us when Unicode is not available.
1743        A microsecond is equal to 1000 nanoseconds or 1⁄1,000 of a millisecond.
1744
1745        INPUT : ms (AKA: MilliSeconds)
1746        OUTPUT: tuple(string time_lapsed, string spoken_time) like format.
1747        OUTPUT Variables: time_lapsed, spoken_time
1748
1749        Example  Input: duration_from_nanoseconds(ns)
1750        **"Millennium:Century:Years:Days:Hours:Minutes:Seconds:MilliSeconds:MicroSeconds:NanoSeconds"**
1751        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')
1752        duration_from_nanoseconds(1234567890123456789012)
1753        """
1754        us, ns = divmod(ns, 1000)
1755        ms, us = divmod(us, 1000)
1756        s, ms = divmod(ms, 1000)
1757        m, s = divmod(s, 60)
1758        h, m = divmod(m, 60)
1759        d, h = divmod(h, 24)
1760        y, d = divmod(d, 365)
1761        c, y = divmod(y, 100)
1762        n, c = divmod(c, 10)
1763        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}"
1764        spoken_time = f"{n: 3d} Millennia, {c: 4d} Century, {y: 3d} Years, {d: 4d} Days, {h: 2d} Hours, {m: 2d} Minutes, {s: 2d} Seconds, {ms: 3d} MilliSeconds, {us: 3d} MicroSeconds, {ns: 3d} NanoSeconds"
1765        return time_lapsed, spoken_time

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:
1767    @staticmethod
1768    def day_to_time(day: int, month: int = 6, year: int = 2024) -> int:  # افتراض أن الشهر هو يونيو والسنة 2024
1769        """
1770        Convert a specific day, month, and year into a timestamp.
1771
1772        Parameters:
1773        day (int): The day of the month.
1774        month (int): The month of the year. Default is 6 (June).
1775        year (int): The year. Default is 2024.
1776
1777        Returns:
1778        int: The timestamp representing the given day, month, and year.
1779
1780        Note:
1781        This method assumes the default month and year if not provided.
1782        """
1783        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:
1785    @staticmethod
1786    def generate_random_date(start_date: datetime.datetime, end_date: datetime.datetime) -> datetime.datetime:
1787        """
1788        Generate a random date between two given dates.
1789
1790        Parameters:
1791        start_date (datetime.datetime): The start date from which to generate a random date.
1792        end_date (datetime.datetime): The end date until which to generate a random date.
1793
1794        Returns:
1795        datetime.datetime: A random date between the start_date and end_date.
1796        """
1797        time_between_dates = end_date - start_date
1798        days_between_dates = time_between_dates.days
1799        random_number_of_days = random.randrange(days_between_dates)
1800        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:
1802    @staticmethod
1803    def generate_random_csv_file(path: str = "data.csv", count: int = 1000, with_rate: bool = False,
1804                                 debug: bool = False) -> int:
1805        """
1806        Generate a random CSV file with specified parameters.
1807
1808        Parameters:
1809        path (str): The path where the CSV file will be saved. Default is "data.csv".
1810        count (int): The number of rows to generate in the CSV file. Default is 1000.
1811        with_rate (bool): If True, a random rate between 1.2% and 12% is added. Default is False.
1812        debug (bool): A flag indicating whether to print debug information.
1813
1814        Returns:
1815        None. The function generates a CSV file at the specified path with the given count of rows.
1816        Each row contains a randomly generated account, description, value, and date.
1817        The value is randomly generated between 1000 and 100000,
1818        and the date is randomly generated between 1950-01-01 and 2023-12-31.
1819        If the row number is not divisible by 13, the value is multiplied by -1.
1820        """
1821        if debug:
1822            print('generate_random_csv_file', f'debug={debug}')
1823        i = 0
1824        with open(path, "w", newline="") as csvfile:
1825            writer = csv.writer(csvfile)
1826            for i in range(count):
1827                account = f"acc-{random.randint(1, 1000)}"
1828                desc = f"Some text {random.randint(1, 1000)}"
1829                value = random.randint(1000, 100000)
1830                date = ZakatTracker.generate_random_date(datetime.datetime(1950, 1, 1),
1831                                                         datetime.datetime(2023, 12, 31)).strftime("%Y-%m-%d %H:%M:%S")
1832                if not i % 13 == 0:
1833                    value *= -1
1834                row = [account, desc, value, date]
1835                if with_rate:
1836                    rate = random.randint(1, 100) * 0.12
1837                    if debug:
1838                        print('before-append', row)
1839                    row.append(rate)
1840                    if debug:
1841                        print('after-append', row)
1842                writer.writerow(row)
1843                i = i + 1
1844        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):
1846    @staticmethod
1847    def create_random_list(max_sum, min_value=0, max_value=10):
1848        """
1849        Creates a list of random integers whose sum does not exceed the specified maximum.
1850
1851        Args:
1852            max_sum: The maximum allowed sum of the list elements.
1853            min_value: The minimum possible value for an element (inclusive).
1854            max_value: The maximum possible value for an element (inclusive).
1855
1856        Returns:
1857            A list of random integers.
1858        """
1859        result = []
1860        current_sum = 0
1861
1862        while current_sum < max_sum:
1863            # Calculate the remaining space for the next element
1864            remaining_sum = max_sum - current_sum
1865            # Determine the maximum possible value for the next element
1866            next_max_value = min(remaining_sum, max_value)
1867            # Generate a random element within the allowed range
1868            next_element = random.randint(min_value, next_max_value)
1869            result.append(next_element)
1870            current_sum += next_element
1871
1872        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:
2055    def test(self, debug: bool = False) -> bool:
2056        if debug:
2057            print('test', f'debug={debug}')
2058        try:
2059
2060            assert self._history()
2061
2062            # Not allowed for duplicate transactions in the same account and time
2063
2064            created = ZakatTracker.time()
2065            self.track(100, 'test-1', 'same', True, created)
2066            failed = False
2067            try:
2068                self.track(50, 'test-1', 'same', True, created)
2069            except:
2070                failed = True
2071            assert failed is True
2072
2073            self.reset()
2074
2075            # Same account transfer
2076            for x in [1, 'a', True, 1.8, None]:
2077                failed = False
2078                try:
2079                    self.transfer(1, x, x, 'same-account', debug=debug)
2080                except:
2081                    failed = True
2082                assert failed is True
2083
2084            # Always preserve box age during transfer
2085
2086            series: list[tuple] = [
2087                (30, 4),
2088                (60, 3),
2089                (90, 2),
2090            ]
2091            case = {
2092                30: {
2093                    'series': series,
2094                    'rest': 150,
2095                },
2096                60: {
2097                    'series': series,
2098                    'rest': 120,
2099                },
2100                90: {
2101                    'series': series,
2102                    'rest': 90,
2103                },
2104                180: {
2105                    'series': series,
2106                    'rest': 0,
2107                },
2108                270: {
2109                    'series': series,
2110                    'rest': -90,
2111                },
2112                360: {
2113                    'series': series,
2114                    'rest': -180,
2115                },
2116            }
2117
2118            selected_time = ZakatTracker.time() - ZakatTracker.TimeCycle()
2119
2120            for total in case:
2121                for x in case[total]['series']:
2122                    self.track(x[0], f"test-{x} ages", 'ages', True, selected_time * x[1])
2123
2124                refs = self.transfer(total, 'ages', 'future', 'Zakat Movement', debug=debug)
2125
2126                if debug:
2127                    print('refs', refs)
2128
2129                ages_cache_balance = self.balance('ages')
2130                ages_fresh_balance = self.balance('ages', False)
2131                rest = case[total]['rest']
2132                if debug:
2133                    print('source', ages_cache_balance, ages_fresh_balance, rest)
2134                assert ages_cache_balance == rest
2135                assert ages_fresh_balance == rest
2136
2137                future_cache_balance = self.balance('future')
2138                future_fresh_balance = self.balance('future', False)
2139                if debug:
2140                    print('target', future_cache_balance, future_fresh_balance, total)
2141                    print('refs', refs)
2142                assert future_cache_balance == total
2143                assert future_fresh_balance == total
2144
2145                for ref in self._vault['account']['ages']['box']:
2146                    ages_capital = self._vault['account']['ages']['box'][ref]['capital']
2147                    ages_rest = self._vault['account']['ages']['box'][ref]['rest']
2148                    future_capital = 0
2149                    if ref in self._vault['account']['future']['box']:
2150                        future_capital = self._vault['account']['future']['box'][ref]['capital']
2151                    future_rest = 0
2152                    if ref in self._vault['account']['future']['box']:
2153                        future_rest = self._vault['account']['future']['box'][ref]['rest']
2154                    if ages_capital != 0 and future_capital != 0 and future_rest != 0:
2155                        if debug:
2156                            print('================================================================')
2157                            print('ages', ages_capital, ages_rest)
2158                            print('future', future_capital, future_rest)
2159                        if ages_rest == 0:
2160                            assert ages_capital == future_capital
2161                        elif ages_rest < 0:
2162                            assert -ages_capital == future_capital
2163                        elif ages_rest > 0:
2164                            assert ages_capital == ages_rest + future_capital
2165                self.reset()
2166                assert len(self._vault['history']) == 0
2167
2168            assert self._history()
2169            assert self._history(False) is False
2170            assert self._history() is False
2171            assert self._history(True)
2172            assert self._history()
2173
2174            self._test_core(True, debug)
2175            self._test_core(False, debug)
2176
2177            transaction = [
2178                (
2179                    20, 'wallet', 1, 800, 800, 800, 4, 5,
2180                    -85, -85, -85, 6, 7,
2181                ),
2182                (
2183                    750, 'wallet', 'safe', 50, 50, 50, 4, 6,
2184                    750, 750, 750, 1, 1,
2185                ),
2186                (
2187                    600, 'safe', 'bank', 150, 150, 150, 1, 2,
2188                    600, 600, 600, 1, 1,
2189                ),
2190            ]
2191            for z in transaction:
2192                self.lock()
2193                x = z[1]
2194                y = z[2]
2195                self.transfer(z[0], x, y, 'test-transfer', debug=debug)
2196                assert self.balance(x) == z[3]
2197                xx = self.accounts()[x]
2198                assert xx == z[3]
2199                assert self.balance(x, False) == z[4]
2200                assert xx == z[4]
2201
2202                s = 0
2203                log = self._vault['account'][x]['log']
2204                for i in log:
2205                    s += log[i]['value']
2206                if debug:
2207                    print('s', s, 'z[5]', z[5])
2208                assert s == z[5]
2209
2210                assert self.box_size(x) == z[6]
2211                assert self.log_size(x) == z[7]
2212
2213                yy = self.accounts()[y]
2214                assert self.balance(y) == z[8]
2215                assert yy == z[8]
2216                assert self.balance(y, False) == z[9]
2217                assert yy == z[9]
2218
2219                s = 0
2220                log = self._vault['account'][y]['log']
2221                for i in log:
2222                    s += log[i]['value']
2223                assert s == z[10]
2224
2225                assert self.box_size(y) == z[11]
2226                assert self.log_size(y) == z[12]
2227
2228            if debug:
2229                pp().pprint(self.check(2.17))
2230
2231            assert not self.nolock()
2232            history_count = len(self._vault['history'])
2233            if debug:
2234                print('history-count', history_count)
2235            assert history_count == 11
2236            assert not self.free(ZakatTracker.time())
2237            assert self.free(self.lock())
2238            assert self.nolock()
2239            assert len(self._vault['history']) == 11
2240
2241            # storage
2242
2243            _path = self.path('test.pickle')
2244            if os.path.exists(_path):
2245                os.remove(_path)
2246            self.save()
2247            assert os.path.getsize(_path) > 0
2248            self.reset()
2249            assert self.recall(False, debug) is False
2250            self.load()
2251            assert self._vault['account'] is not None
2252
2253            # recall
2254
2255            assert self.nolock()
2256            assert len(self._vault['history']) == 11
2257            assert self.recall(False, debug) is True
2258            assert len(self._vault['history']) == 10
2259            assert self.recall(False, debug) is True
2260            assert len(self._vault['history']) == 9
2261
2262            # exchange
2263
2264            self.exchange("cash", 25, 3.75, "2024-06-25")
2265            self.exchange("cash", 22, 3.73, "2024-06-22")
2266            self.exchange("cash", 15, 3.69, "2024-06-15")
2267            self.exchange("cash", 10, 3.66)
2268
2269            for i in range(1, 30):
2270                exchange = self.exchange("cash", i)
2271                rate, description, created = exchange['rate'], exchange['description'], exchange['time']
2272                if debug:
2273                    print(i, rate, description, created)
2274                assert created
2275                if i < 10:
2276                    assert rate == 1
2277                    assert description is None
2278                elif i == 10:
2279                    assert rate == 3.66
2280                    assert description is None
2281                elif i < 15:
2282                    assert rate == 3.66
2283                    assert description is None
2284                elif i == 15:
2285                    assert rate == 3.69
2286                    assert description is not None
2287                elif i < 22:
2288                    assert rate == 3.69
2289                    assert description is not None
2290                elif i == 22:
2291                    assert rate == 3.73
2292                    assert description is not None
2293                elif i >= 25:
2294                    assert rate == 3.75
2295                    assert description is not None
2296                exchange = self.exchange("bank", i)
2297                rate, description, created = exchange['rate'], exchange['description'], exchange['time']
2298                if debug:
2299                    print(i, rate, description, created)
2300                assert created
2301                assert rate == 1
2302                assert description is None
2303
2304            assert len(self._vault['exchange']) > 0
2305            assert len(self.exchanges()) > 0
2306            self._vault['exchange'].clear()
2307            assert len(self._vault['exchange']) == 0
2308            assert len(self.exchanges()) == 0
2309
2310            # حفظ أسعار الصرف باستخدام التواريخ بالنانو ثانية
2311            self.exchange("cash", ZakatTracker.day_to_time(25), 3.75, "2024-06-25")
2312            self.exchange("cash", ZakatTracker.day_to_time(22), 3.73, "2024-06-22")
2313            self.exchange("cash", ZakatTracker.day_to_time(15), 3.69, "2024-06-15")
2314            self.exchange("cash", ZakatTracker.day_to_time(10), 3.66)
2315
2316            for i in [x * 0.12 for x in range(-15, 21)]:
2317                if i <= 0:
2318                    assert len(self.exchange("test", ZakatTracker.time(), i, f"range({i})")) == 0
2319                else:
2320                    assert len(self.exchange("test", ZakatTracker.time(), i, f"range({i})")) > 0
2321
2322            # اختبار النتائج باستخدام التواريخ بالنانو ثانية
2323            for i in range(1, 31):
2324                timestamp_ns = ZakatTracker.day_to_time(i)
2325                exchange = self.exchange("cash", timestamp_ns)
2326                rate, description, created = exchange['rate'], exchange['description'], exchange['time']
2327                if debug:
2328                    print(i, rate, description, created)
2329                assert created
2330                if i < 10:
2331                    assert rate == 1
2332                    assert description is None
2333                elif i == 10:
2334                    assert rate == 3.66
2335                    assert description is None
2336                elif i < 15:
2337                    assert rate == 3.66
2338                    assert description is None
2339                elif i == 15:
2340                    assert rate == 3.69
2341                    assert description is not None
2342                elif i < 22:
2343                    assert rate == 3.69
2344                    assert description is not None
2345                elif i == 22:
2346                    assert rate == 3.73
2347                    assert description is not None
2348                elif i >= 25:
2349                    assert rate == 3.75
2350                    assert description is not None
2351                exchange = self.exchange("bank", i)
2352                rate, description, created = exchange['rate'], exchange['description'], exchange['time']
2353                if debug:
2354                    print(i, rate, description, created)
2355                assert created
2356                assert rate == 1
2357                assert description is None
2358
2359            # csv
2360
2361            csv_count = 1000
2362
2363            for with_rate, path in {
2364                False: 'test-import_csv-no-exchange',
2365                True: 'test-import_csv-with-exchange',
2366            }.items():
2367
2368                if debug:
2369                    print('test_import_csv', with_rate, path)
2370
2371                # csv
2372
2373                csv_path = path + '.csv'
2374                if os.path.exists(csv_path):
2375                    os.remove(csv_path)
2376                c = self.generate_random_csv_file(csv_path, csv_count, with_rate, debug)
2377                if debug:
2378                    print('generate_random_csv_file', c)
2379                assert c == csv_count
2380                assert os.path.getsize(csv_path) > 0
2381                cache_path = self.import_csv_cache_path()
2382                if os.path.exists(cache_path):
2383                    os.remove(cache_path)
2384                self.reset()
2385                (created, found, bad) = self.import_csv(csv_path, debug)
2386                bad_count = len(bad)
2387                if debug:
2388                    print(f"csv-imported: ({created}, {found}, {bad_count}) = count({csv_count})")
2389                tmp_size = os.path.getsize(cache_path)
2390                assert tmp_size > 0
2391                assert created + found + bad_count == csv_count
2392                assert created == csv_count
2393                assert bad_count == 0
2394                (created_2, found_2, bad_2) = self.import_csv(csv_path)
2395                bad_2_count = len(bad_2)
2396                if debug:
2397                    print(f"csv-imported: ({created_2}, {found_2}, {bad_2_count})")
2398                    print(bad)
2399                assert tmp_size == os.path.getsize(cache_path)
2400                assert created_2 + found_2 + bad_2_count == csv_count
2401                assert created == found_2
2402                assert bad_count == bad_2_count
2403                assert found_2 == csv_count
2404                assert bad_2_count == 0
2405                assert created_2 == 0
2406
2407                # payment parts
2408
2409                positive_parts = self.build_payment_parts(100, positive_only=True)
2410                assert self.check_payment_parts(positive_parts) != 0
2411                assert self.check_payment_parts(positive_parts) != 0
2412                all_parts = self.build_payment_parts(300, positive_only=False)
2413                assert self.check_payment_parts(all_parts) != 0
2414                assert self.check_payment_parts(all_parts) != 0
2415                if debug:
2416                    pp().pprint(positive_parts)
2417                    pp().pprint(all_parts)
2418                # dynamic discount
2419                suite = []
2420                count = 3
2421                for exceed in [False, True]:
2422                    case = []
2423                    for parts in [positive_parts, all_parts]:
2424                        part = parts.copy()
2425                        demand = part['demand']
2426                        if debug:
2427                            print(demand, part['total'])
2428                        i = 0
2429                        z = demand / count
2430                        cp = {
2431                            'account': {},
2432                            'demand': demand,
2433                            'exceed': exceed,
2434                            'total': part['total'],
2435                        }
2436                        j = ''
2437                        for x, y in part['account'].items():
2438                            x_exchange = self.exchange(x)
2439                            zz = self.exchange_calc(z, 1, x_exchange['rate'])
2440                            if exceed and zz <= demand:
2441                                i += 1
2442                                y['part'] = zz
2443                                if debug:
2444                                    print(exceed, y)
2445                                cp['account'][x] = y
2446                                case.append(y)
2447                            elif not exceed and y['balance'] >= zz:
2448                                i += 1
2449                                y['part'] = zz
2450                                if debug:
2451                                    print(exceed, y)
2452                                cp['account'][x] = y
2453                                case.append(y)
2454                            j = x
2455                            if i >= count:
2456                                break
2457                        if len(cp['account'][j]) > 0:
2458                            suite.append(cp)
2459                if debug:
2460                    print('suite', len(suite))
2461                # vault = self._vault.copy()
2462                for case in suite:
2463                    # self._vault = vault.copy()
2464                    if debug:
2465                        print('case', case)
2466                    result = self.check_payment_parts(case)
2467                    if debug:
2468                        print('check_payment_parts', result, f'exceed: {exceed}')
2469                    assert result == 0
2470
2471                    report = self.check(2.17, None, debug)
2472                    (valid, brief, plan) = report
2473                    if debug:
2474                        print('valid', valid)
2475                    zakat_result = self.zakat(report, parts=case, debug=debug)
2476                    if debug:
2477                        print('zakat-result', zakat_result)
2478                    assert valid == zakat_result
2479
2480            assert self.save(path + '.pickle')
2481            assert self.export_json(path + '.json')
2482
2483            assert self.export_json("1000-transactions-test.json")
2484            assert self.save("1000-transactions-test.pickle")
2485
2486            self.reset()
2487
2488            # test transfer between accounts with different exchange rate
2489
2490            a_SAR = "Bank (SAR)"
2491            b_USD = "Bank (USD)"
2492            c_SAR = "Safe (SAR)"
2493            # 0: track, 1: check-exchange, 2: do-exchange, 3: transfer
2494            for case in [
2495                (0, a_SAR, "SAR Gift", 1000, 1000),
2496                (1, a_SAR, 1),
2497                (0, b_USD, "USD Gift", 500, 500),
2498                (1, b_USD, 1),
2499                (2, b_USD, 3.75),
2500                (1, b_USD, 3.75),
2501                (3, 100, b_USD, a_SAR, "100 USD -> SAR", 400, 1375),
2502                (0, c_SAR, "Salary", 750, 750),
2503                (3, 375, c_SAR, b_USD, "375 SAR -> USD", 375, 500),
2504                (3, 3.75, a_SAR, b_USD, "3.75 SAR -> USD", 1371.25, 501),
2505            ]:
2506                match (case[0]):
2507                    case 0:  # track
2508                        _, account, desc, x, balance = case
2509                        self.track(value=x, desc=desc, account=account, debug=debug)
2510
2511                        cached_value = self.balance(account, cached=True)
2512                        fresh_value = self.balance(account, cached=False)
2513                        if debug:
2514                            print('account', account, 'cached_value', cached_value, 'fresh_value', fresh_value)
2515                        assert cached_value == balance
2516                        assert fresh_value == balance
2517                    case 1:  # check-exchange
2518                        _, account, expected_rate = case
2519                        t_exchange = self.exchange(account, created=ZakatTracker.time(), debug=debug)
2520                        if debug:
2521                            print('t-exchange', t_exchange)
2522                        assert t_exchange['rate'] == expected_rate
2523                    case 2:  # do-exchange
2524                        _, account, rate = case
2525                        self.exchange(account, rate=rate, debug=debug)
2526                        b_exchange = self.exchange(account, created=ZakatTracker.time(), debug=debug)
2527                        if debug:
2528                            print('b-exchange', b_exchange)
2529                        assert b_exchange['rate'] == rate
2530                    case 3:  # transfer
2531                        _, x, a, b, desc, a_balance, b_balance = case
2532                        self.transfer(x, a, b, desc, debug=debug)
2533
2534                        cached_value = self.balance(a, cached=True)
2535                        fresh_value = self.balance(a, cached=False)
2536                        if debug:
2537                            print('account', a, 'cached_value', cached_value, 'fresh_value', fresh_value)
2538                        assert cached_value == a_balance
2539                        assert fresh_value == a_balance
2540
2541                        cached_value = self.balance(b, cached=True)
2542                        fresh_value = self.balance(b, cached=False)
2543                        if debug:
2544                            print('account', b, 'cached_value', cached_value, 'fresh_value', fresh_value)
2545                        assert cached_value == b_balance
2546                        assert fresh_value == b_balance
2547
2548            # Transfer all in many chunks randomly from B to A
2549            a_SAR_balance = 1371.25
2550            b_USD_balance = 501
2551            b_USD_exchange = self.exchange(b_USD)
2552            amounts = ZakatTracker.create_random_list(b_USD_balance)
2553            if debug:
2554                print('amounts', amounts)
2555            i = 0
2556            for x in amounts:
2557                if debug:
2558                    print(f'{i} - transfer-with-exchange({x})')
2559                self.transfer(x, b_USD, a_SAR, f"{x} USD -> SAR", debug=debug)
2560
2561                b_USD_balance -= x
2562                cached_value = self.balance(b_USD, cached=True)
2563                fresh_value = self.balance(b_USD, cached=False)
2564                if debug:
2565                    print('account', b_USD, 'cached_value', cached_value, 'fresh_value', fresh_value, 'excepted',
2566                          b_USD_balance)
2567                assert cached_value == b_USD_balance
2568                assert fresh_value == b_USD_balance
2569
2570                a_SAR_balance += x * b_USD_exchange['rate']
2571                cached_value = self.balance(a_SAR, cached=True)
2572                fresh_value = self.balance(a_SAR, cached=False)
2573                if debug:
2574                    print('account', a_SAR, 'cached_value', cached_value, 'fresh_value', fresh_value, 'expected',
2575                          a_SAR_balance, 'rate', b_USD_exchange['rate'])
2576                assert cached_value == a_SAR_balance
2577                assert fresh_value == a_SAR_balance
2578                i += 1
2579
2580            # Transfer all in many chunks randomly from C to A
2581            c_SAR_balance = 375
2582            amounts = ZakatTracker.create_random_list(c_SAR_balance)
2583            if debug:
2584                print('amounts', amounts)
2585            i = 0
2586            for x in amounts:
2587                if debug:
2588                    print(f'{i} - transfer-with-exchange({x})')
2589                self.transfer(x, c_SAR, a_SAR, f"{x} SAR -> a_SAR", debug=debug)
2590
2591                c_SAR_balance -= x
2592                cached_value = self.balance(c_SAR, cached=True)
2593                fresh_value = self.balance(c_SAR, cached=False)
2594                if debug:
2595                    print('account', c_SAR, 'cached_value', cached_value, 'fresh_value', fresh_value, 'excepted',
2596                          c_SAR_balance)
2597                assert cached_value == c_SAR_balance
2598                assert fresh_value == c_SAR_balance
2599
2600                a_SAR_balance += x
2601                cached_value = self.balance(a_SAR, cached=True)
2602                fresh_value = self.balance(a_SAR, cached=False)
2603                if debug:
2604                    print('account', a_SAR, 'cached_value', cached_value, 'fresh_value', fresh_value, 'expected',
2605                          a_SAR_balance)
2606                assert cached_value == a_SAR_balance
2607                assert fresh_value == a_SAR_balance
2608                i += 1
2609
2610            assert self.export_json("accounts-transfer-with-exchange-rates.json")
2611            assert self.save("accounts-transfer-with-exchange-rates.pickle")
2612
2613            # check & zakat with exchange rates for many cycles
2614
2615            for rate, values in {
2616                1: {
2617                    'in': [1000, 2000, 10000],
2618                    'exchanged': [1000, 2000, 10000],
2619                    'out': [25, 50, 731.40625],
2620                },
2621                3.75: {
2622                    'in': [200, 1000, 5000],
2623                    'exchanged': [750, 3750, 18750],
2624                    'out': [18.75, 93.75, 1371.38671875],
2625                },
2626            }.items():
2627                a, b, c = values['in']
2628                m, n, o = values['exchanged']
2629                x, y, z = values['out']
2630                if debug:
2631                    print('rate', rate, 'values', values)
2632                for case in [
2633                    (a, 'safe', ZakatTracker.time() - ZakatTracker.TimeCycle(), [
2634                        {'safe': {0: {'below_nisab': x}}},
2635                    ], False, m),
2636                    (b, 'safe', ZakatTracker.time() - ZakatTracker.TimeCycle(), [
2637                        {'safe': {0: {'count': 1, 'total': y}}},
2638                    ], True, n),
2639                    (c, 'cave', ZakatTracker.time() - (ZakatTracker.TimeCycle() * 3), [
2640                        {'cave': {0: {'count': 3, 'total': z}}},
2641                    ], True, o),
2642                ]:
2643                    if debug:
2644                        print(f"############# check(rate: {rate}) #############")
2645                    self.reset()
2646                    self.exchange(account=case[1], created=case[2], rate=rate)
2647                    self.track(value=case[0], desc='test-check', account=case[1], logging=True, created=case[2])
2648
2649                    # assert self.nolock()
2650                    # history_size = len(self._vault['history'])
2651                    # print('history_size', history_size)
2652                    # assert history_size == 2
2653                    assert self.lock()
2654                    assert not self.nolock()
2655                    report = self.check(2.17, None, debug)
2656                    (valid, brief, plan) = report
2657                    assert valid == case[4]
2658                    if debug:
2659                        print('brief', brief)
2660                    assert case[5] == brief[0]
2661                    assert case[5] == brief[1]
2662
2663                    if debug:
2664                        pp().pprint(plan)
2665
2666                    for x in plan:
2667                        assert case[1] == x
2668                        if 'total' in case[3][0][x][0].keys():
2669                            assert case[3][0][x][0]['total'] == brief[2]
2670                            assert plan[x][0]['total'] == case[3][0][x][0]['total']
2671                            assert plan[x][0]['count'] == case[3][0][x][0]['count']
2672                        else:
2673                            assert plan[x][0]['below_nisab'] == case[3][0][x][0]['below_nisab']
2674                    if debug:
2675                        pp().pprint(report)
2676                    result = self.zakat(report, debug=debug)
2677                    if debug:
2678                        print('zakat-result', result, case[4])
2679                    assert result == case[4]
2680                    report = self.check(2.17, None, debug)
2681                    (valid, brief, plan) = report
2682                    assert valid is False
2683
2684            history_size = len(self._vault['history'])
2685            if debug:
2686                print('history_size', history_size)
2687            assert history_size == 3
2688            assert not self.nolock()
2689            assert self.recall(False, debug) is False
2690            self.free(self.lock())
2691            assert self.nolock()
2692
2693            for i in range(3, 0, -1):
2694                history_size = len(self._vault['history'])
2695                if debug:
2696                    print('history_size', history_size)
2697                assert history_size == i
2698                assert self.recall(False, debug) is True
2699
2700            assert self.nolock()
2701            assert self.recall(False, debug) is False
2702
2703            history_size = len(self._vault['history'])
2704            if debug:
2705                print('history_size', history_size)
2706            assert history_size == 0
2707
2708            account_size = len(self._vault['account'])
2709            if debug:
2710                print('account_size', account_size)
2711            assert account_size == 0
2712
2713            report_size = len(self._vault['report'])
2714            if debug:
2715                print('report_size', report_size)
2716            assert report_size == 0
2717
2718            assert self.nolock()
2719            return True
2720        except:
2721            # pp().pprint(self._vault)
2722            assert self.export_json("test-snapshot.json")
2723            assert self.save("test-snapshot.pickle")
2724            raise
def test(debug: bool = False):
2727def test(debug: bool = False):
2728    ledger = ZakatTracker()
2729    start = ZakatTracker.time()
2730    assert ledger.test(debug=debug)
2731    if debug:
2732        print("#########################")
2733        print("######## TEST DONE ########")
2734        print("#########################")
2735        print(ZakatTracker.duration_from_nanoseconds(ZakatTracker.time() - start))
2736        print("#########################")
class Action(enum.Enum):
73class Action(Enum):
74    CREATE = auto()
75    TRACK = auto()
76    LOG = auto()
77    SUB = auto()
78    ADD_FILE = auto()
79    REMOVE_FILE = auto()
80    BOX_TRANSFER = auto()
81    EXCHANGE = auto()
82    REPORT = auto()
83    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):
86class JSONEncoder(json.JSONEncoder):
87    def default(self, obj):
88        if isinstance(obj, Action) or isinstance(obj, MathOperation):
89            return obj.name  # Serialize as the enum member's name
90        elif isinstance(obj, Decimal):
91            return float(obj)
92        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):
87    def default(self, obj):
88        if isinstance(obj, Action) or isinstance(obj, MathOperation):
89            return obj.name  # Serialize as the enum member's name
90        elif isinstance(obj, Decimal):
91            return float(obj)
92        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):
95class MathOperation(Enum):
96    ADDITION = auto()
97    EQUAL = auto()
98    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'>