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

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

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:
302    @staticmethod
303    def time(now: datetime = None) -> int:
304        """
305        Generates a timestamp based on the provided datetime object or the current datetime.
306
307        Parameters:
308        now (datetime, optional): The datetime object to generate the timestamp from.
309        If not provided, the current datetime is used.
310
311        Returns:
312        int: The timestamp in positive nanoseconds since the Unix epoch (January 1, 1970),
313            before 1970 will return in negative until 1000AD.
314        """
315        if now is None:
316            now = datetime.datetime.now()
317        ordinal_day = now.toordinal()
318        ns_in_day = (now - now.replace(hour=0, minute=0, second=0, microsecond=0)).total_seconds() * 10 ** 9
319        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'>:
321    @staticmethod
322    def time_to_datetime(ordinal_ns: int) -> datetime:
323        """
324        Converts an ordinal number (number of days since 1000-01-01) to a datetime object.
325
326        Parameters:
327        ordinal_ns (int): The ordinal number of days since 1000-01-01.
328
329        Returns:
330        datetime: The corresponding datetime object.
331        """
332        ordinal_day = ordinal_ns // 86_400_000_000_000 + 719_163
333        ns_in_day = ordinal_ns % 86_400_000_000_000
334        d = datetime.datetime.fromordinal(ordinal_day)
335        t = datetime.timedelta(seconds=ns_in_day // 10 ** 9)
336        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:
374    def nolock(self) -> bool:
375        """
376        Check if the vault lock is currently not set.
377
378        Returns:
379        bool: True if the vault lock is not set, False otherwise.
380        """
381        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:
383    def lock(self) -> int:
384        """
385        Acquires a lock on the ZakatTracker instance.
386
387        Returns:
388        int: The lock ID. This ID can be used to release the lock later.
389        """
390        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:
392    def vault(self) -> dict:
393        """
394        Returns a copy of the internal vault dictionary.
395
396        This method is used to retrieve the current state of the ZakatTracker object.
397        It provides a snapshot of the internal data structure, allowing for further
398        processing or analysis.
399
400        Returns:
401        dict: A copy of the internal vault dictionary.
402        """
403        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 steps(self) -> dict:
405    def steps(self) -> dict:
406        """
407        Returns a copy of the history of steps taken in the ZakatTracker.
408
409        The history is a dictionary where each key is a unique identifier for a step,
410        and the corresponding value is a dictionary containing information about the step.
411
412        Returns:
413        dict: A copy of the history of steps taken in the ZakatTracker.
414        """
415        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:
417    def free(self, lock: int, auto_save: bool = True) -> bool:
418        """
419        Releases the lock on the database.
420
421        Parameters:
422        lock (int): The lock ID to be released.
423        auto_save (bool): Whether to automatically save the database after releasing the lock.
424
425        Returns:
426        bool: True if the lock is successfully released and (optionally) saved, False otherwise.
427        """
428        if lock == self._vault['lock']:
429            self._vault['lock'] = None
430            if auto_save:
431                return self.save(self.path())
432            return True
433        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:
435    def account_exists(self, account) -> bool:
436        """
437        Check if the given account exists in the vault.
438
439        Parameters:
440        account (str): The account number to check.
441
442        Returns:
443        bool: True if the account exists, False otherwise.
444        """
445        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:
447    def box_size(self, account) -> int:
448        """
449        Calculate the size of the box for a specific account.
450
451        Parameters:
452        account (str): The account number for which the box size needs to be calculated.
453
454        Returns:
455        int: The size of the box for the given account. If the account does not exist, -1 is returned.
456        """
457        if self.account_exists(account):
458            return len(self._vault['account'][account]['box'])
459        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:
461    def log_size(self, account) -> int:
462        """
463        Get the size of the log for a specific account.
464
465        Parameters:
466        account (str): The account number for which the log size needs to be calculated.
467
468        Returns:
469        int: The size of the log for the given account. If the account does not exist, -1 is returned.
470        """
471        if self.account_exists(account):
472            return len(self._vault['account'][account]['log'])
473        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:
475    def recall(self, dry=True, debug=False) -> bool:
476        """
477        Revert the last operation.
478
479        Parameters:
480        dry (bool): If True, the function will not modify the data, but will simulate the operation. Default is True.
481        debug (bool): If True, the function will print debug information. Default is False.
482
483        Returns:
484        bool: True if the operation was successful, False otherwise.
485        """
486        if not self.nolock() or len(self._vault['history']) == 0:
487            return False
488        if len(self._vault['history']) <= 0:
489            return False
490        ref = sorted(self._vault['history'].keys())[-1]
491        if debug:
492            print('recall', ref)
493        memory = self._vault['history'][ref]
494        if debug:
495            print(type(memory), 'memory', memory)
496
497        limit = len(memory) + 1
498        sub_positive_log_negative = 0
499        for i in range(-1, -limit, -1):
500            x = memory[i]
501            if debug:
502                print(type(x), x)
503            match x['action']:
504                case Action.CREATE:
505                    if x['account'] is not None:
506                        if self.account_exists(x['account']):
507                            if debug:
508                                print('account', self._vault['account'][x['account']])
509                            assert len(self._vault['account'][x['account']]['box']) == 0
510                            assert self._vault['account'][x['account']]['balance'] == 0
511                            assert self._vault['account'][x['account']]['count'] == 0
512                            if dry:
513                                continue
514                            del self._vault['account'][x['account']]
515
516                case Action.TRACK:
517                    if x['account'] is not None:
518                        if self.account_exists(x['account']):
519                            if dry:
520                                continue
521                            self._vault['account'][x['account']]['balance'] -= x['value']
522                            self._vault['account'][x['account']]['count'] -= 1
523                            del self._vault['account'][x['account']]['box'][x['ref']]
524
525                case Action.LOG:
526                    if x['account'] is not None:
527                        if self.account_exists(x['account']):
528                            if x['ref'] in self._vault['account'][x['account']]['log']:
529                                if dry:
530                                    continue
531                                if sub_positive_log_negative == -x['value']:
532                                    self._vault['account'][x['account']]['count'] -= 1
533                                    sub_positive_log_negative = 0
534                                box_ref = self._vault['account'][x['account']]['log'][x['ref']]['ref']
535                                if not box_ref is None:
536                                    assert self.box_exists(x['account'], box_ref)
537                                    box_value = self._vault['account'][x['account']]['log'][x['ref']]['value']
538                                    assert box_value < 0
539                                    self._vault['account'][x['account']]['box'][box_ref]['rest'] += -box_value
540                                    self._vault['account'][x['account']]['balance'] += -box_value
541                                    self._vault['account'][x['account']]['count'] -= 1
542                                del self._vault['account'][x['account']]['log'][x['ref']]
543
544                case Action.SUB:
545                    if x['account'] is not None:
546                        if self.account_exists(x['account']):
547                            if x['ref'] in self._vault['account'][x['account']]['box']:
548                                if dry:
549                                    continue
550                                self._vault['account'][x['account']]['box'][x['ref']]['rest'] += x['value']
551                                self._vault['account'][x['account']]['balance'] += x['value']
552                                sub_positive_log_negative = x['value']
553
554                case Action.ADD_FILE:
555                    if x['account'] is not None:
556                        if self.account_exists(x['account']):
557                            if x['ref'] in self._vault['account'][x['account']]['log']:
558                                if x['file'] in self._vault['account'][x['account']]['log'][x['ref']]['file']:
559                                    if dry:
560                                        continue
561                                    del self._vault['account'][x['account']]['log'][x['ref']]['file'][x['file']]
562
563                case Action.REMOVE_FILE:
564                    if x['account'] is not None:
565                        if self.account_exists(x['account']):
566                            if x['ref'] in self._vault['account'][x['account']]['log']:
567                                if dry:
568                                    continue
569                                self._vault['account'][x['account']]['log'][x['ref']]['file'][x['file']] = x['value']
570
571                case Action.BOX_TRANSFER:
572                    if x['account'] is not None:
573                        if self.account_exists(x['account']):
574                            if x['ref'] in self._vault['account'][x['account']]['box']:
575                                if dry:
576                                    continue
577                                self._vault['account'][x['account']]['box'][x['ref']]['rest'] -= x['value']
578
579                case Action.EXCHANGE:
580                    if x['account'] is not None:
581                        if x['account'] in self._vault['exchange']:
582                            if x['ref'] in self._vault['exchange'][x['account']]:
583                                if dry:
584                                    continue
585                                del self._vault['exchange'][x['account']][x['ref']]
586
587                case Action.REPORT:
588                    if x['ref'] in self._vault['report']:
589                        if dry:
590                            continue
591                        del self._vault['report'][x['ref']]
592
593                case Action.ZAKAT:
594                    if x['account'] is not None:
595                        if self.account_exists(x['account']):
596                            if x['ref'] in self._vault['account'][x['account']]['box']:
597                                if x['key'] in self._vault['account'][x['account']]['box'][x['ref']]:
598                                    if dry:
599                                        continue
600                                    match x['math']:
601                                        case MathOperation.ADDITION:
602                                            self._vault['account'][x['account']]['box'][x['ref']][x['key']] -= x[
603                                                'value']
604                                        case MathOperation.EQUAL:
605                                            self._vault['account'][x['account']]['box'][x['ref']][x['key']] = x['value']
606                                        case MathOperation.SUBTRACTION:
607                                            self._vault['account'][x['account']]['box'][x['ref']][x['key']] += x[
608                                                'value']
609
610        if not dry:
611            del self._vault['history'][ref]
612        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:
614    def ref_exists(self, account: str, ref_type: str, ref: int) -> bool:
615        """
616        Check if a specific reference (transaction) exists in the vault for a given account and reference type.
617
618        Parameters:
619        account (str): The account number for which to check the existence of the reference.
620        ref_type (str): The type of reference (e.g., 'box', 'log', etc.).
621        ref (int): The reference (transaction) number to check for existence.
622
623        Returns:
624        bool: True if the reference exists for the given account and reference type, False otherwise.
625        """
626        if account in self._vault['account']:
627            return ref in self._vault['account'][account][ref_type]
628        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:
630    def box_exists(self, account: str, ref: int) -> bool:
631        """
632        Check if a specific box (transaction) exists in the vault for a given account and reference.
633
634        Parameters:
635        - account (str): The account number for which to check the existence of the box.
636        - ref (int): The reference (transaction) number to check for existence.
637
638        Returns:
639        - bool: True if the box exists for the given account and reference, False otherwise.
640        """
641        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:
643    def track(self, value: float = 0, desc: str = '', account: str = 1, logging: bool = True, created: int = None,
644              debug: bool = False) -> int:
645        """
646        This function tracks a transaction for a specific account.
647
648        Parameters:
649        value (float): The value of the transaction. Default is 0.
650        desc (str): The description of the transaction. Default is an empty string.
651        account (str): The account for which the transaction is being tracked. Default is '1'.
652        logging (bool): Whether to log the transaction. Default is True.
653        created (int): The timestamp of the transaction. If not provided, it will be generated. Default is None.
654        debug (bool): Whether to print debug information. Default is False.
655
656        Returns:
657        int: The timestamp of the transaction.
658
659        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.
660
661        Raises:
662        ValueError: The log transaction happened again in the same nanosecond time.
663        ValueError: The box transaction happened again in the same nanosecond time.
664        """
665        if debug:
666            print('track', f'debug={debug}')
667        if created is None:
668            created = self.time()
669        no_lock = self.nolock()
670        self.lock()
671        if not self.account_exists(account):
672            if debug:
673                print(f"account {account} created")
674            self._vault['account'][account] = {
675                'balance': 0,
676                'box': {},
677                'count': 0,
678                'log': {},
679                'hide': False,
680                'zakatable': True,
681            }
682            self._step(Action.CREATE, account)
683        if value == 0:
684            if no_lock:
685                self.free(self.lock())
686            return 0
687        if logging:
688            self._log(value=value, desc=desc, account=account, created=created, ref=None, debug=debug)
689        if debug:
690            print('create-box', created)
691        if self.box_exists(account, created):
692            raise ValueError(f"The box transaction happened again in the same nanosecond time({created}).")
693        if debug:
694            print('created-box', created)
695        self._vault['account'][account]['box'][created] = {
696            'capital': value,
697            'count': 0,
698            'last': 0,
699            'rest': value,
700            'total': 0,
701        }
702        self._step(Action.TRACK, account, ref=created, value=value)
703        if no_lock:
704            self.free(self.lock())
705        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:
707    def log_exists(self, account: str, ref: int) -> bool:
708        """
709        Checks if a specific transaction log entry exists for a given account.
710
711        Parameters:
712        account (str): The account number associated with the transaction log.
713        ref (int): The reference to the transaction log entry.
714
715        Returns:
716        bool: True if the transaction log entry exists, False otherwise.
717        """
718        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:
763    def exchange(self, account, created: int = None, rate: float = None, description: str = None,
764                 debug: bool = False) -> dict:
765        """
766        This method is used to record or retrieve exchange rates for a specific account.
767
768        Parameters:
769        - account (str): The account number for which the exchange rate is being recorded or retrieved.
770        - created (int): The timestamp of the exchange rate. If not provided, the current timestamp will be used.
771        - rate (float): The exchange rate to be recorded. If not provided, the method will retrieve the latest exchange rate.
772        - description (str): A description of the exchange rate.
773
774        Returns:
775        - dict: A dictionary containing the latest exchange rate and its description. If no exchange rate is found,
776        it returns a dictionary with default values for the rate and description.
777        """
778        if debug:
779            print('exchange', f'debug={debug}')
780        if created is None:
781            created = self.time()
782        no_lock = self.nolock()
783        self.lock()
784        if rate is not None:
785            if rate <= 0:
786                return dict()
787            if account not in self._vault['exchange']:
788                self._vault['exchange'][account] = {}
789            if len(self._vault['exchange'][account]) == 0 and rate <= 1:
790                return {"time": created, "rate": 1, "description": None}
791            self._vault['exchange'][account][created] = {"rate": rate, "description": description}
792            self._step(Action.EXCHANGE, account, ref=created, value=rate)
793            if no_lock:
794                self.free(self.lock())
795            if debug:
796                print("exchange-created-1",
797                      f'account: {account}, created: {created}, rate:{rate}, description:{description}')
798
799        if account in self._vault['exchange']:
800            valid_rates = [(ts, r) for ts, r in self._vault['exchange'][account].items() if ts <= created]
801            if valid_rates:
802                latest_rate = max(valid_rates, key=lambda x: x[0])
803                if debug:
804                    print("exchange-read-1",
805                          f'account: {account}, created: {created}, rate:{rate}, description:{description}',
806                          'latest_rate', latest_rate)
807                result = latest_rate[1]
808                result['time'] = latest_rate[0]
809                return result  # إرجاع قاموس يحتوي على المعدل والوصف
810        if debug:
811            print("exchange-read-0", f'account: {account}, created: {created}, rate:{rate}, description:{description}')
812        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:
814    @staticmethod
815    def exchange_calc(x: float, x_rate: float, y_rate: float) -> float:
816        """
817        This function calculates the exchanged amount of a currency.
818
819        Args:
820            x (float): The original amount of the currency.
821            x_rate (float): The exchange rate of the original currency.
822            y_rate (float): The exchange rate of the target currency.
823
824        Returns:
825            float: The exchanged amount of the target currency.
826        """
827        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:
829    def exchanges(self) -> dict:
830        """
831        Retrieve the recorded exchange rates for all accounts.
832
833        Parameters:
834        None
835
836        Returns:
837        dict: A dictionary containing all recorded exchange rates.
838        The keys are account names or numbers, and the values are dictionaries containing the exchange rates.
839        Each exchange rate dictionary has timestamps as keys and exchange rate details as values.
840        """
841        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:
843    def accounts(self) -> dict:
844        """
845        Returns a dictionary containing account numbers as keys and their respective balances as values.
846
847        Parameters:
848        None
849
850        Returns:
851        dict: A dictionary where keys are account numbers and values are their respective balances.
852        """
853        result = {}
854        for i in self._vault['account']:
855            result[i] = self._vault['account'][i]['balance']
856        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:
858    def boxes(self, account) -> dict:
859        """
860        Retrieve the boxes (transactions) associated with a specific account.
861
862        Parameters:
863        account (str): The account number for which to retrieve the boxes.
864
865        Returns:
866        dict: A dictionary containing the boxes associated with the given account.
867        If the account does not exist, an empty dictionary is returned.
868        """
869        if self.account_exists(account):
870            return self._vault['account'][account]['box']
871        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:
873    def logs(self, account) -> dict:
874        """
875        Retrieve the logs (transactions) associated with a specific account.
876
877        Parameters:
878        account (str): The account number for which to retrieve the logs.
879
880        Returns:
881        dict: A dictionary containing the logs associated with the given account.
882        If the account does not exist, an empty dictionary is returned.
883        """
884        if self.account_exists(account):
885            return self._vault['account'][account]['log']
886        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:
888    def add_file(self, account: str, ref: int, path: str) -> int:
889        """
890        Adds a file reference to a specific transaction log entry in the vault.
891
892        Parameters:
893        account (str): The account number associated with the transaction log.
894        ref (int): The reference to the transaction log entry.
895        path (str): The path of the file to be added.
896
897        Returns:
898        int: The reference of the added file. If the account or transaction log entry does not exist, returns 0.
899        """
900        if self.account_exists(account):
901            if ref in self._vault['account'][account]['log']:
902                file_ref = self.time()
903                self._vault['account'][account]['log'][ref]['file'][file_ref] = path
904                no_lock = self.nolock()
905                self.lock()
906                self._step(Action.ADD_FILE, account, ref=ref, file=file_ref)
907                if no_lock:
908                    self.free(self.lock())
909                return file_ref
910        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:
912    def remove_file(self, account: str, ref: int, file_ref: int) -> bool:
913        """
914        Removes a file reference from a specific transaction log entry in the vault.
915
916        Parameters:
917        account (str): The account number associated with the transaction log.
918        ref (int): The reference to the transaction log entry.
919        file_ref (int): The reference of the file to be removed.
920
921        Returns:
922        bool: True if the file reference is successfully removed, False otherwise.
923        """
924        if self.account_exists(account):
925            if ref in self._vault['account'][account]['log']:
926                if file_ref in self._vault['account'][account]['log'][ref]['file']:
927                    x = self._vault['account'][account]['log'][ref]['file'][file_ref]
928                    del self._vault['account'][account]['log'][ref]['file'][file_ref]
929                    no_lock = self.nolock()
930                    self.lock()
931                    self._step(Action.REMOVE_FILE, account, ref=ref, file=file_ref, value=x)
932                    if no_lock:
933                        self.free(self.lock())
934                    return True
935        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:
937    def balance(self, account: str = 1, cached: bool = True) -> int:
938        """
939        Calculate and return the balance of a specific account.
940
941        Parameters:
942        account (str): The account number. Default is '1'.
943        cached (bool): If True, use the cached balance. If False, calculate the balance from the box. Default is True.
944
945        Returns:
946        int: The balance of the account.
947
948        Note:
949        If cached is True, the function returns the cached balance.
950        If cached is False, the function calculates the balance from the box by summing up the 'rest' values of all box items.
951        """
952        if cached:
953            return self._vault['account'][account]['balance']
954        x = 0
955        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:
957    def hide(self, account, status: bool = None) -> bool:
958        """
959        Check or set the hide status of a specific account.
960
961        Parameters:
962        account (str): The account number.
963        status (bool, optional): The new hide status. If not provided, the function will return the current status.
964
965        Returns:
966        bool: The current or updated hide status of the account.
967
968        Raises:
969        None
970
971        Example:
972        >>> tracker = ZakatTracker()
973        >>> ref = tracker.track(51, 'desc', 'account1')
974        >>> tracker.hide('account1')  # Set the hide status of 'account1' to True
975        False
976        >>> tracker.hide('account1', True)  # Set the hide status of 'account1' to True
977        True
978        >>> tracker.hide('account1')  # Get the hide status of 'account1' by default
979        True
980        >>> tracker.hide('account1', False)
981        False
982        """
983        if self.account_exists(account):
984            if status is None:
985                return self._vault['account'][account]['hide']
986            self._vault['account'][account]['hide'] = status
987            return status
988        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:
 990    def zakatable(self, account, status: bool = None) -> bool:
 991        """
 992        Check or set the zakatable status of a specific account.
 993
 994        Parameters:
 995        account (str): The account number.
 996        status (bool, optional): The new zakatable status. If not provided, the function will return the current status.
 997
 998        Returns:
 999        bool: The current or updated zakatable status of the account.
1000
1001        Raises:
1002        None
1003
1004        Example:
1005        >>> tracker = ZakatTracker()
1006        >>> ref = tracker.track(51, 'desc', 'account1')
1007        >>> tracker.zakatable('account1')  # Set the zakatable status of 'account1' to True
1008        True
1009        >>> tracker.zakatable('account1', True)  # Set the zakatable status of 'account1' to True
1010        True
1011        >>> tracker.zakatable('account1')  # Get the zakatable status of 'account1' by default
1012        True
1013        >>> tracker.zakatable('account1', False)
1014        False
1015        """
1016        if self.account_exists(account):
1017            if status is None:
1018                return self._vault['account'][account]['zakatable']
1019            self._vault['account'][account]['zakatable'] = status
1020            return status
1021        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:
1023    def sub(self, x: float, desc: str = '', account: str = 1, created: int = None, debug: bool = False) -> tuple:
1024        """
1025        Subtracts a specified value from an account's balance.
1026
1027        Parameters:
1028        x (float): The amount to be subtracted.
1029        desc (str): A description for the transaction. Defaults to an empty string.
1030        account (str): The account from which the value will be subtracted. Defaults to '1'.
1031        created (int): The timestamp of the transaction. If not provided, the current timestamp will be used.
1032        debug (bool): A flag indicating whether to print debug information. Defaults to False.
1033
1034        Returns:
1035        tuple: A tuple containing the timestamp of the transaction and a list of tuples representing the age of each transaction.
1036
1037        If the amount to subtract is greater than the account's balance,
1038        the remaining amount will be transferred to a new transaction with a negative value.
1039
1040        Raises:
1041        ValueError: The box transaction happened again in the same nanosecond time.
1042        ValueError: The log transaction happened again in the same nanosecond time.
1043        """
1044        if debug:
1045            print('sub', f'debug={debug}')
1046        if x < 0:
1047            return tuple()
1048        if x == 0:
1049            ref = self.track(x, '', account)
1050            return ref, ref
1051        if created is None:
1052            created = self.time()
1053        no_lock = self.nolock()
1054        self.lock()
1055        self.track(0, '', account)
1056        self._log(value=-x, desc=desc, account=account, created=created, ref=None, debug=debug)
1057        ids = sorted(self._vault['account'][account]['box'].keys())
1058        limit = len(ids) + 1
1059        target = x
1060        if debug:
1061            print('ids', ids)
1062        ages = []
1063        for i in range(-1, -limit, -1):
1064            if target == 0:
1065                break
1066            j = ids[i]
1067            if debug:
1068                print('i', i, 'j', j)
1069            rest = self._vault['account'][account]['box'][j]['rest']
1070            if rest >= target:
1071                self._vault['account'][account]['box'][j]['rest'] -= target
1072                self._step(Action.SUB, account, ref=j, value=target)
1073                ages.append((j, target))
1074                target = 0
1075                break
1076            elif target > rest > 0:
1077                chunk = rest
1078                target -= chunk
1079                self._step(Action.SUB, account, ref=j, value=chunk)
1080                ages.append((j, chunk))
1081                self._vault['account'][account]['box'][j]['rest'] = 0
1082        if target > 0:
1083            self.track(-target, desc, account, False, created)
1084            ages.append((created, target))
1085        if no_lock:
1086            self.free(self.lock())
1087        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]:
1089    def transfer(self, amount: int, from_account: str, to_account: str, desc: str = '', created: int = None,
1090                 debug: bool = False) -> list[int]:
1091        """
1092        Transfers a specified value from one account to another.
1093
1094        Parameters:
1095        amount (int): The amount to be transferred.
1096        from_account (str): The account from which the value will be transferred.
1097        to_account (str): The account to which the value will be transferred.
1098        desc (str, optional): A description for the transaction. Defaults to an empty string.
1099        created (int, optional): The timestamp of the transaction. If not provided, the current timestamp will be used.
1100        debug (bool): A flag indicating whether to print debug information. Defaults to False.
1101
1102        Returns:
1103        list[int]: A list of timestamps corresponding to the transactions made during the transfer.
1104
1105        Raises:
1106        ValueError: Transfer to the same account is forbidden.
1107        ValueError: The box transaction happened again in the same nanosecond time.
1108        ValueError: The log transaction happened again in the same nanosecond time.
1109        """
1110        if debug:
1111            print('transfer', f'debug={debug}')
1112        if from_account == to_account:
1113            raise ValueError(f'Transfer to the same account is forbidden. {to_account}')
1114        if amount <= 0:
1115            return []
1116        if created is None:
1117            created = self.time()
1118        (_, ages) = self.sub(amount, desc, from_account, created, debug=debug)
1119        times = []
1120        source_exchange = self.exchange(from_account, created)
1121        target_exchange = self.exchange(to_account, created)
1122
1123        if debug:
1124            print('ages', ages)
1125
1126        for age, value in ages:
1127            target_amount = self.exchange_calc(value, source_exchange['rate'], target_exchange['rate'])
1128            # Perform the transfer
1129            if self.box_exists(to_account, age):
1130                if debug:
1131                    print('box_exists', age)
1132                capital = self._vault['account'][to_account]['box'][age]['capital']
1133                rest = self._vault['account'][to_account]['box'][age]['rest']
1134                if debug:
1135                    print(
1136                        f"Transfer {value} from `{from_account}` to `{to_account}` (equivalent to {target_amount} `{to_account}`).")
1137                selected_age = age
1138                if rest + target_amount > capital:
1139                    self._vault['account'][to_account]['box'][age]['capital'] += target_amount
1140                    selected_age = ZakatTracker.time()
1141                self._vault['account'][to_account]['box'][age]['rest'] += target_amount
1142                self._step(Action.BOX_TRANSFER, to_account, ref=selected_age, value=target_amount)
1143                y = self._log(value=target_amount, desc=f'TRANSFER {from_account} -> {to_account}', account=to_account,
1144                              created=None, ref=None, debug=debug)
1145                times.append((age, y))
1146                continue
1147            y = self.track(target_amount, desc, to_account, logging=True, created=age, debug=debug)
1148            if debug:
1149                print(
1150                    f"Transferred {value} from `{from_account}` to `{to_account}` (equivalent to {target_amount} `{to_account}`).")
1151            times.append(y)
1152        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:
1154    def check(self, silver_gram_price: float, nisab: float = None, debug: bool = False, now: int = None,
1155              cycle: float = None) -> tuple:
1156        """
1157        Check the eligibility for Zakat based on the given parameters.
1158
1159        Parameters:
1160        silver_gram_price (float): The price of a gram of silver.
1161        nisab (float): The minimum amount of wealth required for Zakat. If not provided,
1162                        it will be calculated based on the silver_gram_price.
1163        debug (bool): Flag to enable debug mode.
1164        now (int): The current timestamp. If not provided, it will be calculated using ZakatTracker.time().
1165        cycle (float): The time cycle for Zakat. If not provided, it will be calculated using ZakatTracker.TimeCycle().
1166
1167        Returns:
1168        tuple: A tuple containing a boolean indicating the eligibility for Zakat, a list of brief statistics,
1169        and a dictionary containing the Zakat plan.
1170        """
1171        if debug:
1172            print('check', f'debug={debug}')
1173        if now is None:
1174            now = self.time()
1175        if cycle is None:
1176            cycle = ZakatTracker.TimeCycle()
1177        if nisab is None:
1178            nisab = ZakatTracker.Nisab(silver_gram_price)
1179        plan = {}
1180        below_nisab = 0
1181        brief = [0, 0, 0]
1182        valid = False
1183        for x in self._vault['account']:
1184            if not self.zakatable(x):
1185                continue
1186            _box = self._vault['account'][x]['box']
1187            _log = self._vault['account'][x]['log']
1188            limit = len(_box) + 1
1189            ids = sorted(self._vault['account'][x]['box'].keys())
1190            for i in range(-1, -limit, -1):
1191                j = ids[i]
1192                rest = float(_box[j]['rest'])
1193                if rest <= 0:
1194                    continue
1195                exchange = self.exchange(x, created=self.time())
1196                if debug:
1197                    print('exchanges', self.exchanges())
1198                rest = ZakatTracker.exchange_calc(rest, exchange['rate'], 1)
1199                brief[0] += rest
1200                index = limit + i - 1
1201                epoch = (now - j) / cycle
1202                if debug:
1203                    print(f"Epoch: {epoch}", _box[j])
1204                if _box[j]['last'] > 0:
1205                    epoch = (now - _box[j]['last']) / cycle
1206                if debug:
1207                    print(f"Epoch: {epoch}")
1208                epoch = floor(epoch)
1209                if debug:
1210                    print(f"Epoch: {epoch}", type(epoch), epoch == 0, 1 - epoch, epoch)
1211                if epoch == 0:
1212                    continue
1213                if debug:
1214                    print("Epoch - PASSED")
1215                brief[1] += rest
1216                if rest >= nisab:
1217                    total = 0
1218                    for _ in range(epoch):
1219                        total += ZakatTracker.ZakatCut(float(rest) - float(total))
1220                    if total > 0:
1221                        if x not in plan:
1222                            plan[x] = {}
1223                        valid = True
1224                        brief[2] += total
1225                        plan[x][index] = {
1226                            'total': total,
1227                            'count': epoch,
1228                            'box_time': j,
1229                            'box_capital': _box[j]['capital'],
1230                            'box_rest': _box[j]['rest'],
1231                            'box_last': _box[j]['last'],
1232                            'box_total': _box[j]['total'],
1233                            'box_count': _box[j]['count'],
1234                            'box_log': _log[j]['desc'],
1235                            'exchange_rate': exchange['rate'],
1236                            'exchange_time': exchange['time'],
1237                            'exchange_desc': exchange['description'],
1238                        }
1239                else:
1240                    chunk = ZakatTracker.ZakatCut(float(rest))
1241                    if chunk > 0:
1242                        if x not in plan:
1243                            plan[x] = {}
1244                        if j not in plan[x].keys():
1245                            plan[x][index] = {}
1246                        below_nisab += rest
1247                        brief[2] += chunk
1248                        plan[x][index]['below_nisab'] = chunk
1249                        plan[x][index]['total'] = chunk
1250                        plan[x][index]['count'] = epoch
1251                        plan[x][index]['box_time'] = j
1252                        plan[x][index]['box_capital'] = _box[j]['capital']
1253                        plan[x][index]['box_rest'] = _box[j]['rest']
1254                        plan[x][index]['box_last'] = _box[j]['last']
1255                        plan[x][index]['box_total'] = _box[j]['total']
1256                        plan[x][index]['box_count'] = _box[j]['count']
1257                        plan[x][index]['box_log'] = _log[j]['desc']
1258                        plan[x][index]['exchange_rate'] = exchange['rate']
1259                        plan[x][index]['exchange_time'] = exchange['time']
1260                        plan[x][index]['exchange_desc'] = exchange['description']
1261        valid = valid or below_nisab >= nisab
1262        if debug:
1263            print(f"below_nisab({below_nisab}) >= nisab({nisab})")
1264        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:
1266    def build_payment_parts(self, demand: float, positive_only: bool = True) -> dict:
1267        """
1268        Build payment parts for the Zakat distribution.
1269
1270        Parameters:
1271        demand (float): The total demand for payment in local currency.
1272        positive_only (bool): If True, only consider accounts with positive balance. Default is True.
1273
1274        Returns:
1275        dict: A dictionary containing the payment parts for each account. The dictionary has the following structure:
1276        {
1277            'account': {
1278                'account_id': {'balance': float, 'rate': float, 'part': float},
1279                ...
1280            },
1281            'exceed': bool,
1282            'demand': float,
1283            'total': float,
1284        }
1285        """
1286        total = 0
1287        parts = {
1288            'account': {},
1289            'exceed': False,
1290            'demand': demand,
1291        }
1292        for x, y in self.accounts().items():
1293            if positive_only and y <= 0:
1294                continue
1295            total += y
1296            exchange = self.exchange(x)
1297            parts['account'][x] = {'balance': y, 'rate': exchange['rate'], 'part': 0}
1298        parts['total'] = total
1299        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:
1301    @staticmethod
1302    def check_payment_parts(parts: dict, debug: bool = False) -> int:
1303        """
1304        Checks the validity of payment parts.
1305
1306        Parameters:
1307        parts (dict): A dictionary containing payment parts information.
1308        debug (bool): Flag to enable debug mode.
1309
1310        Returns:
1311        int: Returns 0 if the payment parts are valid, otherwise returns the error code.
1312
1313        Error Codes:
1314        1: 'demand', 'account', 'total', or 'exceed' key is missing in parts.
1315        2: 'balance', 'rate' or 'part' key is missing in parts['account'][x].
1316        3: 'part' value in parts['account'][x] is less than 0.
1317        4: If 'exceed' is False, 'balance' value in parts['account'][x] is less than or equal to 0.
1318        5: If 'exceed' is False, 'part' value in parts['account'][x] is greater than 'balance' value.
1319        6: The sum of 'part' values in parts['account'] does not match with 'demand' value.
1320        """
1321        if debug:
1322            print('check_payment_parts', f'debug={debug}')
1323        for i in ['demand', 'account', 'total', 'exceed']:
1324            if i not in parts:
1325                return 1
1326        exceed = parts['exceed']
1327        for x in parts['account']:
1328            for j in ['balance', 'rate', 'part']:
1329                if j not in parts['account'][x]:
1330                    return 2
1331                if parts['account'][x]['part'] < 0:
1332                    return 3
1333                if not exceed and parts['account'][x]['balance'] <= 0:
1334                    return 4
1335        demand = parts['demand']
1336        z = 0
1337        for _, y in parts['account'].items():
1338            if not exceed and y['part'] > y['balance']:
1339                return 5
1340            z += ZakatTracker.exchange_calc(y['part'], y['rate'], 1)
1341        z = round(z, 2)
1342        demand = round(demand, 2)
1343        if debug:
1344            print('check_payment_parts', f'z = {z}, demand = {demand}')
1345            print('check_payment_parts', type(z), type(demand))
1346            print('check_payment_parts', z != demand)
1347            print('check_payment_parts', str(z) != str(demand))
1348        if z != demand and str(z) != str(demand):
1349            return 6
1350        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:
1352    def zakat(self, report: tuple, parts: Dict[str, Dict | bool | Any] = None, debug: bool = False) -> bool:
1353        """
1354        Perform Zakat calculation based on the given report and optional parts.
1355
1356        Parameters:
1357        report (tuple): A tuple containing the validity of the report, the report data, and the zakat plan.
1358        parts (dict): A dictionary containing the payment parts for the zakat.
1359        debug (bool): A flag indicating whether to print debug information.
1360
1361        Returns:
1362        bool: True if the zakat calculation is successful, False otherwise.
1363        """
1364        if debug:
1365            print('zakat', f'debug={debug}')
1366        valid, _, plan = report
1367        if not valid:
1368            return valid
1369        parts_exist = parts is not None
1370        if parts_exist:
1371            if self.check_payment_parts(parts, debug=debug) != 0:
1372                return False
1373        if debug:
1374            print('######### zakat #######')
1375            print('parts_exist', parts_exist)
1376        no_lock = self.nolock()
1377        self.lock()
1378        report_time = self.time()
1379        self._vault['report'][report_time] = report
1380        self._step(Action.REPORT, ref=report_time)
1381        created = self.time()
1382        for x in plan:
1383            target_exchange = self.exchange(x)
1384            if debug:
1385                print(plan[x])
1386                print('-------------')
1387                print(self._vault['account'][x]['box'])
1388            ids = sorted(self._vault['account'][x]['box'].keys())
1389            if debug:
1390                print('plan[x]', plan[x])
1391            for i in plan[x].keys():
1392                j = ids[i]
1393                if debug:
1394                    print('i', i, 'j', j)
1395                self._step(Action.ZAKAT, account=x, ref=j, value=self._vault['account'][x]['box'][j]['last'],
1396                           key='last',
1397                           math_operation=MathOperation.EQUAL)
1398                self._vault['account'][x]['box'][j]['last'] = created
1399                amount = ZakatTracker.exchange_calc(float(plan[x][i]['total']), 1, float(target_exchange['rate']))
1400                self._vault['account'][x]['box'][j]['total'] += amount
1401                self._step(Action.ZAKAT, account=x, ref=j, value=amount, key='total',
1402                           math_operation=MathOperation.ADDITION)
1403                self._vault['account'][x]['box'][j]['count'] += plan[x][i]['count']
1404                self._step(Action.ZAKAT, account=x, ref=j, value=plan[x][i]['count'], key='count',
1405                           math_operation=MathOperation.ADDITION)
1406                if not parts_exist:
1407                    try:
1408                        self._vault['account'][x]['box'][j]['rest'] -= amount
1409                    except TypeError:
1410                        self._vault['account'][x]['box'][j]['rest'] -= Decimal(amount)
1411                    # self._step(Action.ZAKAT, account=x, ref=j, value=amount, key='rest',
1412                    #            math_operation=MathOperation.SUBTRACTION)
1413                    self._log(-float(amount), desc='zakat-زكاة', account=x, created=None, ref=j, debug=debug)
1414        if parts_exist:
1415            for account, part in parts['account'].items():
1416                if part['part'] == 0:
1417                    continue
1418                if debug:
1419                    print('zakat-part', account, part['rate'])
1420                target_exchange = self.exchange(account)
1421                amount = ZakatTracker.exchange_calc(part['part'], part['rate'], target_exchange['rate'])
1422                self.sub(amount, desc='zakat-part-دفعة-زكاة', account=account, debug=debug)
1423        if no_lock:
1424            self.free(self.lock())
1425        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:
1427    def export_json(self, path: str = "data.json") -> bool:
1428        """
1429        Exports the current state of the ZakatTracker object to a JSON file.
1430
1431        Parameters:
1432        path (str): The path where the JSON file will be saved. Default is "data.json".
1433
1434        Returns:
1435        bool: True if the export is successful, False otherwise.
1436
1437        Raises:
1438        No specific exceptions are raised by this method.
1439        """
1440        with open(path, "w") as file:
1441            json.dump(self._vault, file, indent=4, cls=JSONEncoder)
1442            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:
1444    def save(self, path: str = None) -> bool:
1445        """
1446        Saves the ZakatTracker's current state to a pickle file.
1447
1448        This method serializes the internal data (`_vault`) along with metadata
1449        (Python version, pickle protocol) for future compatibility.
1450
1451        Parameters:
1452        path (str, optional): File path for saving. Defaults to a predefined location.
1453
1454        Returns:
1455        bool: True if the save operation is successful, False otherwise.
1456        """
1457        if path is None:
1458            path = self.path()
1459        with open(path, "wb") as f:
1460            version = f'{version_info.major}.{version_info.minor}.{version_info.micro}'
1461            pickle_protocol = pickle.HIGHEST_PROTOCOL
1462            data = {
1463                'python_version': version,
1464                'pickle_protocol': pickle_protocol,
1465                'data': self._vault,
1466            }
1467            pickle.dump(data, f, protocol=pickle_protocol)
1468            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:
1470    def load(self, path: str = None) -> bool:
1471        """
1472        Load the current state of the ZakatTracker object from a pickle file.
1473
1474        Parameters:
1475        path (str): The path where the pickle file is located. If not provided, it will use the default path.
1476
1477        Returns:
1478        bool: True if the load operation is successful, False otherwise.
1479        """
1480        if path is None:
1481            path = self.path()
1482        if os.path.exists(path):
1483            with open(path, "rb") as f:
1484                data = pickle.load(f)
1485                self._vault = data['data']
1486                return True
1487        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):
1489    def import_csv_cache_path(self):
1490        """
1491        Generates the cache file path for imported CSV data.
1492
1493        This function constructs the file path where cached data from CSV imports
1494        will be stored. The cache file is a pickle file (.pickle extension) appended
1495        to the base path of the object.
1496
1497        Returns:
1498        str: The full path to the import CSV cache file.
1499
1500        Example:
1501            >>> obj = ZakatTracker('/data/reports')
1502            >>> obj.import_csv_cache_path()
1503            '/data/reports.import_csv.pickle'
1504        """
1505        path = self.path()
1506        if path.endswith(".pickle"):
1507            path = path[:-7]
1508        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:
1510    def import_csv(self, path: str = 'file.csv', debug: bool = False) -> tuple:
1511        """
1512        The function reads the CSV file, checks for duplicate transactions, and creates the transactions in the system.
1513
1514        Parameters:
1515        path (str): The path to the CSV file. Default is 'file.csv'.
1516        debug (bool): A flag indicating whether to print debug information.
1517
1518        Returns:
1519        tuple: A tuple containing the number of transactions created, the number of transactions found in the cache,
1520                and a dictionary of bad transactions.
1521
1522        Notes:
1523            * Currency Pair Assumption: This function assumes that the exchange rates stored for each account
1524                                        are appropriate for the currency pairs involved in the conversions.
1525            * The exchange rate for each account is based on the last encountered transaction rate that is not equal
1526                to 1.0 or the previous rate for that account.
1527            * Those rates will be merged into the exchange rates main data, and later it will be used for all subsequent
1528              transactions of the same account within the whole imported and existing dataset when doing `check` and
1529              `zakat` operations.
1530
1531        Example Usage:
1532            The CSV file should have the following format, rate is optional per transaction:
1533            account, desc, value, date, rate
1534            For example:
1535            safe-45, "Some text", 34872, 1988-06-30 00:00:00, 1
1536        """
1537        if debug:
1538            print('import_csv', f'debug={debug}')
1539        cache: list[int] = []
1540        try:
1541            with open(self.import_csv_cache_path(), "rb") as f:
1542                cache = pickle.load(f)
1543        except:
1544            pass
1545        date_formats = [
1546            "%Y-%m-%d %H:%M:%S",
1547            "%Y-%m-%dT%H:%M:%S",
1548            "%Y-%m-%dT%H%M%S",
1549            "%Y-%m-%d",
1550        ]
1551        created, found, bad = 0, 0, {}
1552        data: list[tuple] = []
1553        with open(path, newline='', encoding="utf-8") as f:
1554            i = 0
1555            for row in csv.reader(f, delimiter=','):
1556                i += 1
1557                hashed = hash(tuple(row))
1558                if hashed in cache:
1559                    found += 1
1560                    continue
1561                account = row[0]
1562                desc = row[1]
1563                value = float(row[2])
1564                rate = 1.0
1565                if row[4:5]:  # Empty list if index is out of range
1566                    rate = float(row[4])
1567                date: int = 0
1568                for time_format in date_formats:
1569                    try:
1570                        date = self.time(datetime.datetime.strptime(row[3], time_format))
1571                        break
1572                    except:
1573                        pass
1574                # TODO: not allowed for negative dates
1575                if date == 0 or value == 0:
1576                    bad[i] = row
1577                    continue
1578                if date in data:
1579                    print('import_csv-duplicated(time)', date)
1580                    continue
1581                data.append((date, value, desc, account, rate, hashed))
1582
1583        if debug:
1584            print('import_csv', len(data))
1585        for row in sorted(data, key=lambda x: x[0]):
1586            (date, value, desc, account, rate, hashed) = row
1587            if rate > 1:
1588                self.exchange(account, created=date, rate=rate)
1589            if value > 0:
1590                self.track(value, desc, account, True, date)
1591            elif value < 0:
1592                self.sub(-value, desc, account, date)
1593            created += 1
1594            cache.append(hashed)
1595        with open(self.import_csv_cache_path(), "wb") as f:
1596            pickle.dump(cache, f)
1597        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 duration_from_nanoseconds(ns: int) -> tuple:
1603    @staticmethod
1604    def duration_from_nanoseconds(ns: int) -> tuple:
1605        """
1606        REF https://github.com/JayRizzo/Random_Scripts/blob/master/time_measure.py#L106
1607        Convert NanoSeconds to Human Readable Time Format.
1608        A NanoSeconds is a unit of time in the International System of Units (SI) equal
1609        to one millionth (0.000001 or 10−6 or 1⁄1,000,000) of a second.
1610        Its symbol is μs, sometimes simplified to us when Unicode is not available.
1611        A microsecond is equal to 1000 nanoseconds or 1⁄1,000 of a millisecond.
1612
1613        INPUT : ms (AKA: MilliSeconds)
1614        OUTPUT: tuple(string time_lapsed, string spoken_time) like format.
1615        OUTPUT Variables: time_lapsed, spoken_time
1616
1617        Example  Input: duration_from_nanoseconds(ns)
1618        **"Millennium:Century:Years:Days:Hours:Minutes:Seconds:MilliSeconds:MicroSeconds:NanoSeconds"**
1619        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')
1620        duration_from_nanoseconds(1234567890123456789012)
1621        """
1622        us, ns = divmod(ns, 1000)
1623        ms, us = divmod(us, 1000)
1624        s, ms = divmod(ms, 1000)
1625        m, s = divmod(s, 60)
1626        h, m = divmod(m, 60)
1627        d, h = divmod(h, 24)
1628        y, d = divmod(d, 365)
1629        c, y = divmod(y, 100)
1630        n, c = divmod(c, 10)
1631        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}"
1632        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"
1633        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:
1635    @staticmethod
1636    def day_to_time(day: int, month: int = 6, year: int = 2024) -> int:  # افتراض أن الشهر هو يونيو والسنة 2024
1637        """
1638        Convert a specific day, month, and year into a timestamp.
1639
1640        Parameters:
1641        day (int): The day of the month.
1642        month (int): The month of the year. Default is 6 (June).
1643        year (int): The year. Default is 2024.
1644
1645        Returns:
1646        int: The timestamp representing the given day, month, and year.
1647
1648        Note:
1649        This method assumes the default month and year if not provided.
1650        """
1651        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:
1653    @staticmethod
1654    def generate_random_date(start_date: datetime.datetime, end_date: datetime.datetime) -> datetime.datetime:
1655        """
1656        Generate a random date between two given dates.
1657
1658        Parameters:
1659        start_date (datetime.datetime): The start date from which to generate a random date.
1660        end_date (datetime.datetime): The end date until which to generate a random date.
1661
1662        Returns:
1663        datetime.datetime: A random date between the start_date and end_date.
1664        """
1665        time_between_dates = end_date - start_date
1666        days_between_dates = time_between_dates.days
1667        random_number_of_days = random.randrange(days_between_dates)
1668        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:
1670    @staticmethod
1671    def generate_random_csv_file(path: str = "data.csv", count: int = 1000, with_rate: bool = False,
1672                                 debug: bool = False) -> int:
1673        """
1674        Generate a random CSV file with specified parameters.
1675
1676        Parameters:
1677        path (str): The path where the CSV file will be saved. Default is "data.csv".
1678        count (int): The number of rows to generate in the CSV file. Default is 1000.
1679        with_rate (bool): If True, a random rate between 1.2% and 12% is added. Default is False.
1680        debug (bool): A flag indicating whether to print debug information.
1681
1682        Returns:
1683        None. The function generates a CSV file at the specified path with the given count of rows.
1684        Each row contains a randomly generated account, description, value, and date.
1685        The value is randomly generated between 1000 and 100000,
1686        and the date is randomly generated between 1950-01-01 and 2023-12-31.
1687        If the row number is not divisible by 13, the value is multiplied by -1.
1688        """
1689        if debug:
1690            print('generate_random_csv_file', f'debug={debug}')
1691        i = 0
1692        with open(path, "w", newline="") as csvfile:
1693            writer = csv.writer(csvfile)
1694            for i in range(count):
1695                account = f"acc-{random.randint(1, 1000)}"
1696                desc = f"Some text {random.randint(1, 1000)}"
1697                value = random.randint(1000, 100000)
1698                date = ZakatTracker.generate_random_date(datetime.datetime(1950, 1, 1),
1699                                                         datetime.datetime(2023, 12, 31)).strftime("%Y-%m-%d %H:%M:%S")
1700                if not i % 13 == 0:
1701                    value *= -1
1702                row = [account, desc, value, date]
1703                if with_rate:
1704                    rate = random.randint(1, 100) * 0.12
1705                    if debug:
1706                        print('before-append', row)
1707                    row.append(rate)
1708                    if debug:
1709                        print('after-append', row)
1710                writer.writerow(row)
1711                i = i + 1
1712        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):
1714    @staticmethod
1715    def create_random_list(max_sum, min_value=0, max_value=10):
1716        """
1717        Creates a list of random integers whose sum does not exceed the specified maximum.
1718
1719        Args:
1720            max_sum: The maximum allowed sum of the list elements.
1721            min_value: The minimum possible value for an element (inclusive).
1722            max_value: The maximum possible value for an element (inclusive).
1723
1724        Returns:
1725            A list of random integers.
1726        """
1727        result = []
1728        current_sum = 0
1729
1730        while current_sum < max_sum:
1731            # Calculate the remaining space for the next element
1732            remaining_sum = max_sum - current_sum
1733            # Determine the maximum possible value for the next element
1734            next_max_value = min(remaining_sum, max_value)
1735            # Generate a random element within the allowed range
1736            next_element = random.randint(min_value, next_max_value)
1737            result.append(next_element)
1738            current_sum += next_element
1739
1740        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:
1886    def test(self, debug: bool = False) -> bool:
1887        if debug:
1888            print('test', f'debug={debug}')
1889        try:
1890
1891            assert self._history()
1892
1893            # Not allowed for duplicate transactions in the same account and time
1894
1895            created = ZakatTracker.time()
1896            self.track(100, 'test-1', 'same', True, created)
1897            failed = False
1898            try:
1899                self.track(50, 'test-1', 'same', True, created)
1900            except:
1901                failed = True
1902            assert failed is True
1903
1904            self.reset()
1905
1906            # Same account transfer
1907            for x in [1, 'a', True, 1.8, None]:
1908                failed = False
1909                try:
1910                    self.transfer(1, x, x, 'same-account', debug=debug)
1911                except:
1912                    failed = True
1913                assert failed is True
1914
1915            # Always preserve box age during transfer
1916
1917            series: list[tuple] = [
1918                (30, 4),
1919                (60, 3),
1920                (90, 2),
1921            ]
1922            case = {
1923                30: {
1924                    'series': series,
1925                    'rest': 150,
1926                },
1927                60: {
1928                    'series': series,
1929                    'rest': 120,
1930                },
1931                90: {
1932                    'series': series,
1933                    'rest': 90,
1934                },
1935                180: {
1936                    'series': series,
1937                    'rest': 0,
1938                },
1939                270: {
1940                    'series': series,
1941                    'rest': -90,
1942                },
1943                360: {
1944                    'series': series,
1945                    'rest': -180,
1946                },
1947            }
1948
1949            selected_time = ZakatTracker.time() - ZakatTracker.TimeCycle()
1950
1951            for total in case:
1952                for x in case[total]['series']:
1953                    self.track(x[0], f"test-{x} ages", 'ages', True, selected_time * x[1])
1954
1955                refs = self.transfer(total, 'ages', 'future', 'Zakat Movement', debug=debug)
1956
1957                if debug:
1958                    print('refs', refs)
1959
1960                ages_cache_balance = self.balance('ages')
1961                ages_fresh_balance = self.balance('ages', False)
1962                rest = case[total]['rest']
1963                if debug:
1964                    print('source', ages_cache_balance, ages_fresh_balance, rest)
1965                assert ages_cache_balance == rest
1966                assert ages_fresh_balance == rest
1967
1968                future_cache_balance = self.balance('future')
1969                future_fresh_balance = self.balance('future', False)
1970                if debug:
1971                    print('target', future_cache_balance, future_fresh_balance, total)
1972                    print('refs', refs)
1973                assert future_cache_balance == total
1974                assert future_fresh_balance == total
1975
1976                for ref in self._vault['account']['ages']['box']:
1977                    ages_capital = self._vault['account']['ages']['box'][ref]['capital']
1978                    ages_rest = self._vault['account']['ages']['box'][ref]['rest']
1979                    future_capital = 0
1980                    if ref in self._vault['account']['future']['box']:
1981                        future_capital = self._vault['account']['future']['box'][ref]['capital']
1982                    future_rest = 0
1983                    if ref in self._vault['account']['future']['box']:
1984                        future_rest = self._vault['account']['future']['box'][ref]['rest']
1985                    if ages_capital != 0 and future_capital != 0 and future_rest != 0:
1986                        if debug:
1987                            print('================================================================')
1988                            print('ages', ages_capital, ages_rest)
1989                            print('future', future_capital, future_rest)
1990                        if ages_rest == 0:
1991                            assert ages_capital == future_capital
1992                        elif ages_rest < 0:
1993                            assert -ages_capital == future_capital
1994                        elif ages_rest > 0:
1995                            assert ages_capital == ages_rest + future_capital
1996                self.reset()
1997                assert len(self._vault['history']) == 0
1998
1999            assert self._history()
2000            assert self._history(False) is False
2001            assert self._history() is False
2002            assert self._history(True)
2003            assert self._history()
2004
2005            self._test_core(True, debug)
2006            self._test_core(False, debug)
2007
2008            transaction = [
2009                (
2010                    20, 'wallet', 1, 800, 800, 800, 4, 5,
2011                    -85, -85, -85, 6, 7,
2012                ),
2013                (
2014                    750, 'wallet', 'safe', 50, 50, 50, 4, 6,
2015                    750, 750, 750, 1, 1,
2016                ),
2017                (
2018                    600, 'safe', 'bank', 150, 150, 150, 1, 2,
2019                    600, 600, 600, 1, 1,
2020                ),
2021            ]
2022            for z in transaction:
2023                self.lock()
2024                x = z[1]
2025                y = z[2]
2026                self.transfer(z[0], x, y, 'test-transfer', debug=debug)
2027                assert self.balance(x) == z[3]
2028                xx = self.accounts()[x]
2029                assert xx == z[3]
2030                assert self.balance(x, False) == z[4]
2031                assert xx == z[4]
2032
2033                s = 0
2034                log = self._vault['account'][x]['log']
2035                for i in log:
2036                    s += log[i]['value']
2037                if debug:
2038                    print('s', s, 'z[5]', z[5])
2039                assert s == z[5]
2040
2041                assert self.box_size(x) == z[6]
2042                assert self.log_size(x) == z[7]
2043
2044                yy = self.accounts()[y]
2045                assert self.balance(y) == z[8]
2046                assert yy == z[8]
2047                assert self.balance(y, False) == z[9]
2048                assert yy == z[9]
2049
2050                s = 0
2051                log = self._vault['account'][y]['log']
2052                for i in log:
2053                    s += log[i]['value']
2054                assert s == z[10]
2055
2056                assert self.box_size(y) == z[11]
2057                assert self.log_size(y) == z[12]
2058
2059            if debug:
2060                pp().pprint(self.check(2.17))
2061
2062            assert not self.nolock()
2063            history_count = len(self._vault['history'])
2064            if debug:
2065                print('history-count', history_count)
2066            assert history_count == 11
2067            assert not self.free(ZakatTracker.time())
2068            assert self.free(self.lock())
2069            assert self.nolock()
2070            assert len(self._vault['history']) == 11
2071
2072            # storage
2073
2074            _path = self.path('test.pickle')
2075            if os.path.exists(_path):
2076                os.remove(_path)
2077            self.save()
2078            assert os.path.getsize(_path) > 0
2079            self.reset()
2080            assert self.recall(False, debug) is False
2081            self.load()
2082            assert self._vault['account'] is not None
2083
2084            # recall
2085
2086            assert self.nolock()
2087            assert len(self._vault['history']) == 11
2088            assert self.recall(False, debug) is True
2089            assert len(self._vault['history']) == 10
2090            assert self.recall(False, debug) is True
2091            assert len(self._vault['history']) == 9
2092
2093            # exchange
2094
2095            self.exchange("cash", 25, 3.75, "2024-06-25")
2096            self.exchange("cash", 22, 3.73, "2024-06-22")
2097            self.exchange("cash", 15, 3.69, "2024-06-15")
2098            self.exchange("cash", 10, 3.66)
2099
2100            for i in range(1, 30):
2101                exchange = self.exchange("cash", i)
2102                rate, description, created = exchange['rate'], exchange['description'], exchange['time']
2103                if debug:
2104                    print(i, rate, description, created)
2105                assert created
2106                if i < 10:
2107                    assert rate == 1
2108                    assert description is None
2109                elif i == 10:
2110                    assert rate == 3.66
2111                    assert description is None
2112                elif i < 15:
2113                    assert rate == 3.66
2114                    assert description is None
2115                elif i == 15:
2116                    assert rate == 3.69
2117                    assert description is not None
2118                elif i < 22:
2119                    assert rate == 3.69
2120                    assert description is not None
2121                elif i == 22:
2122                    assert rate == 3.73
2123                    assert description is not None
2124                elif i >= 25:
2125                    assert rate == 3.75
2126                    assert description is not None
2127                exchange = self.exchange("bank", i)
2128                rate, description, created = exchange['rate'], exchange['description'], exchange['time']
2129                if debug:
2130                    print(i, rate, description, created)
2131                assert created
2132                assert rate == 1
2133                assert description is None
2134
2135            assert len(self._vault['exchange']) > 0
2136            assert len(self.exchanges()) > 0
2137            self._vault['exchange'].clear()
2138            assert len(self._vault['exchange']) == 0
2139            assert len(self.exchanges()) == 0
2140
2141            # حفظ أسعار الصرف باستخدام التواريخ بالنانو ثانية
2142            self.exchange("cash", ZakatTracker.day_to_time(25), 3.75, "2024-06-25")
2143            self.exchange("cash", ZakatTracker.day_to_time(22), 3.73, "2024-06-22")
2144            self.exchange("cash", ZakatTracker.day_to_time(15), 3.69, "2024-06-15")
2145            self.exchange("cash", ZakatTracker.day_to_time(10), 3.66)
2146
2147            for i in [x * 0.12 for x in range(-15, 21)]:
2148                if i <= 0:
2149                    assert len(self.exchange("test", ZakatTracker.time(), i, f"range({i})")) == 0
2150                else:
2151                    assert len(self.exchange("test", ZakatTracker.time(), i, f"range({i})")) > 0
2152
2153            # اختبار النتائج باستخدام التواريخ بالنانو ثانية
2154            for i in range(1, 31):
2155                timestamp_ns = ZakatTracker.day_to_time(i)
2156                exchange = self.exchange("cash", timestamp_ns)
2157                rate, description, created = exchange['rate'], exchange['description'], exchange['time']
2158                if debug:
2159                    print(i, rate, description, created)
2160                assert created
2161                if i < 10:
2162                    assert rate == 1
2163                    assert description is None
2164                elif i == 10:
2165                    assert rate == 3.66
2166                    assert description is None
2167                elif i < 15:
2168                    assert rate == 3.66
2169                    assert description is None
2170                elif i == 15:
2171                    assert rate == 3.69
2172                    assert description is not None
2173                elif i < 22:
2174                    assert rate == 3.69
2175                    assert description is not None
2176                elif i == 22:
2177                    assert rate == 3.73
2178                    assert description is not None
2179                elif i >= 25:
2180                    assert rate == 3.75
2181                    assert description is not None
2182                exchange = self.exchange("bank", i)
2183                rate, description, created = exchange['rate'], exchange['description'], exchange['time']
2184                if debug:
2185                    print(i, rate, description, created)
2186                assert created
2187                assert rate == 1
2188                assert description is None
2189
2190            # csv
2191
2192            csv_count = 1000
2193
2194            for with_rate, path in {
2195                False: 'test-import_csv-no-exchange',
2196                True: 'test-import_csv-with-exchange',
2197            }.items():
2198
2199                if debug:
2200                    print('test_import_csv', with_rate, path)
2201
2202                # csv
2203
2204                csv_path = path + '.csv'
2205                if os.path.exists(csv_path):
2206                    os.remove(csv_path)
2207                c = self.generate_random_csv_file(csv_path, csv_count, with_rate, debug)
2208                if debug:
2209                    print('generate_random_csv_file', c)
2210                assert c == csv_count
2211                assert os.path.getsize(csv_path) > 0
2212                cache_path = self.import_csv_cache_path()
2213                if os.path.exists(cache_path):
2214                    os.remove(cache_path)
2215                self.reset()
2216                (created, found, bad) = self.import_csv(csv_path, debug)
2217                bad_count = len(bad)
2218                if debug:
2219                    print(f"csv-imported: ({created}, {found}, {bad_count}) = count({csv_count})")
2220                tmp_size = os.path.getsize(cache_path)
2221                assert tmp_size > 0
2222                assert created + found + bad_count == csv_count
2223                assert created == csv_count
2224                assert bad_count == 0
2225                (created_2, found_2, bad_2) = self.import_csv(csv_path)
2226                bad_2_count = len(bad_2)
2227                if debug:
2228                    print(f"csv-imported: ({created_2}, {found_2}, {bad_2_count})")
2229                    print(bad)
2230                assert tmp_size == os.path.getsize(cache_path)
2231                assert created_2 + found_2 + bad_2_count == csv_count
2232                assert created == found_2
2233                assert bad_count == bad_2_count
2234                assert found_2 == csv_count
2235                assert bad_2_count == 0
2236                assert created_2 == 0
2237
2238                # payment parts
2239
2240                positive_parts = self.build_payment_parts(100, positive_only=True)
2241                assert self.check_payment_parts(positive_parts) != 0
2242                assert self.check_payment_parts(positive_parts) != 0
2243                all_parts = self.build_payment_parts(300, positive_only=False)
2244                assert self.check_payment_parts(all_parts) != 0
2245                assert self.check_payment_parts(all_parts) != 0
2246                if debug:
2247                    pp().pprint(positive_parts)
2248                    pp().pprint(all_parts)
2249                # dynamic discount
2250                suite = []
2251                count = 3
2252                for exceed in [False, True]:
2253                    case = []
2254                    for parts in [positive_parts, all_parts]:
2255                        part = parts.copy()
2256                        demand = part['demand']
2257                        if debug:
2258                            print(demand, part['total'])
2259                        i = 0
2260                        z = demand / count
2261                        cp = {
2262                            'account': {},
2263                            'demand': demand,
2264                            'exceed': exceed,
2265                            'total': part['total'],
2266                        }
2267                        j = ''
2268                        for x, y in part['account'].items():
2269                            x_exchange = self.exchange(x)
2270                            zz = self.exchange_calc(z, 1, x_exchange['rate'])
2271                            if exceed and zz <= demand:
2272                                i += 1
2273                                y['part'] = zz
2274                                if debug:
2275                                    print(exceed, y)
2276                                cp['account'][x] = y
2277                                case.append(y)
2278                            elif not exceed and y['balance'] >= zz:
2279                                i += 1
2280                                y['part'] = zz
2281                                if debug:
2282                                    print(exceed, y)
2283                                cp['account'][x] = y
2284                                case.append(y)
2285                            j = x
2286                            if i >= count:
2287                                break
2288                        if len(cp['account'][j]) > 0:
2289                            suite.append(cp)
2290                if debug:
2291                    print('suite', len(suite))
2292                # vault = self._vault.copy()
2293                for case in suite:
2294                    # self._vault = vault.copy()
2295                    if debug:
2296                        print('case', case)
2297                    result = self.check_payment_parts(case)
2298                    if debug:
2299                        print('check_payment_parts', result, f'exceed: {exceed}')
2300                    assert result == 0
2301
2302                    report = self.check(2.17, None, debug)
2303                    (valid, brief, plan) = report
2304                    if debug:
2305                        print('valid', valid)
2306                    zakat_result = self.zakat(report, parts=case, debug=debug)
2307                    if debug:
2308                        print('zakat-result', zakat_result)
2309                    assert valid == zakat_result
2310
2311            assert self.save(path + '.pickle')
2312            assert self.export_json(path + '.json')
2313
2314            assert self.export_json("1000-transactions-test.json")
2315            assert self.save("1000-transactions-test.pickle")
2316
2317            self.reset()
2318
2319            # test transfer between accounts with different exchange rate
2320
2321            a_SAR = "Bank (SAR)"
2322            b_USD = "Bank (USD)"
2323            c_SAR = "Safe (SAR)"
2324            # 0: track, 1: check-exchange, 2: do-exchange, 3: transfer
2325            for case in [
2326                (0, a_SAR, "SAR Gift", 1000, 1000),
2327                (1, a_SAR, 1),
2328                (0, b_USD, "USD Gift", 500, 500),
2329                (1, b_USD, 1),
2330                (2, b_USD, 3.75),
2331                (1, b_USD, 3.75),
2332                (3, 100, b_USD, a_SAR, "100 USD -> SAR", 400, 1375),
2333                (0, c_SAR, "Salary", 750, 750),
2334                (3, 375, c_SAR, b_USD, "375 SAR -> USD", 375, 500),
2335                (3, 3.75, a_SAR, b_USD, "3.75 SAR -> USD", 1371.25, 501),
2336            ]:
2337                match (case[0]):
2338                    case 0:  # track
2339                        _, account, desc, x, balance = case
2340                        self.track(value=x, desc=desc, account=account, debug=debug)
2341
2342                        cached_value = self.balance(account, cached=True)
2343                        fresh_value = self.balance(account, cached=False)
2344                        if debug:
2345                            print('account', account, 'cached_value', cached_value, 'fresh_value', fresh_value)
2346                        assert cached_value == balance
2347                        assert fresh_value == balance
2348                    case 1:  # check-exchange
2349                        _, account, expected_rate = case
2350                        t_exchange = self.exchange(account, created=ZakatTracker.time(), debug=debug)
2351                        if debug:
2352                            print('t-exchange', t_exchange)
2353                        assert t_exchange['rate'] == expected_rate
2354                    case 2:  # do-exchange
2355                        _, account, rate = case
2356                        self.exchange(account, rate=rate, debug=debug)
2357                        b_exchange = self.exchange(account, created=ZakatTracker.time(), debug=debug)
2358                        if debug:
2359                            print('b-exchange', b_exchange)
2360                        assert b_exchange['rate'] == rate
2361                    case 3:  # transfer
2362                        _, x, a, b, desc, a_balance, b_balance = case
2363                        self.transfer(x, a, b, desc, debug=debug)
2364
2365                        cached_value = self.balance(a, cached=True)
2366                        fresh_value = self.balance(a, cached=False)
2367                        if debug:
2368                            print('account', a, 'cached_value', cached_value, 'fresh_value', fresh_value)
2369                        assert cached_value == a_balance
2370                        assert fresh_value == a_balance
2371
2372                        cached_value = self.balance(b, cached=True)
2373                        fresh_value = self.balance(b, cached=False)
2374                        if debug:
2375                            print('account', b, 'cached_value', cached_value, 'fresh_value', fresh_value)
2376                        assert cached_value == b_balance
2377                        assert fresh_value == b_balance
2378
2379            # Transfer all in many chunks randomly from B to A
2380            a_SAR_balance = 1371.25
2381            b_USD_balance = 501
2382            b_USD_exchange = self.exchange(b_USD)
2383            amounts = ZakatTracker.create_random_list(b_USD_balance)
2384            if debug:
2385                print('amounts', amounts)
2386            i = 0
2387            for x in amounts:
2388                if debug:
2389                    print(f'{i} - transfer-with-exchange({x})')
2390                self.transfer(x, b_USD, a_SAR, f"{x} USD -> SAR", debug=debug)
2391
2392                b_USD_balance -= x
2393                cached_value = self.balance(b_USD, cached=True)
2394                fresh_value = self.balance(b_USD, cached=False)
2395                if debug:
2396                    print('account', b_USD, 'cached_value', cached_value, 'fresh_value', fresh_value, 'excepted',
2397                          b_USD_balance)
2398                assert cached_value == b_USD_balance
2399                assert fresh_value == b_USD_balance
2400
2401                a_SAR_balance += x * b_USD_exchange['rate']
2402                cached_value = self.balance(a_SAR, cached=True)
2403                fresh_value = self.balance(a_SAR, cached=False)
2404                if debug:
2405                    print('account', a_SAR, 'cached_value', cached_value, 'fresh_value', fresh_value, 'expected',
2406                          a_SAR_balance, 'rate', b_USD_exchange['rate'])
2407                assert cached_value == a_SAR_balance
2408                assert fresh_value == a_SAR_balance
2409                i += 1
2410
2411            # Transfer all in many chunks randomly from C to A
2412            c_SAR_balance = 375
2413            amounts = ZakatTracker.create_random_list(c_SAR_balance)
2414            if debug:
2415                print('amounts', amounts)
2416            i = 0
2417            for x in amounts:
2418                if debug:
2419                    print(f'{i} - transfer-with-exchange({x})')
2420                self.transfer(x, c_SAR, a_SAR, f"{x} SAR -> a_SAR", debug=debug)
2421
2422                c_SAR_balance -= x
2423                cached_value = self.balance(c_SAR, cached=True)
2424                fresh_value = self.balance(c_SAR, cached=False)
2425                if debug:
2426                    print('account', c_SAR, 'cached_value', cached_value, 'fresh_value', fresh_value, 'excepted',
2427                          c_SAR_balance)
2428                assert cached_value == c_SAR_balance
2429                assert fresh_value == c_SAR_balance
2430
2431                a_SAR_balance += x
2432                cached_value = self.balance(a_SAR, cached=True)
2433                fresh_value = self.balance(a_SAR, cached=False)
2434                if debug:
2435                    print('account', a_SAR, 'cached_value', cached_value, 'fresh_value', fresh_value, 'expected',
2436                          a_SAR_balance)
2437                assert cached_value == a_SAR_balance
2438                assert fresh_value == a_SAR_balance
2439                i += 1
2440
2441            assert self.export_json("accounts-transfer-with-exchange-rates.json")
2442            assert self.save("accounts-transfer-with-exchange-rates.pickle")
2443
2444            # check & zakat with exchange rates for many cycles
2445
2446            for rate, values in {
2447                1: {
2448                    'in': [1000, 2000, 10000],
2449                    'exchanged': [1000, 2000, 10000],
2450                    'out': [25, 50, 731.40625],
2451                },
2452                3.75: {
2453                    'in': [200, 1000, 5000],
2454                    'exchanged': [750, 3750, 18750],
2455                    'out': [18.75, 93.75, 1371.38671875],
2456                },
2457            }.items():
2458                a, b, c = values['in']
2459                m, n, o = values['exchanged']
2460                x, y, z = values['out']
2461                if debug:
2462                    print('rate', rate, 'values', values)
2463                for case in [
2464                    (a, 'safe', ZakatTracker.time() - ZakatTracker.TimeCycle(), [
2465                        {'safe': {0: {'below_nisab': x}}},
2466                    ], False, m),
2467                    (b, 'safe', ZakatTracker.time() - ZakatTracker.TimeCycle(), [
2468                        {'safe': {0: {'count': 1, 'total': y}}},
2469                    ], True, n),
2470                    (c, 'cave', ZakatTracker.time() - (ZakatTracker.TimeCycle() * 3), [
2471                        {'cave': {0: {'count': 3, 'total': z}}},
2472                    ], True, o),
2473                ]:
2474                    if debug:
2475                        print(f"############# check(rate: {rate}) #############")
2476                    self.reset()
2477                    self.exchange(account=case[1], created=case[2], rate=rate)
2478                    self.track(value=case[0], desc='test-check', account=case[1], logging=True, created=case[2])
2479
2480                    # assert self.nolock()
2481                    # history_size = len(self._vault['history'])
2482                    # print('history_size', history_size)
2483                    # assert history_size == 2
2484                    assert self.lock()
2485                    assert not self.nolock()
2486                    report = self.check(2.17, None, debug)
2487                    (valid, brief, plan) = report
2488                    assert valid == case[4]
2489                    if debug:
2490                        print('brief', brief)
2491                    assert case[5] == brief[0]
2492                    assert case[5] == brief[1]
2493
2494                    if debug:
2495                        pp().pprint(plan)
2496
2497                    for x in plan:
2498                        assert case[1] == x
2499                        if 'total' in case[3][0][x][0].keys():
2500                            assert case[3][0][x][0]['total'] == brief[2]
2501                            assert plan[x][0]['total'] == case[3][0][x][0]['total']
2502                            assert plan[x][0]['count'] == case[3][0][x][0]['count']
2503                        else:
2504                            assert plan[x][0]['below_nisab'] == case[3][0][x][0]['below_nisab']
2505                    if debug:
2506                        pp().pprint(report)
2507                    result = self.zakat(report, debug=debug)
2508                    if debug:
2509                        print('zakat-result', result, case[4])
2510                    assert result == case[4]
2511                    report = self.check(2.17, None, debug)
2512                    (valid, brief, plan) = report
2513                    assert valid is False
2514
2515            history_size = len(self._vault['history'])
2516            if debug:
2517                print('history_size', history_size)
2518            assert history_size == 3
2519            assert not self.nolock()
2520            assert self.recall(False, debug) is False
2521            self.free(self.lock())
2522            assert self.nolock()
2523
2524            for i in range(3, 0, -1):
2525                history_size = len(self._vault['history'])
2526                if debug:
2527                    print('history_size', history_size)
2528                assert history_size == i
2529                assert self.recall(False, debug) is True
2530
2531            assert self.nolock()
2532            assert self.recall(False, debug) is False
2533
2534            history_size = len(self._vault['history'])
2535            if debug:
2536                print('history_size', history_size)
2537            assert history_size == 0
2538
2539            account_size = len(self._vault['account'])
2540            if debug:
2541                print('account_size', account_size)
2542            assert account_size == 0
2543
2544            report_size = len(self._vault['report'])
2545            if debug:
2546                print('report_size', report_size)
2547            assert report_size == 0
2548
2549            assert self.nolock()
2550            return True
2551        except:
2552            # pp().pprint(self._vault)
2553            assert self.export_json("test-snapshot.json")
2554            assert self.save("test-snapshot.pickle")
2555            raise
def test(debug: bool = False):
2558def test(debug: bool = False):
2559    ledger = ZakatTracker()
2560    start = ZakatTracker.time()
2561    assert ledger.test(debug=debug)
2562    if debug:
2563        print("#########################")
2564        print("######## TEST DONE ########")
2565        print("#########################")
2566        print(ZakatTracker.duration_from_nanoseconds(ZakatTracker.time() - start))
2567        print("#########################")
class Action(enum.Enum):
72class Action(Enum):
73    CREATE = auto()
74    TRACK = auto()
75    LOG = auto()
76    SUB = auto()
77    ADD_FILE = auto()
78    REMOVE_FILE = auto()
79    BOX_TRANSFER = auto()
80    EXCHANGE = auto()
81    REPORT = auto()
82    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):
85class JSONEncoder(json.JSONEncoder):
86    def default(self, obj):
87        if isinstance(obj, Action) or isinstance(obj, MathOperation):
88            return obj.name  # Serialize as the enum member's name
89        elif isinstance(obj, Decimal):
90            return float(obj)
91        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):
86    def default(self, obj):
87        if isinstance(obj, Action) or isinstance(obj, MathOperation):
88            return obj.name  # Serialize as the enum member's name
89        elif isinstance(obj, Decimal):
90            return float(obj)
91        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):
94class MathOperation(Enum):
95    ADDITION = auto()
96    EQUAL = auto()
97    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'>