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

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

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